00001 <?php
00010 function wfSpecialMovepage( $par = null ) {
00011 global $wgUser, $wgOut, $wgRequest, $action;
00012
00013 # Check for database lock
00014 if ( wfReadOnly() ) {
00015 $wgOut->readOnlyPage();
00016 return;
00017 }
00018
00019 $target = isset( $par ) ? $par : $wgRequest->getVal( 'target' );
00020
00021
00022 $oldTitleText = $wgRequest->getVal( 'wpOldTitle', $target );
00023 $newTitleText = $wgRequest->getText( 'wpNewTitle' );
00024
00025 $oldTitle = Title::newFromText( $oldTitleText );
00026 $newTitle = Title::newFromText( $newTitleText );
00027
00028 if( is_null( $oldTitle ) ) {
00029 $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
00030 return;
00031 }
00032 if( !$oldTitle->exists() ) {
00033 $wgOut->showErrorPage( 'nopagetitle', 'nopagetext' );
00034 return;
00035 }
00036
00037 # Check rights
00038 $permErrors = $oldTitle->getUserPermissionsErrors( 'move', $wgUser );
00039 if( !empty( $permErrors ) ) {
00040 $wgOut->showPermissionsErrorPage( $permErrors );
00041 return;
00042 }
00043
00044 $form = new MovePageForm( $oldTitle, $newTitle );
00045
00046 if ( 'submit' == $action && $wgRequest->wasPosted()
00047 && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
00048 $form->doSubmit();
00049 } else {
00050 $form->showForm( '' );
00051 }
00052 }
00053
00058 class MovePageForm {
00059 var $oldTitle, $newTitle; # Objects
00060 var $reason; # Text input
00061 var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect, $moveOverShared; # Checks
00062
00063 private $watch = false;
00064
00065 function __construct( $oldTitle, $newTitle ) {
00066 global $wgRequest, $wgUser;
00067 $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
00068 $this->oldTitle = $oldTitle;
00069 $this->newTitle = $newTitle;
00070 $this->reason = $wgRequest->getText( 'wpReason' );
00071 if ( $wgRequest->wasPosted() ) {
00072 $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false );
00073 $this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', false );
00074 $this->leaveRedirect = $wgRequest->getBool( 'wpLeaveRedirect', false );
00075 } else {
00076 $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true );
00077 $this->fixRedirects = $wgRequest->getBool( 'wpFixRedirects', true );
00078 $this->leaveRedirect = $wgRequest->getBool( 'wpLeaveRedirect', true );
00079 }
00080 $this->moveSubpages = $wgRequest->getBool( 'wpMovesubpages', false );
00081 $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' );
00082 $this->moveOverShared = $wgRequest->getBool( 'wpMoveOverSharedFile', false );
00083 $this->watch = $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn();
00084 }
00085
00092 function showForm( $err ) {
00093 global $wgOut, $wgUser, $wgContLang, $wgFixDoubleRedirects;
00094
00095 $skin = $wgUser->getSkin();
00096
00097 $oldTitleLink = $skin->link( $this->oldTitle );
00098
00099 $wgOut->setPagetitle( wfMsg( 'move-page', $this->oldTitle->getPrefixedText() ) );
00100 $wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) );
00101
00102 $newTitle = $this->newTitle;
00103
00104 if( !$newTitle ) {
00105 # Show the current title as a default
00106 # when the form is first opened.
00107 $newTitle = $this->oldTitle;
00108 }
00109 else {
00110 if( empty($err) ) {
00111 # If a title was supplied, probably from the move log revert
00112 # link, check for validity. We can then show some diagnostic
00113 # information and save a click.
00114 $newerr = $this->oldTitle->isValidMoveOperation( $newTitle );
00115 if( $newerr ) {
00116 $err = $newerr[0];
00117 }
00118 }
00119 }
00120
00121 if ( !empty($err) && $err[0] == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
00122 $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle->getPrefixedText() );
00123 $movepagebtn = wfMsg( 'delete_and_move' );
00124 $submitVar = 'wpDeleteAndMove';
00125 $confirm = "
00126 <tr>
00127 <td></td>
00128 <td class='mw-input'>" .
00129 Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) .
00130 "</td>
00131 </tr>";
00132 $err = '';
00133 } else {
00134 if ($this->oldTitle->getNamespace() == NS_USER && !$this->oldTitle->isSubpage() ) {
00135 $wgOut->wrapWikiMsg( "<div class=\"error mw-moveuserpage-warning\">\n$1\n</div>", 'moveuserpage-warning' );
00136 }
00137 $wgOut->addWikiMsg( 'movepagetext' );
00138 $movepagebtn = wfMsg( 'movepagebtn' );
00139 $submitVar = 'wpMove';
00140 $confirm = false;
00141 }
00142
00143 if ( !empty($err) && $err[0] == 'file-exists-sharedrepo' && $wgUser->isAllowed( 'reupload-shared' ) ) {
00144 $wgOut->addWikiMsg( 'move-over-sharedrepo', $newTitle->getPrefixedText() );
00145 $submitVar = 'wpMoveOverSharedFile';
00146 $err = '';
00147 }
00148
00149 $oldTalk = $this->oldTitle->getTalkPage();
00150 $considerTalk = ( !$this->oldTitle->isTalkPage() && $oldTalk->exists() );
00151
00152 $dbr = wfGetDB( DB_SLAVE );
00153 if ( $wgFixDoubleRedirects ) {
00154 $hasRedirects = $dbr->selectField( 'redirect', '1',
00155 array(
00156 'rd_namespace' => $this->oldTitle->getNamespace(),
00157 'rd_title' => $this->oldTitle->getDBkey(),
00158 ) , __METHOD__ );
00159 } else {
00160 $hasRedirects = false;
00161 }
00162
00163 if ( $considerTalk ) {
00164 $wgOut->addWikiMsg( 'movepagetalktext' );
00165 }
00166
00167 $titleObj = SpecialPage::getTitleFor( 'Movepage' );
00168 $token = htmlspecialchars( $wgUser->editToken() );
00169
00170 if ( !empty($err) ) {
00171 $wgOut->setSubtitle( wfMsg( 'formerror' ) );
00172 if( $err[0] == 'hookaborted' ) {
00173 $hookErr = $err[1];
00174 $errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n";
00175 $wgOut->addHTML( $errMsg );
00176 } else {
00177 $wgOut->wrapWikiMsg( '<p><strong class="error">$1</strong></p>', $err );
00178 }
00179 }
00180
00181 if ( $this->oldTitle->isProtected( 'move' ) ) {
00182 # Is the title semi-protected?
00183 if ( $this->oldTitle->isSemiProtected( 'move' ) ) {
00184 $noticeMsg = 'semiprotectedpagemovewarning';
00185 $classes[] = 'mw-textarea-sprotected';
00186 } else {
00187 # Then it must be protected based on static groups (regular)
00188 $noticeMsg = 'protectedpagemovewarning';
00189 $classes[] = 'mw-textarea-protected';
00190 }
00191 $wgOut->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" );
00192 $wgOut->addWikiMsg( $noticeMsg );
00193 LogEventsList::showLogExtract( $wgOut, 'protect', $this->oldTitle->getPrefixedText(), '', array( 'lim' => 1 ) );
00194 $wgOut->addHTML( "</div>\n" );
00195 }
00196
00197 $wgOut->addHTML(
00198 Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) .
00199 Xml::openElement( 'fieldset' ) .
00200 Xml::element( 'legend', null, wfMsg( 'move-page-legend' ) ) .
00201 Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-movepage-table' ) ) .
00202 "<tr>
00203 <td class='mw-label'>" .
00204 wfMsgHtml( 'movearticle' ) .
00205 "</td>
00206 <td class='mw-input'>
00207 <strong>{$oldTitleLink}</strong>
00208 </td>
00209 </tr>
00210 <tr>
00211 <td class='mw-label'>" .
00212 Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) .
00213 "</td>
00214 <td class='mw-input'>" .
00215 Xml::input( 'wpNewTitle', 40, $wgContLang->recodeForEdit( $newTitle->getPrefixedText() ), array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
00216 Xml::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
00217 "</td>
00218 </tr>
00219 <tr>
00220 <td class='mw-label'>" .
00221 Xml::label( wfMsg( 'movereason' ), 'wpReason' ) .
00222 "</td>
00223 <td class='mw-input'>" .
00224 Html::element( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2,
00225 'maxlength' => 200 ), $this->reason ) .
00226 "</td>
00227 </tr>"
00228 );
00229
00230 if( $considerTalk ) {
00231 $wgOut->addHTML( "
00232 <tr>
00233 <td></td>
00234 <td class='mw-input'>" .
00235 Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) .
00236 "</td>
00237 </tr>"
00238 );
00239 }
00240
00241 if ( $wgUser->isAllowed( 'suppressredirect' ) ) {
00242 $wgOut->addHTML( "
00243 <tr>
00244 <td></td>
00245 <td class='mw-input' >" .
00246 Xml::checkLabel( wfMsg( 'move-leave-redirect' ), 'wpLeaveRedirect',
00247 'wpLeaveRedirect', $this->leaveRedirect ) .
00248 "</td>
00249 </tr>"
00250 );
00251 }
00252
00253 if ( $hasRedirects ) {
00254 $wgOut->addHTML( "
00255 <tr>
00256 <td></td>
00257 <td class='mw-input' >" .
00258 Xml::checkLabel( wfMsg( 'fix-double-redirects' ), 'wpFixRedirects',
00259 'wpFixRedirects', $this->fixRedirects ) .
00260 "</td>
00261 </tr>"
00262 );
00263 }
00264
00265 if( ($this->oldTitle->hasSubpages() || $this->oldTitle->getTalkPage()->hasSubpages())
00266 && $this->oldTitle->userCan( 'move-subpages' ) )
00267 {
00268 global $wgMaximumMovedPages, $wgLang;
00269
00270 $wgOut->addHTML( "
00271 <tr>
00272 <td></td>
00273 <td class=\"mw-input\">" .
00274 Xml::check(
00275 'wpMovesubpages',
00276 # Don't check the box if we only have talk subpages to
00277 # move and we aren't moving the talk page.
00278 $this->moveSubpages && ($this->oldTitle->hasSubpages() || $this->moveTalk),
00279 array( 'id' => 'wpMovesubpages' )
00280 ) . ' ' .
00281 Xml::tags( 'label', array( 'for' => 'wpMovesubpages' ),
00282 wfMsgExt(
00283 ( $this->oldTitle->hasSubpages()
00284 ? 'move-subpages'
00285 : 'move-talk-subpages' ),
00286 array( 'parseinline' ),
00287 $wgLang->formatNum( $wgMaximumMovedPages ),
00288 # $2 to allow use of PLURAL in message.
00289 $wgMaximumMovedPages
00290 )
00291 ) .
00292 "</td>
00293 </tr>"
00294 );
00295 }
00296
00297 $watchChecked = $wgUser->isLoggedIn() && ($this->watch || $wgUser->getBoolOption( 'watchmoves' )
00298 || $this->oldTitle->userIsWatching());
00299 # Don't allow watching if user is not logged in
00300 if( $wgUser->isLoggedIn() ) {
00301 $wgOut->addHTML( "
00302 <tr>
00303 <td></td>
00304 <td class='mw-input'>" .
00305 Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) .
00306 "</td>
00307 </tr>");
00308 }
00309
00310 $wgOut->addHTML( "
00311 {$confirm}
00312 <tr>
00313 <td> </td>
00314 <td class='mw-submit'>" .
00315 Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) .
00316 "</td>
00317 </tr>" .
00318 Xml::closeElement( 'table' ) .
00319 Xml::hidden( 'wpEditToken', $token ) .
00320 Xml::closeElement( 'fieldset' ) .
00321 Xml::closeElement( 'form' ) .
00322 "\n"
00323 );
00324
00325 $this->showLogFragment( $this->oldTitle, $wgOut );
00326 $this->showSubpages( $this->oldTitle, $wgOut );
00327
00328 }
00329
00330 function doSubmit() {
00331 global $wgOut, $wgUser, $wgRequest, $wgMaximumMovedPages, $wgLang;
00332 global $wgFixDoubleRedirects;
00333
00334 if ( $wgUser->pingLimiter( 'move' ) ) {
00335 $wgOut->rateLimited();
00336 return;
00337 }
00338
00339 $ot = $this->oldTitle;
00340 $nt = $this->newTitle;
00341
00342 # Delete to make way if requested
00343 if ( $wgUser->isAllowed( 'delete' ) && $this->deleteAndMove ) {
00344 $article = new Article( $nt );
00345
00346 # Disallow deletions of big articles
00347 $bigHistory = $article->isBigDeletion();
00348 if( $bigHistory && !$nt->userCan( 'bigdelete' ) ) {
00349 global $wgLang, $wgDeleteRevisionsLimit;
00350 $this->showForm( array('delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
00351 return;
00352 }
00353
00354
00355 $file = wfLocalFile( $nt );
00356 if( $file->exists() ) {
00357 $file->delete( wfMsgForContent( 'delete_and_move_reason' ), false );
00358 }
00359
00360
00361 $article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) );
00362 }
00363
00364 # don't allow moving to pages with # in
00365 if ( !$nt || $nt->getFragment() != '' ) {
00366 $this->showForm( 'badtitletext' );
00367 return;
00368 }
00369
00370 # Show a warning if the target file exists on a shared repo
00371 if ( $nt->getNamespace() == NS_FILE
00372 && !( $this->moveOverShared && $wgUser->isAllowed( 'reupload-shared' ) )
00373 && !RepoGroup::singleton()->getLocalRepo()->findFile( $nt )
00374 && wfFindFile( $nt ) )
00375 {
00376 $this->showForm( array('file-exists-sharedrepo') );
00377 return;
00378
00379 }
00380
00381 if ( $wgUser->isAllowed( 'suppressredirect' ) ) {
00382 $createRedirect = $this->leaveRedirect;
00383 } else {
00384 $createRedirect = true;
00385 }
00386
00387 # Do the actual move.
00388 $error = $ot->moveTo( $nt, true, $this->reason, $createRedirect );
00389 if ( $error !== true ) {
00390 # FIXME: show all the errors in a list, not just the first one
00391 $this->showForm( reset( $error ) );
00392 return;
00393 }
00394
00395 if ( $wgFixDoubleRedirects && $this->fixRedirects ) {
00396 DoubleRedirectJob::fixRedirects( 'move', $ot, $nt );
00397 }
00398
00399 wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ot , &$nt ) ) ;
00400
00401 $wgOut->setPagetitle( wfMsg( 'pagemovedsub' ) );
00402
00403 $oldUrl = $ot->getFullUrl( 'redirect=no' );
00404 $newUrl = $nt->getFullUrl();
00405 $oldText = $ot->getPrefixedText();
00406 $newText = $nt->getPrefixedText();
00407 $oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>";
00408 $newLink = "<span class='plainlinks'>[$newUrl $newText]</span>";
00409
00410 $msgName = $createRedirect ? 'movepage-moved-redirect' : 'movepage-moved-noredirect';
00411 $wgOut->addWikiMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
00412 $wgOut->addWikiMsg( $msgName );
00413
00414 # Now we move extra pages we've been asked to move: subpages and talk
00415 # pages. First, if the old page or the new page is a talk page, we
00416 # can't move any talk pages: cancel that.
00417 if( $ot->isTalkPage() || $nt->isTalkPage() ) {
00418 $this->moveTalk = false;
00419 }
00420
00421 if( !$ot->userCan( 'move-subpages' ) ) {
00422 $this->moveSubpages = false;
00423 }
00424
00425 # Next make a list of id's. This might be marginally less efficient
00426 # than a more direct method, but this is not a highly performance-cri-
00427 # tical code path and readable code is more important here.
00428 #
00429 # Note: this query works nicely on MySQL 5, but the optimizer in MySQL
00430 # 4 might get confused. If so, consider rewriting as a UNION.
00431 #
00432 # If the target namespace doesn't allow subpages, moving with subpages
00433 # would mean that you couldn't move them back in one operation, which
00434 # is bad. FIXME: A specific error message should be given in this
00435 # case.
00436
00437
00438 $dbr = wfGetDB( DB_MASTER );
00439 if( $this->moveSubpages && (
00440 MWNamespace::hasSubpages( $nt->getNamespace() ) || (
00441 $this->moveTalk &&
00442 MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
00443 )
00444 ) ) {
00445 $conds = array(
00446 'page_title' . $dbr->buildLike( $ot->getDBkey() . '/', $dbr->anyString() )
00447 .' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
00448 );
00449 $conds['page_namespace'] = array();
00450 if( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
00451 $conds['page_namespace'] []= $ot->getNamespace();
00452 }
00453 if( $this->moveTalk && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() ) ) {
00454 $conds['page_namespace'] []= $ot->getTalkPage()->getNamespace();
00455 }
00456 } elseif( $this->moveTalk ) {
00457 $conds = array(
00458 'page_namespace' => $ot->getTalkPage()->getNamespace(),
00459 'page_title' => $ot->getDBkey()
00460 );
00461 } else {
00462 # Skip the query
00463 $conds = null;
00464 }
00465
00466 $extraPages = array();
00467 if( !is_null( $conds ) ) {
00468 $extraPages = TitleArray::newFromResult(
00469 $dbr->select( 'page',
00470 array( 'page_id', 'page_namespace', 'page_title' ),
00471 $conds,
00472 __METHOD__
00473 )
00474 );
00475 }
00476
00477 $extraOutput = array();
00478 $skin = $wgUser->getSkin();
00479 $count = 1;
00480 foreach( $extraPages as $oldSubpage ) {
00481 if( $ot->equals( $oldSubpage ) ) {
00482 # Already did this one.
00483 continue;
00484 }
00485
00486 $newPageName = preg_replace(
00487 '#^'.preg_quote( $ot->getDBkey(), '#' ).'#',
00488 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
00489 $oldSubpage->getDBkey()
00490 );
00491 if( $oldSubpage->isTalkPage() ) {
00492 $newNs = $nt->getTalkPage()->getNamespace();
00493 } else {
00494 $newNs = $nt->getSubjectPage()->getNamespace();
00495 }
00496 # Bug 14385: we need makeTitleSafe because the new page names may
00497 # be longer than 255 characters.
00498 $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
00499 if( !$newSubpage ) {
00500 $oldLink = $skin->linkKnown( $oldSubpage );
00501 $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink,
00502 htmlspecialchars(Title::makeName( $newNs, $newPageName )));
00503 continue;
00504 }
00505
00506 # This was copy-pasted from Renameuser, bleh.
00507 if ( $newSubpage->exists() && !$oldSubpage->isValidMoveTarget( $newSubpage ) ) {
00508 $link = $skin->linkKnown( $newSubpage );
00509 $extraOutput []= wfMsgHtml( 'movepage-page-exists', $link );
00510 } else {
00511 $success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect );
00512 if( $success === true ) {
00513 if ( $this->fixRedirects ) {
00514 DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage );
00515 }
00516 $oldLink = $skin->linkKnown(
00517 $oldSubpage,
00518 null,
00519 array(),
00520 array( 'redirect' => 'no' )
00521 );
00522 $newLink = $skin->linkKnown( $newSubpage );
00523 $extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink );
00524 ++$count;
00525 if( $count >= $wgMaximumMovedPages ) {
00526 $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $wgLang->formatNum( $wgMaximumMovedPages ) );
00527 break;
00528 }
00529 } else {
00530 $oldLink = $skin->linkKnown( $oldSubpage );
00531 $newLink = $skin->link( $newSubpage );
00532 $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, $newLink );
00533 }
00534 }
00535
00536 }
00537
00538 if( $extraOutput !== array() ) {
00539 $wgOut->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
00540 }
00541
00542 # Deal with watches (we don't watch subpages)
00543 if( $this->watch && $wgUser->isLoggedIn() ) {
00544 $wgUser->addWatch( $ot );
00545 $wgUser->addWatch( $nt );
00546 } else {
00547 $wgUser->removeWatch( $ot );
00548 $wgUser->removeWatch( $nt );
00549 }
00550
00551 # Re-clear the file redirect cache, which may have been polluted by
00552 # parsing in messages above. See CR r56745.
00553 # FIXME: needs a more robust solution inside FileRepo.
00554 if( $ot->getNamespace() == NS_FILE ) {
00555 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $ot );
00556 }
00557 }
00558
00559 function showLogFragment( $title, &$out ) {
00560 $out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'move' ) ) );
00561 LogEventsList::showLogExtract( $out, 'move', $title->getPrefixedText() );
00562 }
00563
00564 function showSubpages( $title, $out ) {
00565 global $wgUser, $wgLang;
00566
00567 if( !MWNamespace::hasSubpages( $title->getNamespace() ) )
00568 return;
00569
00570 $subpages = $title->getSubpages();
00571 $count = $subpages instanceof TitleArray ? $subpages->count() : 0;
00572
00573 $out->wrapWikiMsg( '== $1 ==', array( 'movesubpage', $count ) );
00574
00575 # No subpages.
00576 if ( $count == 0 ) {
00577 $out->addWikiMsg( 'movenosubpage' );
00578 return;
00579 }
00580
00581 $out->addWikiMsg( 'movesubpagetext', $wgLang->formatNum( $count ) );
00582 $skin = $wgUser->getSkin();
00583 $out->addHTML( "<ul>\n" );
00584
00585 foreach( $subpages as $subpage ) {
00586 $link = $skin->link( $subpage );
00587 $out->addHTML( "<li>$link</li>\n" );
00588 }
00589 $out->addHTML( "</ul>\n" );
00590 }
00591 }
00592