* Added redirect to section feature. Use it wisely.
authorTim Starling <tstarling@users.mediawiki.org>
Fri, 8 Dec 2006 06:09:15 +0000 (06:09 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Fri, 8 Dec 2006 06:09:15 +0000 (06:09 +0000)
* Added a configuration variable allowing the "break out of framesets" feature to be switched on and off. Off by default -- there's all sorts of flashy frameset gadgets around, outright abuse is more rare, especially for small default installs.
* Refactored URL fragment handling.
* Made Title::secureAndSplit() slightly more legible.
* Refactored makeGlobalVariablesScript() -- it's not particularly convenient or maintainable to pass all global variables through the template array. The array is there for BC, not flexibility.

RELEASE-NOTES
includes/Article.php
includes/DefaultSettings.php
includes/Linker.php
includes/OutputPage.php
includes/Skin.php
includes/Title.php
includes/Xml.php
skins/common/wikibits.js

index 0efdf9a..0157af6 100644 (file)
@@ -250,6 +250,10 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
 * Enable QueryPage classes to override list formatting
 * (bug 5485) Show number of intervening revisions in diff view
 * (bug 8100) Fix XHTML validity in Taiwanese localization
+* Added redirect to section feature. Use it wisely.
+* Added a configuration variable allowing the "break out of framesets" feature 
+  to be switched on and off ($wgBreakFrames). Off by default.
+
 
 == Languages updated ==
 
index f998147..edf7aea 100644 (file)
@@ -691,6 +691,12 @@ class Article {
                                $redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' );
                                $s = wfMsg( 'redirectedfrom', $redir );
                                $wgOut->setSubtitle( $s );
+
+                               // Set the fragment if one was specified in the redirect
+                               if ( strval( $this->mTitle->getFragment() ) != '' ) {
+                                       $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() );
+                                       $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" );
+                               }
                                $wasRedirected = true;
                        }
                } elseif ( !empty( $rdfrom ) ) {
@@ -778,7 +784,7 @@ class Article {
                                if( !$wasRedirected && $this->isCurrent() ) {
                                        $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) );
                                }
-                               $link = $sk->makeLinkObj( $rt );
+                               $link = $sk->makeLinkObj( $rt, $rt->getFullText() );
 
                                $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT" />' .
                                  '<span class="redirectText">'.$link.'</span>' );
@@ -2666,7 +2672,7 @@ class Article {
        public static function getRedirectAutosummary( $text ) {
                $rt = Title::newFromRedirect( $text );
                if( is_object( $rt ) )
-                       return wfMsgForContent( 'autoredircomment', $rt->getPrefixedText() );
+                       return wfMsgForContent( 'autoredircomment', $rt->getFullText() );
                else
                        return '';
        }
index 36255bd..a972ad2 100644 (file)
@@ -2334,4 +2334,18 @@ $wgParserTestFiles = array(
        "$IP/maintenance/parserTests.txt",
 );
 
+/**
+ * Break out of framesets. This can be used to prevent external sites from
+ * framing your site with ads.  
+ */
+$wgBreakFrames = false;
+
+/**
+ * Break frameset exception list
+ */
+$wgBreakFramesExceptions = array(
+       'babelfish.altavista.com',
+       'translate.google.com',
+);
+
 ?>
index f7b5c0a..d74770c 100644 (file)
@@ -213,22 +213,6 @@ class Linker {
                                        $trail = $m[2];
                                }
                        }
-
-                       # Check for anchors, normalize the anchor
-
-                       $parts = explode( '#', $u, 2 );
-                       if ( count( $parts ) == 2 ) {
-                               $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace(' ', '_', $parts[1] ) ) );
-                               $replacearray = array(
-                                       '%3A' => ':',
-                                       '%' => '.'
-                               );
-                               $u = $parts[0] . '#' .
-                                    str_replace( array_keys( $replacearray ),
-                                                array_values( $replacearray ),
-                                                $anchor );
-                       }
-
                        $t = "<a href=\"{$u}\"{$style}>{$text}{$inside}</a>";
 
                        wfProfileOut( $fname );
@@ -307,12 +291,7 @@ class Linker {
                                        $text = htmlspecialchars( $nt->getFragment() );
                                }
                        }
-                       $anchor = urlencode( Sanitizer::decodeCharReferences( str_replace( ' ', '_', $nt->getFragment() ) ) );
-                       $replacearray = array(
-                               '%3A' => ':',
-                               '%' => '.'
-                       );
-                       $u .= '#' . str_replace(array_keys($replacearray),array_values($replacearray),$anchor);
+                       $u .= $nt->getFragmentForURL();
                }
                if ( $text == '' ) {
                        $text = htmlspecialchars( $nt->getPrefixedText() );
index 7cfd108..bbb068b 100644 (file)
@@ -72,6 +72,16 @@ class OutputPage {
        function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); }
        function addKeyword( $text ) { array_push( $this->mKeywords, $text ); }
        function addScript( $script ) { $this->mScripts .= $script; }
+
+       /**
+        * Add a self-contained script tag with the given contents
+        * @param string $script JavaScript text, no <script> tags
+        */
+       function addInlineScript( $script ) {
+               global $wgJsMimeType;
+               $this->mScripts .= "<script type=\"$wgJsMimeType\"><!--\n$script\n--></script>";
+       }
+
        function getScript() { return $this->mScripts; }
 
        function setETag($tag) { $this->mETag = $tag; }
index 28945a7..982d842 100644 (file)
@@ -270,60 +270,61 @@ class Skin extends Linker {
                $out->out( "\n</body></html>" );
        }
 
-       static function makeGlobalVariablesScript( $data ) {
-               $r = '<script type= "' . $data['jsmimetype'] . '">
-                       var skin = "' . Xml::escapeJsString( $data['skinname'] ) . '";
-                       var stylepath = "' . Xml::escapeJsString( $data['stylepath'] ) . '";
-
-                       var wgArticlePath = "' . Xml::escapeJsString( $data['articlepath'] ) . '";
-                       var wgScriptPath = "' . Xml::escapeJsString( $data['scriptpath'] ) . '";
-                       var wgServer = "' . Xml::escapeJsString( $data['serverurl'] ) . '";
-
-                       var wgCanonicalNamespace = "' . Xml::escapeJsString( $data['nscanonical'] ) . '";
-                       var wgNamespaceNumber = ' . (int)$data['nsnumber'] . ';
-                       var wgPageName = "' . Xml::escapeJsString( $data['titleprefixeddbkey'] ) . '";
-                       var wgTitle = "' . Xml::escapeJsString( $data['titletext'] ) . '";
-                       var wgArticleId = ' . (int)$data['articleid'] . ';
-                       var wgCurRevisionId = ' . ( int ) $data['currevisionid'] . ';
-                       var wgIsArticle = ' . ( $data['isarticle'] ? 'true' : 'false' ) . ';
-               
-                       var wgUserName = ' . ( $data['username'] == NULL ? 'null' : ( '"' . Xml::escapeJsString( $data['username'] ) . '"' ) ) . ';
-                       var wgUserLanguage = "' . Xml::escapeJsString( $data['userlang'] ) . '";
-                       var wgContentLanguage = "' . Xml::escapeJsString( $data['lang'] ) . '";
-               </script>
-               ';
+       static function makeVariablesScript( $data ) {
+               global $wgJsMimeType;
+
+               $r = "<script type= \"$wgJsMimeType\"><!--\n";
+               foreach ( $data as $name => $value ) {
+                       $encValue = Xml::encodeJsVar( $value );
+                       $r .= "var $name = $encValue;\n";
+               }
+               $r .= "--></script>\n";
 
                return $r;
        }
 
-       function getHeadScripts() {
-               global $wgStylePath, $wgUser, $wgAllowUserJs, $wgJsMimeType, $wgStyleVersion;
+       /**
+        * Make a <script> tag containing global variables
+        * @param array $data Associative array containing one element:
+        *     skinname => the skin name
+        * The odd calling convention is for backwards compatibility
+        */
+       static function makeGlobalVariablesScript( $data ) {
+               global $wgStylePath, $wgUser;
                global $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgLang;
                global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle;
+               global $wgBreakFrames, $wgBreakFramesExceptions;
 
                $ns = $wgTitle->getNamespace();
                $nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText();
                
                $vars = array( 
-                       'jsmimetype' => $wgJsMimeType,
-                       'skinname' => $this->getSkinName(),
+                       'skin' => $data['skinname'],
                        'stylepath' => $wgStylePath,
-                       'articlepath' => $wgArticlePath,
-                       'scriptpath' => $wgScriptPath,
-                       'serverurl' => $wgServer,
-                       'nscanonical' => $nsname,
-                       'nsnumber' => $wgTitle->getNamespace(),
-                       'titleprefixeddbkey' => $wgTitle->getPrefixedDBKey(),
-                       'titletext' => $wgTitle->getText(),
-                       'articleid' => $wgTitle->getArticleId(),
-                       'currevisionid' => isset( $wgArticle ) ? $wgArticle->getLatest() : 0,
-                       'isarticle' => $wgOut->isArticle(),
-                       'username' => $wgUser->isAnon() ? NULL : $wgUser->getName(),
-                       'userlang' => $wgLang->getCode(),
-                       'lang' => $wgContLang->getCode(),
+                       'wgArticlePath' => $wgArticlePath,
+                       'wgScriptPath' => $wgScriptPath,
+                       'wgServer' => $wgServer,
+                       'wgCanonicalNamespace' => $nsname,
+                       'wgNamespaceNumber' => $wgTitle->getNamespace(),
+                       'wgPageName' => $wgTitle->getPrefixedDBKey(),
+                       'wgTitle' => $wgTitle->getText(),
+                       'wgArticleId' => $wgTitle->getArticleId(),
+                       'wgIsArticle' => $wgOut->isArticle(),
+                       'wgUserName' => $wgUser->isAnon() ? NULL : $wgUser->getName(),
+                       'wgUserLanguage' => $wgLang->getCode(),
+                       'wgContentLanguage' => $wgContLang->getCode(),
+                       'wgBreakFrames' => $wgBreakFrames,
+                       'wgBreakFramesExceptions' => $wgBreakFramesExceptions,
+                       'wgCurRevisionId' => isset( $wgArticle ) ? $wgArticle->getLatest() : 0,
                );
 
-               $r = self::makeGlobalVariablesScript( $vars );
+               return self::makeVariablesScript( $vars );
+       }
+
+       function getHeadScripts() {
+               global $wgStylePath, $wgUser, $wgAllowUserJs, $wgJsMimeType, $wgStyleVersion;
+
+               $r = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) );
 
                $r .= "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js?$wgStyleVersion\"></script>\n";
                global $wgUseSiteJs;
index d836d14..e564eb6 100644 (file)
@@ -567,6 +567,19 @@ class Title {
                }
        }
 
+       /**
+        * Escape a text fragment, say from a link, for a URL
+        */
+       static function escapeFragmentForURL( $fragment ) {
+               $fragment = trim( str_replace( ' ', '_', $fragment ), '_' );
+               $fragment = urlencode( Sanitizer::decodeCharReferences( $fragment ) );
+               $replaceArray = array(
+                       '%3A' => ':',
+                       '%' => '.'
+               );
+               return strtr( $fragment, $replaceArray );
+       }
+
 #----------------------------------------------------------------------------
 #      Other stuff
 #----------------------------------------------------------------------------
@@ -639,11 +652,24 @@ class Title {
         */
        function getInterwiki() { return $this->mInterwiki; }
        /**
-        * Get the Title fragment (i.e. the bit after the #)
+        * Get the Title fragment (i.e. the bit after the #) in text form
         * @return string
         * @access public
         */
        function getFragment() { return $this->mFragment; }
+       /**
+        * Get the fragment in URL form, including the "#" character if there is one
+        *
+        * @return string
+        * @access public
+        */
+       function getFragmentForURL() {
+               if ( $this->mFragment == '' ) {
+                       return '';
+               } else {
+                       return '#' . Title::escapeFragmentForURL( $this->mFragment );
+               }
+       }
        /**
         * Get the default namespace index, for when there is no namespace
         * @return int
@@ -803,9 +829,7 @@ class Title {
                }
 
                # Finally, add the fragment.
-               if ( '' != $this->mFragment ) {
-                       $url .= '#' . $this->mFragment;
-               }
+               $url .= $this->getFragmentForURL();
 
                wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
                return $url;
@@ -1442,41 +1466,41 @@ class Title {
 
                # Clean up whitespace
                #
-               $t = preg_replace( '/[ _]+/', '_', $this->mDbkeyform );
-               $t = trim( $t, '_' );
+               $dbkey = preg_replace( '/[ _]+/', '_', $this->mDbkeyform );
+               $dbkey = trim( $dbkey, '_' );
 
-               if ( '' == $t ) {
+               if ( '' == $dbkey ) {
                        return false;
                }
 
-               if( false !== strpos( $t, UTF8_REPLACEMENT ) ) {
+               if( false !== strpos( $dbkey, UTF8_REPLACEMENT ) ) {
                        # Contained illegal UTF-8 sequences or forbidden Unicode chars.
                        return false;
                }
 
-               $this->mDbkeyform = $t;
+               $this->mDbkeyform = $dbkey;
 
                # Initial colon indicates main namespace rather than specified default
                # but should not create invalid {ns,title} pairs such as {0,Project:Foo}
-               if ( ':' == $t{0} ) {
+               if ( ':' == $dbkey{0} ) {
                        $this->mNamespace = NS_MAIN;
-                       $t = substr( $t, 1 ); # remove the colon but continue processing
+                       $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing
                }
 
                # Namespace or interwiki prefix
                $firstPass = true;
                do {
                        $m = array();
-                       if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $t, $m ) ) {
+                       if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $dbkey, $m ) ) {
                                $p = $m[1];
                                $lowerNs = $wgContLang->lc( $p );
                                if ( $ns = Namespace::getCanonicalIndex( $lowerNs ) ) {
                                        # Canonical namespace
-                                       $t = $m[2];
+                                       $dbkey = $m[2];
                                        $this->mNamespace = $ns;
                                } elseif ( $ns = $wgContLang->getNsIndex( $lowerNs )) {
                                        # Ordinary namespace
-                                       $t = $m[2];
+                                       $dbkey = $m[2];
                                        $this->mNamespace = $ns;
                                } elseif( $this->getInterwikiLink( $p ) ) {
                                        if( !$firstPass ) {
@@ -1486,12 +1510,12 @@ class Title {
                                        }
 
                                        # Interwiki link
-                                       $t = $m[2];
+                                       $dbkey = $m[2];
                                        $this->mInterwiki = $wgContLang->lc( $p );
 
                                        # Redundant interwiki prefix to the local wiki
                                        if ( 0 == strcasecmp( $this->mInterwiki, $wgLocalInterwiki ) ) {
-                                               if( $t == '' ) {
+                                               if( $dbkey == '' ) {
                                                        # Can't have an empty self-link
                                                        return false;
                                                }
@@ -1503,9 +1527,9 @@ class Title {
 
                                        # If there's an initial colon after the interwiki, that also
                                        # resets the default namespace
-                                       if ( $t !== '' && $t[0] == ':' ) {
+                                       if ( $dbkey !== '' && $dbkey[0] == ':' ) {
                                                $this->mNamespace = NS_MAIN;
-                                               $t = substr( $t, 1 );
+                                               $dbkey = substr( $dbkey, 1 );
                                        }
                                }
                                # If there's no recognized interwiki or namespace,
@@ -1513,25 +1537,24 @@ class Title {
                        }
                        break;
                } while( true );
-               $r = $t;
 
                # We already know that some pages won't be in the database!
                #
                if ( '' != $this->mInterwiki || NS_SPECIAL == $this->mNamespace ) {
                        $this->mArticleID = 0;
                }
-               $f = strstr( $r, '#' );
-               if ( false !== $f ) {
-                       $this->mFragment = substr( $f, 1 );
-                       $r = substr( $r, 0, strlen( $r ) - strlen( $f ) );
+               $fragment = strstr( $dbkey, '#' );
+               if ( false !== $fragment ) {
+                       $this->setFragment( $fragment );
+                       $dbkey = substr( $dbkey, 0, strlen( $dbkey ) - strlen( $fragment ) );
                        # remove whitespace again: prevents "Foo_bar_#"
                        # becoming "Foo_bar_"
-                       $r = preg_replace( '/_*$/', '', $r );
+                       $dbkey = preg_replace( '/_*$/', '', $dbkey );
                }
 
                # Reject illegal characters.
                #
-               if( preg_match( $rxTc, $r ) ) {
+               if( preg_match( $rxTc, $dbkey ) ) {
                        return false;
                }
 
@@ -1540,19 +1563,26 @@ class Title {
                 * often be unreachable due to the way web browsers deal
                 * with 'relative' URLs. Forbid them explicitly.
                 */
-               if ( strpos( $r, '.' ) !== false &&
-                    ( $r === '.' || $r === '..' ||
-                      strpos( $r, './' ) === 0  ||
-                      strpos( $r, '../' ) === 0 ||
-                      strpos( $r, '/./' ) !== false ||
-                      strpos( $r, '/../' ) !== false ) )
+               if ( strpos( $dbkey, '.' ) !== false &&
+                    ( $dbkey === '.' || $dbkey === '..' ||
+                      strpos( $dbkey, './' ) === 0  ||
+                      strpos( $dbkey, '../' ) === 0 ||
+                      strpos( $dbkey, '/./' ) !== false ||
+                      strpos( $dbkey, '/../' ) !== false ) )
                {
                        return false;
                }
 
-               # We shouldn't need to query the DB for the size.
-               #$maxSize = $dbr->textFieldSize( 'page', 'page_title' );
-               if ( strlen( $r ) > 255 ) {
+               /**
+                * Limit the size of titles to 255 bytes.
+                * This is typically the size of the underlying database field.
+                * We make an exception for special pages, which don't need to be stored
+                * in the database, and may edge over 255 bytes due to subpage syntax 
+                * for long titles, e.g. [[Special:Block/Long name]]
+                */
+               if ( ( $this->mNamespace != NS_SPECIAL && strlen( $dbkey ) > 255 ) ||
+                 strlen( $dbkey ) > 512 ) 
+               {
                        return false;
                }
 
@@ -1565,9 +1595,7 @@ class Title {
                 * site might be case-sensitive.
                 */
                if( $wgCapitalLinks && $this->mInterwiki == '') {
-                       $t = $wgContLang->ucfirst( $r );
-               } else {
-                       $t = $r;
+                       $dbkey = $wgContLang->ucfirst( $dbkey );
                }
 
                /**
@@ -1575,26 +1603,39 @@ class Title {
                 * "empty" local links can only be self-links
                 * with a fragment identifier.
                 */
-               if( $t == '' &&
+               if( $dbkey == '' &&
                        $this->mInterwiki == '' &&
                        $this->mNamespace != NS_MAIN ) {
                        return false;
                }
 
                // Any remaining initial :s are illegal.
-               if ( $t !== '' && ':' == $t{0} ) {
+               if ( $dbkey !== '' && ':' == $dbkey{0} ) {
                        return false;
                }
                
                # Fill fields
-               $this->mDbkeyform = $t;
-               $this->mUrlform = wfUrlencode( $t );
+               $this->mDbkeyform = $dbkey;
+               $this->mUrlform = wfUrlencode( $dbkey );
 
-               $this->mTextform = str_replace( '_', ' ', $t );
+               $this->mTextform = str_replace( '_', ' ', $dbkey );
 
                return true;
        }
 
+       /**
+        * Set the fragment for this title
+        * This is kind of bad, since except for this rarely-used function, Title objects
+        * are immutable. The reason this is here is because it's better than setting the 
+        * members directly, which is what Linker::formatComment was doing previously.
+        *
+        * @param string $fragment text
+        * @access kind of public
+        */
+       function setFragment( $fragment ) {
+               $this->mFragment = trim( str_replace( '_', ' ', substr( $fragment, 1 ) ), ' ' );
+       }
+
        /**
         * Get a Title object associated with the talk page of this article
         * @return Title the object for the talk page
index 202f990..509ed9a 100644 (file)
@@ -254,6 +254,33 @@ class Xml {
                return strtr( $string, $pairs );
        }
 
+       /**
+        * Encode a variable of unknown type to JavaScript.
+        * Doesn't support hashtables just yet.
+        */
+       public static function encodeJsVar( $value ) {
+               if ( is_bool( $value ) ) {
+                       $s = $value ? 'true' : 'false';
+               } elseif ( is_null( $value ) ) {
+                       $s = 'null';
+               } elseif ( is_int( $value ) ) {
+                       $s = $value;
+               } elseif ( is_array( $value ) ) {
+                       $s = '[';
+                       foreach ( $value as $name => $elt ) {
+                               if ( $s != '[' ) {
+                                       $s .= ', ';
+                               }
+                               $s .= self::encodeJsVar( $elt );
+                       }
+                       $s .= ']';
+               } else {
+                       $s = '"' . self::escapeJsString( $value ) . '"';
+               }
+               return $s;
+       }
+       
+
        /**
         * Check if a string is well-formed XML.
         * Must include the surrounding tag.
index d20ca1c..fe0278a 100644 (file)
@@ -47,9 +47,12 @@ if (typeof stylepath != 'undefined' && typeof skin != 'undefined') {
                document.write('<link rel="stylesheet" type="text/css" href="'+stylepath+'/'+skin+'/KHTMLFixes.css">');
        }
 }
-// Un-trap us from framesets
-if (window.top != window) {
-       window.top.location = window.location;
+
+if (wgBreakFrames) {
+       // Un-trap us from framesets
+       if (window.top != window && wgBreakFramesExceptions.indexOf(window.top.location.hostname) == -1) {
+               window.top.location = window.location;
+       }
 }
 
 // for enhanced RecentChanges
@@ -843,6 +846,18 @@ function sortableTables() {
        }
 }
 
+function redirectToFragment(fragment) {
+       if (is_gecko) {
+               // Mozilla needs to wait until after load, otherwise the window doesn't scroll
+               addOnloadHook(function () {
+                       if (window.location.hash == "")
+                               window.location.hash = fragment;
+               });
+       } else {
+               if (window.location.hash == "")
+                       window.location.hash = fragment;
+       }
+}
 
 function runOnloadHook() {
        // don't run anything below this for non-dom browsers