]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/Export.php
MediaWiki 1.11.0
[autoinstallsdev/mediawiki.git] / includes / Export.php
1 <?php
2 # Copyright (C) 2003, 2005, 2006 Brion Vibber <brion@pobox.com>
3 # http://www.mediawiki.org/
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # http://www.gnu.org/copyleft/gpl.html
19
20
21 /**
22  *
23  * @addtogroup SpecialPage
24  */
25 class WikiExporter {
26         var $list_authors = false ; # Return distinct author list (when not returning full history)
27         var $author_list = "" ;
28
29         const FULL = 0;
30         const CURRENT = 1;
31
32         const BUFFER = 0;
33         const STREAM = 1;
34
35         const TEXT = 0;
36         const STUB = 1;
37
38         /**
39          * If using WikiExporter::STREAM to stream a large amount of data,
40          * provide a database connection which is not managed by
41          * LoadBalancer to read from: some history blob types will
42          * make additional queries to pull source data while the
43          * main query is still running.
44          *
45          * @param Database $db
46          * @param mixed $history one of WikiExporter::FULL or WikiExporter::CURRENT, or an
47          *                       associative array:
48          *                         offset: non-inclusive offset at which to start the query
49          *                         limit: maximum number of rows to return
50          *                         dir: "asc" or "desc" timestamp order
51          * @param int $buffer one of WikiExporter::BUFFER or WikiExporter::STREAM
52          */
53         function __construct( &$db, $history = WikiExporter::CURRENT,
54                         $buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) {
55                 $this->db =& $db;
56                 $this->history = $history;
57                 $this->buffer  = $buffer;
58                 $this->writer  = new XmlDumpWriter();
59                 $this->sink    = new DumpOutput();
60                 $this->text    = $text;
61         }
62
63         /**
64          * Set the DumpOutput or DumpFilter object which will receive
65          * various row objects and XML output for filtering. Filters
66          * can be chained or used as callbacks.
67          *
68          * @param mixed $callback
69          */
70         function setOutputSink( &$sink ) {
71                 $this->sink =& $sink;
72         }
73
74         function openStream() {
75                 $output = $this->writer->openStream();
76                 $this->sink->writeOpenStream( $output );
77         }
78
79         function closeStream() {
80                 $output = $this->writer->closeStream();
81                 $this->sink->writeCloseStream( $output );
82         }
83
84         /**
85          * Dumps a series of page and revision records for all pages
86          * in the database, either including complete history or only
87          * the most recent version.
88          */
89         function allPages() {
90                 return $this->dumpFrom( '' );
91         }
92
93         /**
94          * Dumps a series of page and revision records for those pages
95          * in the database falling within the page_id range given.
96          * @param int $start Inclusive lower limit (this id is included)
97          * @param int $end   Exclusive upper limit (this id is not included)
98          *                   If 0, no upper limit.
99          */
100         function pagesByRange( $start, $end ) {
101                 $condition = 'page_id >= ' . intval( $start );
102                 if( $end ) {
103                         $condition .= ' AND page_id < ' . intval( $end );
104                 }
105                 return $this->dumpFrom( $condition );
106         }
107
108         /**
109          * @param Title $title
110          */
111         function pageByTitle( $title ) {
112                 return $this->dumpFrom(
113                         'page_namespace=' . $title->getNamespace() .
114                         ' AND page_title=' . $this->db->addQuotes( $title->getDbKey() ) );
115         }
116
117         function pageByName( $name ) {
118                 $title = Title::newFromText( $name );
119                 if( is_null( $title ) ) {
120                         return new WikiError( "Can't export invalid title" );
121                 } else {
122                         return $this->pageByTitle( $title );
123                 }
124         }
125
126         function pagesByName( $names ) {
127                 foreach( $names as $name ) {
128                         $this->pageByName( $name );
129                 }
130         }
131
132
133         // -------------------- private implementation below --------------------
134
135         # Generates the distinct list of authors of an article
136         # Not called by default (depends on $this->list_authors)
137         # Can be set by Special:Export when not exporting whole history
138         function do_list_authors ( $page , $revision , $cond ) {
139                 $fname = "do_list_authors" ;
140                 wfProfileIn( $fname );
141                 $this->author_list = "<contributors>";
142                 //rev_deleted
143                 $nothidden = '(rev_deleted & '.Revision::DELETED_USER.') = 0';
144                 
145                 $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} WHERE page_id=rev_page AND $nothidden AND " . $cond ;
146                 $result = $this->db->query( $sql, $fname );
147                 $resultset = $this->db->resultObject( $result );
148                 while( $row = $resultset->fetchObject() ) {
149                         $this->author_list .= "<contributor>" . 
150                                 "<username>" . 
151                                 htmlentities( $row->rev_user_text )  . 
152                                 "</username>" . 
153                                 "<id>" . 
154                                 $row->rev_user .
155                                 "</id>" . 
156                                 "</contributor>";
157                 }
158                 wfProfileOut( $fname );
159                 $this->author_list .= "</contributors>";
160         }
161
162         function dumpFrom( $cond = '' ) {
163                 $fname = 'WikiExporter::dumpFrom';
164                 wfProfileIn( $fname );
165
166                 $page     = $this->db->tableName( 'page' );
167                 $revision = $this->db->tableName( 'revision' );
168                 $text     = $this->db->tableName( 'text' );
169
170                 $order = 'ORDER BY page_id';
171                 $limit = '';
172
173                 if( $this->history == WikiExporter::FULL ) {
174                         $join = 'page_id=rev_page';
175                 } elseif( $this->history == WikiExporter::CURRENT ) {
176                         if ( $this->list_authors && $cond != '' )  { // List authors, if so desired
177                                 $this->do_list_authors ( $page , $revision , $cond );
178                         }
179                         $join = 'page_id=rev_page AND page_latest=rev_id';
180                 } elseif ( is_array( $this->history ) ) {
181                         $join = 'page_id=rev_page';
182                         if ( $this->history['dir'] == 'asc' ) {
183                                 $op = '>';
184                                 $order .= ', rev_timestamp';
185                         } else {
186                                 $op = '<';
187                                 $order .= ', rev_timestamp DESC';
188                         }
189                         if ( !empty( $this->history['offset'] ) ) {
190                                 $join .= " AND rev_timestamp $op " . $this->db->addQuotes(
191                                         $this->db->timestamp( $this->history['offset'] ) );
192                         }
193                         if ( !empty( $this->history['limit'] ) ) {
194                                 $limitNum = intval( $this->history['limit'] );
195                                 if ( $limitNum > 0 ) {
196                                         $limit = "LIMIT $limitNum";
197                                 }
198                         }
199                 } else {
200                         wfProfileOut( $fname );
201                         return new WikiError( "$fname given invalid history dump type." );
202                 }
203                 $where = ( $cond == '' ) ? '' : "$cond AND";
204
205                 if( $this->buffer == WikiExporter::STREAM ) {
206                         $prev = $this->db->bufferResults( false );
207                 }
208                 if( $cond == '' ) {
209                         // Optimization hack for full-database dump
210                         $revindex = $pageindex = $this->db->useIndexClause("PRIMARY");
211                         $straight = ' /*! STRAIGHT_JOIN */ ';
212                 } else {
213                         $pageindex = '';
214                         $revindex = '';
215                         $straight = '';
216                 }
217                 if( $this->text == WikiExporter::STUB ) {
218                         $sql = "SELECT $straight * FROM
219                                         $page $pageindex,
220                                         $revision $revindex
221                                         WHERE $where $join
222                                         $order $limit";
223                 } else {
224                         $sql = "SELECT $straight * FROM
225                                         $page $pageindex,
226                                         $revision $revindex,
227                                         $text
228                                         WHERE $where $join AND rev_text_id=old_id
229                                         $order $limit";
230                 }
231                 $result = $this->db->query( $sql, $fname );
232                 $wrapper = $this->db->resultObject( $result );
233                 $this->outputStream( $wrapper );
234
235                 if ( $this->list_authors ) {
236                         $this->outputStream( $wrapper );
237                 }
238
239                 if( $this->buffer == WikiExporter::STREAM ) {
240                         $this->db->bufferResults( $prev );
241                 }
242
243                 wfProfileOut( $fname );
244         }
245
246         /**
247          * Runs through a query result set dumping page and revision records.
248          * The result set should be sorted/grouped by page to avoid duplicate
249          * page records in the output.
250          *
251          * The result set will be freed once complete. Should be safe for
252          * streaming (non-buffered) queries, as long as it was made on a
253          * separate database connection not managed by LoadBalancer; some
254          * blob storage types will make queries to pull source data.
255          *
256          * @param ResultWrapper $resultset
257          * @access private
258          */
259         function outputStream( $resultset ) {
260                 $last = null;
261                 while( $row = $resultset->fetchObject() ) {
262                         if( is_null( $last ) ||
263                                 $last->page_namespace != $row->page_namespace ||
264                                 $last->page_title     != $row->page_title ) {
265                                 if( isset( $last ) ) {
266                                         $output = $this->writer->closePage();
267                                         $this->sink->writeClosePage( $output );
268                                 }
269                                 $output = $this->writer->openPage( $row );
270                                 $this->sink->writeOpenPage( $row, $output );
271                                 $last = $row;
272                         }
273                         $output = $this->writer->writeRevision( $row );
274                         $this->sink->writeRevision( $row, $output );
275                 }
276                 if( isset( $last ) ) {
277                         $output = $this->author_list . $this->writer->closePage();
278                         $this->sink->writeClosePage( $output );
279                 }
280                 $resultset->free();
281         }
282 }
283
284 /**
285  * @addtogroup Dump
286  */
287 class XmlDumpWriter {
288
289         /**
290          * Returns the export schema version.
291          * @return string
292          */
293         function schemaVersion() {
294                 return "0.3"; // FIXME: upgrade to 0.4 when updated XSD is ready, for the revision deletion bits
295         }
296
297         /**
298          * Opens the XML output stream's root <mediawiki> element.
299          * This does not include an xml directive, so is safe to include
300          * as a subelement in a larger XML stream. Namespace and XML Schema
301          * references are included.
302          *
303          * Output will be encoded in UTF-8.
304          *
305          * @return string
306          */
307         function openStream() {
308                 global $wgContLanguageCode;
309                 $ver = $this->schemaVersion();
310                 return wfElement( 'mediawiki', array(
311                         'xmlns'              => "http://www.mediawiki.org/xml/export-$ver/",
312                         'xmlns:xsi'          => "http://www.w3.org/2001/XMLSchema-instance",
313                         'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
314                                                 "http://www.mediawiki.org/xml/export-$ver.xsd",
315                         'version'            => $ver,
316                         'xml:lang'           => $wgContLanguageCode ),
317                         null ) .
318                         "\n" .
319                         $this->siteInfo();
320         }
321
322         function siteInfo() {
323                 $info = array(
324                         $this->sitename(),
325                         $this->homelink(),
326                         $this->generator(),
327                         $this->caseSetting(),
328                         $this->namespaces() );
329                 return "  <siteinfo>\n    " .
330                         implode( "\n    ", $info ) .
331                         "\n  </siteinfo>\n";
332         }
333
334         function sitename() {
335                 global $wgSitename;
336                 return wfElement( 'sitename', array(), $wgSitename );
337         }
338
339         function generator() {
340                 global $wgVersion;
341                 return wfElement( 'generator', array(), "MediaWiki $wgVersion" );
342         }
343
344         function homelink() {
345                 return wfElement( 'base', array(), Title::newMainPage()->getFullUrl() );
346         }
347
348         function caseSetting() {
349                 global $wgCapitalLinks;
350                 // "case-insensitive" option is reserved for future
351                 $sensitivity = $wgCapitalLinks ? 'first-letter' : 'case-sensitive';
352                 return wfElement( 'case', array(), $sensitivity );
353         }
354
355         function namespaces() {
356                 global $wgContLang;
357                 $spaces = "  <namespaces>\n";
358                 foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) {
359                         $spaces .= '      ' . wfElement( 'namespace', array( 'key' => $ns ), $title ) . "\n";
360                 }
361                 $spaces .= "    </namespaces>";
362                 return $spaces;
363         }
364
365         /**
366          * Closes the output stream with the closing root element.
367          * Call when finished dumping things.
368          */
369         function closeStream() {
370                 return "</mediawiki>\n";
371         }
372
373
374         /**
375          * Opens a <page> section on the output stream, with data
376          * from the given database row.
377          *
378          * @param object $row
379          * @return string
380          * @access private
381          */
382         function openPage( $row ) {
383                 $out = "  <page>\n";
384                 $title = Title::makeTitle( $row->page_namespace, $row->page_title );
385                 $out .= '    ' . wfElementClean( 'title', array(), $title->getPrefixedText() ) . "\n";
386                 $out .= '    ' . wfElement( 'id', array(), strval( $row->page_id ) ) . "\n";
387                 if( '' != $row->page_restrictions ) {
388                         $out .= '    ' . wfElement( 'restrictions', array(),
389                                 strval( $row->page_restrictions ) ) . "\n";
390                 }
391                 return $out;
392         }
393
394         /**
395          * Closes a <page> section on the output stream.
396          *
397          * @access private
398          */
399         function closePage() {
400                 return "  </page>\n";
401         }
402
403         /**
404          * Dumps a <revision> section on the output stream, with
405          * data filled in from the given database row.
406          *
407          * @param object $row
408          * @return string
409          * @access private
410          */
411         function writeRevision( $row ) {
412                 $fname = 'WikiExporter::dumpRev';
413                 wfProfileIn( $fname );
414
415                 $out  = "    <revision>\n";
416                 $out .= "      " . wfElement( 'id', null, strval( $row->rev_id ) ) . "\n";
417
418                 $ts = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
419                 $out .= "      " . wfElement( 'timestamp', null, $ts ) . "\n";
420
421                 if( $row->rev_deleted & Revision::DELETED_USER ) {
422                         $out .= "      " . wfElement( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
423                 } else {
424                         $out .= "      <contributor>\n";
425                         if( $row->rev_user ) {
426                                 $out .= "        " . wfElementClean( 'username', null, strval( $row->rev_user_text ) ) . "\n";
427                                 $out .= "        " . wfElement( 'id', null, strval( $row->rev_user ) ) . "\n";
428                         } else {
429                                 $out .= "        " . wfElementClean( 'ip', null, strval( $row->rev_user_text ) ) . "\n";
430                         }
431                         $out .= "      </contributor>\n";
432                 }
433
434                 if( $row->rev_minor_edit ) {
435                         $out .=  "      <minor/>\n";
436                 }
437                 if( $row->rev_deleted & Revision::DELETED_COMMENT ) {
438                         $out .= "      " . wfElement( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
439                 } elseif( $row->rev_comment != '' ) {
440                         $out .= "      " . wfElementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n";
441                 }
442
443                 if( $row->rev_deleted & Revision::DELETED_TEXT ) {
444                         $out .= "      " . wfElement( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
445                 } elseif( isset( $row->old_text ) ) {
446                         // Raw text from the database may have invalid chars
447                         $text = strval( Revision::getRevisionText( $row ) );
448                         $out .= "      " . wfElementClean( 'text',
449                                 array( 'xml:space' => 'preserve' ),
450                                 strval( $text ) ) . "\n";
451                 } else {
452                         // Stub output
453                         $out .= "      " . wfElement( 'text',
454                                 array( 'id' => $row->rev_text_id ),
455                                 "" ) . "\n";
456                 }
457
458                 $out .= "    </revision>\n";
459
460                 wfProfileOut( $fname );
461                 return $out;
462         }
463
464 }
465
466
467 /**
468  * Base class for output stream; prints to stdout or buffer or whereever.
469  * @addtogroup Dump
470  */
471 class DumpOutput {
472         function writeOpenStream( $string ) {
473                 $this->write( $string );
474         }
475
476         function writeCloseStream( $string ) {
477                 $this->write( $string );
478         }
479
480         function writeOpenPage( $page, $string ) {
481                 $this->write( $string );
482         }
483
484         function writeClosePage( $string ) {
485                 $this->write( $string );
486         }
487
488         function writeRevision( $rev, $string ) {
489                 $this->write( $string );
490         }
491
492         /**
493          * Override to write to a different stream type.
494          * @return bool
495          */
496         function write( $string ) {
497                 print $string;
498         }
499 }
500
501 /**
502  * Stream outputter to send data to a file.
503  * @addtogroup Dump
504  */
505 class DumpFileOutput extends DumpOutput {
506         var $handle;
507
508         function DumpFileOutput( $file ) {
509                 $this->handle = fopen( $file, "wt" );
510         }
511
512         function write( $string ) {
513                 fputs( $this->handle, $string );
514         }
515 }
516
517 /**
518  * Stream outputter to send data to a file via some filter program.
519  * Even if compression is available in a library, using a separate
520  * program can allow us to make use of a multi-processor system.
521  * @addtogroup Dump
522  */
523 class DumpPipeOutput extends DumpFileOutput {
524         function DumpPipeOutput( $command, $file = null ) {
525                 if( !is_null( $file ) ) {
526                         $command .=  " > " . wfEscapeShellArg( $file );
527                 }
528                 $this->handle = popen( $command, "w" );
529         }
530 }
531
532 /**
533  * Sends dump output via the gzip compressor.
534  * @addtogroup Dump
535  */
536 class DumpGZipOutput extends DumpPipeOutput {
537         function DumpGZipOutput( $file ) {
538                 parent::DumpPipeOutput( "gzip", $file );
539         }
540 }
541
542 /**
543  * Sends dump output via the bgzip2 compressor.
544  * @addtogroup Dump
545  */
546 class DumpBZip2Output extends DumpPipeOutput {
547         function DumpBZip2Output( $file ) {
548                 parent::DumpPipeOutput( "bzip2", $file );
549         }
550 }
551
552 /**
553  * Sends dump output via the p7zip compressor.
554  * @addtogroup Dump
555  */
556 class Dump7ZipOutput extends DumpPipeOutput {
557         function Dump7ZipOutput( $file ) {
558                 $command = "7za a -bd -si " . wfEscapeShellArg( $file );
559                 // Suppress annoying useless crap from p7zip
560                 // Unfortunately this could suppress real error messages too
561                 $command .= ' >' . wfGetNull() . ' 2>&1';
562                 parent::DumpPipeOutput( $command );
563         }
564 }
565
566
567
568 /**
569  * Dump output filter class.
570  * This just does output filtering and streaming; XML formatting is done
571  * higher up, so be careful in what you do.
572  * @addtogroup Dump
573  */
574 class DumpFilter {
575         function DumpFilter( &$sink ) {
576                 $this->sink =& $sink;
577         }
578
579         function writeOpenStream( $string ) {
580                 $this->sink->writeOpenStream( $string );
581         }
582
583         function writeCloseStream( $string ) {
584                 $this->sink->writeCloseStream( $string );
585         }
586
587         function writeOpenPage( $page, $string ) {
588                 $this->sendingThisPage = $this->pass( $page, $string );
589                 if( $this->sendingThisPage ) {
590                         $this->sink->writeOpenPage( $page, $string );
591                 }
592         }
593
594         function writeClosePage( $string ) {
595                 if( $this->sendingThisPage ) {
596                         $this->sink->writeClosePage( $string );
597                         $this->sendingThisPage = false;
598                 }
599         }
600
601         function writeRevision( $rev, $string ) {
602                 if( $this->sendingThisPage ) {
603                         $this->sink->writeRevision( $rev, $string );
604                 }
605         }
606
607         /**
608          * Override for page-based filter types.
609          * @return bool
610          */
611         function pass( $page ) {
612                 return true;
613         }
614 }
615
616 /**
617  * Simple dump output filter to exclude all talk pages.
618  * @addtogroup Dump
619  */
620 class DumpNotalkFilter extends DumpFilter {
621         function pass( $page ) {
622                 return !Namespace::isTalk( $page->page_namespace );
623         }
624 }
625
626 /**
627  * Dump output filter to include or exclude pages in a given set of namespaces.
628  * @addtogroup Dump
629  */
630 class DumpNamespaceFilter extends DumpFilter {
631         var $invert = false;
632         var $namespaces = array();
633
634         function DumpNamespaceFilter( &$sink, $param ) {
635                 parent::DumpFilter( $sink );
636
637                 $constants = array(
638                         "NS_MAIN"           => NS_MAIN,
639                         "NS_TALK"           => NS_TALK,
640                         "NS_USER"           => NS_USER,
641                         "NS_USER_TALK"      => NS_USER_TALK,
642                         "NS_PROJECT"        => NS_PROJECT,
643                         "NS_PROJECT_TALK"   => NS_PROJECT_TALK,
644                         "NS_IMAGE"          => NS_IMAGE,
645                         "NS_IMAGE_TALK"     => NS_IMAGE_TALK,
646                         "NS_MEDIAWIKI"      => NS_MEDIAWIKI,
647                         "NS_MEDIAWIKI_TALK" => NS_MEDIAWIKI_TALK,
648                         "NS_TEMPLATE"       => NS_TEMPLATE,
649                         "NS_TEMPLATE_TALK"  => NS_TEMPLATE_TALK,
650                         "NS_HELP"           => NS_HELP,
651                         "NS_HELP_TALK"      => NS_HELP_TALK,
652                         "NS_CATEGORY"       => NS_CATEGORY,
653                         "NS_CATEGORY_TALK"  => NS_CATEGORY_TALK );
654
655                 if( $param{0} == '!' ) {
656                         $this->invert = true;
657                         $param = substr( $param, 1 );
658                 }
659
660                 foreach( explode( ',', $param ) as $key ) {
661                         $key = trim( $key );
662                         if( isset( $constants[$key] ) ) {
663                                 $ns = $constants[$key];
664                                 $this->namespaces[$ns] = true;
665                         } elseif( is_numeric( $key ) ) {
666                                 $ns = intval( $key );
667                                 $this->namespaces[$ns] = true;
668                         } else {
669                                 throw new MWException( "Unrecognized namespace key '$key'\n" );
670                         }
671                 }
672         }
673
674         function pass( $page ) {
675                 $match = isset( $this->namespaces[$page->page_namespace] );
676                 return $this->invert xor $match;
677         }
678 }
679
680
681 /**
682  * Dump output filter to include only the last revision in each page sequence.
683  * @addtogroup Dump
684  */
685 class DumpLatestFilter extends DumpFilter {
686         var $page, $pageString, $rev, $revString;
687
688         function writeOpenPage( $page, $string ) {
689                 $this->page = $page;
690                 $this->pageString = $string;
691         }
692
693         function writeClosePage( $string ) {
694                 if( $this->rev ) {
695                         $this->sink->writeOpenPage( $this->page, $this->pageString );
696                         $this->sink->writeRevision( $this->rev, $this->revString );
697                         $this->sink->writeClosePage( $string );
698                 }
699                 $this->rev = null;
700                 $this->revString = null;
701                 $this->page = null;
702                 $this->pageString = null;
703         }
704
705         function writeRevision( $rev, $string ) {
706                 if( $rev->rev_id == $this->page->page_latest ) {
707                         $this->rev = $rev;
708                         $this->revString = $string;
709                 }
710         }
711 }
712
713 /**
714  * Base class for output stream; prints to stdout or buffer or whereever.
715  * @addtogroup Dump
716  */
717 class DumpMultiWriter {
718         function DumpMultiWriter( $sinks ) {
719                 $this->sinks = $sinks;
720                 $this->count = count( $sinks );
721         }
722
723         function writeOpenStream( $string ) {
724                 for( $i = 0; $i < $this->count; $i++ ) {
725                         $this->sinks[$i]->writeOpenStream( $string );
726                 }
727         }
728
729         function writeCloseStream( $string ) {
730                 for( $i = 0; $i < $this->count; $i++ ) {
731                         $this->sinks[$i]->writeCloseStream( $string );
732                 }
733         }
734
735         function writeOpenPage( $page, $string ) {
736                 for( $i = 0; $i < $this->count; $i++ ) {
737                         $this->sinks[$i]->writeOpenPage( $page, $string );
738                 }
739         }
740
741         function writeClosePage( $string ) {
742                 for( $i = 0; $i < $this->count; $i++ ) {
743                         $this->sinks[$i]->writeClosePage( $string );
744                 }
745         }
746
747         function writeRevision( $rev, $string ) {
748                 for( $i = 0; $i < $this->count; $i++ ) {
749                         $this->sinks[$i]->writeRevision( $rev, $string );
750                 }
751         }
752 }
753
754 function xmlsafe( $string ) {
755         $fname = 'xmlsafe';
756         wfProfileIn( $fname );
757
758         /**
759          * The page may contain old data which has not been properly normalized.
760          * Invalid UTF-8 sequences or forbidden control characters will make our
761          * XML output invalid, so be sure to strip them out.
762          */
763         $string = UtfNormal::cleanUp( $string );
764
765         $string = htmlspecialchars( $string );
766         wfProfileOut( $fname );
767         return $string;
768 }
769
770