(bug 32827) "[Regression] Block log for IP ranges not shown on Special:Block"
[lhc/web/wiklou.git] / includes / Export.php
index c64f0b4..35a1b5b 100644 (file)
@@ -41,6 +41,7 @@ class WikiExporter {
        const CURRENT = 2;
        const STABLE = 4; // extension defined
        const LOGS = 8;
+       const RANGE = 16;
 
        const BUFFER = 0;
        const STREAM = 1;
@@ -55,8 +56,9 @@ class WikiExporter {
         * make additional queries to pull source data while the
         * main query is still running.
         *
-        * @param $db Database
-        * @param $history Mixed: one of WikiExporter::FULL or WikiExporter::CURRENT,
+        * @param $db DatabaseBase
+        * @param $history Mixed: one of WikiExporter::FULL, WikiExporter::CURRENT,
+        *                 WikiExporter::RANGE or WikiExporter::STABLE,
         *                 or an associative array:
         *                   offset: non-inclusive offset at which to start the query
         *                   limit: maximum number of rows to return
@@ -119,6 +121,21 @@ class WikiExporter {
                return $this->dumpFrom( $condition );
        }
 
+       /**
+        * Dumps a series of page and revision records for those pages
+        * in the database with revisions falling within the rev_id range given.
+        * @param $start Int: inclusive lower limit (this id is included)
+        * @param $end   Int: Exclusive upper limit (this id is not included)
+        *                   If 0, no upper limit.
+        */
+       public function revsByRange( $start, $end ) {
+               $condition = 'rev_id >= ' . intval( $start );
+               if ( $end ) {
+                       $condition .= ' AND rev_id < ' . intval( $end );
+               }
+               return $this->dumpFrom( $condition );
+       }
+
        /**
         * @param $title Title
         */
@@ -259,6 +276,10 @@ class WikiExporter {
                                        wfProfileOut( __METHOD__ );
                                        throw new MWException( __METHOD__ . " given invalid history dump type." );
                                }
+                       } elseif ( $this->history & WikiExporter::RANGE ) {
+                               # Dump of revisions within a specified range
+                               $join['revision'] = array( 'INNER JOIN', 'page_id=rev_page' );
+                               $opts['ORDER BY'] = 'rev_page ASC, rev_id ASC';
                        } else {
                                # Uknown history specification parameter?
                                wfProfileOut( __METHOD__ );
@@ -354,14 +375,12 @@ class WikiExporter {
  * @ingroup Dump
  */
 class XmlDumpWriter {
-       protected $firstPageWritten = 0, $lastPageWritten = 0, $pageInProgress = 0;
-
        /**
         * Returns the export schema version.
         * @return string
         */
        function schemaVersion() {
-               return "0.5";
+               return "0.6";
        }
 
        /**
@@ -457,12 +476,23 @@ class XmlDumpWriter {
        function openPage( $row ) {
                $out = "  <page>\n";
                $title = Title::makeTitle( $row->page_namespace, $row->page_title );
-               $out .= '    ' . Xml::elementClean( 'title', array(), $title->getPrefixedText() ) . "\n";
+               $out .= '    ' . Xml::elementClean( 'title', array(), self::canonicalTitle( $title ) ) . "\n";
+               $out .= '    ' . Xml::element( 'ns', array(), strval( $row->page_namespace) ) . "\n";
                $out .= '    ' . Xml::element( 'id', array(), strval( $row->page_id ) ) . "\n";
-               $this->pageInProgress = $row->page_id;
                if ( $row->page_is_redirect ) {
-                       $out .= '    ' . Xml::element( 'redirect', array() ) . "\n";
+                       $page = WikiPage::factory( $title );
+                       $redirect = $page->getRedirectTarget();
+                       if ( $redirect instanceOf Title && $redirect->isValidRedirectTarget() ) {
+                               $out .= '    ' . Xml::element( 'redirect', array( 'title' => self::canonicalTitle( $redirect ) ) ) . "\n";
+                       }
+               }
+
+               if ( $row->rev_sha1 ) {
+                       $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
+               } else {
+                       $out .= "      <sha1/>\n";
                }
+
                if ( $row->page_restrictions != '' ) {
                        $out .= '    ' . Xml::element( 'restrictions', array(),
                                strval( $row->page_restrictions ) ) . "\n";
@@ -477,13 +507,10 @@ class XmlDumpWriter {
         * Closes a <page> section on the output stream.
         *
         * @access private
+        * @return string
         */
        function closePage() {
                return "  </page>\n";
-               if ( !$this->firstPageWritten ) {
-                       $this->firstPageWritten = $this->pageInProgress;
-               }
-               $this->lastPageWritten = $this->pageInProgress;
        }
 
        /**
@@ -524,12 +551,12 @@ class XmlDumpWriter {
                        // Raw text from the database may have invalid chars
                        $text = strval( Revision::getRevisionText( $row ) );
                        $out .= "      " . Xml::elementClean( 'text',
-                               array( 'xml:space' => 'preserve', 'bytes' => $row->rev_len ),
+                               array( 'xml:space' => 'preserve', 'bytes' => intval( $row->rev_len ) ),
                                strval( $text ) ) . "\n";
                } else {
                        // Stub output
                        $out .= "      " . Xml::element( 'text',
-                               array( 'id' => $row->rev_text_id, 'bytes' => $row->rev_len ),
+                               array( 'id' => $row->rev_text_id, 'bytes' => intval( $row->rev_len ) ),
                                "" ) . "\n";
                }
 
@@ -576,7 +603,7 @@ class XmlDumpWriter {
                        $out .= "      " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
                } else {
                        $title = Title::makeTitle( $row->log_namespace, $row->log_title );
-                       $out .= "      " . Xml::elementClean( 'logtitle', null, $title->getPrefixedText() ) . "\n";
+                       $out .= "      " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
                        $out .= "      " . Xml::elementClean( 'params',
                                array( 'xml:space' => 'preserve' ),
                                strval( $row->log_params ) ) . "\n";
@@ -595,7 +622,7 @@ class XmlDumpWriter {
 
        function writeContributor( $id, $text ) {
                $out = "      <contributor>\n";
-               if ( $id ) {
+               if ( $id || !IP::isValid( $text ) ) {
                        $out .= "        " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
                        $out .= "        " . Xml::element( 'id', null, strval( $id ) ) . "\n";
                } else {
@@ -607,6 +634,7 @@ class XmlDumpWriter {
 
        /**
         * Warning! This data is potentially inconsistent. :(
+        * @return string
         */
        function writeUploads( $row, $dumpContents = false ) {
                if ( $row->page_namespace == NS_IMAGE ) {
@@ -630,7 +658,7 @@ class XmlDumpWriter {
         */
        function writeUpload( $file, $dumpContents = false ) {
                if ( $file->isOld() ) {
-                       $archiveName = "      " . 
+                       $archiveName = "      " .
                                Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n";
                } else {
                        $archiveName = '';
@@ -638,7 +666,7 @@ class XmlDumpWriter {
                if ( $dumpContents ) {
                        # Dump file as base64
                        # Uses only XML-safe characters, so does not need escaping
-                       $contents = '      <contents encoding="base64">' . 
+                       $contents = '      <contents encoding="base64">' .
                                chunk_split( base64_encode( file_get_contents( $file->getPath() ) ) ) .
                                "      </contents>\n";
                } else {
@@ -649,7 +677,7 @@ class XmlDumpWriter {
                        $this->writeContributor( $file->getUser( 'id' ), $file->getUser( 'text' ) ) .
                        "      " . Xml::elementClean( 'comment', null, $file->getDescription() ) . "\n" .
                        "      " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
-                       $archiveName . 
+                       $archiveName .
                        "      " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" .
                        "      " . Xml::element( 'size', null, $file->getSize() ) . "\n" .
                        "      " . Xml::element( 'sha1base36', null, $file->getSha1() ) . "\n" .
@@ -658,6 +686,30 @@ class XmlDumpWriter {
                        "    </upload>\n";
        }
 
+       /**
+        * Return prefixed text form of title, but using the content language's
+        * canonical namespace. This skips any special-casing such as gendered
+        * user namespaces -- which while useful, are not yet listed in the
+        * XML <siteinfo> data so are unsafe in export.
+        *
+        * @param Title $title
+        * @return string
+        * @since 1.18
+        */
+       public static function canonicalTitle( Title $title ) {
+               if ( $title->getInterwiki() ) {
+                       return $title->getPrefixedText();
+               }
+
+               global $wgContLang;
+               $prefix = str_replace( '_', ' ', $wgContLang->getNsText( $title->getNamespace() ) );
+
+               if ( $prefix !== '' ) {
+                       $prefix .= ':';
+               }
+
+               return $prefix . $title->getText();
+       }
 }
 
 
@@ -699,7 +751,7 @@ class DumpOutput {
        }
 
        /**
-        * Close the old file, move it to a specified name, 
+        * Close the old file, move it to a specified name,
         * and reopen new file with the old name. Use this
         * for writing out a file in multiple pieces
         * at specified checkpoints (e.g. every n hours).
@@ -709,18 +761,23 @@ class DumpOutput {
                return;
        }
 
-       // TODO: document
-       function closeAndRename( $newname ) {
-               return;
-       }
-
-       // TODO: document
-       function rename( $newname ) {
+       /**
+        * Close the old file, and move it to a specified name.
+        * Use this for the last piece of a file written out
+        * at specified checkpoints (e.g. every n hours).
+        * @param $newname mixed File name. May be a string or an array with one element
+        * @param $open bool If true, a new file with the old filename will be opened again for writing (default: false)
+        */
+       function closeAndRename( $newname, $open = false ) {
                return;
        }
 
-       // TODO: document
-       function getFilename() {
+       /**
+        * Returns the name of the file or files which are
+        * being written to, if there are any.
+        * @return null
+        */
+       function getFilenames() {
                return NULL;
        }
 }
@@ -730,8 +787,7 @@ class DumpOutput {
  * @ingroup Dump
  */
 class DumpFileOutput extends DumpOutput {
-       var $handle;
-       var $filename;
+       protected $handle, $filename;
 
        function __construct( $file ) {
                $this->handle = fopen( $file, "wt" );
@@ -743,51 +799,38 @@ class DumpFileOutput extends DumpOutput {
        }
 
        function closeRenameAndReopen( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
-                       }
-                       else {
-                               $newname = $newname[0];
-                       }
-               }
-               if ( $newname ) {
-                       fclose( $this->handle );
-                       rename( $this->filename, $newname );
-                       $this->handle = fopen( $this->filename, "wt" );
-               }
+               $this->closeAndRename( $newname, true );
        }
 
-       function closeAndRename( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
+       function renameOrException( $newname ) {
+                       if (! rename( $this->filename, $newname ) ) {
+                               throw new MWException( __METHOD__ . ": rename of file {$this->filename} to $newname failed\n" );
                        }
-                       else {
-                               $newname = $newname[0];
-                       }
-               }
-               if ( $newname ) {
-                       fclose( $this->handle );
-                       rename( $this->filename, $newname );
-               }
        }
 
-       function rename( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
-                       }
-                       else {
+       function checkRenameArgCount( $newname ) {
+               if ( is_array( $newname ) ) {
+                       if ( count( $newname ) > 1 ) {
+                               throw new MWException( __METHOD__ . ": passed multiple arguments for rename of single file\n" );
+                       } else {
                                $newname = $newname[0];
                        }
                }
+               return $newname;
+       }
+
+       function closeAndRename( $newname, $open = false ) {
+               $newname = $this->checkRenameArgCount( $newname );
                if ( $newname ) {
-                       rename( $this->filename, $newname );
+                       fclose( $this->handle );
+                       $this->renameOrException( $newname );
+                       if ( $open ) {
+                               $this->handle = fopen( $this->filename, "wt" );
+                       }
                }
        }
 
-       function getFilename() {
+       function getFilenames() {
                return $this->filename;
        }
 }
@@ -805,13 +848,13 @@ class DumpPipeOutput extends DumpFileOutput {
                if ( !is_null( $file ) ) {
                        $command .=  " > " . wfEscapeShellArg( $file );
                }
-               
-               $this->startCommand($command);
+
+               $this->startCommand( $command );
                $this->command = $command;
                $this->filename = $file;
        }
 
-       function startCommand($command) {
+       function startCommand( $command ) {
                $spec = array(
                        0 => array( "pipe", "r" ),
                );
@@ -820,59 +863,24 @@ class DumpPipeOutput extends DumpFileOutput {
                $this->handle = $pipes[0];
        }
 
-       /**
-        * Close the old file, move it to a specified name, 
-        * and reopen new file with the old name. 
-        */
        function closeRenameAndReopen( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
-                       }
-                       else {
-                               $newname = $newname[0];
-                       }
-               }
-               if ( $newname ) {
-                       fclose( $this->handle );
-                       proc_close($this->procOpenResource);
-                       rename( $this->filename, $newname );
-                       $command = $this->command;
-                       $command .=  " > " . wfEscapeShellArg( $this->filename );
-                       $this->startCommand($command);
-               }
+               $this->closeAndRename( $newname, true );
        }
 
-       function closeAndRename( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
-                       }
-                       else {
-                               $newname = $newname[0];
-                       }
-               }
+       function closeAndRename( $newname, $open = false ) {
+               $newname = $this->checkRenameArgCount( $newname );
                if ( $newname ) {
-#                      pclose( $this->handle );
                        fclose( $this->handle );
-                       proc_close($this->procOpenResource);
-                       rename( $this->filename, $newname );
-               }
-       }
-
-       function rename( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
+                       proc_close( $this->procOpenResource );
+                       $this->renameOrException( $newname );
+                       if ( $open ) {
+                               $command = $this->command;
+                               $command .=  " > " . wfEscapeShellArg( $this->filename );
+                               $this->startCommand( $command );
                        }
-                       else {
-                               $newname = $newname[0];
-                       }
-               }
-               if ( $newname ) {
-                       rename( $this->filename, $newname );
                }
        }
+
 }
 
 /**
@@ -900,63 +908,30 @@ class DumpBZip2Output extends DumpPipeOutput {
  * @ingroup Dump
  */
 class Dump7ZipOutput extends DumpPipeOutput {
-       protected $filename;
-
        function __construct( $file ) {
-               $command = "7za a -bd -si " . wfEscapeShellArg( $file );
-               // Suppress annoying useless crap from p7zip
-               // Unfortunately this could suppress real error messages too
-               $command .= ' >' . wfGetNull() . ' 2>&1';
+               $command = $this->setup7zCommand( $file );
                parent::__construct( $command );
                $this->filename = $file;
        }
 
-       function closeRenameAndReopen( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
-                       }
-                       else {
-                               $newname = $newname[0];
-                       }
-               }
-               if ( $newname ) {
-                       fclose( $this->handle );
-                       proc_close($this->procOpenResource);
-                       rename( $this->filename, $newname );
-                       $command = "7za a -bd -si " . wfEscapeShellArg( $file );
-                       $command .= ' >' . wfGetNull() . ' 2>&1';
-                       $this->startCommand($command);
-               }
+       function setup7zCommand( $file ) {
+               $command = "7za a -bd -si " . wfEscapeShellArg( $file );
+               // Suppress annoying useless crap from p7zip
+               // Unfortunately this could suppress real error messages too
+               $command .= ' >' . wfGetNull() . ' 2>&1';
+               return( $command );
        }
 
-       function closeAndRename( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
-                       }
-                       else {
-                               $newname = $newname[0];
-                       }
-               }
+       function closeAndRename( $newname, $open = false ) {
+               $newname = $this->checkRenameArgCount( $newname );
                if ( $newname ) {
                        fclose( $this->handle );
-                       proc_close($this->procOpenResource);
-                       rename( $this->filename, $newname );
-               }
-       }
-
-       function rename( $newname ) {
-               if ( is_array($newname) ) {
-                       if (count($newname) > 1) {
-                               throw new MWException(__METHOD__ . ": passed multiple arguments for rename of single file\n");
+                       proc_close( $this->procOpenResource );
+                       $this->renameOrException( $newname );
+                       if ( $open ) {
+                               $command = $this->setup7zCommand( $this->filename );
+                               $this->startCommand( $command );
                        }
-                       else {
-                               $newname = $newname[0];
-                       }
-               }
-               if ( $newname ) {
-                       rename( $this->filename, $newname );
                }
        }
 }
@@ -1010,16 +985,12 @@ class DumpFilter {
                $this->sink->closeRenameAndReopen( $newname );
        }
 
-       function closeAndRename( $newname ) {
-               $this->sink->closeAndRename( $newname );
+       function closeAndRename( $newname, $open = false ) {
+               $this->sink->closeAndRename( $newname, $open );
        }
 
-       function rename( $newname ) {
-               $this->sink->rename( $newname );
-       }
-
-       function getFilename() {
-               return $this->sink->getFilename();
+       function getFilenames() {
+               return $this->sink->getFilenames();
        }
 
        /**
@@ -1171,26 +1142,19 @@ class DumpMultiWriter {
        }
 
        function closeRenameAndReopen( $newnames ) {
-               for( $i = 0; $i < $this->count; $i++ ) {
-                       $this->sinks[$i]->closeRenameAndReopen( $newnames[$i] );
-               }
+               $this->closeAndRename( $newnames, true );
        }
 
-       function closeAndRename( $newname ) {
-               for( $i = 0; $i < $this->count; $i++ ) {
-                       $this->sinks[$i]->closeAndRename( $newnames[$i] );
-               }
-       }
-       function rename( $newnames ) {
-               for( $i = 0; $i < $this->count; $i++ ) {
-                       $this->sinks[$i]->rename( $newnames[$i] );
+       function closeAndRename( $newnames, $open = false ) {
+               for ( $i = 0; $i < $this->count; $i++ ) {
+                       $this->sinks[$i]->closeAndRename( $newnames[$i], $open );
                }
        }
 
-       function getFilename() {
+       function getFilenames() {
                $filenames = array();
-               for( $i = 0; $i < $this->count; $i++ ) {
-                       $filenames[] =  $this->sinks[$i]->getFilename();
+               for ( $i = 0; $i < $this->count; $i++ ) {
+                       $filenames[] =  $this->sinks[$i]->getFilenames();
                }
                return $filenames;
        }