Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.09% covered (warning)
69.09%
76 / 110
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoQuery
69.09% covered (warning)
69.09%
76 / 110
0.00% covered (danger)
0.00%
0 / 2
77.43
0.00% covered (danger)
0.00%
0 / 1
 run
72.73% covered (warning)
72.73%
72 / 99
0.00% covered (danger)
0.00%
0 / 1
55.09
 setBacklinks
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
8.12
1<?php
2/**
3 * CargoQuery - class for the #cargo_query parser function.
4 *
5 * @author Yaron Koren
6 * @ingroup Cargo
7 */
8use MediaWiki\MediaWikiServices;
9
10class CargoQuery {
11
12    /**
13     * Handles the #cargo_query parser function - calls a query on the
14     * Cargo data stored in the database.
15     *
16     * @param Parser $parser
17     * @return string|array Error message string, or an array holding output text and format flags
18     */
19    public static function run( $parser ) {
20        global $wgCargoIgnoreBacklinks;
21
22        $params = func_get_args();
23        array_shift( $params ); // we already know the $parser...
24
25        $tablesStr = null;
26        $fieldsStr = null;
27        $whereStr = null;
28        $joinOnStr = null;
29        $groupByStr = null;
30        $havingStr = null;
31        $orderByStr = null;
32        $limitStr = null;
33        $offsetStr = null;
34        $noHTML = false;
35        $format = 'auto'; // default
36        $displayParams = [];
37
38        foreach ( $params as $param ) {
39            $parts = explode( '=', $param, 2 );
40
41            if ( count( $parts ) == 1 ) {
42                if ( $param == 'no html' ) {
43                    $noHTML = true;
44                }
45                continue;
46            }
47            if ( count( $parts ) > 2 ) {
48                continue;
49            }
50            $key = trim( $parts[0] );
51            $value = trim( $parts[1] );
52            if ( $key == 'tables' || $key == 'table' ) {
53                $tablesStr = $value;
54            } elseif ( $key == 'fields' ) {
55                $fieldsStr = $value;
56            } elseif ( $key == 'where' ) {
57                $whereStr = $value;
58            } elseif ( $key == 'join on' ) {
59                $joinOnStr = $value;
60            } elseif ( $key == 'group by' ) {
61                $groupByStr = $value;
62            } elseif ( $key == 'having' ) {
63                $havingStr = $value;
64            } elseif ( $key == 'order by' ) {
65                $orderByStr = $value;
66            } elseif ( $key == 'limit' ) {
67                $limitStr = $value;
68            } elseif ( $key == 'offset' ) {
69                $offsetStr = $value;
70            } elseif ( $key == 'format' ) {
71                $format = $value;
72            } else {
73                // We'll assume it's going to the formatter.
74                $displayParams[$key] = $value;
75            }
76        }
77        // Special handling.
78        if ( $format == 'dynamic table' && $orderByStr != null ) {
79            $displayParams['order by'] = $orderByStr;
80        }
81
82        try {
83            $sqlQuery = CargoSQLQuery::newFromValues( $tablesStr, $fieldsStr, $whereStr, $joinOnStr,
84                $groupByStr, $havingStr, $orderByStr, $limitStr, $offsetStr );
85            // If this is a non-grouped query, make a 2nd query just
86            // for _pageID (since the original query won't always
87            // have a _pageID field) in order to populate the
88            // cargo_backlinks table.
89            // Also remove the limit from this 2nd query so that it
90            // can include all results.
91            // Fetch results title only if "cargo_backlinks" table exists
92            $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
93            $dbr = $lb->getConnectionRef( DB_REPLICA );
94            if ( !$wgCargoIgnoreBacklinks && !$sqlQuery->isAggregating() && $dbr->tableExists( 'cargo_backlinks' ) ) {
95                $newFieldsStr = $fieldsStr;
96                // $fieldsToCollectForPageIDs allows us to
97                // collect all those special fields' values in
98                // the results
99                $fieldsToCollectForPageIDs = [];
100                foreach ( $sqlQuery->mAliasedTableNames as $alias => $table ) {
101                    // Ignore helper tables.
102                    if ( strpos( $table, '__' ) !== false ) {
103                        continue;
104                    }
105                    $fieldFullName = "cargo_backlink_page_id_$alias";
106                    $fieldsToCollectForPageIDs[] = $fieldFullName;
107                    $newFieldsStr = "$alias._pageID=$fieldFullName" . $newFieldsStr;
108                }
109                $sqlQueryJustForResultsTitle = CargoSQLQuery::newFromValues(
110                    $tablesStr, $newFieldsStr, $whereStr, $joinOnStr,
111                    $groupByStr, $havingStr, $orderByStr, '', $offsetStr
112                );
113                $queryResultsJustForResultsTitle = $sqlQueryJustForResultsTitle->run();
114            }
115        } catch ( Exception $e ) {
116            return CargoUtils::formatError( $e->getMessage() );
117        }
118
119        $pageIDsForBacklinks = [];
120        if ( isset( $queryResultsJustForResultsTitle ) ) {
121            // Collect all special _pageID entries.
122            foreach ( $fieldsToCollectForPageIDs as $fieldToCollectForPageIds ) {
123                $pageIDsForBacklinks = array_merge( $pageIDsForBacklinks, array_column( $queryResultsJustForResultsTitle, $fieldToCollectForPageIds ) );
124            }
125            $pageIDsForBacklinks = array_unique( $pageIDsForBacklinks );
126        }
127
128        $queryDisplayer = CargoQueryDisplayer::newFromSQLQuery( $sqlQuery );
129        $queryDisplayer->mFormat = $format;
130        $queryDisplayer->mDisplayParams = $displayParams;
131        $queryDisplayer->mParser = $parser;
132        $formatter = $queryDisplayer->getFormatter( $parser->getOutput(), $parser );
133
134        // Let the format run the query itself, if it wants to.
135        if ( $formatter->isDeferred() ) {
136            // @TODO - fix this inefficiency. Right now a
137            // CargoSQLQuery object is constructed three times for
138            // deferred formats: the first two times here and the
139            // 3rd by Special:CargoExport. It's the first
140            // construction that involves a bunch of text
141            // processing, and is unneeded.
142            // However, this first CargoSQLQuery is passed to
143            // the CargoQueryDisplayer, which in turn uses it
144            // to figure out the formatting class, so that we
145            // know whether it is a deferred class or not. The
146            // class is based in part on the set of fields in the
147            // query, so in theory (though not in practice),
148            // whether or not it's deferred could depend on the
149            // fields in the query, making the first 'Query
150            // necessary. There has to be some better way, though.
151            $sqlQuery = CargoSQLQuery::newFromValues( $tablesStr, $fieldsStr, $whereStr, $joinOnStr,
152                $groupByStr, $havingStr, $orderByStr, $limitStr, $offsetStr );
153            $text = $formatter->queryAndDisplay( [ $sqlQuery ], $displayParams );
154            self::setBackLinks( $parser, $pageIDsForBacklinks );
155            return [ $text, 'noparse' => true, 'isHTML' => true ];
156        }
157
158        // If the query limit was set to 0, no need to run the query -
159        // all we need to do is show the "more results" link, then exit.
160        if ( $sqlQuery->mQueryLimit == 0 ) {
161            $text = $queryDisplayer->viewMoreResultsLink( true );
162            return [ $text, 'noparse' => true, 'isHTML' => true ];
163        }
164
165        try {
166            $queryResults = $sqlQuery->run();
167        } catch ( Exception $e ) {
168            return CargoUtils::formatError( $e->getMessage() );
169        }
170
171        // Finally, do the display.
172        $text = $queryDisplayer->displayQueryResults( $formatter, $queryResults );
173        // If there are no results, then - given that we already know
174        // that the limit was not set to 0 - we just need to display an
175        // automatic message, so there's no need for special parsing.
176        if ( count( $queryResults ) == 0 ) {
177            return $text;
178        }
179        // No errors? Let's save our reverse links.
180        self::setBackLinks( $parser, $pageIDsForBacklinks );
181
182        // The 'template' format gets special parsing, because
183        // it can be used to display a larger component, like a table,
184        // which means that everything needs to be parsed together
185        // instead of one instance at a time. Also, the template will
186        // contain wikitext, not HTML.
187        $displayHTML = ( !$noHTML && $format != 'template' );
188
189        // If there are (seemingly) more results than what we showed,
190        // show a "View more" link that links to Special:ViewData.
191        if ( count( $queryResults ) == $sqlQuery->mQueryLimit ) {
192            $text .= $queryDisplayer->viewMoreResultsLink( $displayHTML );
193        }
194
195        if ( $displayHTML ) {
196            return [ $text, 'noparse' => true, 'isHTML' => true ];
197        } else {
198            return [ $text, 'noparse' => false ];
199        }
200    }
201
202    /**
203     * Store the list of page IDs referenced by this query in the parser output.
204     * @param Parser $parser
205     * @param int[] $backlinkPageIds List of referenced page IDs to store.
206     */
207    private static function setBacklinks( Parser $parser, array $backlinkPageIds ): void {
208        $parserOutput = $parser->getOutput();
209
210        // MW 1.38 compatibility
211        if ( method_exists( $parserOutput, 'appendExtensionData' ) ) {
212            foreach ( $backlinkPageIds as $pageId ) {
213                $parserOutput->appendExtensionData( CargoBackLinks::BACKLINKS_DATA_KEY, $pageId );
214            }
215        } else {
216            $backlinks = (array)$parserOutput->getExtensionData( CargoBackLinks::BACKLINKS_DATA_KEY );
217            foreach ( $backlinkPageIds as $pageId ) {
218                $backlinks[$pageId] = true;
219            }
220
221            $parserOutput->setExtensionData(
222                CargoBackLinks::BACKLINKS_DATA_KEY,
223                $backlinks
224            );
225        }
226    }
227
228}