Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
69.09% |
76 / 110 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
CargoQuery | |
69.09% |
76 / 110 |
|
0.00% |
0 / 2 |
77.43 | |
0.00% |
0 / 1 |
run | |
72.73% |
72 / 99 |
|
0.00% |
0 / 1 |
55.09 | |||
setBacklinks | |
36.36% |
4 / 11 |
|
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 | */ |
8 | use MediaWiki\MediaWikiServices; |
9 | |
10 | class 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 | } |