War on wfElement() and friends. Call the Xml members directly, rather than using...
[lhc/web/wiklou.git] / includes / Linker.php
index c795874..7c52476 100644 (file)
@@ -124,7 +124,9 @@ class Linker {
                if ( $t->isRedirect() ) {
                        # Page is a redirect
                        $colour = 'mw-redirect';
-               } elseif ( $threshold > 0 && $t->getLength() < $threshold && MWNamespace::isContent( $t->getNamespace() ) ) {
+               } elseif ( $threshold > 0 && 
+                          $t->exists() && $t->getLength() < $threshold &&
+                          MWNamespace::isContent( $t->getNamespace() ) ) {
                        # Page is a stub
                        $colour = 'stub';
                }
@@ -132,139 +134,183 @@ class Linker {
        }
 
        /**
-        * This function returns an HTML link to the given target.  It serves a few purposes:
-        *   1) If $target is a Title, the correct URL to link to will be figured out automatically.
-        *   2) It automatically adds the usual classes for various types of link targets: "new" for red links, "extern" for external links, etc.
+        * This function returns an HTML link to the given target.  It serves a few
+        * purposes:
+        *   1) If $target is a Title, the correct URL to link to will be figured
+        *      out automatically.
+        *   2) It automatically adds the usual classes for various types of link
+        *      targets: "new" for red links, "stub" for short articles, etc.
         *   3) It escapes all attribute values safely so there's no risk of XSS.
-        *   4) It provides a default tooltip if the target is a Title (the page name of the target).
+        *   4) It provides a default tooltip if the target is a Title (the page
+        *      name of the target).
+        * link() replaces the old functions in the makeLink() family.
         *
-        * @param $target        Title  Can currently only be a Title, but this may change.
-        * @param $text          string The HTML contents of the <a> element, i.e., the link text.  This is raw HTML and will not be escaped.  If null, defaults to the page name of the Title or Image, or the text of the URL if $target is a URL.
-        * @param $query         array  The query string to append to the URL you're linking to, in key => value array form.  Useful mainly for Titles and Images.  Query keys and values will be URL-encoded.
-        * @param $customAttribs array  A key => value array of extra HTML attributes, such as title and class.  (href is ignored.)  Classes will be merged with the default classes, while other attributes will replace default attributes.  All passed attribute values will be HTML-escaped.  A false attribute value means to suppress that attribute.
+        * @param $target        Title  Can currently only be a Title, but this may
+        *   change to support Images, literal URLs, etc.
+        * @param $text          string The HTML contents of the <a> element, i.e.,
+        *   the link text.  This is raw HTML and will not be escaped.  If null,
+        *   defaults to the prefixed text of the Title; or if the Title is just a
+        *   fragment, the contents of the fragment.
+        * @param $customAttribs array  A key => value array of extra HTML attri-
+        *   butes, such as title and class.  (href is ignored.)  Classes will be
+        *   merged with the default classes, while other attributes will replace
+        *   default attributes.  All passed attribute values will be HTML-escaped.
+        *   A false attribute value means to suppress that attribute.
+        * @param $query         array  The query string to append to the URL
+        *   you're linking to, in key => value array form.  Query keys and values
+        *   will be URL-encoded.
         * @param $options       mixed  String or array of strings:
         *     'known': Page is known to exist, so don't check if it does.
         *     'broken': Page is known not to exist, so don't check if it does.
-        *     'noclasses': Don't add any classes automatically (includes "new", "stub", "mw-redirect").  Only use the class attribute provided, if any.
+        *     'noclasses': Don't add any classes automatically (includes "new",
+        *       "stub", "mw-redirect", "extiw").  Only use the class attribute
+        *       provided, if any, so you get a simple blue link with no funny i-
+        *       cons.
+        *     'forcearticlepath': Use the article path always, even with a querystring.
+        *       Has compatibility issues on some setups, so avoid wherever possible.
         * @return string HTML <a> attribute
         */
        public function link( $target, $text = null, $customAttribs = array(), $query = array(), $options = array() ) {
                wfProfileIn( __METHOD__ );
-               if( !($target instanceof Title) ) {
-                       throw new MWException( 'Linker::link passed invalid target' );
+               if( !$target instanceof Title ) {
+                       return "<!-- ERROR -->$text";
                }
                $options = (array)$options;
 
-               # Normalize the Title if it's a special page
-               if( $target->getNamespace() == NS_SPECIAL ) {
-                       list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $target->getDBkey() );
-                       if( $name ) {
-                               $target = SpecialPage::getTitleFor( $name, $subpage );
-                       }
+               $ret = null;
+               if( !wfRunHooks( 'LinkBegin', array( $this, $target, &$text,
+               &$customAttribs, &$query, &$options, &$ret ) ) ) {
+                       wfProfileOut( __METHOD__ );
+                       return $ret;
                }
 
+               # Normalize the Title if it's a special page
+               $target = $this->normaliseSpecialPage( $target );
+
                # If we don't know whether the page exists, let's find out.
+               wfProfileIn( __METHOD__ . '-checkPageExistence' );
                if( !in_array( 'known', $options ) and !in_array( 'broken', $options ) ) {
-                       if( $target->getNamespace() == NS_SPECIAL ) {
-                               if( SpecialPage::exists( $target->getDbKey() ) ) {
-                                       $options []= 'known';
-                               } else {
-                                       $options []= 'broken';
-                               }
-                       } elseif( $target->isAlwaysKnown() or
-                       ($target->getPrefixedText() == '' and $target->getFragment() != '')
-                       or $target->exists() ) {
+                       if( $target->isKnown() ) {
                                $options []= 'known';
                        } else {
-                               # Either it exists
                                $options []= 'broken';
                        }
                }
+               wfProfileOut( __METHOD__ . '-checkPageExistence' );
+
+               $oldquery = array();
+               if( in_array( "forcearticlepath", $options ) && $query ){
+                       $oldquery = $query;
+                       $query = array();
+               }
 
                # Note: we want the href attribute first, for prettiness.
                $attribs = array( 'href' => $this->linkUrl( $target, $query, $options ) );
+               if( in_array( 'forcearticlepath', $options ) && $oldquery ){
+                       $attribs['href'] = wfAppendQuery( $attribs['href'], wfArrayToCgi( $oldquery ) );
+               }
+
                $attribs = array_merge(
                        $attribs,
                        $this->linkAttribs( $target, $customAttribs, $options )
                );
                if( is_null( $text ) ) {
-                       $text = $this->linkText( $target, $options );
+                       $text = $this->linkText( $target );
                }
 
-               $ret = Xml::element( 'a', $attribs, $text, false );
+               $ret = null;
+               if( wfRunHooks( 'LinkEnd', array( $this, $target, $options, &$text, &$attribs, &$ret ) ) ) {
+                       $ret = Xml::openElement( 'a', $attribs ) . $text . Xml::closeElement( 'a' );
+               }
 
                wfProfileOut( __METHOD__ );
                return $ret;
        }
 
        private function linkUrl( $target, $query, $options ) {
-               # If it's a broken link, add the appropriate query pieces.  This over-
-               # writes the default action!
-               if( in_array( 'broken', $options ) ) {
+               wfProfileIn( __METHOD__ );
+               # We don't want to include fragments for broken links, because they
+               # generally make no sense.
+               if( in_array( 'broken', $options ) and $target->mFragment !== '' ) {
+                       $target = clone $target;
+                       $target->mFragment = '';
+               }
+
+               # If it's a broken link, add the appropriate query pieces, unless
+               # there's already an action specified, or unless 'edit' makes no sense
+               # (i.e., for a nonexistent special page).
+               if( in_array( 'broken', $options ) and empty( $query['action'] )
+               and $target->getNamespace() != NS_SPECIAL ) {
                        $query['action'] = 'edit';
                        $query['redlink'] = '1';
                }
-
-               $queryString = array();
-               foreach( $query as $key => $val ) {
-                       $queryString []= urlencode( $key ) . '=' . urlencode( $val );
-               }
-               $queryString = implode( '&', $queryString );
-
-               if( $target->isExternal() ) {
-                       return $target->getFullURL( $queryString );
-               }
-               return $target->getLocalURL( $queryString );
+               $ret = $target->getLinkUrl( $query );
+               wfProfileOut( __METHOD__ );
+               return $ret;
        }
 
        private function linkAttribs( $target, $attribs, $options ) {
+               wfProfileIn( __METHOD__ );
                global $wgUser;
                $defaults = array();
 
-               # First get a default title attribute.
-               if( in_array( 'known', $options ) ) {
-                       $defaults['title'] = $target->getPrefixedText();
-               } else {
-                       $defaults['title'] = wfMsg( 'red-link-title', $target->getPrefixedText() );
-               }
-
                if( !in_array( 'noclasses', $options ) ) {
-                       # Now build the classes.  This is the bulk of what we're doing.
+                       wfProfileIn( __METHOD__ . '-getClasses' );
+                       # Now build the classes.
                        $classes = array();
 
                        if( in_array( 'broken', $options ) ) {
-                               $classes []= 'new';
+                               $classes[] = 'new';
+                       }
+
+                       if( $target->isExternal() ) {
+                               $classes[] = 'extiw';
                        }
 
                        # Note that redirects never count as stubs here.
                        if ( $target->isRedirect() ) {
-                               $classes []= 'mw-redirect';
+                               $classes[] = 'mw-redirect';
                        } elseif( $target->isContentPage() ) {
+                               # Check for stub.
                                $threshold = $wgUser->getOption( 'stubthreshold' );
-                               if( $threshold > 0 and $target->getLength() < $threshold ) {
-                                       $classes []= 'stub';
+                               if( $threshold > 0 and $target->exists() and $target->getLength() < $threshold ) {
+                                       $classes[] = 'stub';
                                }
                        }
                        if( $classes != array() ) {
                                $defaults['class'] = implode( ' ', $classes );
                        }
+                       wfProfileOut( __METHOD__ . '-getClasses' );
+               }
+
+               # Get a default title attribute.
+               if( in_array( 'known', $options ) ) {
+                       $defaults['title'] = $target->getPrefixedText();
+               } else {
+                       $defaults['title'] = wfMsg( 'red-link-title', $target->getPrefixedText() );
                }
 
                # Finally, merge the custom attribs with the default ones, and iterate
                # over that, deleting all "false" attributes.
-               if( !empty( $attribs['class'] ) and !empty( $defaults['class'] ) ) {
-                       $attribs['class'] .= ' '.$defaults['class'];
-               }
                $ret = array();
-               foreach( array_merge( $defaults, $attribs ) as $key => $val ) {
+               $merged = Sanitizer::mergeAttributes( $defaults, $attribs );
+               foreach( $merged as $key => $val ) {
+                       # A false value suppresses the attribute, and we don't want the
+                       # href attribute to be overridden.
                        if( $key != 'href' and $val !== false ) {
                                $ret[$key] = $val;
                        }
                }
+               wfProfileOut( __METHOD__ );
                return $ret;
        }
 
-       private function linkText( $target, $options ) {
+       private function linkText( $target ) {
+               # We might be passed a non-Title by make*LinkObj().  Fail gracefully.
+               if( !$target instanceof Title ) {
+                       return '';
+               }
+
                # If the target is just a fragment, with no title, we return the frag-
                # ment text.  Otherwise, we return the title text itself.
                if( $target->getPrefixedText() === '' and $target->getFragment() !== '' ) {
@@ -274,6 +320,8 @@ class Linker {
        }
 
        /**
+        * @deprecated Use link()
+        *
         * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call
         * it if you already have a title object handy. See makeLinkObj for further documentation.
         *
@@ -299,6 +347,8 @@ class Linker {
        }
 
        /**
+        * @deprecated Use link()
+        *
         * This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call
         * it if you already have a title object handy. See makeKnownLinkObj for further documentation.
         *
@@ -320,6 +370,8 @@ class Linker {
        }
 
        /**
+        * @deprecated Use link()
+        *
         * This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call
         * it if you already have a title object handy. See makeBrokenLinkObj for further documentation.
         *
@@ -341,7 +393,7 @@ class Linker {
        }
 
        /**
-        * @deprecated use makeColouredLinkObj
+        * @deprecated Use link()
         *
         * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call
         * it if you already have a title object handy. See makeStubLinkObj for further documentation.
@@ -354,6 +406,7 @@ class Linker {
         *                      the end of the link.
         */
        function makeStubLink( $title, $text = '', $query = '', $trail = '' ) {
+               wfDeprecated( __METHOD__ );
                $nt = Title::newFromText( $title );
                if ( $nt instanceof Title ) {
                        return $this->makeStubLinkObj( $nt, $text, $query, $trail );
@@ -364,6 +417,8 @@ class Linker {
        }
 
        /**
+        * @deprecated Use link()
+        *
         * Make a link for a title which may or may not be in the database. If you need to
         * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each
         * call to this will result in a DB query.
@@ -377,65 +432,25 @@ class Linker {
         *                      the end of the link.
         * @param $prefix String: optional prefix. As trail, only before instead of after.
         */
-       function makeLinkObj( Title $nt, $text= '', $query = '', $trail = '', $prefix = '' ) {
+       function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) {
                global $wgUser;
                wfProfileIn( __METHOD__ );
 
-               if ( $nt->isExternal() ) {
-                       $u = $nt->getFullURL();
-                       $link = $nt->getPrefixedURL();
-                       if ( '' == $text ) { $text = $nt->getPrefixedText(); }
-                       $style = $this->getInterwikiLinkAttributes( $link, $text, 'extiw' );
-
-                       $inside = '';
-                       if ( '' != $trail ) {
-                               $m = array();
-                               if ( preg_match( '/^([a-z]+)(.*)$$/sD', $trail, $m ) ) {
-                                       $inside = $m[1];
-                                       $trail = $m[2];
-                               }
-                       }
-                       $t = "<a href=\"{$u}\"{$style}>{$text}{$inside}</a>";
-
-                       wfProfileOut( __METHOD__ );
-                       return $t;
-               } elseif ( $nt->isAlwaysKnown() ) {
-                       # Image links, special page links and self-links with fragments are always known.
-                       $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
-               } else {
-                       wfProfileIn( __METHOD__.'-immediate' );
+               $query = wfCgiToArray( $query );
+               list( $inside, $trail ) = Linker::splitTrail( $trail );
+               if( $text === '' ) {
+                       $text = $this->linkText( $nt );
+               }
 
-                       # Handles links to special pages which do not exist in the database:
-                       if( $nt->getNamespace() == NS_SPECIAL ) {
-                               if( SpecialPage::exists( $nt->getDBkey() ) ) {
-                                       $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
-                               } else {
-                                       $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix );
-                               }
-                               wfProfileOut( __METHOD__.'-immediate' );
-                               wfProfileOut( __METHOD__ );
-                               return $retVal;
-                       }
+               $ret = $this->link( $nt, "$prefix$text$inside", array(), $query ) . $trail;
 
-                       # Work out link colour immediately
-                       $aid = $nt->getArticleID() ;
-                       if ( 0 == $aid ) {
-                               $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix );
-                       } else {
-                               $colour = '';
-                               if ( $nt->isContentPage() ) {
-                                       $threshold = $wgUser->getOption('stubthreshold');
-                                       $colour = $this->getLinkColour( $nt, $threshold );
-                               }
-                               $retVal = $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix );
-                       }
-                       wfProfileOut( __METHOD__.'-immediate' );
-               }
                wfProfileOut( __METHOD__ );
-               return $retVal;
+               return $ret;
        }
 
        /**
+        * @deprecated Use link()
+        *
         * Make a link for a title which definitely exists. This is faster than makeLinkObj because
         * it doesn't have to do a database query. It's also valid for interwiki titles and special
         * pages.
@@ -449,37 +464,29 @@ class Linker {
         * @param $style  String: style to apply - if empty, use getInternalLinkAttributesObj instead
         * @return the a-element
         */
-       function makeKnownLinkObj( Title $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
+       function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
                wfProfileIn( __METHOD__ );
 
-               $nt = $this->normaliseSpecialPage( $title );
-
-               $u = $nt->escapeLocalURL( $query );
-               if ( $nt->getFragment() != '' ) {
-                       if( $nt->getPrefixedDbkey() == '' ) {
-                               $u = '';
-                               if ( '' == $text ) {
-                                       $text = htmlspecialchars( $nt->getFragment() );
-                               }
-                       }
-                       $u .= $nt->getFragmentForURL();
-               }
                if ( $text == '' ) {
-                       $text = htmlspecialchars( $nt->getPrefixedText() );
-               }
-               if ( $style == '' ) {
-                       $style = $this->getInternalLinkAttributesObj( $nt, $text );
+                       $text = $this->linkText( $title );
                }
+               $attribs = Sanitizer::mergeAttributes(
+                       Sanitizer::decodeTagAttributes( $aprops ),
+                       Sanitizer::decodeTagAttributes( $style )
+               );
+               $query = wfCgiToArray( $query );
+               list( $inside, $trail ) = Linker::splitTrail( $trail );
 
-               if ( $aprops !== '' ) $aprops = " $aprops";
+               $ret = $this->link( $title, "$prefix$text$inside", $attribs, $query,
+                       array( 'known', 'noclasses' ) ) . $trail;
 
-               list( $inside, $trail ) = Linker::splitTrail( $trail );
-               $r = "<a href=\"{$u}\"{$style}{$aprops}>{$prefix}{$text}{$inside}</a>{$trail}";
                wfProfileOut( __METHOD__ );
-               return $r;
+               return $ret;
        }
 
        /**
+        * @deprecated Use link()
+        *
         * Make a red link to the edit page of a given title.
         *
         * @param $nt Title object of the target page
@@ -489,37 +496,24 @@ class Linker {
         *                      be included in the link text. Other characters will be appended after
         *                      the end of the link.
         */
-       function makeBrokenLinkObj( Title $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
+       function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
                wfProfileIn( __METHOD__ );
 
-               $nt = $this->normaliseSpecialPage( $title );
-
-               if( $nt->getNamespace() == NS_SPECIAL ) {
-                       $q = $query;
-               } else if ( '' == $query ) {
-                       $q = 'action=edit&redlink=1';
-               } else {
-                       $q = 'action=edit&redlink=1&'.$query;
-               }
-               $u = $nt->escapeLocalURL( $q );
-
-               $titleText = $nt->getPrefixedText();
-               if ( '' == $text ) {
-                       $text = htmlspecialchars( $titleText );
-               }
-               $titleAttr = wfMsg( 'red-link-title', $titleText );
-               $style = $this->getInternalLinkAttributesObj( $nt, $text, 'new', $titleAttr );
                list( $inside, $trail ) = Linker::splitTrail( $trail );
+               if( $text === '' ) {
+                       $text = $this->linkText( $title );
+               }
+               $nt = $this->normaliseSpecialPage( $title );
 
-               wfRunHooks( 'BrokenLink', array( &$this, $nt, $query, &$u, &$style, &$prefix, &$text, &$inside, &$trail ) );
-               $s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}";
+               $ret = $this->link( $title, "$prefix$text$inside", array(),
+                       wfCgiToArray( $query ), 'broken' ) . $trail;
 
                wfProfileOut( __METHOD__ );
-               return $s;
+               return $ret;
        }
 
        /**
-        * @deprecated use makeColouredLinkObj
+        * @deprecated Use link()
         *
         * Make a brown link to a short article.
         *
@@ -536,6 +530,8 @@ class Linker {
        }
 
        /**
+        * @deprecated Use link()
+        *
         * Make a coloured link.
         *
         * @param $nt Title object of the target page
@@ -589,7 +585,9 @@ class Linker {
                if ( $title->getNamespace() == NS_SPECIAL ) {
                        list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $title->getDBkey() );
                        if ( !$name ) return $title;
-                       return SpecialPage::getTitleFor( $name, $subpage );
+                       $ret = SpecialPage::getTitleFor( $name, $subpage );
+                       $ret->mFragment = $title->getFragment();
+                       return $ret;
                } else {
                        return $title;
                }
@@ -690,6 +688,9 @@ class Linker {
         *                          bottom, text-bottom)
         *          alt             Alternate text for image (i.e. alt attribute). Plain text.
         *          caption         HTML for image caption.
+        *          link-url        URL to link to
+        *          link-title      Title object to link to
+        *          no-link         Boolean, suppress description link
         *
         * @param array $handlerParams Associative array of media handler parameters, to be passed
         *       to transform(). Typical keys are "width" and "page".
@@ -718,11 +719,12 @@ class Linker {
                $page = isset( $hp['page'] ) ? $hp['page'] : false;
                if ( !isset( $fp['align'] ) ) $fp['align'] = '';
                if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
+               # Backward compatibility, title used to always be equal to alt text
+               if ( !isset( $fp['title'] ) ) $fp['title'] = $fp['alt'];
 
                $prefix = $postfix = '';
 
-               if ( 'center' == $fp['align'] )
-               {
+               if ( 'center' == $fp['align'] ) {
                        $prefix  = '<div class="center">';
                        $postfix = '</div>';
                        $fp['align']   = 'none';
@@ -753,7 +755,6 @@ class Linker {
                }
 
                if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) {
-
                        # Create a thumbnail. Alignment depends on language
                        # writing direction, # right aligned for left-to-right-
                        # languages ("Western languages"), left-aligned
@@ -786,15 +787,26 @@ class Linker {
                if ( !$thumb ) {
                        $s = $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true );
                } else {
-                       $s = $thumb->toHtml( array(
-                               'desc-link' => true,
-                               'desc-query' => $query,
+                       $params = array(
                                'alt' => $fp['alt'],
+                               'title' => $fp['title'],
                                'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false ,
-                               'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ) );
+                               'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false );
+                       if ( !empty( $fp['link-url'] ) ) {
+                               $params['custom-url-link'] = $fp['link-url'];
+                       } elseif ( !empty( $fp['link-title'] ) ) {
+                               $params['custom-title-link'] = $fp['link-title'];
+                       } elseif ( !empty( $fp['no-link'] ) ) {
+                               // No link
+                       } else {
+                               $params['desc-link'] = true;
+                               $params['desc-query'] = $query;
+                       }
+
+                       $s = $thumb->toHtml( $params );
                }
                if ( '' != $fp['align'] ) {
-                       $s = "<div class=\"float{$fp['align']}\"><span>{$s}</span></div>";
+                       $s = "<div class=\"float{$fp['align']}\">{$s}</div>";
                }
                return str_replace("\n", ' ',$prefix.$s.$postfix);
        }
@@ -826,6 +838,8 @@ class Linker {
                $page = isset( $hp['page'] ) ? $hp['page'] : false;
                if ( !isset( $fp['align'] ) ) $fp['align'] = 'right';
                if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
+               # Backward compatibility, title used to always be equal to alt text
+               if ( !isset( $fp['title'] ) ) $fp['title'] = $fp['alt'];
                if ( !isset( $fp['caption'] ) ) $fp['caption'] = '';
 
                if ( empty( $hp['width'] ) ) {
@@ -839,7 +853,7 @@ class Linker {
                } else {
                        if ( isset( $fp['manualthumb'] ) ) {
                                # Use manually specified thumbnail
-                               $manual_title = Title::makeTitleSafe( NS_IMAGE, $fp['manualthumb'] );
+                               $manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] );
                                if( $manual_title ) {
                                        $manual_img = wfFindFile( $manual_title );
                                        if ( $manual_img ) {
@@ -885,6 +899,7 @@ class Linker {
                } else {
                        $s .= $thumb->toHtml( array(
                                'alt' => $fp['alt'],
+                               'title' => $fp['title'],
                                'img-class' => 'thumbimage',
                                'desc-link' => true,
                                'desc-query' => $query ) );
@@ -944,7 +959,7 @@ class Linker {
 
        /** @deprecated use Linker::makeMediaLinkObj() */
        function makeMediaLink( $name, $unused = '', $text = '', $time = false ) {
-               $nt = Title::makeTitleSafe( NS_IMAGE, $name );
+               $nt = Title::makeTitleSafe( NS_FILE, $name );
                return $this->makeMediaLinkObj( $nt, $text, $time );
        }
 
@@ -993,11 +1008,10 @@ class Linker {
        }
 
        /** @todo document */
-       function makeExternalLink( $url, $text, $escape = true, $linktype = '', $ns = null ) {
-               $style = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype );
-               global $wgNoFollowLinks, $wgNoFollowNsExceptions;
-               if( $wgNoFollowLinks && !(isset($ns) && in_array($ns, $wgNoFollowNsExceptions)) ) {
-                       $style .= ' rel="nofollow"';
+       function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) {
+               $attribsText = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype );
+               if ( $attribs ) {
+                       $attribsText .= Xml::expandAttributes( $attribs );
                }
                $url = htmlspecialchars( $url );
                if( $escape ) {
@@ -1009,7 +1023,7 @@ class Linker {
                        wfDebug("Hook LinkerMakeExternalLink changed the output of link with url {$url} and text {$text} to {$link}", true);
                        return $link;
                }
-               return '<a href="'.$url.'"'.$style.'>'.$text.'</a>';
+               return '<a href="'.$url.'"'.$attribsText.'>'.$text.'</a>';
        }
 
        /**
@@ -1020,13 +1034,12 @@ class Linker {
         * @private
         */
        function userLink( $userId, $userText ) {
-               $encName = htmlspecialchars( $userText );
                if( $userId == 0 ) {
                        $page = SpecialPage::getTitleFor( 'Contributions', $userText );
                } else {
                        $page = Title::makeTitle( NS_USER, $userText );
                }
-               return $this->link( $page, $encName );
+               return $this->link( $page, htmlspecialchars( $userText ), array( 'class' => 'mw-userlink' ) );
        }
 
        /**
@@ -1066,7 +1079,7 @@ class Linker {
                }
 
                if( $items ) {
-                       return ' (' . implode( ' | ', $items ) . ')';
+                       return ' <span class="mw-usertoollinks">(' . implode( ' | ', $items ) . ')</span>';
                } else {
                        return '';
                }
@@ -1117,7 +1130,8 @@ class Linker {
                if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
                        $link = wfMsgHtml( 'rev-deleted-user' );
                } else if( $rev->userCan( Revision::DELETED_USER ) ) {
-                       $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() );
+                       $link = $this->userLink( $rev->getUser( Revision::FOR_THIS_USER ), 
+                               $rev->getUserText( Revision::FOR_THIS_USER ) );
                } else {
                        $link = wfMsgHtml( 'rev-deleted-user' );
                }
@@ -1137,8 +1151,10 @@ class Linker {
                if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
                        $link = wfMsgHtml( 'rev-deleted-user' );
                } else if( $rev->userCan( Revision::DELETED_USER ) ) {
-                       $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) .
-                       ' ' . $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
+                       $userId = $rev->getUser( Revision::FOR_THIS_USER );
+                       $userText = $rev->getUserText( Revision::FOR_THIS_USER ); 
+                       $link = $this->userLink( $userId, $userText ) .
+                               ' ' . $this->userToolLinks( $userId, $userText );
                } else {
                        $link = wfMsgHtml( 'rev-deleted-user' );
                }
@@ -1169,7 +1185,8 @@ class Linker {
 
                # Sanitize text a bit:
                $comment = str_replace( "\n", " ", $comment );
-               $comment = htmlspecialchars( $comment );
+               # Allow HTML entities (for bug 13815)
+               $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
 
                # Render autocomments and make links:
                $comment = $this->formatAutoComments( $comment, $title, $local );
@@ -1192,45 +1209,63 @@ class Linker {
         *
         * @todo Document the $local parameter.
         */
-       private function formatAutocomments( $comment, $title = NULL, $local = false ) {
-               $match = array();
-               while (preg_match('!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment,$match)) {
-                       $pre=$match[1];
-                       $auto=$match[2];
-                       $post=$match[3];
-                       $link='';
-                       if( $title ) {
-                               $section = $auto;
-
-                               # Generate a valid anchor name from the section title.
-                               # Hackish, but should generally work - we strip wiki
-                               # syntax, including the magic [[: that is used to
-                               # "link rather than show" in case of images and
-                               # interlanguage links.
-                               $section = str_replace( '[[:', '', $section );
-                               $section = str_replace( '[[', '', $section );
-                               $section = str_replace( ']]', '', $section );
-                               if ( $local ) {
-                                       $sectionTitle = Title::newFromText( '#' . $section);
-                               } else {
-                                       $sectionTitle = wfClone( $title );
-                                       $sectionTitle->mFragment = $section;
-                               }
-                               $link = $this->link( $sectionTitle, wfMsgForContent( 'sectionlink' ) );
-                       }
-                       $auto = $link . $auto;
-                       if( $pre ) {
-                               # written summary $presep autocomment (summary /* section */)
-                               $auto = wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) ) . $auto;
+       private function formatAutocomments( $comment, $title = null, $local = false ) {
+               // Bah!
+               $this->autocommentTitle = $title;
+               $this->autocommentLocal = $local;
+               $comment = preg_replace_callback(
+                       '!(.*)/\*\s*(.*?)\s*\*/(.*)!',
+                       array( $this, 'formatAutocommentsCallback' ),
+                       $comment );
+               unset( $this->autocommentTitle );
+               unset( $this->autocommentLocal );
+               return $comment;
+       }
+       
+       private function formatAutocommentsCallback( $match ) {
+               $title = $this->autocommentTitle;
+               $local = $this->autocommentLocal;
+               
+               $pre=$match[1];
+               $auto=$match[2];
+               $post=$match[3];
+               $link='';
+               if( $title ) {
+                       $section = $auto;
+
+                       # Generate a valid anchor name from the section title.
+                       # Hackish, but should generally work - we strip wiki
+                       # syntax, including the magic [[: that is used to
+                       # "link rather than show" in case of images and
+                       # interlanguage links.
+                       $section = str_replace( '[[:', '', $section );
+                       $section = str_replace( '[[', '', $section );
+                       $section = str_replace( ']]', '', $section );
+                       if ( $local ) {
+                               $sectionTitle = Title::newFromText( '#' . $section );
+                       } else {
+                               $sectionTitle = Title::makeTitleSafe( $title->getNamespace(), 
+                                       $title->getDBkey(), $section );
                        }
-                       if( $post ) {
-                               # autocomment $postsep written summary (/* section */ summary)
-                               $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) );
+                       if ( $sectionTitle ) {
+                               $link = $this->link( $sectionTitle,
+                                       wfMsgForContent( 'sectionlink' ), array(), array(),
+                                       'noclasses' );
+                       } else {
+                               $link = '';
                        }
-                       $auto = '<span class="autocomment">' . $auto . '</span>';
-                       $comment = $pre . $auto . $post;
                }
-
+               $auto = "$link$auto";
+               if( $pre ) {
+                       # written summary $presep autocomment (summary /* section */)
+                       $auto = wfMsgExt( 'autocomment-prefix', array( 'escapenoentities', 'content' ) ) . $auto;
+               }
+               if( $post ) {
+                       # autocomment $postsep written summary (/* section */ summary)
+                       $auto .= wfMsgExt( 'colon-separator', array( 'escapenoentities', 'content' ) );
+               }
+               $auto = '<span class="autocomment">' . $auto . '</span>';
+               $comment = $pre . $auto . $post;
                return $comment;
        }
 
@@ -1325,7 +1360,8 @@ class Linker {
                if( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
                        $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
                } else if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
-                       $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle(), $local );
+                       $block = $this->commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
+                               $rev->getTitle(), $local );
                } else {
                        $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
                }
@@ -1385,8 +1421,8 @@ class Linker {
                 . "</ul>\n</td></tr></table>"
                 . '<script type="' . $wgJsMimeType . '">'
                 . ' if (window.showTocToggle) {'
-                . ' var tocShowText = "' . wfEscapeJsString( wfMsg('showtoc') ) . '";'
-                . ' var tocHideText = "' . wfEscapeJsString( wfMsg('hidetoc') ) . '";'
+                . ' var tocShowText = "' . Xml::escapeJsString( wfMsg('showtoc') ) . '";'
+                . ' var tocHideText = "' . Xml::escapeJsString( wfMsg('hidetoc') ) . '";'
                 . ' showTocToggle();'
                 . ' } '
                 . "</script>\n";
@@ -1437,7 +1473,7 @@ class Linker {
                if( !is_null( $tooltip ) ) {
                        $attribs['title'] = wfMsg( 'editsectionhint', $tooltip );
                }
-               $url = $this->link( $nt, wfMsg('editsection'),
+               $link = $this->link( $nt, wfMsg('editsection'),
                        $attribs,
                        array( 'action' => 'edit', 'section' => $section ),
                        array( 'noclasses', 'known' )
@@ -1451,19 +1487,19 @@ class Linker {
                        $attribs = " title=\"$attribs\"";
                }
                $result = null;
-               wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $attribs, $url, &$result ) );
+               wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $attribs, $link, &$result ) );
                if( !is_null( $result ) ) {
                        # For reverse compatibility, add the brackets *after* the hook is
                        # run, and even add them to hook-provided text.  (This is the main
                        # reason that the EditSectionLink hook is deprecated in favor of
                        # DoEditSectionLink: it can't change the brackets or the span.)
-                       $result = wfMsgHtml( 'editsection-brackets', $url );
+                       $result = wfMsgHtml( 'editsection-brackets', $result );
                        return "<span class=\"editsection\">$result</span>";
                }
 
                # Add the brackets and the span, and *then* run the nice new hook, with
                # clean and non-redundant arguments.
-               $result = wfMsgHtml( 'editsection-brackets', $url );
+               $result = wfMsgHtml( 'editsection-brackets', $link );
                $result = "<span class=\"editsection\">$result</span>";
 
                wfRunHooks( 'DoEditSectionLink', array( $this, $nt, $section, $tooltip, &$result ) );
@@ -1537,14 +1573,18 @@ class Linker {
        public function buildRollbackLink( $rev ) {
                global $wgRequest, $wgUser;
                $title = $rev->getTitle();
-               $query = array( 'action' => 'rollback' );
+               $query = array(
+                       'action' => 'rollback',
+                       'from' => $rev->getUserText()
+               );
                if( $wgRequest->getBool( 'bot' ) ) {
                        $query['bot'] = '1';
                }
                $query['token'] = $wgUser->editToken( array( $title->getPrefixedText(),
                        $rev->getUserText() ) );
-               return $this->link( $title, wfMsgHtml( 'rollbacklink' ), array(),
-                       $query, 'known' );
+               return $this->link( $title, wfMsgHtml( 'rollbacklink' ),
+                       array( 'title' => wfMsg( 'tooltip-rollback' ) ),
+                       $query, array( 'known', 'noclasses' ) );
        }
 
        /**
@@ -1592,7 +1632,12 @@ class Linker {
                                } else {
                                        $protected = '';
                                }
-                               $outText .= '<li>' . $sk->link( $titleObj ) . ' ' . $protected . '</li>';
+                               if( $titleObj->quickUserCan( 'edit' ) ) {
+                                       $editLink = $sk->makeLinkObj( $titleObj, wfMsg('editlink'), 'action=edit' );
+                               } else {
+                                       $editLink = $sk->makeLinkObj( $titleObj, wfMsg('viewsourcelink'), 'action=edit' );
+                               }
+                               $outText .= '<li>' . $sk->link( $titleObj ) . ' (' . $editLink . ') ' . $protected . '</li>';
                        }
                        $outText .= '</ul>';
                }
@@ -1642,35 +1687,37 @@ class Linker {
        }
 
        /**
-        * Given the id of an interface element, constructs the appropriate title
-        * and accesskey attributes from the system messages.  (Note, this is usu-
-        * ally the id but isn't always, because sometimes the accesskey needs to
-        * go on a different element than the id, for reverse-compatibility, etc.)
-        *
-        * @param string $name Id of the element, minus prefixes.
-        * @return string title and accesskey attributes, ready to drop in an
-        *   element (e.g., ' title="This does something [x]" accesskey="x"').
+        * @deprecated Returns raw bits of HTML, use titleAttrib() and accesskey()
         */
-       public function tooltipAndAccesskey($name) {
-               $fname="Linker::tooltipAndAccesskey";
-               wfProfileIn($fname);
-               $out = '';
-
-               $tooltip = wfMsg('tooltip-'.$name);
-               if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') {
-                       // Compatibility: formerly some tooltips had [alt-.] hardcoded
-                       $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
-                       $out .= ' title="'.htmlspecialchars($tooltip);
+       public function tooltipAndAccesskey( $name ) {
+               # FIXME: If Sanitizer::expandAttributes() treated "false" as "output
+               # no attribute" instead of "output '' as value for attribute", this
+               # would be three lines.
+               $attribs = array(
+                       'title' => $this->titleAttrib( $name, 'withaccess' ),
+                       'accesskey' => $this->accesskey( $name )
+               );
+               if ( $attribs['title'] === false ) {
+                       unset( $attribs['title'] );
                }
-               $accesskey = wfMsg('accesskey-'.$name);
-               if ($accesskey && $accesskey != '-' && !wfEmptyMsg('accesskey-'.$name, $accesskey)) {
-                       if ($out) $out .= " [$accesskey]\" accesskey=\"$accesskey\"";
-                       else $out .= " title=\"[$accesskey]\" accesskey=\"$accesskey\"";
-               } elseif ($out) {
-                       $out .= '"';
+               if ( $attribs['accesskey'] === false ) {
+                       unset( $attribs['accesskey'] );
                }
-               wfProfileOut($fname);
-               return $out;
+               return Xml::expandAttributes( $attribs );
+       }
+
+       /** @deprecated Returns raw bits of HTML, use titleAttrib() */
+       public function tooltip( $name, $options = null ) {
+               # FIXME: If Sanitizer::expandAttributes() treated "false" as "output
+               # no attribute" instead of "output '' as value for attribute", this
+               # would be two lines.
+               $tooltip = $this->titleAttrib( $name, $options );
+               if ( $tooltip === false ) {
+                       return '';
+               }
+               return Xml::expandAttributes( array(
+                       'title' => $this->titleAttrib( $name, $options )
+               ) );
        }
 
        /**
@@ -1679,18 +1726,65 @@ class Linker {
         * isn't always, because sometimes the accesskey needs to go on a different
         * element than the id, for reverse-compatibility, etc.)
         *
-        * @param string $name Id of the element, minus prefixes.
-        * @return string title attribute, ready to drop in an element
-        * (e.g., ' title="This does something"').
+        * @param string $name    Id of the element, minus prefixes.
+        * @param mixed  $options null or the string 'withaccess' to add an access-
+        *   key hint
+        * @return string Contents of the title attribute (which you must HTML-
+        *   escape), or false for no title attribute
         */
-       public function tooltip($name) {
-               $out = '';
+       public function titleAttrib( $name, $options = null ) {
+               wfProfileIn( __METHOD__ );
+
+               $tooltip = wfMsg( "tooltip-$name" );
+               # Compatibility: formerly some tooltips had [alt-.] hardcoded
+               $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
 
-               $tooltip = wfMsg('tooltip-'.$name);
-               if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') {
-                       $out = ' title="'.htmlspecialchars($tooltip).'"';
+               # Message equal to '-' means suppress it.
+               if ( wfEmptyMsg( "tooltip-$name", $tooltip ) || $tooltip == '-' ) {
+                       $tooltip = false;
                }
 
-               return $out;
+               if ( $options == 'withaccess' ) {
+                       $accesskey = $this->accesskey( $name );
+                       if( $accesskey !== false ) {
+                               if ( $tooltip === false || $tooltip === '' ) {
+                                       $tooltip = "[$accesskey]";
+                               } else {
+                                       $tooltip .= " [$accesskey]";
+                               }
+                       }
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $tooltip;
+       }
+
+       /**
+        * Given the id of an interface element, constructs the appropriate
+        * accesskey attribute from the system messages.  (Note, this is usually
+        * the id but isn't always, because sometimes the accesskey needs to go on
+        * a different element than the id, for reverse-compatibility, etc.)
+        *
+        * @param string $name    Id of the element, minus prefixes.
+        * @return string Contents of the accesskey attribute (which you must HTML-
+        *   escape), or false for no accesskey attribute
+        */
+       public function accesskey( $name ) {
+               wfProfileIn( __METHOD__ );
+
+               $accesskey = wfMsg( "accesskey-$name" );
+
+               # FIXME: Per standard MW behavior, a value of '-' means to suppress the
+               # attribute, but this is broken for accesskey: that might be a useful
+               # value.
+               if( $accesskey != ''
+               && $accesskey != '-'
+               && !wfEmptyMsg( "accesskey-$name", $accesskey ) ) {
+                       wfProfileOut( __METHOD__ );
+                       return $accesskey;
+               }
+
+               wfProfileOut( __METHOD__ );
+               return false;
        }
 }