Display permissions specific to the API (such as writeapi and apihighlimits) on actio...
[lhc/web/wiklou.git] / includes / Linker.php
index 6f502cf..b913ebf 100644 (file)
@@ -21,6 +21,7 @@ class Linker {
         * @deprecated
         */
        function postParseLinkColour( $s = null ) {
+               wfDeprecated( __METHOD__ );
                return null;
        }
 
@@ -130,6 +131,168 @@ class Linker {
                return $colour;
        }
 
+       /**
+        * 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).
+        * link() replaces the old functions in the makeLink() family.
+        *
+        * @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 $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 $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", "extiw").  Only use the class attribute
+        *       provided, if any, so you get a simple blue link with no funny i-
+        *       cons.
+        * @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' );
+               }
+               $options = (array)$options;
+
+               # 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() ) {
+                               $options []= 'known';
+                       } else {
+                               $options []= 'broken';
+                       }
+               }
+               wfProfileOut( __METHOD__ . '-checkPageExistence' );
+
+               # Note: we want the href attribute first, for prettiness.
+               $attribs = array( 'href' => $this->linkUrl( $target, $query, $options ) );
+               $attribs = array_merge(
+                       $attribs,
+                       $this->linkAttribs( $target, $customAttribs, $options )
+               );
+               if( is_null( $text ) ) {
+                       $text = $this->linkText( $target );
+               }
+
+               $ret = Xml::openElement( 'a', $attribs )
+                       . $text
+                       . Xml::closeElement( 'a' );
+
+               wfProfileOut( __METHOD__ );
+               return $ret;
+       }
+
+       private function linkUrl( $target, $query, $options ) {
+               wfProfileIn( __METHOD__ );
+               # 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->getLinkUrl( $query );
+               wfProfileOut( __METHOD__ );
+               return $ret;
+       }
+
+       private function linkAttribs( $target, $attribs, $options ) {
+               wfProfileIn( __METHOD__ );
+               global $wgUser;
+               $defaults = array();
+
+               if( !in_array( 'noclasses', $options ) ) {
+                       wfProfileIn( __METHOD__ . '-getClasses' );
+                       # Build the classes.
+                       $classes = array();
+
+                       if( in_array( 'broken', $options ) ) {
+                               $classes[] = 'new';
+                       }
+
+                       if( $target->isExternal() ) {
+                               $classes[] = 'extiw';
+                       }
+
+                       # Note that redirects never count as stubs here.
+                       if ( $target->isRedirect() ) {
+                               $classes[] = 'mw-redirect';
+                       } elseif( $target->isContentPage() ) {
+                               # Check for stub.
+                               $threshold = $wgUser->getOption( 'stubthreshold' );
+                               if( $threshold > 0 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.
+               $ret = array();
+               $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 ) {
+               # 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() !== '' ) {
+                       return htmlspecialchars( $target->getFragment() );
+               }
+               return htmlspecialchars( $target->getPrefixedText() );
+       }
+
        /**
         * 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.
@@ -234,16 +397,10 @@ class Linker {
         *                      the end of the link.
         * @param $prefix String: optional prefix. As trail, only before instead of after.
         */
-       function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) {
+       function makeLinkObj( Title $nt, $text= '', $query = '', $trail = '', $prefix = '' ) {
                global $wgUser;
                wfProfileIn( __METHOD__ );
 
-               if ( !$nt instanceof Title ) {
-                       # Fail gracefully
-                       wfProfileOut( __METHOD__ );
-                       return "<!-- ERROR -->{$prefix}{$text}{$trail}";
-               }
-
                if ( $nt->isExternal() ) {
                        $u = $nt->getFullURL();
                        $link = $nt->getPrefixedURL();
@@ -312,15 +469,9 @@ class Linker {
         * @param $style  String: style to apply - if empty, use getInternalLinkAttributesObj instead
         * @return the a-element
         */
-       function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
+       function makeKnownLinkObj( Title $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
                wfProfileIn( __METHOD__ );
 
-               if ( !$title instanceof Title ) {
-                       # Fail gracefully
-                       wfProfileOut( __METHOD__ );
-                       return "<!-- ERROR -->{$prefix}{$text}{$trail}";
-               }
-
                $nt = $this->normaliseSpecialPage( $title );
 
                $u = $nt->escapeLocalURL( $query );
@@ -340,7 +491,7 @@ class Linker {
                        $style = $this->getInternalLinkAttributesObj( $nt, $text );
                }
 
-               if ( $aprops !== '' ) $aprops = ' ' . $aprops;
+               if ( $aprops !== '' ) $aprops = " $aprops";
 
                list( $inside, $trail ) = Linker::splitTrail( $trail );
                $r = "<a href=\"{$u}\"{$style}{$aprops}>{$prefix}{$text}{$inside}</a>{$trail}";
@@ -358,15 +509,9 @@ class Linker {
         *                      be included in the link text. Other characters will be appended after
         *                      the end of the link.
         */
-       function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
+       function makeBrokenLinkObj( Title $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
                wfProfileIn( __METHOD__ );
 
-               if ( !$title instanceof Title ) {
-                       # Fail gracefully
-                       wfProfileOut( __METHOD__ );
-                       return "<!-- ERROR -->{$prefix}{$text}{$trail}";
-               }
-
                $nt = $this->normaliseSpecialPage( $title );
 
                if( $nt->getNamespace() == NS_SPECIAL ) {
@@ -406,6 +551,7 @@ class Linker {
         *                      the end of the link.
         */
        function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+               wfDeprecated( __METHOD__ );
                return $this->makeColouredLinkObj( $nt, 'stub', $text, $query, $trail, $prefix );
        }
 
@@ -421,7 +567,6 @@ class Linker {
         *                      the end of the link.
         */
        function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) {
-
                if($colour != ''){
                        $style = $this->getInternalLinkAttributesObj( $nt, $text, $colour );
                } else $style = '';
@@ -464,7 +609,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;
                }
@@ -483,6 +630,7 @@ class Linker {
 
        /** Obsolete alias */
        function makeImage( $url, $alt = '' ) {
+               wfDeprecated( __METHOD__ );
                return $this->makeExternalImage( $url, $alt );
        }
 
@@ -581,7 +729,7 @@ class Linker {
                global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright;
                if ( $file && !$file->allowInlineDisplay() ) {
                        wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" );
-                       return $this->makeKnownLinkObj( $title );
+                       return $this->link( $title );
                }
 
                // Shortcuts
@@ -894,15 +1042,12 @@ class Linker {
         * @private
         */
        function userLink( $userId, $userText ) {
-               $encName = htmlspecialchars( $userText );
                if( $userId == 0 ) {
-                       $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
-                       return $this->makeKnownLinkObj( $contribsPage,
-                               $encName);
+                       $page = SpecialPage::getTitleFor( 'Contributions', $userText );
                } else {
-                       $userPage = Title::makeTitle( NS_USER, $userText );
-                       return $this->makeLinkObj( $userPage, $encName );
+                       $page = Title::makeTitle( NS_USER, $userText );
                }
+               return $this->link( $page, htmlspecialchars( $userText ) );
        }
 
        /**
@@ -926,15 +1071,16 @@ class Linker {
                }
                if( $userId ) {
                        // check if the user has an edit
+                       $attribs = array();
                        if( $redContribsWhenNoEdits ) {
                                $count = !is_null($edits) ? $edits : User::edits( $userId );
-                               $style = ($count == 0) ? " class='new'" : '';
-                       } else {
-                               $style = '';
+                               if( $count == 0 ) {
+                                       $attribs['class'] = 'new';
+                               }
                        }
                        $contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
 
-                       $items[] = $this->makeKnownLinkObj( $contribsPage, wfMsgHtml( 'contribslink' ), '', '', '', '', $style );
+                       $items[] = $this->link( $contribsPage, wfMsgHtml( 'contribslink' ), $attribs );
                }
                if( $blockable && $wgUser->isAllowed( 'block' ) ) {
                        $items[] = $this->blockLink( $userId, $userText );
@@ -966,7 +1112,7 @@ class Linker {
         */
        function userTalkLink( $userId, $userText ) {
                $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
-               $userTalkLink = $this->makeLinkObj( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) );
+               $userTalkLink = $this->link( $userTalkPage, wfMsgHtml( 'talkpagelinktext' ) );
                return $userTalkLink;
        }
 
@@ -978,8 +1124,7 @@ class Linker {
         */
        function blockLink( $userId, $userText ) {
                $blockPage = SpecialPage::getTitleFor( 'Blockip', $userText );
-               $blockLink = $this->makeKnownLinkObj( $blockPage,
-                       wfMsgHtml( 'blocklink' ) );
+               $blockLink = $this->link( $blockPage, wfMsgHtml( 'blocklink' ) );
                return $blockLink;
        }
 
@@ -1045,8 +1190,7 @@ class Linker {
 
                # Sanitize text a bit:
                $comment = str_replace( "\n", " ", $comment );
-               # Allow HTML entities (for bug 13815)
-               $comment = Sanitizer::escapeHtmlAllowEntities( $comment );
+               $comment = htmlspecialchars( $comment );
 
                # Render autocomments and make links:
                $comment = $this->formatAutoComments( $comment, $title, $local );
@@ -1069,7 +1213,7 @@ class Linker {
         *
         * @todo Document the $local parameter.
         */
-       private function formatAutocomments( $comment, $title = NULL, $local = false ) {
+       private function formatAutocomments( $comment, $title = null, $local = false ) {
                $match = array();
                while (preg_match('!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment,$match)) {
                        $pre=$match[1];
@@ -1088,12 +1232,14 @@ class Linker {
                                $section = str_replace( '[[', '', $section );
                                $section = str_replace( ']]', '', $section );
                                if ( $local ) {
-                                       $sectionTitle = Title::newFromText( '#' . $section);
+                                       $sectionTitle = Title::newFromText( '#' . $section );
                                } else {
-                                       $sectionTitle = wfClone( $title );
+                                       $sectionTitle = clone( $title );
                                        $sectionTitle->mFragment = $section;
                                }
-                               $link = $this->makeKnownLinkObj( $sectionTitle, wfMsgForContent( 'sectionlink' ) );
+                               $link = $this->link( $sectionTitle,
+                                       wfMsgForContent( 'sectionlink' ), array(), array(),
+                                       'noclasses' );
                        }
                        $auto = $link . $auto;
                        if( $pre ) {
@@ -1277,8 +1423,9 @@ class Linker {
         * @param $section Integer: section number.
         */
        public function editSectionLinkForOther( $title, $section ) {
+               wfDeprecated( __METHOD__ );
                $title = Title::newFromText( $title );
-               return $this->doEditSectionLink( $title, $section, '', 'EditSectionLinkForOther' );
+               return $this->doEditSectionLink( $title, $section );
        }
 
        /**
@@ -1286,49 +1433,64 @@ class Linker {
         * @param $section Integer: section number.
         * @param $hint Link String: title, or default if omitted or empty
         */
-       public function editSectionLink( Title $nt, $section, $hint='' ) {
-               if( $hint != '' ) {
-                       $hint = wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) );
-                       $hint = " title=\"$hint\"";
-               }
-               return $this->doEditSectionLink( $nt, $section, $hint, 'EditSectionLink' );
+       public function editSectionLink( Title $nt, $section, $hint = '' ) {
+               wfDeprecated( __METHOD__ );
+               if( $hint === '' ) {
+                       # No way to pass an actual empty $hint here!  The new interface al-
+                       # lows this, so we have to do this for compatibility.
+                       $hint = null;
+               }
+               return $this->doEditSectionLink( $nt, $section, $hint );
        }
 
        /**
-        * Implement editSectionLink and editSectionLinkForOther.
+        * Create a section edit link.  This supersedes editSectionLink() and
+        * editSectionLinkForOther().
         *
-        * @param $nt      Title object
-        * @param $section Integer, section number
-        * @param $hint    String, for HTML title attribute
-        * @param $hook    String, name of hook to run
-        * @return         String, HTML to use for edit link
+        * @param $nt      Title  The title being linked to (may not be the same as
+        *   $wgTitle, if the section is included from a template)
+        * @param $section string The designation of the section being pointed to,
+        *   to be included in the link, like "&section=$section"
+        * @param $tooltip string The tooltip to use for the link: will be escaped
+        *   and wrapped in the 'editsectionhint' message
+        * @return         string HTML to use for edit link
         */
-       protected function doEditSectionLink( Title $nt, $section, $hint, $hook ) {
-               global $wgContLang;
-               $editurl = '&section='.$section;
-               $url = $this->makeKnownLinkObj(
-                       $nt,
-                       htmlspecialchars(wfMsg('editsection')),
-                       'action=edit'.$editurl,
-                       '', '', '',  $hint
+       public function doEditSectionLink( Title $nt, $section, $tooltip = null ) {
+               $attribs = array();
+               if( !is_null( $tooltip ) ) {
+                       $attribs['title'] = wfMsg( 'editsectionhint', $tooltip );
+               }
+               $url = $this->link( $nt, wfMsg('editsection'),
+                       $attribs,
+                       array( 'action' => 'edit', 'section' => $section ),
+                       array( 'noclasses', 'known' )
                );
-               $result = null;
 
-               // The two hooks have slightly different interfaces . . .
-               if( $hook == 'EditSectionLink' ) {
-                       wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $hint, $url, &$result ) );
-               } elseif( $hook == 'EditSectionLinkForOther' ) {
-                       wfRunHooks( 'EditSectionLinkForOther', array( &$this, $nt, $section, $url, &$result ) );
+               # Run the old hook.  This takes up half of the function . . . hopefully
+               # we can rid of it someday.
+               $attribs = '';
+               if( $tooltip ) {
+                       $attribs = wfMsgHtml( 'editsectionhint', htmlspecialchars( $tooltip ) );
+                       $attribs = " title=\"$attribs\"";
                }
-
-               // For reverse compatibility, add the brackets *after* the hook is run,
-               // and even add them to hook-provided text.
-               if( is_null( $result ) ) {
+               $result = null;
+               wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $attribs, $url, &$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 );
-               } else {
-                       $result = wfMsgHtml( 'editsection-brackets', $result );
+                       return "<span class=\"editsection\">$result</span>";
                }
-               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 = "<span class=\"editsection\">$result</span>";
+
+               wfRunHooks( 'DoEditSectionLink', array( $this, $nt, $section, $tooltip, &$result ) );
+               return $result;
        }
 
        /**
@@ -1398,14 +1560,17 @@ class Linker {
        public function buildRollbackLink( $rev ) {
                global $wgRequest, $wgUser;
                $title = $rev->getTitle();
-               $extra  = $wgRequest->getBool( 'bot' ) ? '&bot=1' : '';
-               $extra .= '&token=' . urlencode( $wgUser->editToken( array( $title->getPrefixedText(),
-                       $rev->getUserText() ) ) );
-               return $this->makeKnownLinkObj(
-                       $title,
-                       wfMsgHtml( 'rollbacklink' ),
-                       'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extra
+               $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, array( 'known', 'noclasses' ) );
        }
 
        /**
@@ -1453,7 +1618,7 @@ class Linker {
                                } else {
                                        $protected = '';
                                }
-                               $outText .= '<li>' . $sk->makeLinkObj( $titleObj ) . ' ' . $protected . '</li>';
+                               $outText .= '<li>' . $sk->link( $titleObj ) . ' ' . $protected . '</li>';
                        }
                        $outText .= '</ul>';
                }
@@ -1482,7 +1647,7 @@ class Linker {
                        $outText .= '</div><ul>';
 
                        foreach ( $hiddencats as $titleObj ) {
-                               $outText .= '<li>' . $sk->makeKnownLinkObj( $titleObj ) . '</li>'; # If it's hidden, it must exist - no need to check with a LinkBatch
+                               $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 .= '</ul>';
                }