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
20 along with PhpWiki; if not, write to the Free Software
21 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 function Upgrade (&$request) {
55 $this->request =& $request;
56 $this->dbi =& $request->_dbi; // no reference for dbadmin ?
57 $this->phpwiki_version = $this->current_db_version = phpwiki_version();
58 //$this->current_db_version = 1030.13; // should be stored in the db. should be phpwiki_version
60 $this->db_version = $this->dbi->get_db_version();
61 $this->isSQL = $this->dbi->_backend->isSQL();
65 * TODO: check for the pgsrc_version number, not the revision mtime only
67 function doPgsrcUpdate($pagename, $path, $filename) {
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 echo "$path/$pagename: ",_("always skip the HomePage."),
73 _(" skipped"),".<br />\n";
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');
86 $pageinfo = $parts[0];
87 $stat = stat($path."/".$filename);
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'];
96 $new_mtime = $stat[9];
97 if ($new_mtime > $page_mtime) {
98 echo "$path/$pagename: ",_("newer than the existing page."),
99 _(" replace "),"($new_mtime > $page_mtime)","<br />\n";
100 LoadAny($this->request, $path."/".$filename);
103 echo "$path/$pagename: ",_("older than the existing page."),
104 _(" skipped"),".<br />\n";
107 echo "$path/$pagename: ",("unknown format."),
108 _(" skipped"),".<br />\n";
111 echo sprintf(_("%s does not exist"),$pagename),"<br />\n";
112 LoadAny($this->request, $path."/".$filename);
118 * If a matching pgsrc => pluginname exists
119 * Need the English filename (required precondition: urlencode == urldecode).
121 function isActionPage($filename) {
123 global $AllActionPages;
125 return (in_array($filename, $AllActionPages));
128 function CheckActionPageUpdate() {
129 echo "<h3>",sprintf(_("check for necessary %s updates"),
130 _("ActionPage")),"</h3>\n";
131 // 1.3.13 before we pull in all missing pages, we rename existing ones
132 $this->_rename_page_helper(_("_AuthInfo"), _("DebugAuthInfo"));
133 // this is in some templates. so we keep the old name
134 //$this->_rename_page_helper($this->dbi, _("DebugInfo"), _("DebugBackendInfo"));
135 $this->_rename_page_helper(_("_GroupInfo"), _("GroupAuthInfo")); //never officially existed
136 $this->_rename_page_helper("InterWikiKarte", "InterWikiListe"); // german only
138 $path = FindFile('pgsrc');
139 $pgsrc = new fileSet($path);
140 // most actionpages have the same name as the plugin
141 $loc_path = FindLocalizedFile('pgsrc');
142 foreach ($pgsrc->getFiles() as $filename) {
143 if (substr($filename,-1,1) == '~') continue;
144 if (substr($filename,-5,5) == '.orig') continue;
145 $pagename = urldecode($filename);
146 if ($this->isActionPage($filename)) {
147 $translation = gettext($pagename);
148 if ($translation == $pagename)
149 $this->doPgsrcUpdate($pagename, $path, $filename);
150 elseif (FindLocalizedFile('pgsrc/'.urlencode($translation),1))
151 $this->doPgsrcUpdate($translation, $loc_path, urlencode($translation));
153 $this->doPgsrcUpdate($pagename, $path, $filename);
158 // see loadsave.php for saving new pages.
159 function CheckPgsrcUpdate() {
160 echo "<h3>",sprintf(_("check for necessary %s updates"),
162 if ($this->db_version < 1030.12200612) {
163 echo "<h4>",_("rename to Help: pages"),"</h4>\n";
165 $path = FindLocalizedFile(WIKI_PGSRC);
166 $pgsrc = new fileSet($path);
167 // fixme: verification, ...
168 foreach ($pgsrc->getFiles() as $filename) {
169 if (substr($filename,-1,1) == '~') continue;
170 if (substr($filename,-5,5) == '.orig') continue;
171 $pagename = urldecode($filename);
172 if (!$this->isActionPage($filename)) {
173 // There're a lot of now unneeded pages around.
174 // At first rename the BlaPlugin pages to Help/<pagename> and then to the update.
175 if ($this->db_version < 1030.12200612) {
176 $this->_rename_to_help_page($pagename);
178 $this->doPgsrcUpdate($pagename,$path,$filename);
182 // Now check some theme specific pgsrc files (blog, wikilens, custom).
184 $path = $WikiTheme->file("pgsrc");
185 // TBD: the call to fileSet prints a warning:
186 // Notice: Unable to open directory 'themes/MonoBook/pgsrc' for reading
187 $pgsrc = new fileSet($path);
188 if ($pgsrc->getFiles()) {
189 echo "<h3>",sprintf(_("check for additional theme %s updates"),
191 foreach ($pgsrc->getFiles() as $filename) {
192 if (substr($filename,-1,1) == '~') continue;
193 if (substr($filename,-5,5) == '.orig') continue;
194 $pagename = urldecode($filename);
195 $this->doPgsrcUpdate($pagename,$path,$filename);
201 function _rename_page_helper($oldname, $pagename) {
202 echo sprintf(_("rename %s to %s"), $oldname, $pagename)," ...";
203 if ($this->dbi->isWikiPage($oldname) and !$this->dbi->isWikiPage($pagename)) {
204 if ($this->dbi->_backend->rename_page($oldname, $pagename))
205 echo _("OK")," <br />\n";
207 echo " <b><font color=\"red\">", _("FAILED"), "</font></b>",
210 echo _(" skipped")," <br />\n";
214 function _rename_to_help_page($pagename) {
215 $newprefix = _("Help") . "/";
216 if (substr($pagename,0,strlen($newprefix)) != $newprefix) return;
217 $oldname = substr($pagename,strlen($newprefix));
218 $this->_rename_page_helper($oldname, $pagename);
222 * TODO: Search table definition in appropriate schema
224 * Supported: mysql and generic SQL, for ADODB and PearDB.
226 function installTable($table, $backend_type) {
228 if (!$this->isSQL) return;
229 echo _("MISSING")," ... \n";
230 $backend = &$this->dbi->_backend->_dbh;
232 $schema = findFile("schemas/${backend_type}.sql");
234 echo " ",_("FAILED"),": ",sprintf(_("no schema %s found"),
235 "schemas/${backend_type}.sql")," ... <br />\n";
239 extract($this->dbi->_backend->_table_names);
240 $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
243 assert($session_tbl);
244 if ($backend_type == 'mysql') {
245 $this->dbi->genericSqlQuery("
246 CREATE TABLE $session_tbl (
247 sess_id CHAR(32) NOT NULL DEFAULT '',
248 sess_data BLOB NOT NULL,
249 sess_date INT UNSIGNED NOT NULL,
250 sess_ip CHAR(15) NOT NULL,
251 PRIMARY KEY (sess_id),
255 $this->dbi->genericSqlQuery("
256 CREATE TABLE $session_tbl (
257 sess_id CHAR(32) NOT NULL DEFAULT '',
258 sess_data ".($backend_type == 'pgsql'?'TEXT':'BLOB')." NOT NULL,
260 sess_ip CHAR(15) NOT NULL
262 $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX sess_id ON $session_tbl (sess_id)");
264 $this->dbi->genericSqlQuery("CREATE INDEX sess_date on session (sess_date)");
265 echo " ",_("CREATED");
268 $pref_tbl = $prefix.'pref';
269 if ($backend_type == 'mysql') {
270 $this->dbi->genericSqlQuery("
271 CREATE TABLE $pref_tbl (
272 userid CHAR(48) BINARY NOT NULL UNIQUE,
273 prefs TEXT NULL DEFAULT '',
277 $this->dbi->genericSqlQuery("
278 CREATE TABLE $pref_tbl (
279 userid CHAR(48) NOT NULL,
280 prefs TEXT NULL DEFAULT ''
282 $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX userid ON $pref_tbl (userid)");
284 echo " ",_("CREATED");
287 $member_tbl = $prefix.'member';
288 if ($backend_type == 'mysql') {
289 $this->dbi->genericSqlQuery("
290 CREATE TABLE $member_tbl (
291 userid CHAR(48) BINARY NOT NULL,
292 groupname CHAR(48) BINARY NOT NULL DEFAULT 'users',
297 $this->dbi->genericSqlQuery("
298 CREATE TABLE $member_tbl (
299 userid CHAR(48) NOT NULL,
300 groupname CHAR(48) NOT NULL DEFAULT 'users'
302 $this->dbi->genericSqlQuery("CREATE INDEX userid ON $member_tbl (userid)");
303 $this->dbi->genericSqlQuery("CREATE INDEX groupname ON $member_tbl (groupname)");
305 echo " ",_("CREATED");
308 $rating_tbl = $prefix.'rating';
309 if ($backend_type == 'mysql') {
310 $this->dbi->genericSqlQuery("
311 CREATE TABLE $rating_tbl (
312 dimension INT(4) NOT NULL,
313 raterpage INT(11) NOT NULL,
314 rateepage INT(11) NOT NULL,
315 ratingvalue FLOAT NOT NULL,
316 rateeversion INT(11) NOT NULL,
317 tstamp TIMESTAMP(14) NOT NULL,
318 PRIMARY KEY (dimension, raterpage, rateepage)
321 $this->dbi->genericSqlQuery("
322 CREATE TABLE $rating_tbl (
323 dimension INT(4) NOT NULL,
324 raterpage INT(11) NOT NULL,
325 rateepage INT(11) NOT NULL,
326 ratingvalue FLOAT NOT NULL,
327 rateeversion INT(11) NOT NULL,
328 tstamp TIMESTAMP(14) NOT NULL
330 $this->dbi->genericSqlQuery("CREATE UNIQUE INDEX rating"
331 ." ON $rating_tbl (dimension, raterpage, rateepage)");
333 echo " ",_("CREATED");
336 $log_tbl = $prefix.'accesslog';
337 // fields according to http://www.outoforder.cc/projects/apache/mod_log_sql/docs-2.0/#id2756178
339 A User Agent agent varchar(255) Mozilla/4.0 (compat; MSIE 6.0; Windows)
340 a CGi request arguments request_args varchar(255) user=Smith&cart=1231&item=532
341 b Bytes transfered bytes_sent int unsigned 32561
342 c??? Text of cookie cookie varchar(255) Apache=sdyn.fooonline.net 1300102700823
343 f Local filename requested request_file varchar(255) /var/www/html/books-cycroad.html
344 H HTTP request_protocol request_protocol varchar(10) HTTP/1.1
345 h Name of remote host remote_host varchar(50) blah.foobar.com
346 I Request ID (from modd_unique_id) id char(19) POlFcUBRH30AAALdBG8
347 l Ident user info remote_logname varcgar(50) bobby
348 M Machine ID??? machine_id varchar(25) web01
349 m HTTP request method request_method varchar(10) GET
350 P httpd cchild PID child_pid smallint unsigned 3215
351 p http port server_port smallint unsigned 80
352 R Referer referer varchar(255) http://www.biglinks4u.com/linkpage.html
353 r Request in full form request_line varchar(255) GET /books-cycroad.html HTTP/1.1
354 S Time of request in UNIX time_t format time_stamp int unsigned 1005598029
355 T Seconds to service request request_duration smallint unsigned 2
356 t Time of request in human format request_time char(28) [02/Dec/2001:15:01:26 -0800]
357 U Request in simple form request_uri varchar(255) /books-cycroad.html
358 u User info from HTTP auth remote_user varchar(50) bobby
359 v Virtual host servicing the request virtual_host varchar(255)
361 $this->dbi->genericSqlQuery("
362 CREATE TABLE $log_tbl (
363 time_stamp int unsigned,
364 remote_host varchar(100),
365 remote_user varchar(50),
366 request_method varchar(10),
367 request_line varchar(255),
368 request_args varchar(255),
369 request_uri varchar(255),
370 request_time char(28),
371 status smallint unsigned,
372 bytes_sent smallint unsigned,
373 referer varchar(255),
375 request_duration float
377 $this->dbi->genericSqlQuery("CREATE INDEX log_time ON $log_tbl (time_stamp)");
378 $this->dbi->genericSqlQuery("CREATE INDEX log_host ON $log_tbl (remote_host)");
379 echo " ",_("CREATED");
386 * Update from ~1.3.4 to current.
387 * tables: Only session, user, pref and member
388 * jeffs-hacks database api (around 1.3.2) later:
389 * people should export/import their pages if using that old versions.
391 function CheckDatabaseUpdate() {
392 global $DBAuthParams, $DBParams;
394 echo "<h3>",sprintf(_("check for necessary %s updates"),
396 " - ", DATABASE_TYPE,"</h3>\n";
397 $dbadmin = $this->request->getArg('dbadmin');
400 if (isset($dbadmin['cancel'])) {
401 echo _("CANCEL")," <br />\n";
405 echo "db version: we want ", $this->current_db_version, "\n<br />";
406 echo "db version: we have ", $this->db_version, "\n<br />";
407 if ($this->db_version >= $this->current_db_version) {
408 echo _("OK"), "<br />\n";
412 $backend_type = $this->dbi->_backend->backendType();
414 echo "<h4>",_("Backend type: "),$backend_type,"</h4>\n";
415 $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
416 $tables = $this->dbi->_backend->listOfTables();
417 foreach (explode(':','session:pref:member') as $table) {
418 echo sprintf(_("check for table %s"), $table)," ...";
419 if (!in_array($prefix.$table, $tables)) {
420 $this->installTable($table, $backend_type);
422 echo _("OK")," <br />\n";
427 if ($this->phpwiki_version >= 1030.12200612 and $this->db_version < 1030.13) {
428 if ($this->isSQL and preg_match("/(pgsql|postgres)/", $backend_type)) {
429 trigger_error("You need to upgrade to schema/psql-initialize.sql manually!",
431 // $this->_upgrade_psql_tsearch2();
433 $this->_upgrade_relation_links();
436 if (ACCESS_LOG_SQL and $this->isSQL) {
437 $table = "accesslog";
438 echo sprintf(_("check for table %s"), $table)," ...";
439 if (!in_array($prefix.$table, $tables)) {
440 $this->installTable($table, $backend_type);
442 echo _("OK")," <br />\n";
445 if ($this->isSQL and (class_exists("RatingsUserFactory") or $this->dbi->isWikiPage(_("RateIt")))) {
447 echo sprintf(_("check for table %s"), $table)," ...";
448 if (!in_array($prefix.$table, $tables)) {
449 $this->installTable($table, $backend_type);
451 echo _("OK")," <br />\n";
454 $backend = &$this->dbi->_backend->_dbh;
456 extract($this->dbi->_backend->_table_names);
458 // 1.3.8 added session.sess_ip
459 if ($this->isSQL and $this->phpwiki_version >= 1030.08 and USE_DB_SESSION
460 and isset($this->request->_dbsession))
462 echo _("check for new session.sess_ip column")," ... ";
463 $database = $this->dbi->_backend->database();
464 assert(!empty($DBParams['db_session_table']));
465 $session_tbl = $prefix . $DBParams['db_session_table'];
466 $sess_fields = $this->dbi->_backend->listOfFields($database, $session_tbl);
469 } elseif (!strstr(strtolower(join(':', $sess_fields)), "sess_ip")) {
470 // TODO: postgres test (should be able to add columns at the end, but not in between)
471 echo "<b>",_("ADDING"),"</b>"," ... ";
472 $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl ADD sess_ip CHAR(15) NOT NULL");
473 $this->dbi->genericSqlQuery("CREATE INDEX sess_date ON $session_tbl (sess_date)");
478 if (substr($backend_type,0,5) == 'mysql') {
479 // upgrade to 4.1.8 destroyed my session table:
480 // sess_id => varchar(10), sess_data => varchar(5). For others obviously also.
481 echo _("check for mysql session.sess_id sanity")," ... ";
482 $result = $this->dbi->genericSqlQuery("DESCRIBE $session_tbl");
483 if (DATABASE_TYPE == 'SQL') {
484 $iter = new WikiDB_backend_PearDB_generic_iter($backend, $result);
485 } elseif (DATABASE_TYPE == 'ADODB') {
486 $iter = new WikiDB_backend_ADODB_generic_iter($backend, $result,
487 array("Field", "Type", "Null", "Key", "Default", "Extra"));
488 } elseif (DATABASE_TYPE == 'PDO') {
489 $iter = new WikiDB_backend_PDO_generic_iter($backend, $result);
491 while ($col = $iter->next()) {
492 if ($col["Field"] == 'sess_id' and !strstr(strtolower($col["Type"]), 'char(32)')) {
493 $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl CHANGE sess_id"
494 ." sess_id CHAR(32) NOT NULL");
495 echo "sess_id ", $col["Type"], " ", _("fixed"), " => CHAR(32) ";
497 if ($col["Field"] == 'sess_ip' and !strstr(strtolower($col["Type"]), 'char(15)')) {
498 $this->dbi->genericSqlQuery("ALTER TABLE $session_tbl CHANGE sess_ip"
499 ." sess_ip CHAR(15) NOT NULL");
500 echo "sess_ip ", $col["Type"], " ", _("fixed"), " => CHAR(15) ";
503 echo _("OK"), "<br />\n";
508 ALTER TABLE link ADD relation INT DEFAULT 0;
509 CREATE INDEX linkrelation ON link (relation);
512 // mysql >= 4.0.4 requires LOCK TABLE privileges
513 if (substr($backend_type,0,5) == 'mysql') {
514 echo _("check for mysql LOCK TABLE privilege")," ...";
515 $mysql_version = $this->dbi->_backend->_serverinfo['version'];
516 if ($mysql_version > 400.40) {
517 if (!empty($this->dbi->_backend->_parsedDSN))
518 $parseDSN = $this->dbi->_backend->_parsedDSN;
519 elseif (function_exists('parseDSN')) // ADODB or PDO
520 $parseDSN = parseDSN($DBParams['dsn']);
522 $parseDSN = DB::parseDSN($DBParams['dsn']);
523 $username = $this->dbi->_backend->qstr($parseDSN['username']);
525 $query = "SELECT lock_tables_priv FROM mysql.db WHERE user='$username'";
526 //mysql_select_db("mysql", $this->dbi->_backend->connection());
527 $db_fields = $this->dbi->_backend->listOfFields("mysql", "db");
528 if (!strstr(strtolower(join(':', $db_fields)), "lock_tables_priv")) {
529 echo join(':', $db_fields);
530 die("lock_tables_priv missing. The DB Admin must run mysql_fix_privilege_tables");
532 $row = $this->dbi->_backend->getRow($query);
533 if (isset($row[0]) and $row[0] == 'N') {
534 $this->dbi->genericSqlQuery("UPDATE mysql.db SET lock_tables_priv='Y'"
535 ." WHERE mysql.user='$username'");
536 $this->dbi->genericSqlQuery("FLUSH PRIVILEGES");
537 echo "mysql.db user='$username'", _("fixed"), "<br />\n";
540 $query = "SELECT lock_tables_priv FROM mysql.user WHERE user='$username'";
541 $row = $this->dbi->_backend->getRow($query);
542 if ($row and $row[0] == 'N') {
543 $this->dbi->genericSqlQuery("UPDATE mysql.user SET lock_tables_priv='Y'"
544 ." WHERE mysql.user='$username'");
545 $this->dbi->genericSqlQuery("FLUSH PRIVILEGES");
546 echo "mysql.user user='$username'", _("fixed"), "<br />\n";
548 echo " <b><font color=\"red\">", _("FAILED"), "</font></b>: ",
549 "Neither mysql.db nor mysql.user has a user='$username'"
550 ." or the lock_tables_priv field",
553 echo _("OK"), "<br />\n";
556 echo _("OK"), "<br />\n";
558 //mysql_select_db($this->dbi->_backend->database(), $this->dbi->_backend->connection());
560 echo sprintf(_("version <em>%s</em> not affected"), $mysql_version),"<br />\n";
564 // 1.3.10 mysql requires page.id auto_increment
565 // mysql, mysqli or mysqlt
566 if ($this->phpwiki_version >= 1030.099 and substr($backend_type,0,5) == 'mysql'
567 and DATABASE_TYPE != 'PDO')
569 echo _("check for mysql page.id auto_increment flag")," ...";
570 assert(!empty($page_tbl));
571 $database = $this->dbi->_backend->database();
572 $fields = mysql_list_fields($database, $page_tbl, $this->dbi->_backend->connection());
573 $columns = mysql_num_fields($fields);
574 for ($i = 0; $i < $columns; $i++) {
575 if (mysql_field_name($fields, $i) == 'id') {
576 $flags = mysql_field_flags($fields, $i);
577 //DONE: something was wrong with ADODB here.
578 if (!strstr(strtolower($flags), "auto_increment")) {
579 echo "<b>",_("ADDING"),"</b>"," ... ";
580 // MODIFY col_def valid since mysql 3.22.16,
581 // older mysql's need CHANGE old_col col_def
582 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE id"
583 ." id INT NOT NULL AUTO_INCREMENT");
584 $fields = mysql_list_fields($database, $page_tbl);
585 if (!strstr(strtolower(mysql_field_flags($fields, $i)), "auto_increment"))
586 echo " <b><font color=\"red\">", _("FAILED"), "</font></b><br />\n";
588 echo _("OK"), "<br />\n";
590 echo _("OK"), "<br />\n";
595 mysql_free_result($fields);
598 // Check for mysql 4.1.x/5.0.0a binary search problem.
599 // http://bugs.mysql.com/bug.php?id=4398
600 // "select * from page where LOWER(pagename) like '%search%'" does not apply LOWER!
601 // Confirmed for 4.1.0alpha,4.1.3-beta,5.0.0a; not yet tested for 4.1.2alpha,
602 // On windows only, though utf8 would be useful elsewhere also.
603 // Illegal mix of collations (latin1_bin,IMPLICIT) and
604 // (utf8_general_ci, COERCIBLE) for operation '='])
605 if (isWindows() and substr($backend_type,0,5) == 'mysql') {
606 echo _("check for mysql 4.1.x/5.0.0 binary search on windows problem")," ...";
607 $mysql_version = $this->dbi->_backend->_serverinfo['version'];
608 if ($mysql_version < 401.0) {
609 echo sprintf(_("version <em>%s</em>"), $mysql_version)," ",
610 _("not affected"),"<br />\n";
611 } elseif ($mysql_version >= 401.6) { // FIXME: since which version?
612 $row = $this->dbi->_backend->getRow("SHOW CREATE TABLE $page_tbl");
613 $result = join(" ", $row);
614 if (strstr(strtolower($result), "character set")
615 and strstr(strtolower($result), "collate"))
617 echo _("OK"), "<br />\n";
619 //SET CHARACTER SET latin1
621 if ($charset == 'iso-8859-1') $charset = 'latin1';
622 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE pagename "
623 ."pagename VARCHAR(100) "
624 ."CHARACTER SET '$charset' COLLATE '$charset"."_bin' NOT NULL");
625 echo sprintf(_("version <em>%s</em>"), $mysql_version),
626 " <b>",_("FIXED"),"</b>",
629 } elseif (DATABASE_TYPE != 'PDO') {
630 // check if already fixed
631 extract($this->dbi->_backend->_table_names);
632 assert(!empty($page_tbl));
633 $database = $this->dbi->_backend->database();
634 $fields = mysql_list_fields($database, $page_tbl, $this->dbi->_backend->connection());
635 $columns = mysql_num_fields($fields);
636 for ($i = 0; $i < $columns; $i++) {
637 if (mysql_field_name($fields, $i) == 'pagename') {
638 $flags = mysql_field_flags($fields, $i);
639 // I think it was fixed with 4.1.6, but I tested it only with 4.1.8
640 if ($mysql_version > 401.0 and $mysql_version < 401.6) {
641 // remove the binary flag
642 if (strstr(strtolower($flags), "binary")) {
643 // FIXME: on duplicate pagenames this will fail!
644 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl CHANGE pagename"
645 ." pagename VARCHAR(100) NOT NULL");
646 echo sprintf(_("version <em>%s</em>"), $mysql_version),
647 "<b>",_("FIXED"),"</b>"
656 if ($this->isSQL and ACCESS_LOG_SQL & 2) {
657 echo _("check for ACCESS_LOG_SQL passwords in POST requests")," ...";
658 // Don't display passwords in POST requests (up to 2005-02-04 12:03:20)
659 $res = $this->dbi->genericSqlIter("SELECT time_stamp, remote_host, " .
660 "request_args FROM ${prefix}accesslog WHERE request_args LIKE " .
661 "'%s:6:\"passwd\"%' AND request_args NOT LIKE '%s:6:\"passwd\";" .
662 "s:15:\"<not displayed>\"%'");
664 while ($row = $res->next()) {
665 $args = preg_replace("/(s:6:\"passwd\";s:15:\").*(\")/",
666 "$1<not displayed>$2", $row["request_args"]);
667 $ts = $row["time_stamp"];
668 $rh = $row["remote_host"];
669 $this->dbi->genericSqlQuery("UPDATE ${prefix}accesslog SET " .
670 "request_args='$args' WHERE time_stamp=$ts AND " .
671 "remote_host='$rh'");
675 echo "<b>",_("FIXED"),"</b>", "<br />\n";
677 echo _("OK"),"<br />\n";
679 if ($this->phpwiki_version >= 1030.13) {
680 echo _("check for ACCESS_LOG_SQL remote_host varchar(50)")," ...";
681 $database = $this->dbi->_backend->database();
682 $accesslog_tbl = $prefix . 'accesslog';
683 $fields = $this->dbi->_backend->listOfFields($database, $accesslog_tbl);
686 } elseif (strstr(strtolower(join(':', $sess_fields)), "remote_host")) {
687 // TODO: how to check size, already done?
688 echo "<b>",_("FIXING"),"remote_host</b>"," ... ";
689 $this->dbi->genericSqlQuery("ALTER TABLE $accesslog_tbl CHANGE remote_host VARCHAR(100)");
696 $this->_upgrade_cached_html();
698 if ($this->db_version < $this->current_db_version) {
699 $this->dbi->set_db_version($this->current_db_version);
700 $this->db_version = $this->dbi->get_db_version();
701 echo "db version: upgrade to ", $this->db_version," ";
702 echo _("OK"), "<br />\n";
710 * Filter SQL missing permissions errors.
712 * A wrong DBADMIN user will not be able to connect
713 * @see _is_false_error, ErrorManager
716 function _dbpermission_filter($err) {
717 if ( $err->isWarning() ) {
718 global $ErrorManager;
719 $this->error_caught = 1;
720 $ErrorManager->_postponed_errors[] = $err;
726 function _try_dbadmin_user ($user, $passwd) {
727 global $DBParams, $DBAuthParams;
728 $AdminParams = $DBParams;
729 if (DATABASE_TYPE == 'SQL')
730 $dsn = DB::parseDSN($AdminParams['dsn']);
732 $dsn = parseDSN($AdminParams['dsn']);
734 $AdminParams['dsn'] = sprintf("%s://%s:%s@%s/%s",
740 $AdminParams['_tryroot_from_upgrade'] = 1;
741 // add error handler to warn about missing permissions for DBADMIN_USER
742 global $ErrorManager;
743 $ErrorManager->pushErrorHandler(new WikiMethodCb($this, '_dbpermission_filter'));
744 $this->error_caught = 0;
745 $this->dbi = WikiDB::open($AdminParams);
746 if (!$this->error_caught) return true;
747 // FAILED: redo our connection with the wikiuser
748 $this->dbi = WikiDB::open($DBParams);
749 $ErrorManager->flushPostponedErrors();
750 $ErrorManager->popErrorHandler();
754 function _db_init () {
755 if (!$this->isSQL) return;
757 /* SQLite never needs admin params */
758 $backend_type = $this->dbi->_backend->backendType();
759 if (substr($backend_type,0,6)=="sqlite") {
762 $dbadmin_user = 'root';
763 if ($dbadmin = $this->request->getArg('dbadmin')) {
764 $dbadmin_user = $dbadmin['user'];
765 if (isset($dbadmin['cancel'])) {
767 } elseif (!empty($dbadmin_user)) {
768 if ($this->_try_dbadmin_user($dbadmin['user'], $dbadmin['passwd']))
771 } elseif (DBADMIN_USER) {
772 if ($this->_try_dbadmin_user(DBADMIN_USER, DBADMIN_PASSWD))
775 // Check if the privileges are enough. Need CREATE and ALTER perms.
776 // And on windows: SELECT FROM mysql, possibly: UPDATE mysql.
777 $form = HTML::form(array("method" => "post",
778 "action" => $this->request->getPostURL(),
779 "accept-charset"=>$GLOBALS['charset']),
780 HTML::p(_("Upgrade requires database privileges to CREATE and ALTER the phpwiki database."),
782 _("And on windows at least the privilege to SELECT FROM mysql, and possibly UPDATE mysql")),
783 HiddenInputs(array('action' => 'upgrade',
784 'overwrite' => $this->request->getArg('overwrite'))),
785 HTML::table(array("cellspacing"=>4),
786 HTML::tr(HTML::td(array('align'=>'right'),
787 _("DB admin user:")),
788 HTML::td(HTML::input(array('name'=>"dbadmin[user]",
791 'value'=>$dbadmin_user)))),
792 HTML::tr(HTML::td(array('align'=>'right'),
793 _("DB admin password:")),
794 HTML::td(HTML::input(array('name'=>"dbadmin[passwd]",
797 'maxlength'=>256)))),
798 HTML::tr(HTML::td(array('align'=>'center', 'colspan' => 2),
799 Button("submit:", _("Submit"), 'wikiaction'),
801 Button("submit:dbadmin[cancel]", _("Cancel"),
804 echo "</div><!-- content -->\n";
805 echo asXML(Template("bottom"));
806 echo "</body></html>\n";
807 $this->request->finish();
812 * if page.cached_html does not exists:
813 * put _cached_html from pagedata into a new seperate blob,
814 * not into the huge serialized string.
816 * It is only rarelely needed: for current page only, if-not-modified,
817 * but was extracetd for every simple page iteration.
819 function _upgrade_cached_html ( $verbose=true ) {
821 if (!$this->isSQL) return;
823 if ($this->phpwiki_version >= 1030.10) {
825 echo _("check for extra page.cached_html column")," ... ";
826 $database = $this->dbi->_backend->database();
827 extract($this->dbi->_backend->_table_names);
828 $fields = $this->dbi->_backend->listOfFields($database, $page_tbl);
830 echo _("SKIP"), "<br />\n";
833 if (!strstr(strtolower(join(':', $fields)), "cached_html")) {
835 echo "<b>",_("ADDING"),"</b>"," ... ";
836 $backend_type = $this->dbi->_backend->backendType();
837 if (substr($backend_type,0,5) == 'mysql')
838 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl ADD cached_html MEDIUMBLOB");
840 $this->dbi->genericSqlQuery("ALTER TABLE $page_tbl ADD cached_html BLOB");
842 echo "<b>",_("CONVERTING"),"</b>"," ... ";
843 $count = _convert_cached_html();
845 echo $count, " ", _("OK"), "<br />\n";
848 echo _("OK"), "<br />\n";
855 * move _cached_html for all pages from pagedata into a new seperate blob.
856 * decoupled from action=upgrade, so that it can be used by a WikiAdminUtils button also.
858 function _convert_cached_html () {
860 if (!$this->isSQL) return;
861 //if (!in_array(DATABASE_TYPE, array('SQL','ADODB'))) return;
863 $pages = $this->dbi->getAllPages();
864 $cache =& $this->dbi->_cache;
866 extract($this->dbi->_backend->_table_names);
867 while ($page = $pages->next()) {
868 $pagename = $page->getName();
869 $data = $this->dbi->_backend->get_pagedata($pagename);
870 if (!empty($data['_cached_html'])) {
871 $cached_html = $data['_cached_html'];
872 $data['_cached_html'] = '';
873 $cache->update_pagedata($pagename, $data);
874 // store as blob, not serialized
875 $this->dbi->genericSqlQuery("UPDATE $page_tbl SET cached_html=? WHERE pagename=?",
876 array($cached_html, $pagename));
884 * upgrade to 1.3.13 link structure.
886 function _upgrade_relation_links ( $verbose=true ) {
887 if ($this->phpwiki_version >= 1030.12200610 and $this->isSQL) {
888 echo _("check for relation field in link table")," ...";
889 $database = $this->dbi->_backend->database();
890 $link_tbl = $prefix . 'link';
891 $fields = $this->dbi->_backend->listOfFields($database, $link_tbl);
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);");
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) ... ";
909 $this->dbi->_backend->_linkdb->rebuild();
913 $this->dbi->_backend->rebuild();
915 echo _("OK"), "<br />\n";
919 function CheckPluginUpdate() {
922 echo "<h3>",sprintf(_("check for necessary %s updates"),
923 _("plugin argument")),"</h3>\n";
925 $this->_configUpdates = array();
926 $this->_configUpdates[] = new UpgradePluginEntry
927 ($this, array('key' => 'plugin_randompage_numpages',
928 'fixed_with' => 1012.0,
929 //'header' => _("change RandomPage pages => numpages"),
930 //'notice' =>_("found RandomPage plugin"),
931 'check_args' => array("plugin RandomPage pages",
932 "/(<\?\s*plugin\s+ RandomPage\s+)pages/",
934 $this->_configUpdates[] = new UpgradePluginEntry
935 ($this, array('key' => 'plugin_createtoc_position',
936 'fixed_with' => 1013.0,
937 //'header' => _("change CreateToc align => position"),
938 //'notice' =>_("found CreateToc plugin"),
939 'check_args' => array("plugin CreateToc align",
940 "/(<\?\s*plugin\s+ CreateToc[^\?]+)align/",
943 if (empty($this->_configUpdates)) return;
944 foreach ($this->_configUpdates as $update) {
945 $pages = $this->dbi->fullSearch($this->check_args[0]);
946 while ($page = $allpages->next()) {
947 $current = $page->getCurrentRevision();
948 $pagetext = $current->getPackedContent();
949 $update->check($this->check_args[1], $this->check_args[2], $pagetext, $page, $current);
959 * preg_replace over local file.
960 * Only line-orientated matches possible.
962 function fixLocalFile($match, $replace, $filename) {
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));
969 if (is_writable($filename)) {
970 $in = fopen($filename, "rb");
971 $out = fopen($tmp = tempnam(getUploadFilePath(),"cfg"), "wb");
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);
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);
985 $s = $new . $linesep;
994 $reason = sprintf(_("%s not found in %s"), $match, $filename);
996 return array($found, $reason);
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));
1005 return array(false, sprintf(_("file %s is not writable"), $filename));
1009 function CheckConfigUpdate () {
1010 echo "<h3>",sprintf(_("check for necessary %s updates"),
1011 "config.ini"),"</h3>\n";
1012 $entry = new UpgradeConfigEntry
1013 ($this, array('key' => 'cache_control_none',
1014 'fixed_with' => 1012.0,
1015 'header' => sprintf(_("check for %s"),"CACHE_CONTROL = NONE"),
1016 'applicable_args' => 'CACHE_CONTROL',
1017 'notice' => _("CACHE_CONTROL is set to 'NONE', and must be changed to 'NO_CACHE'"),
1018 'check_args' => array("/^\s*CACHE_CONTROL\s*=\s*NONE/", "CACHE_CONTROL = NO_CACHE")));
1019 $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined_and_empty'));
1020 $this->_configUpdates[] = $entry;
1022 $entry = new UpgradeConfigEntry
1023 ($this, array('key' => 'group_method_none',
1024 'fixed_with' => 1012.0,
1025 'header' => sprintf(_("check for %s"), "GROUP_METHOD = NONE"),
1026 'applicable_args' => 'GROUP_METHOD',
1027 'notice' =>_("GROUP_METHOD is set to NONE, and must be changed to \"NONE\""),
1028 'check_args' => array("/^\s*GROUP_METHOD\s*=\s*NONE/", "GROUP_METHOD = \"NONE\"")));
1029 $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined_and_empty'));
1030 $this->_configUpdates[] = $entry;
1032 $entry = new UpgradeConfigEntry
1033 ($this, array('key' => 'blog_empty_default_prefix',
1034 'fixed_with' => 1013.0,
1035 'header' => sprintf(_("check for %s"), "BLOG_EMPTY_DEFAULT_PREFIX"),
1036 'applicable_args' => 'BLOG_EMPTY_DEFAULT_PREFIX',
1037 'notice' =>_("fix BLOG_EMPTY_DEFAULT_PREFIX into BLOG_DEFAULT_EMPTY_PREFIX"),
1038 'check_args' => array("/BLOG_EMPTY_DEFAULT_PREFIX\s*=/","BLOG_DEFAULT_EMPTY_PREFIX =")));
1039 $entry->setApplicableCb(new WikiMethodCb($entry, '_applicable_defined'));
1040 $this->_configUpdates[] = $entry;
1042 // TODO: find extra file updates
1043 if (empty($this->_configUpdates)) return;
1044 foreach ($this->_configUpdates as $update) {
1054 * Add an upgrade item to be checked.
1056 * @param $parent object The parent Upgrade class to inherit the version properties
1057 * @param $key string A short unique key to store success in the WikiDB
1058 * @param $fixed_with double @see phpwiki_version() number
1059 * @param $header string Optional header to be printed always even if not applicable
1060 * @param $applicable WikiCallback Optional callback boolean applicable()
1061 * @param $notice string Description of the check
1062 * @param $method WikiCallback Optional callback array method(array)
1063 * //param All other args are passed to $method
1065 function UpgradeEntry(&$parent, $params) {
1066 $this->parent =& $parent; // get the properties db_version
1067 foreach (array('key' => 'required',
1068 // the wikidb stores the version when we actually fixed that.
1069 'fixed_with' => 'required',
1070 'header' => '', // always printed
1071 'applicable_cb' => null, // method to check if applicable
1072 'applicable_args' => array(), // might be the config name
1074 'check_cb' => null, // method to apply
1075 'check_args' => array())
1078 if (!isset($params[$k])) { // default
1079 if ($v == 'required') trigger_error("Required arg $k missing", E_USER_ERROR);
1080 else $this->{$k} = $v;
1082 $this->{$k} = $params[$k];
1085 if (!is_array($this->applicable_args)) // single arg convenience shortcut
1086 $this->applicable_args = array($this->applicable_args);
1087 if (!is_array($this->check_args)) // single arg convenience shortcut
1088 $this->check_args = array($this->check_args);
1089 if ($this->notice === '' and count($this->applicable_args) > 0)
1090 $this->notice = 'Check for '.join(', ', $this->applicable_args);
1091 $this->_db_key = "_upgrade";
1092 $this->upgrade = $this->parent->dbi->get($this->_db_key);
1095 function setApplicableCb($object) {
1096 $this->applicable_cb =& $object;
1098 function _check_if_already_fixed() {
1100 if (!isset($this->upgrade['name'])) return false;
1101 // override with force?
1102 if ($this->parent->request->getArg('force')) return false;
1103 // already fixed and with an ok version
1104 if ($this->upgrade['name'] >= $this->fixed_with) return $this->upgrade['name'];
1105 // already fixed but with an older version. do it again.
1109 // store in db no to fix again
1110 $this->upgrade['name'] = $this->parent->phpwiki_version;
1111 $this->parent->dbi->set($this->_db_key, $this->upgrade);
1112 echo "<b>",_("FIXED"),"</b>";
1113 if (isset($this->reason))
1114 echo ": ", $this->reason;
1120 echo " <b><font color=\"red\">", _("FAILED"), "</font></b>";
1121 if (isset($this->reason))
1122 echo ": ", $this->reason;
1127 function skip() { // not applicable
1128 if (isset($this->silent_skip)) return true;
1129 echo _(" skipped"),".<br />\n";
1133 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 {
1165 function _applicable_defined() {
1166 return (boolean)defined($this->applicable_args[0]);
1168 function _applicable_defined_and_empty() {
1169 $const = $this->applicable_args[0];
1170 return (boolean)(defined($const) and !constant($const));
1172 function default_method ($args) {
1174 $replace = $args[1];
1175 return $this->parent->fixLocalFile($match, $replace, "config/config.ini");
1177 } // class UpdateConfigEntry
1179 /* This is different */
1180 class UpgradePluginEntry extends UpgradeEntry {
1183 * check all pages for a plugin match
1185 var $silent_skip = 1;
1187 function default_method (&$args) {
1189 $replace = $args[1];
1190 $pagetext =& $args[2];
1192 $current =& $args[4];
1193 if (preg_match($match, $pagetext)) {
1194 echo $page->getName()," ",$this->notice," ... ";
1195 if ($newtext = preg_replace($match, $replace, $pagetext)) {
1196 $meta = $current->_data;
1197 $meta['summary'] = "upgrade: ".$this->header;
1198 $page->save($newtext, $current->getVersion() + 1, $meta);
1205 } // class UpdatePluginEntry
1208 * fix custom themes which are not in our distribution
1209 * this should be optional
1211 class UpgradeThemeEntry extends UpgradeEntry {
1213 function default_method (&$args) {
1215 $replace = $args[1];
1216 $template = $args[2];
1219 function fixThemeTemplate($match, $new, $template) {
1220 // for all custom themes
1221 $ourthemes = explode(":","blog:Crao:default:Hawaiian:MacOSX:MonoBook:Portland:shamino_com:SpaceWiki:wikilens:Wordpress");
1222 $themedir = NormalizeLocalFileName("themes");
1223 $dh = opendir($themedir);
1224 while ($r = readdir($dh)) {
1225 if (filetype($r) == 'dir' and $r[0] != '.' and !is_array($r, $ourthemes))
1226 $customthemes[] = $r;
1230 foreach ($customthemes as $customtheme) {
1231 $template = FindFile("themes/$customtheme/templates/$template");
1232 $do = $this->parent->fixLocalFile($match, $new, template);
1235 $errors .= $do[1]." ";
1239 return array($success, $errors);
1246 * Upgrade: Base class for multipage worksteps
1247 * identify, validate, display options, next step
1252 // TODO: At which step are we?
1253 // validate and do it again or go on with next step.
1255 /** entry function from lib/main.php
1257 function DoUpgrade(&$request) {
1259 if (!$request->_user->isAdmin()) {
1260 $request->_notAuthorized(WIKIAUTH_ADMIN);
1262 HTML::div(array('class' => 'disabled-plugin'),
1263 fmt("Upgrade disabled: user != isAdmin")));
1266 // TODO: StartLoadDump should turn on implicit_flush.
1267 @ini_set("implicit_flush", true);
1268 StartLoadDump($request, _("Upgrading this PhpWiki"));
1269 $upgrade = new Upgrade($request);
1270 //if (!$request->getArg('noindex'))
1271 // CheckOldIndexUpdate($request); // index.php => config.ini to upgrade from < 1.3.10
1272 if (!$request->getArg('nodb'))
1273 $upgrade->CheckDatabaseUpdate($request); // first check cached_html and friends
1274 if (!$request->getArg('nopgsrc')) {
1275 $upgrade->CheckActionPageUpdate($request);
1276 $upgrade->CheckPgsrcUpdate($request);
1278 if (!$request->getArg('noplugin'))
1279 $upgrade->CheckPluginUpdate($request);
1280 if (!$request->getArg('noconfig'))
1281 $upgrade->CheckConfigUpdate($request);
1282 // This is optional and should be linked. In EndLoadDump or PhpWikiAdministration?
1283 //if ($request->getArg('theme'))
1284 // $upgrade->CheckThemeUpdate($request);
1285 EndLoadDump($request);
1292 // c-basic-offset: 4
1293 // c-hanging-comment-ender-p: nil
1294 // indent-tabs-mode: nil