Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.81% covered (success)
94.81%
73 / 77
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
GrepPages
94.81% covered (success)
94.81%
73 / 77
75.00% covered (warning)
75.00%
3 / 4
20.06
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 init
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 execute
86.67% covered (warning)
86.67%
26 / 30
0.00% covered (danger)
0.00%
0 / 1
11.29
 findPages
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2// phpcs:disable MediaWiki.Files.ClassMatchesFilename.NotMatch
3use MediaWiki\Content\TextContent;
4use MediaWiki\Language\Language;
5use MediaWiki\Maintenance\Maintenance;
6use MediaWiki\Page\WikiPageFactory;
7use MediaWiki\Revision\RevisionRecord;
8use MediaWiki\Title\Title;
9use MediaWiki\WikiMap\WikiMap;
10use Wikimedia\Rdbms\IExpression;
11use Wikimedia\Rdbms\LikeValue;
12
13// @codeCoverageIgnoreStart
14require_once __DIR__ . '/Maintenance.php';
15// @codeCoverageIgnoreEnd
16
17/**
18 * Search pages for a given regex
19 *
20 * @ingroup Maintenance
21 */
22class GrepPages extends Maintenance {
23    /** @var Language */
24    private $contLang;
25
26    /** @var WikiPageFactory */
27    private $wikiPageFactory;
28
29    public function __construct() {
30        parent::__construct();
31        $this->addDescription( 'Search the source text of pages for lines matching ' .
32            'a given regex, and print the lines.' );
33        $this->addOption( 'prefix',
34            'Title prefix. Can be specified more than once. ' .
35            'Use e.g. --prefix=Talk: to search an entire namespace.',
36            false, true, false, true );
37        $this->addOption( 'show-wiki', 'Add the wiki ID to the output' );
38        $this->addOption( 'pages-with-matches',
39            'Suppress normal output; instead print the title of each page ' .
40            'from which output would normally have been printed.',
41            false, false, 'l' );
42        $this->addArg( 'regex', 'The regex to search for' );
43    }
44
45    private function init() {
46        $services = $this->getServiceContainer();
47        $this->contLang = $services->getContentLanguage();
48        $this->wikiPageFactory = $services->getWikiPageFactory();
49    }
50
51    public function execute() {
52        $this->init();
53
54        $showWiki = $this->getOption( 'show-wiki' );
55        $wikiId = WikiMap::getCurrentWikiId();
56        $prefix = $this->getOption( 'prefix' );
57        $regex = $this->getArg( 0 );
58        $titleOnly = $this->hasOption( 'pages-with-matches' );
59
60        if ( ( $regex[0] ?? '' ) === '/' ) {
61            $delimRegex = $regex;
62        } else {
63            $delimRegex = '{' . $regex . '}';
64        }
65
66        foreach ( $this->findPages( $prefix ) as $page ) {
67            $content = $page->getContent( RevisionRecord::RAW );
68            $titleText = $page->getTitle()->getPrefixedDBkey();
69            if ( !$content ) {
70                $this->error( "Page has no content: $titleText" );
71                continue;
72            }
73            if ( !$content instanceof TextContent ) {
74                $this->error( "Page has a non-text content model: $titleText" );
75                continue;
76            }
77
78            $text = $content->getText();
79
80            if ( $titleOnly ) {
81                if ( preg_match( $delimRegex, $text ) ) {
82                    if ( $showWiki ) {
83                        echo "$wikiId\t$titleText\n";
84                    } else {
85                        echo "$titleText\n";
86                    }
87                }
88            } else {
89                foreach ( StringUtils::explode( "\n", $text ) as $lineNum => $line ) {
90                    $lineNum++;
91                    if ( preg_match( $delimRegex, $line ) ) {
92                        if ( $showWiki ) {
93                            echo "$wikiId\t$titleText:$lineNum:$line\n";
94                        } else {
95                            echo "$titleText:$lineNum:$line\n";
96                        }
97                    }
98                }
99            }
100        }
101    }
102
103    public function findPages( $prefixes = null ) {
104        $dbr = $this->getReplicaDB();
105        $orConds = [];
106        if ( $prefixes !== null ) {
107            foreach ( $prefixes as $prefix ) {
108                $colonPos = strpos( $prefix, ':' );
109                if ( $colonPos !== false ) {
110                    $ns = $this->contLang->getNsIndex( substr( $prefix, 0, $colonPos ) );
111                    $prefixDBkey = substr( $prefix, $colonPos + 1 );
112                } else {
113                    $ns = NS_MAIN;
114                    $prefixDBkey = $prefix;
115                }
116                $prefixExpr = $dbr->expr( 'page_namespace', '=', $ns );
117                if ( $prefixDBkey !== '' ) {
118                    $prefixExpr = $prefixExpr->and(
119                        'page_title',
120                        IExpression::LIKE,
121                        new LikeValue( $prefixDBkey, $dbr->anyString() )
122                    );
123                }
124                $orConds[] = $prefixExpr;
125            }
126        }
127        $lastId = 0;
128        do {
129            $res = $dbr->newSelectQueryBuilder()
130                ->queryInfo( WikiPage::getQueryInfo() )
131                ->where( $orConds ? $dbr->orExpr( $orConds ) : [] )
132                ->andWhere( $dbr->expr( 'page_id', '>', $lastId ) )
133                ->limit( 200 )
134                ->caller( __METHOD__ )
135                ->fetchResultSet();
136            foreach ( $res as $row ) {
137                $title = Title::newFromRow( $row );
138                yield $this->wikiPageFactory->newFromTitle( $title );
139                $lastId = $row->page_id;
140            }
141        } while ( $res->numRows() );
142    }
143}
144
145// @codeCoverageIgnoreStart
146$maintClass = GrepPages::class;
147require_once RUN_MAINTENANCE_IF_MAIN;
148// @codeCoverageIgnoreEnd