Tweak r41788 - Use findFile() to check current version if $time given
[lhc/web/wiklou.git] / includes / GlobalFunctions.php
index f5a2660..0c25e05 100644 (file)
@@ -12,6 +12,9 @@ require_once dirname(__FILE__) . '/LogPage.php';
 require_once dirname(__FILE__) . '/normal/UtfNormalUtil.php';
 require_once dirname(__FILE__) . '/XmlFunctions.php';
 
+// Hide compatibility functions from Doxygen
+/// @cond
+
 /**
  * Compatibility functions
  *
@@ -87,6 +90,9 @@ if ( !function_exists( 'array_diff_key' ) ) {
        }
 }
 
+/// @endcond
+
+
 /**
  * Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
  */
@@ -145,16 +151,31 @@ function wfRandom() {
 }
 
 /**
- * We want / and : to be included as literal characters in our title URLs.
+ * We want some things to be included as literal characters in our title URLs
+ * for prettiness, which urlencode encodes by default.  According to RFC 1738,
+ * all of the following should be safe:
+ *
+ * ;:@&=$-_.+!*'(),
+ *
+ * But + is not safe because it's used to indicate a space; &= are only safe in
+ * paths and not in queries (and we don't distinguish here); ' seems kind of
+ * scary; and urlencode() doesn't touch -_. to begin with.  Plus, although /
+ * is reserved, we don't care.  So the list we unescape is:
+ *
+ * ;:@$!*(),/
+ *
  * %2F in the page titles seems to fatally break for some reason.
  *
  * @param $s String:
  * @return string
 */
-function wfUrlencode ( $s ) {
+function wfUrlencode( $s ) {
        $s = urlencode( $s );
-       $s = preg_replace( '/%3[Aa]/', ':', $s );
-       $s = preg_replace( '/%2[Ff]/', '/', $s );
+       $s = str_ireplace(
+               array( '%3B','%3A','%40','%24','%21','%2A','%28','%29','%2C','%2F' ),
+               array(   ';',  ':',  '@',  '$',  '!',  '*',  '(',  ')',  ',',  '/' ),
+               $s
+       );
 
        return $s;
 }
@@ -210,6 +231,20 @@ function wfDebug( $text, $logonly = false ) {
        }
 }
 
+/**
+ * Send a line giving PHP memory usage.
+ * @param $exact Bool : print exact values instead of kilobytes (default: false)
+ */
+function wfDebugMem( $exact = false ) {
+       $mem = memory_get_usage();
+       if( !$exact ) {
+               $mem = floor( $mem / 1024 ) . ' kilobytes';
+       } else {
+               $mem .= ' bytes';
+       }
+       wfDebug( "Memory usage: $mem\n" );
+}
+
 /**
  * Send a line to a supplementary debug log file, if configured, or main debug log if not.
  * $wgDebugLogGroups[$logGroup] should be set to a filename to send to a separate log.
@@ -220,12 +255,17 @@ function wfDebug( $text, $logonly = false ) {
  *                     log file is specified, (default true)
  */
 function wfDebugLog( $logGroup, $text, $public = true ) {
-       global $wgDebugLogGroups;
+       global $wgDebugLogGroups, $wgShowHostnames;
        if( $text{strlen( $text ) - 1} != "\n" ) $text .= "\n";
        if( isset( $wgDebugLogGroups[$logGroup] ) ) {
                $time = wfTimestamp( TS_DB );
                $wiki = wfWikiID();
-               wfErrorLog( "$time $wiki: $text", $wgDebugLogGroups[$logGroup] );
+               if ( $wgShowHostnames ) {
+                       $host = wfHostname();
+               } else {
+                       $host = '';
+               }
+               wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] );
        } else if ( $public === true ) {
                wfDebug( $text, true );
        }
@@ -245,16 +285,50 @@ function wfLogDBError( $text ) {
 }
 
 /**
- * Log to a file without getting "file size exceeded" signals
+ * Log to a file without getting "file size exceeded" signals.
+ * 
+ * Can also log to TCP or UDP with the syntax udp://host:port/prefix. This will 
+ * send lines to the specified port, prefixed by the specified prefix and a space.
  */
 function wfErrorLog( $text, $file ) {
-       wfSuppressWarnings();
-       $exists = file_exists( $file );
-       $size = $exists ? filesize( $file ) : false;
-       if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) {
-               error_log( $text, 3, $file );
+       if ( substr( $file, 0, 4 ) == 'udp:' ) {
+               if ( preg_match( '!^(tcp|udp):(?://)?\[([0-9a-fA-F:]+)\]:(\d+)(?:/(.*))?$!', $file, $m ) ) {
+                       // IPv6 bracketed host
+                       $protocol = $m[1];
+                       $host = $m[2];
+                       $port = $m[3];
+                       $prefix = isset( $m[4] ) ? $m[4] : false;
+               } elseif ( preg_match( '!^(tcp|udp):(?://)?([a-zA-Z0-9.-]+):(\d+)(?:/(.*))?$!', $file, $m ) ) {
+                       $protocol = $m[1];
+                       $host = $m[2];
+                       $port = $m[3];
+                       $prefix = isset( $m[4] ) ? $m[4] : false;
+               } else {
+                       throw new MWException( __METHOD__.": Invalid UDP specification" );
+               }
+               // Clean it up for the multiplexer
+               if ( strval( $prefix ) !== '' ) {
+                       $text = preg_replace( '/^/m', $prefix . ' ', $text );
+                       if ( substr( $text, -1 ) != "\n" ) {
+                               $text .= "\n";
+                       }
+               }
+
+               $sock = fsockopen( "$protocol://$host", $port );
+               if ( !$sock ) {
+                       return;
+               }
+               fwrite( $sock, $text );
+               fclose( $sock );
+       } else {
+               wfSuppressWarnings();
+               $exists = file_exists( $file );
+               $size = $exists ? filesize( $file ) : false;
+               if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) {
+                       error_log( $text, 3, $file );
+               }
+               wfRestoreWarnings();
        }
-       wfRestoreWarnings();
 }
 
 /**
@@ -319,6 +393,47 @@ function wfReadOnlyReason() {
        return $wgReadOnly;
 }
 
+/**
+ * Return a Language object from $langcode
+ * @param $langcode Mixed: either:
+ *                  - a Language object
+ *                  - code of the language to get the message for, if it is
+ *                    a valid code create a language for that language, if
+ *                    it is a string but not a valid code then make a basic
+ *                    language object
+ *                  - a boolean: if it's false then use the current users
+ *                    language (as a fallback for the old parameter
+ *                    functionality), or if it is true then use the wikis
+ * @return Language object
+ */
+function wfGetLangObj( $langcode = false ){
+       # Identify which language to get or create a language object for.
+       if( $langcode instanceof Language )
+               # Great, we already have the object!
+               return $langcode;
+               
+       global $wgContLang;
+       if( $langcode === $wgContLang->getCode() || $langcode === true )
+               # $langcode is the language code of the wikis content language object.
+               # or it is a boolean and value is true
+               return $wgContLang;
+       
+       global $wgLang;
+       if( $langcode === $wgLang->getCode() || $langcode === false )
+               # $langcode is the language code of user language object.
+               # or it was a boolean and value is false
+               return $wgLang;
+
+       $validCodes = array_keys( Language::getLanguageNames() );
+       if( in_array( $langcode, $validCodes ) )
+               # $langcode corresponds to a valid language.
+               return Language::factory( $langcode );
+
+       # $langcode is a string, but not a valid language code; use content language.
+       wfDebug( 'Invalid language code passed to wfGetLangObj, falling back to content language.' );
+       return $wgContLang;
+}
+
 /**
  * Get a message from anywhere, for the current user language.
  *
@@ -458,7 +573,7 @@ function wfMsgWeirdKey ( $key ) {
  * @private
  */
 function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) {
-       global $wgParser, $wgContLang, $wgMessageCache, $wgLang;
+       global $wgContLang, $wgMessageCache;
 
        wfRunHooks('NormalizeMessageKey', array(&$key, &$useDB, &$langCode, &$transform));
        
@@ -469,21 +584,7 @@ function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) {
                        $message = $wgMessageCache->transform( $message );
                }
        } else {
-               if( $langCode === true ) {
-                       $lang = &$wgContLang;
-               } elseif( $langCode === false ) {
-                       $lang = &$wgLang;
-               } else {
-                       $validCodes = array_keys( Language::getLanguageNames() );
-                       if( in_array( $langCode, $validCodes ) ) {
-                               # $langcode corresponds to a valid language.
-                               $lang = Language::factory( $langCode );
-                       } else {
-                               # $langcode is a string, but not a valid language code; use content language.
-                               $lang =& $wgContLang;
-                               wfDebug( 'Invalid language code passed to wfMsgGetKey, falling back to content language.' );
-                       }
-               }
+               $lang = wfGetLangObj( $langCode );
 
                # MessageCache::get() does this already, Language::getMessage() doesn't
                # ISSUE: Should we try to handle "message/lang" here too?
@@ -565,16 +666,19 @@ function wfMsgWikiHtml( $key ) {
 /**
  * Returns message in the requested format
  * @param string $key Key of the message
- * @param array $options Processing rules:
- *  <i>parse</i>: parses wikitext to html
- *  <i>parseinline</i>: parses wikitext to html and removes the surrounding p's added by parser or tidy
- *  <i>escape</i>: filters message through htmlspecialchars
- *  <i>escapenoentities</i>: same, but allows entity references like &nbsp; through
- *  <i>replaceafter</i>: parameters are substituted after parsing or escaping
- *  <i>parsemag</i>: transform the message using magic phrases
- *  <i>content</i>: fetch message for content language instead of interface
- *  <i>language</i>: language code to fetch message for (overriden by <i>content</i>), its behaviour
- *                   with parser, parseinline and parsemag is undefined.
+ * @param array $options Processing rules.  Can take the following options:
+ *   <i>parse</i>: parses wikitext to html
+ *   <i>parseinline</i>: parses wikitext to html and removes the surrounding
+ *       p's added by parser or tidy
+ *   <i>escape</i>: filters message through htmlspecialchars
+ *   <i>escapenoentities</i>: same, but allows entity references like &nbsp; through
+ *   <i>replaceafter</i>: parameters are substituted after parsing or escaping
+ *   <i>parsemag</i>: transform the message using magic phrases
+ *   <i>content</i>: fetch message for content language instead of interface
+ * Also can accept a single associative argument, of the form 'language' => 'xx':
+ *   <i>language</i>: Language object or language code to fetch message for
+ *       (overriden by <i>content</i>), its behaviour with parser, parseinline
+ *       and parsemag is undefined.
  * Behavior for conflicting options (e.g., parse+parseinline) is undefined.
  */
 function wfMsgExt( $key, $options ) {
@@ -583,9 +687,18 @@ function wfMsgExt( $key, $options ) {
        $args = func_get_args();
        array_shift( $args );
        array_shift( $args );
-
-       if( !is_array($options) ) {
-               $options = array($options);
+       $options = (array)$options;
+
+       foreach( $options as $arrayKey => $option ) {
+               if( !preg_match( '/^[0-9]+|language$/', $arrayKey ) ) {
+                       # An unknown index, neither numeric nor "language"
+                       trigger_error( "wfMsgExt called with incorrect parameter key $arrayKey", E_USER_WARNING );
+               } elseif( preg_match( '/^[0-9]+$/', $arrayKey ) && !in_array( $option,
+               array( 'parse', 'parseinline', 'escape', 'escapenoentities',
+               'replaceafter', 'parsemag', 'content' ) ) ) {
+                       # A numeric index with unknown value
+                       trigger_error( "wfMsgExt called with incorrect parameter $option", E_USER_WARNING );
+               }
        }
 
        if( in_array('content', $options) ) {
@@ -593,12 +706,7 @@ function wfMsgExt( $key, $options ) {
                $langCode = true;
        } elseif( array_key_exists('language', $options) ) {
                $forContent = false;
-               $langCode = $options['language'];
-               $validCodes = array_keys( Language::getLanguageNames() );
-               if( !in_array($options['language'], $validCodes) ) {
-                       # Fallback to en, instead of whatever interface language we might have
-                       $langCode = 'en';
-               }
+               $langCode = wfGetLangObj( $options['language'] );
        } else {
                $forContent = false;
                $langCode = false;
@@ -628,9 +736,7 @@ function wfMsgExt( $key, $options ) {
        if ( in_array('escape', $options) ) {
                $string = htmlspecialchars ( $string );
        } elseif ( in_array( 'escapenoentities', $options ) ) {
-               $string = htmlspecialchars( $string );
-               $string = str_replace( '&amp;', '&', $string );
-               $string = Sanitizer::normalizeCharReferences( $string );
+               $string = Sanitizer::escapeHtmlAllowEntities( $string );
        }
 
        if( in_array('replaceafter', $options) ) {
@@ -707,18 +813,25 @@ function wfDebugDieBacktrace( $msg = '' ) {
  * @return string
  */
 function wfHostname() {
-       if ( function_exists( 'posix_uname' ) ) {
-               // This function not present on Windows
-               $uname = @posix_uname();
-       } else {
-               $uname = false;
-       }
-       if( is_array( $uname ) && isset( $uname['nodename'] ) ) {
-               return $uname['nodename'];
-       } else {
-               # This may be a virtual server.
-               return $_SERVER['SERVER_NAME'];
+       static $host;
+       if ( is_null( $host ) ) {
+               if ( function_exists( 'posix_uname' ) ) {
+                       // This function not present on Windows
+                       $uname = @posix_uname();
+               } else {
+                       $uname = false;
+               }
+               if( is_array( $uname ) && isset( $uname['nodename'] ) ) {
+                       $host = $uname['nodename'];
+               } elseif ( getenv( 'COMPUTERNAME' ) ) {
+                       # Windows computer name
+                       $host = getenv( 'COMPUTERNAME' );
+               } else {
+                       # This may be a virtual server.
+                       $host = $_SERVER['SERVER_NAME'];
+               }
        }
+       return $host;
 }
 
        /**
@@ -1028,6 +1141,34 @@ function wfArrayToCGI( $array1, $array2 = NULL )
        return $cgi;
 }
 
+/**
+ * This is the logical opposite of wfArrayToCGI(): it accepts a query string as
+ * its argument and returns the same string in array form.  This allows compa-
+ * tibility with legacy functions that accept raw query strings instead of nice
+ * arrays.  Of course, keys and values are urldecode()d.  Don't try passing in-
+ * valid query strings, or it will explode.
+ *
+ * @param $query string Query string
+ * @return array Array version of input
+ */
+function wfCgiToArray( $query ) {
+       if( isset( $query[0] ) and $query[0] == '?' ) {
+               $query = substr( $query, 1 );
+       }
+       $bits = explode( '&', $query );
+       $ret = array();
+       foreach( $bits as $bit ) {
+               if( $bit === '' ) {
+                       continue;
+               }
+               list( $key, $value ) = explode( '=', $bit );
+               $key = urldecode( $key );
+               $value = urldecode( $value );
+               $ret[$key] = $value;
+       }
+       return $ret;
+}
+
 /**
  * Append a query string to an existing URL, which may or may not already
  * have query string parameters already. If so, they will be combined.
@@ -1074,9 +1215,14 @@ function wfPurgeSquidServers ($urlArr) {
 /**
  * Windows-compatible version of escapeshellarg()
  * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg()
- * function puts single quotes in regardless of OS
+ * function puts single quotes in regardless of OS.
+ *
+ * Also fixes the locale problems on Linux in PHP 5.2.6+ (bug backported to 
+ * earlier distro releases of PHP)
  */
 function wfEscapeShellArg( ) {
+       wfInitShellLocale();
+
        $args = func_get_args();
        $first = true;
        $retVal = '';
@@ -1127,7 +1273,7 @@ function wfMerge( $old, $mine, $yours, &$result ){
 
        # This check may also protect against code injection in
        # case of broken installations.
-       if(! file_exists( $wgDiff3 ) ){
+       if( !$wgDiff3 || !file_exists( $wgDiff3 ) ) {
                wfDebug( "diff3 not found\n" );
                return false;
        }
@@ -1317,6 +1463,7 @@ function wfResetOutputBuffers( $resetGzipEncoding=true ) {
                                // Reset the 'Content-Encoding' field set by this handler
                                // so we can start fresh.
                                header( 'Content-Encoding:' );
+                               break;
                        }
                }
        }
@@ -1643,7 +1790,7 @@ function swap( &$x, &$y ) {
 }
 
 function wfGetCachedNotice( $name ) {
-       global $wgOut, $parserMemc;
+       global $wgOut, $wgRenderHashAppend, $parserMemc;
        $fname = 'wfGetCachedNotice';
        wfProfileIn( $fname );
 
@@ -1665,7 +1812,9 @@ function wfGetCachedNotice( $name ) {
                }
        }
 
-       $cachedNotice = $parserMemc->get( wfMemcKey( $name ) );
+       // Use the extra hash appender to let eg SSL variants separately cache.
+       $key = wfMemcKey( $name . $wgRenderHashAppend );
+       $cachedNotice = $parserMemc->get( $key );
        if( is_array( $cachedNotice ) ) {
                if( md5( $notice ) == $cachedNotice['hash'] ) {
                        $notice = $cachedNotice['html'];
@@ -1679,7 +1828,7 @@ function wfGetCachedNotice( $name ) {
        if( $needParse ) {
                if( is_object( $wgOut ) ) {
                        $parsed = $wgOut->parse( $notice );
-                       $parserMemc->set( wfMemcKey( $name ), array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 );
+                       $parserMemc->set( $key, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 );
                        $notice = $parsed;
                } else {
                        wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' );
@@ -1713,22 +1862,64 @@ function wfGetNamespaceNotice() {
        return $namespaceNotice;
 }
 
+/**
+ * Gets and returns the site-wide notice
+ * @return $siteNotice The site-wide notice as set by $wgSiteNotice or MediaWiki:Sitenotice interface message
+ */
 function wfGetSiteNotice() {
-       global $wgUser, $wgSiteNotice;
+       global $wgUser, $wgMajorSiteNoticeID, $wgTitle;
+       global $wgCookiePrefix, $wgCookieExpiration, $wgCookiePath;
        $fname = 'wfGetSiteNotice';
        wfProfileIn( $fname );
        $siteNotice = '';
+       $loggedIn = false;
+       $spTitle = SpecialPage::getTitleFor( 'DismissNotice' );
+       $spUrl = $spTitle->escapeFullURL( array( 'returnto' => $wgTitle->getPrefixedURL() ) );
+       
+       if( $wgUser instanceOf User && $wgUser->isLoggedIn() ) {
+               $loggedIn = true;
+               $siteNotice = wfGetCachedNotice('sitenotice');
+               if($siteNotice === false)
+                       return '';
+       } else {
+               $siteNotice = wfGetCachedNotice('anonnotice');
+               if($siteNotice === false) {
+                       $siteNotice = wfGetCachedNotice('sitenotice');
+                       if($siteNotice === false)
+                               return '';
+               }
+       }
+       
+       $msgClose = wfMsg( 'sitenotice_close' );
+       $id = intval( $wgMajorSiteNoticeID ) . "." . intval( wfMsgForContent( 'sitenotice_id' ) );
 
        if( wfRunHooks( 'SiteNoticeBefore', array( &$siteNotice ) ) ) {
-               if( is_object( $wgUser ) && $wgUser->isLoggedIn() ) {
-                       $siteNotice = wfGetCachedNotice( 'sitenotice' );
-               } else {
-                       $anonNotice = wfGetCachedNotice( 'anonnotice' );
-                       if( !$anonNotice ) {
-                               $siteNotice = wfGetCachedNotice( 'sitenotice' );
-                       } else {
-                               $siteNotice = $anonNotice;
-                       }
+               if( $loggedIn ) {
+                       //it is hidden
+                       if( isset($_COOKIE[$wgCookiePrefix . 'DismissSiteNotice']) && $_COOKIE[$wgCookiePrefix . 'DismissSiteNotice'] == $id )
+                               return '';
+                       $siteNotice = <<<EOT
+<table width="100%" id="mw-dismissable-notice"><tr><td width="80%">$siteNotice</td>
+<td width="20%" align="right">[<a id="dismissLink" href="$spUrl">$msgClose</a>]</td></tr></table>
+<script type="text/javascript" language="JavaScript">
+<!--
+var cookieName = "{$wgCookiePrefix}DismissSiteNotice=";
+var cookiePos = document.cookie.indexOf(cookieName);
+var siteNoticeID = "{$id}";
+
+var dismissLink = document.getElementById('dismissLink');
+dismissLink.href = "javascript:dismissNotice();";
+
+function dismissNotice() {
+               var date = new Date();
+               date.setTime(date.getTime() + {$wgCookieExpiration});
+               document.cookie = cookieName + siteNoticeID + "; expires="+date.toGMTString() + "; path={$wgCookiePath}";
+               var element = document.getElementById('siteNotice');
+               element.parentNode.removeChild(element);
+}
+-->
+</script>
+EOT;
                }
                if( !$siteNotice ) {
                        $siteNotice = wfGetCachedNotice( 'default' );
@@ -2000,6 +2191,7 @@ function wfShellExec( $cmd, &$retval=null ) {
                $retval = 1;
                return "Unable to run external programs in safe mode.";
        }
+       wfInitShellLocale();
 
        if ( php_uname( 's' ) == 'Linux' ) {
                $time = intval( ini_get( 'max_execution_time' ) );
@@ -2024,8 +2216,26 @@ function wfShellExec( $cmd, &$retval=null ) {
        passthru( $cmd, $retval );
        $output = ob_get_contents();
        ob_end_clean();
+
+       if ( $retval == 127 ) {
+               wfDebugLog( 'exec', "Possibly missing executable file: $cmd\n" );
+       }
        return $output;
+}
 
+/**
+ * Workaround for http://bugs.php.net/bug.php?id=45132
+ * escapeshellarg() destroys non-ASCII characters if LANG is not a UTF-8 locale
+ */
+function wfInitShellLocale() {
+       static $done = false;
+       if ( $done ) return;
+       $done = true;
+       global $wgShellLocale;
+       if ( !wfIniGetBool( 'safe_mode' ) ) {
+               putenv( "LC_CTYPE=$wgShellLocale" );
+               setlocale( LC_CTYPE, $wgShellLocale );
+       }
 }
 
 /**
@@ -2540,7 +2750,7 @@ function wfSplitWikiID( $wiki ) {
  * will always return the same object, unless the underlying connection or load
  * balancer is manually destroyed.
  */
-function &wfGetDB( $db = DB_LAST, $groups = array(), $wiki = false ) {
+function &wfGetDB( $db, $groups = array(), $wiki = false ) {
        return wfGetLB( $wiki )->getConnection( $db, $groups, $wiki );
 }
 
@@ -2580,8 +2790,8 @@ function wfFindFile( $title, $time = false, $flags = 0 ) {
  * Get an object referring to a locally registered file.
  * Returns a valid placeholder object if the file does not exist.
  */
-function wfLocalFile( $title ) {
-       return RepoGroup::singleton()->getLocalRepo()->newFile( $title );
+function wfLocalFile( $title, $time = false ) {
+       return RepoGroup::singleton()->getLocalRepo()->findFile( $title, $time );
 }
 
 /**
@@ -2626,6 +2836,8 @@ function wfBoolToStr( $value ) {
  * @param string $extensionName Name of extension to load messages from\for.
  * @param string $langcode Language to load messages for, or false for default
  *                         behvaiour (en, content language and user language).
+ * @since r24808 (v1.11) Using this method of loading extension messages will not work
+ * on MediaWiki prior to that
  */
 function wfLoadExtensionMessages( $extensionName, $langcode = false ) {
        global $wgExtensionMessagesFiles, $wgMessageCache, $wgLang, $wgContLang;