]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/upgrade.php
updated docs: submitted new mysql bugreport (#1491 did not fix it)
[SourceForge/phpwiki.git] / lib / upgrade.php
1 <?php //-*-php-*-
2 rcs_id('$Id: upgrade.php,v 1.22 2004-07-03 17:21:28 rurban Exp $');
3
4 /*
5  Copyright 2004 $ThePhpWikiProgrammingTeam
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
20  along with PhpWiki; if not, write to the Free Software
21  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22  */
23
24
25 /**
26  * Upgrade the WikiDB and config settings after installing a new 
27  * PhpWiki upgrade.
28  * Status: experimental, no queries for verification yet, no db update,
29  *         no merge conflict
30  *
31  * Installation on an existing PhpWiki database needs some 
32  * additional worksteps. Each step will require multiple pages.
33  *
34  * This is the plan:
35  *  1. Check for new or changed database schema and update it 
36  *     according to some predefined upgrade tables. (medium)
37  *  2. Check for new or changed (localized) pgsrc/ pages and ask 
38  *     for upgrading these. Check timestamps, upgrade silently or 
39  *     show diffs if existing. Overwrite or merge (easy)
40  *  3. Check for new or changed or deprecated index.php/config.ini settings
41  *     and help in upgrading these. (hard)
42  *  3a Convert old-style index.php into config/config.ini. (easy)
43  *  4. Check for changed plugin invocation arguments. (hard)
44  *  5. Check for changed theme variables. (hard)
45  *  6. Convert the automatic update to a class-based multi-page 
46  *     version. (hard)
47  *
48  * @author: Reini Urban
49  */
50 require_once("lib/loadsave.php");
51 //define('DBADMIN_USER','rurban');
52 //define('DBADMIN_PASSWD','');
53
54 /**
55  * TODO: check for the pgsrc_version number, not the revision
56  */
57 function doPgsrcUpdate(&$request,$pagename,$path,$filename) {
58     $dbi = $request->getDbh(); 
59     $page = $dbi->getPage($pagename);
60     if ($page->exists()) {
61         // check mtime: update automatically if pgsrc is newer
62         $rev = $page->getCurrentRevision();
63         $page_mtime = $rev->get('mtime');
64         $data  = implode("", file($path."/".$filename));
65         if (($parts = ParseMimeifiedPages($data))) {
66             usort($parts, 'SortByPageVersion');
67             reset($parts);
68             $pageinfo = $parts[0];
69             $stat  = stat($path."/".$filename);
70             $new_mtime = @$pageinfo['versiondata']['mtime'];
71             if (!$new_mtime)
72                 $new_mtime = @$pageinfo['versiondata']['lastmodified'];
73             if (!$new_mtime)
74                 $new_mtime = @$pageinfo['pagedata']['date'];
75             if (!$new_mtime)
76                 $new_mtime = $stat[9];
77             if ($new_mtime > $page_mtime) {
78                 echo "$path/$pagename: ",_("newer than the existing page."),
79                     _(" replace "),"($new_mtime &gt; $page_mtime)","<br />\n";
80                 LoadAny($request,$path."/".$filename);
81                 echo "<br />\n";
82             } else {
83                 echo "$path/$pagename: ",_("older than the existing page."),
84                     _(" skipped"),".<br />\n";
85             }
86         } else {
87             echo "$path/$pagename: ",("unknown format."),
88                     _(" skipped"),".<br />\n";
89         }
90     } else {
91         echo sprintf(_("%s does not exist"),$pagename),"<br />\n";
92         LoadAny($request,$path."/".$filename);
93         echo "<br />\n";
94     }
95 }
96
97 /** need the english filename (required precondition: urlencode == urldecode)
98  *  returns the plugin name.
99  */ 
100 function isActionPage($filename) {
101     static $special = array("DebugInfo"         => "_BackendInfo",
102                             "PhpWikiRecentChanges" => "RssFeed",
103                             "ProjectSummary"    => "RssFeed",
104                             "RecentReleases"    => "RssFeed",
105                             );
106     $base = preg_replace("/\..{1,4}$/","",basename($filename));
107     if (isset($special[$base])) return $special[$base];
108     if (FindFile("lib/plugin/".$base.".php",true)) return $base;
109     else return false;
110 }
111
112 function CheckActionPageUpdate(&$request) {
113     echo "<h3>",_("check for necessary ActionPage updates"),"</h3>\n";
114     $dbi = $request->getDbh(); 
115     $path = FindFile('pgsrc');
116     $pgsrc = new fileSet($path);
117     // most actionpages have the same name as the plugin
118     $loc_path = FindLocalizedFile('pgsrc');
119     foreach ($pgsrc->getFiles() as $filename) {
120         if (substr($filename,-1,1) == '~') continue;
121         $pagename = urldecode($filename);
122         if (isActionPage($filename)) {
123             $translation = gettext($pagename);
124             if ($translation == $pagename)
125                 doPgsrcUpdate($request, $pagename, $path, $filename);
126             elseif (FindLocalizedFile('pgsrc/'.urlencode($translation),1))
127                 doPgsrcUpdate($request, $translation, $loc_path, 
128                               urlencode($translation));
129             else
130                 doPgsrcUpdate($request, $pagename, $path, $filename);
131         }
132     }
133 }
134
135 // see loadsave.php for saving new pages.
136 function CheckPgsrcUpdate(&$request) {
137     echo "<h3>",_("check for necessary pgsrc updates"),"</h3>\n";
138     $dbi = $request->getDbh(); 
139     $path = FindLocalizedFile(WIKI_PGSRC);
140     $pgsrc = new fileSet($path);
141     // fixme: verification, ...
142     $isHomePage = false;
143     foreach ($pgsrc->getFiles() as $filename) {
144         if (substr($filename,-1,1) == '~') continue;
145         $pagename = urldecode($filename);
146         // don't ever update the HomePage
147         if (defined(HOME_PAGE))
148             if ($pagename == HOME_PAGE) $isHomePage = true;
149         else
150             if ($pagename == _("HomePage")) $isHomePage = true;
151         if ($pagename == "HomePage") $isHomePage = true;
152         if ($isHomePage) {
153             echo "$path/$pagename: ",_("always skip the HomePage."),
154                 _(" skipped"),".<br />\n";
155             $isHomePage = false;
156             continue;
157         }
158         if (!isActionPage($filename)) {
159             doPgsrcUpdate($request,$pagename,$path,$filename);
160         }
161     }
162     return;
163 }
164
165 /**
166  * TODO: Search table definition in appropriate schema
167  *       and create it.
168  * Supported: mysql and generic SQL, for ADODB and PearDB.
169  */
170 function installTable(&$dbh, $table, $backend_type) {
171     global $DBParams;
172     if (!in_array($DBParams['dbtype'],array('SQL','ADODB'))) return;
173     echo _("MISSING")," ... \n";
174     $backend = &$dbh->_backend->_dbh;
175     /*
176     $schema = findFile("schemas/${backend_type}.sql");
177     if (!$schema) {
178         echo "  ",_("FAILED"),": ",sprintf(_("no schema %s found"),"schemas/${backend_type}.sql")," ... <br />\n";
179         return false;
180     }
181     */
182     extract($dbh->_backend->_table_names);
183     $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
184     switch ($table) {
185     case 'session':
186         assert($session_tbl);
187         if ($backend_type == 'mysql') {
188             $dbh->genericQuery("
189 CREATE TABLE $session_tbl (
190         sess_id         CHAR(32) NOT NULL DEFAULT '',
191         sess_data       BLOB NOT NULL,
192         sess_date       INT UNSIGNED NOT NULL,
193         sess_ip         CHAR(15) NOT NULL,
194         PRIMARY KEY (sess_id),
195         INDEX (sess_date)
196 )");
197         } else {
198             $dbh->genericQuery("
199 CREATE TABLE $session_tbl (
200         sess_id         CHAR(32) NOT NULL DEFAULT '',
201         sess_data       ".($backend_type == 'pgsql'?'TEXT':'BLOB')." NOT NULL,
202         sess_date       INT,
203         sess_ip         CHAR(15) NOT NULL
204 )");
205             $dbh->genericQuery("CREATE UNIQUE INDEX sess_id ON $session_tbl (sess_id)");
206         }
207         $dbh->genericQuery("CREATE INDEX sess_date on session (sess_date)");
208         break;
209     case 'user':
210         $user_tbl = $prefix.'user';
211         if ($backend_type == 'mysql') {
212             $dbh->genericQuery("
213 CREATE TABLE $user_tbl (
214         userid  CHAR(48) BINARY NOT NULL UNIQUE,
215         passwd  CHAR(48) BINARY DEFAULT '',
216         PRIMARY KEY (userid)
217 )");
218         } else {
219             $dbh->genericQuery("
220 CREATE TABLE $user_tbl (
221         userid  CHAR(48) NOT NULL,
222         passwd  CHAR(48) DEFAULT ''
223 )");
224             $dbh->genericQuery("CREATE UNIQUE INDEX userid ON $user_tbl (userid)");
225         }
226         break;
227     case 'pref':
228         $pref_tbl = $prefix.'pref';
229         if ($backend_type == 'mysql') {
230             $dbh->genericQuery("
231 CREATE TABLE $pref_tbl (
232         userid  CHAR(48) BINARY NOT NULL UNIQUE,
233         prefs   TEXT NULL DEFAULT '',
234         PRIMARY KEY (userid)
235 )");
236         } else {
237             $dbh->genericQuery("
238 CREATE TABLE $pref_tbl (
239         userid  CHAR(48) NOT NULL,
240         prefs   TEXT NULL DEFAULT '',
241 )");
242             $dbh->genericQuery("CREATE UNIQUE INDEX userid ON $pref_tbl (userid)");
243         }
244         break;
245     case 'member':
246         $member_tbl = $prefix.'member';
247         if ($backend_type == 'mysql') {
248             $dbh->genericQuery("
249 CREATE TABLE $member_tbl (
250         userid    CHAR(48) BINARY NOT NULL,
251         groupname CHAR(48) BINARY NOT NULL DEFAULT 'users',
252         INDEX (userid),
253         INDEX (groupname)
254 )");
255         } else {
256             $dbh->genericQuery("
257 CREATE TABLE $member_tbl (
258         userid    CHAR(48) NOT NULL,
259         groupname CHAR(48) NOT NULL DEFAULT 'users',
260 )");
261             $dbh->genericQuery("CREATE INDEX userid ON $member_tbl (userid)");
262             $dbh->genericQuery("CREATE INDEX groupname ON $member_tbl (groupname)");
263         }
264         break;
265     case 'rating':
266         $rating_tbl = $prefix.'rating';
267         if ($backend_type == 'mysql') {
268             $dbh->genericQuery("
269 CREATE TABLE $rating_tbl (
270         dimension INT(4) NOT NULL,
271         raterpage INT(11) NOT NULL,
272         rateepage INT(11) NOT NULL,
273         ratingvalue FLOAT NOT NULL,
274         rateeversion INT(11) NOT NULL,
275         tstamp TIMESTAMP(14) NOT NULL,
276         PRIMARY KEY (dimension, raterpage, rateepage)
277 )");
278         } else {
279             $dbh->genericQuery("
280 CREATE TABLE $rating_tbl (
281         dimension INT(4) NOT NULL,
282         raterpage INT(11) NOT NULL,
283         rateepage INT(11) NOT NULL,
284         ratingvalue FLOAT NOT NULL,
285         rateeversion INT(11) NOT NULL,
286         tstamp TIMESTAMP(14) NOT NULL,
287 )");
288             $dbh->genericQuery("CREATE UNIQUE INDEX rating ON $rating_tbl (dimension, raterpage, rateepage)");
289         }
290         break;
291     }
292     echo "  ",_("CREATED"),"<br />\n";
293 }
294
295 /**
296  * currently update only session, user, pref and member
297  * jeffs-hacks database api (around 1.3.2) later
298  *   people should export/import their pages if using that old versions.
299  */
300 function CheckDatabaseUpdate(&$request) {
301     global $DBParams, $DBAuthParams;
302     if (!in_array($DBParams['dbtype'], array('SQL','ADODB'))) return;
303     echo "<h3>",_("check for necessary database updates"),"</h3>\n";
304     if (defined('DBADMIN_USER')) {
305         // if need to connect as the root user, for alter permissions
306         $AdminParams = $DBParams;
307         if ($DBParams['dbtype'] == 'SQL')
308             $dsn = DB::parseDSN($AdminParams['dsn']);
309         else
310             $dsn = parseDSN($AdminParams['dsn']);
311         $AdminParams['dsn'] = sprintf("%s://%s:%s@%s/%s",
312                                       $dsn['phptype'],
313                                       DBADMIN_USER,
314                                       DBADMIN_PASSWD,
315                                       $dsn['hostspec'],
316                                       $dsn['database']);
317         $dbh = WikiDB::open($AdminParams);
318     } else {
319         $dbh = &$request->_dbi;
320     }
321     $tables = $dbh->_backend->listOfTables();
322     $backend_type = $dbh->_backend->backendType();
323     $prefix = isset($DBParams['prefix']) ? $DBParams['prefix'] : '';
324     extract($dbh->_backend->_table_names);
325     foreach (explode(':','session:user:pref:member') as $table) {
326         echo sprintf(_("check for table %s"), $table)," ...";
327         if (!in_array($prefix.$table, $tables)) {
328             installTable($dbh, $table, $backend_type);
329         } else {
330             echo _("OK")," <br />\n";
331         }
332     }
333     $backend = &$dbh->_backend->_dbh;
334     // 1.3.8 added session.sess_ip
335     if (phpwiki_version() >= 1030.08 and USE_DB_SESSION and isset($request->_dbsession)) {
336         echo _("check for new session.sess_ip column")," ... ";
337         $database = $dbh->_backend->database();
338         assert(!empty($DBParams['db_session_table']));
339         $session_tbl = $prefix . $DBParams['db_session_table'];
340         $sess_fields = $dbh->_backend->listOfFields($database, $session_tbl);
341         if (!strstr(strtolower(join(':', $sess_fields)),"sess_ip")) {
342             echo "<b>",_("ADDING"),"</b>"," ... ";              
343             $dbh->genericQuery("ALTER TABLE $session_tbl ADD sess_ip CHAR(15) NOT NULL");
344         } else {
345             echo _("OK");
346         }
347         echo "<br />\n";
348     }
349     // 1.3.10 mysql requires page.id auto_increment
350     // mysql, mysqli or mysqlt
351     if (phpwiki_version() >= 1030.099 and substr($backend_type,0,5) == 'mysql') {
352         echo _("check for page.id auto_increment flag")," ...";
353         assert(!empty($page_tbl));
354         $database = $dbh->_backend->database();
355         $fields = mysql_list_fields($database, $page_tbl, $dbh->_backend->connection());
356         $columns = mysql_num_fields($fields); 
357         for ($i = 0; $i < $columns; $i++) {
358             if (mysql_field_name($fields, $i) == 'id') {
359                 $flags = mysql_field_flags($fields, $i);
360                 //FIXME: something wrong with ADODB here!
361                 if (!strstr(strtolower($flags),"auto_increment")) {
362                     echo "<b>",_("ADDING"),"</b>"," ... ";              
363                     // MODIFY col_def valid since mysql 3.22.16,
364                     // older mysql's need CHANGE old_col col_def
365                     $dbh->genericQuery("ALTER TABLE $page_tbl CHANGE id id INT NOT NULL AUTO_INCREMENT");
366                     $fields = mysql_list_fields($database, $page_tbl);
367                     if (!strstr(strtolower(mysql_field_flags($fields, $i)),"auto_increment"))
368                         echo " <b><font color=\"red\">",_("FAILED"),"</font></b><br />\n";
369                     else     
370                         echo _("OK"),"<br />\n";
371                 } else {
372                     echo _("OK"),"<br />\n";                            
373                 }
374                 break;
375             }
376         }
377         mysql_free_result($fields);
378     }
379     // check for mysql 4.1.x/5.0.0a binary search bug.
380     //   http://bugs.mysql.com/bug.php?id=4398
381     // "select * from page where LOWER(pagename) like '%search%'" does not apply LOWER!
382     // confirmed for 4.1.0alpha,4.1.3-beta,5.0.0a; not yet tested for 4.1.2alpha,
383     if (substr($backend_type,0,5) == 'mysql') {
384         echo _("check for mysql 4.1.x/5.0.0 binary search problem")," ...";
385         $result = mysql_query("SELECT VERSION()",$dbh->_backend->connection());
386         $row = mysql_fetch_row($result);
387         $mysql_version = $row[0];
388         $arr = explode('.',$mysql_version);
389         $version = (string)(($arr[0] * 100) + $arr[1]) . "." . (integer)$arr[2];
390         if ($version >= 401.0) {
391             $dbh->genericQuery("ALTER TABLE $page_tbl CHANGE pagename pagename VARCHAR(100) NOT NULL;");
392             echo sprintf(_("version <em>%s</em> <b>FIXED</b>"), $mysql_version),"<br />\n";     
393         } else {
394             echo sprintf(_("version <em>%s</em> not affected"), $mysql_version),"<br />\n";
395         }
396     }
397     return;
398 }
399
400 function fixConfigIni($match, $new) {
401     $file = FindFile("config/config.ini");
402     $found = false;
403     if (is_writable($file)) {
404         $in = fopen($file,"rb");
405         $out = fopen($tmp = tempnam(FindFile("uploads"),"cfg"),"wb");
406         if (isWindows())
407             $tmp = str_replace("/","\\",$tmp);
408         while ($s = fgets($in)) {
409             if (preg_match($match, $s)) {
410                 $s = $new . (isWindows() ? "\r\n" : "\n");
411                 $found = true;
412             }
413             fputs($out, $s);
414         }
415         fclose($in);
416         fclose($out);
417         if (!$found) {
418             echo " <b><font color=\"red\">",_("FAILED"),"</font></b>: ",
419                 sprintf(_("%s not found"), $match);
420             unlink($out);
421         } else {
422             @unlink("$file.bak");
423             @rename($file,"$file.bak");
424             if (rename($tmp, $file))
425                 echo " <b>",_("FIXED"),"</b>";
426             else {
427                 echo " <b>",_("FAILED"),"</b>: ";
428                 sprintf(_("couldn't move %s to %s"), $tmp, $file);
429                 return false;
430             }
431         }
432         return $found;
433     } else {
434         echo " <b><font color=\"red\">",_("FAILED"),"</font></b>: ",
435             sprintf(_("%s is not writable"), $file);
436         return false;
437     }
438 }
439
440 function CheckConfigUpdate(&$request) {
441     echo "<h3>",_("check for necessary config updates"),"</h3>\n";
442     echo _("check for old CACHE_CONTROL = NONE")," ... ";
443     if (defined('CACHE_CONTROL') and CACHE_CONTROL == '') {
444         echo "<br />&nbsp;&nbsp;",_("CACHE_CONTROL is set to 'NONE', and must be changed to 'NO_CACHE'")," ...";
445         fixConfigIni("/^\s*CACHE_CONTROL\s*=\s*NONE/","CACHE_CONTROL = NO_CACHE");
446     } else {
447         echo _("OK");
448     }
449     echo "<br />\n";
450 }
451
452 /**
453  * TODO:
454  *
455  * Upgrade: Base class for multipage worksteps
456  * identify, validate, display options, next step
457  */
458 class Upgrade {
459 }
460
461 class Upgrade_CheckPgsrc extends Upgrade {
462 }
463
464 class Upgrade_CheckDatabaseUpdate extends Upgrade {
465 }
466
467 // TODO: At which step are we? 
468 // validate and do it again or go on with next step.
469
470 /** entry function from lib/main.php
471  */
472 function DoUpgrade($request) {
473
474     if (!$request->_user->isAdmin()) {
475         $request->_notAuthorized(WIKIAUTH_ADMIN);
476         $request->finish(
477                          HTML::div(array('class' => 'disabled-plugin'),
478                                    fmt("Upgrade disabled: user != isAdmin")));
479         return;
480     }
481
482     StartLoadDump($request, _("Upgrading this PhpWiki"));
483     CheckActionPageUpdate($request);
484     CheckDatabaseUpdate($request);
485     CheckPgsrcUpdate($request);
486     //CheckThemeUpdate($request);
487     CheckConfigUpdate($request);
488     EndLoadDump($request);
489 }
490
491
492 /**
493  $Log: not supported by cvs2svn $
494  Revision 1.21  2004/07/03 16:51:05  rurban
495  optional DBADMIN_USER:DBADMIN_PASSWD for action=upgrade (if no ALTER permission)
496  added atomic mysql REPLACE for PearDB as in ADODB
497  fixed _lock_tables typo links => link
498  fixes unserialize ADODB bug in line 180
499
500  Revision 1.20  2004/07/03 14:48:18  rurban
501  Tested new mysql 4.1.3-beta: binary search bug as fixed.
502  => fixed action=upgrade,
503  => version check in PearDB also (as in ADODB)
504
505  Revision 1.19  2004/06/19 12:19:09  rurban
506  slightly improved docs
507
508  Revision 1.18  2004/06/19 11:47:17  rurban
509  added CheckConfigUpdate: CACHE_CONTROL = NONE => NO_CACHE
510
511  Revision 1.17  2004/06/17 11:31:50  rurban
512  check necessary localized actionpages
513
514  Revision 1.16  2004/06/16 10:38:58  rurban
515  Disallow refernces in calls if the declaration is a reference
516  ("allow_call_time_pass_reference clean").
517    PhpWiki is now allow_call_time_pass_reference = Off clean,
518    but several external libraries may not.
519    In detail these libs look to be affected (not tested):
520    * Pear_DB odbc
521    * adodb oracle
522
523  Revision 1.15  2004/06/07 19:50:40  rurban
524  add owner field to mimified dump
525
526  Revision 1.14  2004/06/07 18:38:18  rurban
527  added mysql 4.1.x search fix
528
529  Revision 1.13  2004/06/04 20:32:53  rurban
530  Several locale related improvements suggested by Pierrick Meignen
531  LDAP fix by John Cole
532  reanable admin check without ENABLE_PAGEPERM in the admin plugins
533
534  Revision 1.12  2004/05/18 13:59:15  rurban
535  rename simpleQuery to genericQuery
536
537  Revision 1.11  2004/05/15 13:06:17  rurban
538  skip the HomePage, at first upgrade the ActionPages, then the database, then the rest
539
540  Revision 1.10  2004/05/15 01:19:41  rurban
541  upgrade prefix fix by Kai Krakow
542
543  Revision 1.9  2004/05/14 11:33:03  rurban
544  version updated to 1.3.11pre
545  upgrade stability fix
546
547  Revision 1.8  2004/05/12 10:49:55  rurban
548  require_once fix for those libs which are loaded before FileFinder and
549    its automatic include_path fix, and where require_once doesn't grok
550    dirname(__FILE__) != './lib'
551  upgrade fix with PearDB
552  navbar.tmpl: remove spaces for IE &nbsp; button alignment
553
554  Revision 1.7  2004/05/06 17:30:38  rurban
555  CategoryGroup: oops, dos2unix eol
556  improved phpwiki_version:
557    pre -= .0001 (1.3.10pre: 1030.099)
558    -p1 += .001 (1.3.9-p1: 1030.091)
559  improved InstallTable for mysql and generic SQL versions and all newer tables so far.
560  abstracted more ADODB/PearDB methods for action=upgrade stuff:
561    backend->backendType(), backend->database(),
562    backend->listOfFields(),
563    backend->listOfTables(),
564
565  Revision 1.6  2004/05/03 15:05:36  rurban
566  + table messages
567
568  Revision 1.4  2004/05/02 21:26:38  rurban
569  limit user session data (HomePageHandle and auth_dbi have to invalidated anyway)
570    because they will not survive db sessions, if too large.
571  extended action=upgrade
572  some WikiTranslation button work
573  revert WIKIAUTH_UNOBTAINABLE (need it for main.php)
574  some temp. session debug statements
575
576  Revision 1.3  2004/04/29 22:33:30  rurban
577  fixed sf.net bug #943366 (Kai Krakow)
578    couldn't load localized url-undecoded pagenames
579
580  Revision 1.2  2004/03/12 15:48:07  rurban
581  fixed explodePageList: wrong sortby argument order in UnfoldSubpages
582  simplified lib/stdlib.php:explodePageList
583
584  */
585
586 // For emacs users
587 // Local Variables:
588 // mode: php
589 // tab-width: 8
590 // c-basic-offset: 4
591 // c-hanging-comment-ender-p: nil
592 // indent-tabs-mode: nil
593 // End:
594 ?>