check that $wgArticle is an instance of the Article class in Skin::pageStats() per...
[lhc/web/wiklou.git] / includes / Linker.php
index 1145352..e323e47 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';
                }
@@ -147,117 +149,145 @@ class Linker {
         *   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 page name of the Title.
-        * @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.
+        *   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.
+        *       "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';
                }
-               $ret = $target->getLocalURL( $query );
-               if( $target->getFragment() !== '' ) {
-                       $ret .= '#'.$target->getFragment();
-               }
+               $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
@@ -271,10 +301,16 @@ class Linker {
                                $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() !== '' ) {
@@ -284,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.
         *
@@ -309,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.
         *
@@ -330,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.
         *
@@ -351,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.
@@ -364,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 );
@@ -374,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.
@@ -387,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.
@@ -459,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
@@ -499,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.
         *
@@ -546,6 +530,8 @@ class Linker {
        }
 
        /**
+        * @deprecated Use link()
+        *
         * Make a coloured link.
         *
         * @param $nt Title object of the target page
@@ -599,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;
                }
@@ -700,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".
@@ -728,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';
@@ -763,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
@@ -796,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);
        }
@@ -836,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'] ) ) {
@@ -849,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 ) {
@@ -895,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 ) );
@@ -954,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 );
        }
 
@@ -1003,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 ) {
@@ -1019,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>';
        }
 
        /**
@@ -1030,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' ) );
        }
 
        /**
@@ -1076,7 +1079,7 @@ class Linker {
                }
 
                if( $items ) {
-                       return ' (' . implode( ' | ', $items ) . ')';
+                       return ' <span class="mw-usertoollinks">(' . implode( ' | ', $items ) . ')</span>';
                } else {
                        return '';
                }
@@ -1127,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' );
                }
@@ -1147,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' );
                }
@@ -1179,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 );
@@ -1202,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->setFragment( $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;
        }
 
@@ -1335,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>";
                }
@@ -1395,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";
@@ -1447,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' )
@@ -1461,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 ) );
@@ -1493,7 +1519,7 @@ class Linker {
         * @return string HTML headline
         */
        public function makeHeadline( $level, $attribs, $anchor, $text, $link ) {
-               return "<h$level id=\"$anchor\"$attribs$link <span class=\"mw-headline\">$text</span></h$level>";
+               return "<a name=\"$anchor\" id=\"$anchor\"></a><h$level$attribs$link <span class=\"mw-headline\">$text</span></h$level>";
        }
 
        /**
@@ -1556,8 +1582,9 @@ class Linker {
                }
                $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' ) );
        }
 
        /**
@@ -1569,12 +1596,9 @@ class Linker {
         * @param bool $section Whether this is for a section edit
         * @return string HTML output
         */
-       public function formatTemplates( $templates, $preview = false, $section = false) {
-               global $wgUser;
+       public function formatTemplates( $templates, $preview = false, $section = false ) {
                wfProfileIn( __METHOD__ );
 
-               $sk = $wgUser->getSkin();
-
                $outText = '';
                if ( count( $templates ) > 0 ) {
                        # Do a batch existence check
@@ -1593,7 +1617,7 @@ class Linker {
                        } else {
                                $outText .= wfMsgExt( 'templatesused', array( 'parse' ) );
                        }
-                       $outText .= '</div><ul>';
+                       $outText .= "</div><ul>\n";
 
                        usort( $templates, array( 'Title', 'compare' ) );
                        foreach ( $templates as $titleObj ) {
@@ -1605,7 +1629,12 @@ class Linker {
                                } else {
                                        $protected = '';
                                }
-                               $outText .= '<li>' . $sk->link( $titleObj ) . ' ' . $protected . '</li>';
+                               if( $titleObj->quickUserCan( 'edit' ) ) {
+                                       $editLink = $this->makeLinkObj( $titleObj, wfMsg('editlink'), 'action=edit' );
+                               } else {
+                                       $editLink = $this->makeLinkObj( $titleObj, wfMsg('viewsourcelink'), 'action=edit' );
+                               }
+                               $outText .= '<li>' . $this->link( $titleObj ) . ' (' . $editLink . ') ' . $protected . '</li>';
                        }
                        $outText .= '</ul>';
                }
@@ -1620,21 +1649,19 @@ class Linker {
         * or similar
         * @return string HTML output
         */
-       public function formatHiddenCategories( $hiddencats) {
-               global $wgUser, $wgLang;
+       public function formatHiddenCategories( $hiddencats ) {
+               global $wgLang;
                wfProfileIn( __METHOD__ );
 
-               $sk = $wgUser->getSkin();
-
                $outText = '';
                if ( count( $hiddencats ) > 0 ) {
                        # Construct the HTML
                        $outText = '<div class="mw-hiddenCategoriesExplanation">';
                        $outText .= wfMsgExt( 'hiddencategories', array( 'parse' ), $wgLang->formatnum( count( $hiddencats ) ) );
-                       $outText .= '</div><ul>';
+                       $outText .= "</div><ul>\n";
 
                        foreach ( $hiddencats as $titleObj ) {
-                               $outText .= '<li>' . $sk->link( $titleObj, null, array(), array(), 'known' ) . '</li>'; # If it's hidden, it must exist - no need to check with a LinkBatch
+                               $outText .= '<li>' . $this->link( $titleObj, null, array(), array(), 'known' ) . "</li>\n"; # If it's hidden, it must exist - no need to check with a LinkBatch
                        }
                        $outText .= '</ul>';
                }
@@ -1655,35 +1682,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'] );
+               }
+               if ( $attribs['accesskey'] === false ) {
+                       unset( $attribs['accesskey'] );
                }
-               $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 .= '"';
+               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 '';
                }
-               wfProfileOut($fname);
-               return $out;
+               return Xml::expandAttributes( array(
+                       'title' => $this->titleAttrib( $name, $options )
+               ) );
        }
 
        /**
@@ -1692,18 +1721,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 titleAttrib( $name, $options = null ) {
+               wfProfileIn( __METHOD__ );
+
+               $tooltip = wfMsg( "tooltip-$name" );
+               # Compatibility: formerly some tooltips had [alt-.] hardcoded
+               $tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
+
+               # Message equal to '-' means suppress it.
+               if ( wfEmptyMsg( "tooltip-$name", $tooltip ) || $tooltip == '-' ) {
+                       $tooltip = false;
+               }
+
+               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 tooltip($name) {
-               $out = '';
+       public function accesskey( $name ) {
+               wfProfileIn( __METHOD__ );
+
+               $accesskey = wfMsg( "accesskey-$name" );
 
-               $tooltip = wfMsg('tooltip-'.$name);
-               if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') {
-                       $out = ' title="'.htmlspecialchars($tooltip).'"';
+               # 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;
                }
 
-               return $out;
+               wfProfileOut( __METHOD__ );
+               return false;
        }
 }