Update docs to caution against use of create_function(). Per bug 15476.
[lhc/web/wiklou.git] / includes / Linker.php
index 29e51cd..ea7c387 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';
                }
@@ -149,14 +151,14 @@ class Linker {
         *   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 $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 $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.
@@ -168,18 +170,20 @@ class Linker {
         */
        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' );
+               $ret = null;
+               if( !wfRunHooks( 'LinkBegin', array( $this, $target, &$text,
+               &$customAttribs, &$query, &$options, &$ret ) ) ) {
+                       wfProfileOut( __METHOD__ );
+                       return $ret;
+               }
+
+               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 );
-                       }
-               }
+               $target = $this->normaliseSpecialPage( $target );
 
                # If we don't know whether the page exists, let's find out.
                wfProfileIn( __METHOD__ . '-checkPageExistence' );
@@ -207,12 +211,16 @@ class Linker {
                        $this->linkAttribs( $target, $customAttribs, $options )
                );
                if( is_null( $text ) ) {
-                       $text = $this->linkText( $target, $options );
+                       $text = $this->linkText( $target );
                }
 
-               $ret = Xml::openElement( 'a', $attribs )
-                       . $text
-                       . Xml::closeElement( 'a' );
+               $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;
@@ -220,9 +228,18 @@ class Linker {
 
        private function linkUrl( $target, $query, $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.
-               if( in_array( 'broken', $options ) and empty( $query['action'] ) ) {
+               # 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';
                }
@@ -236,14 +253,8 @@ class Linker {
                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 ) ) {
+                       wfProfileIn( __METHOD__ . '-getClasses' );
                        # Now build the classes.
                        $classes = array();
 
@@ -261,13 +272,21 @@ class Linker {
                        } elseif( $target->isContentPage() ) {
                                # Check for stub.
                                $threshold = $wgUser->getOption( 'stubthreshold' );
-                               if( $threshold > 0 and $target->getLength() < $threshold ) {
+                               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
@@ -285,7 +304,12 @@ class Linker {
                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() !== '' ) {
@@ -295,6 +319,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.
         *
@@ -320,6 +346,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.
         *
@@ -341,6 +369,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.
         *
@@ -362,7 +392,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.
@@ -375,6 +405,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 );
@@ -385,6 +416,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.
@@ -398,65 +431,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.
@@ -470,37 +463,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
@@ -510,37 +495,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.
         *
@@ -557,6 +529,8 @@ class Linker {
        }
 
        /**
+        * @deprecated Use link()
+        *
         * Make a coloured link.
         *
         * @param $nt Title object of the target page
@@ -610,7 +584,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;
                }
@@ -1189,7 +1165,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 );
@@ -1213,44 +1190,62 @@ 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;
+               // 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;
        }
 
@@ -1457,7 +1452,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' )
@@ -1471,19 +1466,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 ) );
@@ -1566,7 +1561,8 @@ class Linker {
                }
                $query['token'] = $wgUser->editToken( array( $title->getPrefixedText(),
                        $rev->getUserText() ) );
-               return $this->link( $title, wfMsgHtml( 'rollbacklink' ), array(),
+               return $this->link( $title, wfMsgHtml( 'rollbacklink' ),
+                       array( 'title' => wfMsg( 'tooltip-rollback' ) ),
                        $query, array( 'known', 'noclasses' ) );
        }
 
@@ -1674,26 +1670,29 @@ class Linker {
         * @return string title and accesskey attributes, ready to drop in an
         *   element (e.g., ' title="This does something [x]" accesskey="x"').
         */
-       public function tooltipAndAccesskey($name) {
-               $fname="Linker::tooltipAndAccesskey";
-               wfProfileIn($fname);
-               $out = '';
+       public function tooltipAndAccesskey( $name ) {
+               wfProfileIn( __METHOD__ );
+               $attribs = array();
 
-               $tooltip = wfMsg('tooltip-'.$name);
-               if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') {
+               $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);
+                       $attribs['title'] = $tooltip;
                }
-               $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 .= '"';
+
+               $accesskey = wfMsg( "accesskey-$name" );
+               if( $accesskey && $accesskey != '-' &&
+               !wfEmptyMsg( "accesskey-$name", $accesskey ) ) {
+                       if( isset( $attribs['title'] ) ) {
+                               $attribs['title'] .= " [$accesskey]";
+                       }
+                       $attribs['accesskey'] = $accesskey;
                }
-               wfProfileOut($fname);
-               return $out;
+
+               $ret = Xml::expandAttributes( $attribs );
+               wfProfileOut( __METHOD__ );
+               return $ret;
        }
 
        /**
@@ -1702,18 +1701,32 @@ 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.
+        * @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 title attribute, ready to drop in an element
         * (e.g., ' title="This does something"').
         */
-       public function tooltip($name) {
-               $out = '';
+       public function tooltip( $name, $options = null ) {
+               wfProfileIn( __METHOD__ );
+
+               $attribs = array();
+
+               $tooltip = wfMsg( "tooltip-$name" );
+               if( !wfEmptyMsg( "tooltip-$name", $tooltip ) && $tooltip != '-' ) {
+                       $attribs['title'] = $tooltip;
+               }
 
-               $tooltip = wfMsg('tooltip-'.$name);
-               if (!wfEmptyMsg('tooltip-'.$name, $tooltip) && $tooltip != '-') {
-                       $out = ' title="'.htmlspecialchars($tooltip).'"';
+               if( isset( $attribs['title'] ) && $options == 'withaccess' ) {
+                       $accesskey = wfMsg( "accesskey-$name" );
+                       if( $accesskey && $accesskey != '-' &&
+                       !wfEmptyMsg( "accesskey-$name", $accesskey ) ) {
+                               $attribs['title'] .= " [$accesskey]";
+                       }
                }
 
-               return $out;
+               $ret = Xml::expandAttributes( $attribs );
+               wfProfileOut( __METHOD__ );
+               return $ret;
        }
 }