23use Wikimedia\Purtle\RdfWriter;
24use Wikimedia\Purtle\TurtleRdfWriter;
28require_once __DIR__ .
'/Maintenance.php';
41 private const SPARQL_INSERT = <<<SPARQL
51 private const SPARQL_DELETE = <<<SPARQLD
71 private $categoriesRdf;
86 parent::__construct();
88 $this->addDescription(
"Generate RDF dump of category changes in a wiki." );
90 $this->setBatchSize( 200 );
91 $this->addOption(
'output',
"Output file (default is stdout). Will be overwritten.",
false,
93 $this->addOption(
'start',
'Starting timestamp (inclusive), in ISO or MediaWiki format.',
95 $this->addOption(
'end',
'Ending timestamp (exclusive), in ISO or MediaWiki format.',
true,
104 $this->rdfWriter =
new TurtleRdfWriter();
105 $this->categoriesRdf =
new CategoriesRdf( $this->rdfWriter );
110 $startTS =
new MWTimestamp( $this->getOption(
"start" ) );
112 $endTS =
new MWTimestamp( $this->getOption(
"end" ) );
114 $rcMaxAge = $this->getConfig()->get( MainConfigNames::RCMaxAge );
116 if ( (
int)$now->getTimestamp( TS_UNIX ) - (
int)$startTS->getTimestamp( TS_UNIX ) > $rcMaxAge ) {
117 $this->error(
"Start timestamp too old, maximum RC age is $rcMaxAge!" );
119 if ( (
int)$now->getTimestamp( TS_UNIX ) - (
int)$endTS->getTimestamp( TS_UNIX ) > $rcMaxAge ) {
120 $this->error(
"End timestamp too old, maximum RC age is $rcMaxAge!" );
123 $this->startTS = $startTS->getTimestamp();
124 $this->endTS = $endTS->getTimestamp();
126 $outFile = $this->getOption(
'output',
'php://stdout' );
127 if ( $outFile ===
'-' ) {
128 $outFile =
'php://stdout';
131 $output = fopen( $outFile,
'wb' );
133 $this->categoriesRdf->setupPrefixes();
134 $this->rdfWriter->start();
136 $prefixes = $this->
getRdf();
139 $prefixes = preg_replace( [
'/^@/m',
'/\s*[.]$/m' ],
'', $prefixes );
140 fwrite( $output, $prefixes );
142 $dbr = $this->getDB(
DB_REPLICA, [
'vslow' ] );
160 fwrite( $output, $this->
updateTS( $this->endTS ) );
167 private function getInsertRdf() {
168 $rdfText = $this->
getRdf();
172 return sprintf( self::SPARQL_INSERT, $rdfText );
183 private function getCategoriesUpdate(
IReadableDatabase $dbr, $deleteUrls, $pages, $mark ) {
184 if ( !$deleteUrls ) {
189 $this->writeParentCategories( $dbr, $pages );
192 return "# $mark\n" . sprintf( self::SPARQL_DELETE, implode(
' ', $deleteUrls ) ) .
193 $this->getInsertRdf();
204 $this->categoriesRdf->writeCategoryLinkData( $pages[$row->cl_from], $row->cl_to );
214 $dumpUrl =
'<' . $this->categoriesRdf->getDumpURI() .
'>';
218 $dumpUrl schema:dateModified ?o .
221 $dumpUrl schema:dateModified ?o .
224 $dumpUrl schema:dateModified
"$ts"^^xsd:dateTime .
239 private function setupChangesIterator(
245 $tables = [
'recentchanges',
'page_props',
'category' ];
246 if ( $extra_tables ) {
247 $tables = array_merge( $tables, $extra_tables );
254 $this->addTimestampConditions( $it, $dbr );
258 'LEFT JOIN', [
'pp_propname' =>
'hiddencat',
'pp_page = rc_cur_id' ]
261 'LEFT JOIN', [
'cat_title = rc_title' ]
284 $it = $this->setupChangesIterator( $dbr, [], [], $fname );
299 $it = $this->setupChangesIterator(
301 [
'page_title',
'page_namespace' ],
308 'rc_log_type' =>
'move',
312 'page' => [
'JOIN',
'rc_cur_id = page_id' ],
314 $this->addIndex( $it );
330 $this->addTimestampConditions( $it, $dbr );
334 'rc_log_type' =>
'delete',
335 'rc_log_action' =>
'delete',
339 'NOT EXISTS (SELECT * FROM page WHERE page_id = rc_cur_id)',
341 $this->addIndex( $it );
354 $it = $this->setupChangesIterator( $dbr, [], [], $fname );
358 'rc_log_type' =>
'delete',
359 'rc_log_action' =>
'restore',
362 'EXISTS (SELECT page_id FROM page WHERE page_id = rc_cur_id)',
364 $this->addIndex( $it );
376 $it = $this->setupChangesIterator( $dbr, [], [], $fname );
382 $this->addIndex( $it );
393 $dbr->
expr(
'rc_timestamp',
'>=', $dbr->
timestamp( $this->startTS ) ),
394 $dbr->
expr(
'rc_timestamp',
'<', $dbr->
timestamp( $this->endTS ) ),
404 'USE INDEX' => [
'recentchanges' =>
'rc_new_name_timestamp' ]
419 [
'cl_from',
'cl_to' ],
423 'cl_type' =>
'subcat',
428 return new RecursiveIteratorIterator( $it );
436 return $this->rdfWriter->drain();
449 foreach ( $batch as $row ) {
451 $deleteUrls[] =
'<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) .
'>';
452 $this->processed[$row->rc_cur_id] =
true;
454 fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, [],
"Deletes" ) );
462 private function writeCategoryData( $row ) {
463 $this->categoriesRdf->writeCategoryData(
465 $row->pp_propname ===
'hiddencat',
466 (
int)$row->cat_pages - (
int)$row->cat_subcats - (
int)$row->cat_files,
467 (
int)$row->cat_subcats
479 foreach ( $batch as $row ) {
480 $deleteUrls[] =
'<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) .
'>';
482 if ( isset( $this->processed[$row->rc_cur_id] ) ) {
491 $row->rc_title = $row->page_title;
492 $this->writeCategoryData( $row );
493 $pages[$row->rc_cur_id] = $row->page_title;
494 $this->processed[$row->rc_cur_id] =
true;
497 fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, $pages,
"Moves" ) );
506 fwrite( $output,
"# Restores\n" );
511 foreach ( $batch as $row ) {
512 if ( isset( $this->processed[$row->rc_cur_id] ) ) {
516 $this->writeCategoryData( $row );
517 $pages[$row->rc_cur_id] = $row->rc_title;
518 $this->processed[$row->rc_cur_id] =
true;
525 $this->writeParentCategories( $dbr, $pages );
527 fwrite( $output, $this->getInsertRdf() );
536 fwrite( $output,
"# Additions\n" );
540 foreach ( $batch as $row ) {
541 if ( isset( $this->processed[$row->rc_cur_id] ) ) {
545 $this->writeCategoryData( $row );
546 $pages[$row->rc_cur_id] = $row->rc_title;
547 $this->processed[$row->rc_cur_id] =
true;
554 $this->writeParentCategories( $dbr, $pages );
555 fwrite( $output, $this->getInsertRdf() );
573 foreach ( $batch as $row ) {
576 if ( isset( $this->processed[$row->rc_cur_id] ) ) {
580 $this->writeCategoryData( $row );
581 $pages[$row->rc_cur_id] = $row->rc_title;
582 $this->processed[$row->rc_cur_id] =
true;
583 $deleteUrls[] =
'<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) .
'>';
586 fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, $pages,
"Edits" ) );
596 $processedTitle = [];
612 foreach ( $batch as $row ) {
613 $childPages[$row->rc_cur_id] =
true;
614 $parentCats[$row->rc_title] =
true;
625 'rc_title' =>
'page_title',
632 ->leftJoin(
'page_props',
null, [
'pp_propname' =>
'hiddencat',
'pp_page = page_id' ] )
633 ->leftJoin(
'category',
null, [
'cat_title = page_title' ] )
634 ->where( [
'page_namespace' =>
NS_CATEGORY,
'page_id' => array_keys( $childPages ) ] )
635 ->caller( __METHOD__ )->fetchResultSet();
636 foreach ( $childRows as $row ) {
637 if ( isset( $this->processed[$row->page_id] ) ) {
641 $this->writeCategoryData( $row );
642 if ( $row->page_id ) {
643 $pages[$row->page_id] = $row->rc_title;
644 $deleteUrls[] =
'<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) .
'>';
645 $this->processed[$row->page_id] =
true;
655 'rc_title' =>
'cat_title',
662 ->leftJoin(
'page',
null, [
'page_title = cat_title',
'page_namespace' =>
NS_CATEGORY ] )
663 ->leftJoin(
'page_props',
null, [
'pp_propname' =>
'hiddencat',
'pp_page = page_id' ] )
664 ->where( [
'cat_title' => array_map(
'strval', array_keys( $parentCats ) ) ] )
665 ->caller( __METHOD__ )->fetchResultSet();
666 foreach ( $parentRows as $row ) {
667 if ( $row->page_id && isset( $this->processed[$row->page_id] ) ) {
671 if ( isset( $processedTitle[$row->rc_title] ) ) {
675 $this->writeCategoryData( $row );
676 if ( $row->page_id ) {
677 $pages[$row->page_id] = $row->rc_title;
678 $deleteUrls[] =
'<' . $this->categoriesRdf->labelToUrl( $row->rc_title ) .
'>';
679 $this->processed[$row->page_id] =
true;
681 $processedTitle[$row->rc_title] =
true;
685 fwrite( $output, $this->getCategoriesUpdate( $dbr, $deleteUrls, $pages,
"Changes" ) );
692require_once RUN_MAINTENANCE_IF_MAIN;
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Allows iterating a large number of rows in batches transparently.
addConditions(array $conditions)
addJoinConditions(array $conditions)
setFetchColumns(array $columns)
setCaller( $caller)
Use ->setCaller( METHOD ) to indicate which code is using this class.
addOptions(array $options)
Maintenance script to provide RDF representation of the recent changes in category tree.
initialize()
Initialize external service classes.
handleRestores(IReadableDatabase $dbr, $output)
handleMoves(IReadableDatabase $dbr, $output)
getNewCatsIterator(IReadableDatabase $dbr, $fname)
Fetch newly created categories.
getDeletedCatsIterator(IReadableDatabase $dbr, $fname)
Fetch deleted categories.
handleCategorization(IReadableDatabase $dbr, $output)
Handles categorization changes.
true[] $processed
List of processed page IDs, so we don't try to process same thing twice.
getRestoredCatsIterator(IReadableDatabase $dbr, $fname)
Fetch restored categories.
getMovedCatsIterator(IReadableDatabase $dbr, $fname)
Fetch moved categories.
handleEdits(IReadableDatabase $dbr, $output)
Handle edits for category texts.
getRdf()
Get accumulated RDF.
getChangedCatsIterator(IReadableDatabase $dbr, $type, $fname)
Fetch categorization changes or edits.
updateTS( $timestamp)
Generate SPARQL Update code for updating dump timestamp.
handleAdds(IReadableDatabase $dbr, $output)
handleDeletes(IReadableDatabase $dbr, $output)
Handle category deletes.
getCategoryLinksIterator(IReadableDatabase $dbr, array $ids, $fname)
Get iterator for links for categories.
A class containing constants representing the names of configuration variables.