Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
63.39% |
71 / 112 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
CargoQuery | |
63.39% |
71 / 112 |
|
0.00% |
0 / 2 |
104.16 | |
0.00% |
0 / 1 |
run | |
66.34% |
67 / 101 |
|
0.00% |
0 / 1 |
74.54 | |||
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 ( $groupByStr == '' && !$wgCargoIgnoreBacklinks && $dbr->tableExists( 'cargo_backlinks' ) ) { |
95 | $allTables = array_keys( $sqlQuery->mAliasedTableNames ); |
96 | $allTables = array_filter( $allTables ); |
97 | $newFieldsStr = $fieldsStr; |
98 | // $fieldsToCollectForPageIDs allows us to |
99 | // collect all those special fields' values in |
100 | // the results |
101 | $fieldsToCollectForPageIDs = []; |
102 | foreach ( $allTables as $table ) { |
103 | // Ignore helper tables. |
104 | if ( strpos( $table, '__' ) !== false ) { |
105 | continue; |
106 | } |
107 | $fieldFullName = "cargo_backlink_page_id_$table"; |
108 | $fieldsToCollectForPageIDs[] = $fieldFullName; |
109 | $newFieldsStr = "$table._pageID=$fieldFullName, " . $newFieldsStr; |
110 | } |
111 | $sqlQueryJustForResultsTitle = CargoSQLQuery::newFromValues( |
112 | $tablesStr, $newFieldsStr, $whereStr, $joinOnStr, |
113 | $groupByStr, $havingStr, $orderByStr, '', $offsetStr |
114 | ); |
115 | $queryResultsJustForResultsTitle = $sqlQueryJustForResultsTitle->run(); |
116 | } |
117 | } catch ( Exception $e ) { |
118 | return CargoUtils::formatError( $e->getMessage() ); |
119 | } |
120 | |
121 | $pageIDsForBacklinks = []; |
122 | if ( isset( $queryResultsJustForResultsTitle ) ) { |
123 | // Collect all special _pageID entries. |
124 | foreach ( $fieldsToCollectForPageIDs as $fieldToCollectForPageIds ) { |
125 | $pageIDsForBacklinks = array_merge( $pageIDsForBacklinks, array_column( $queryResultsJustForResultsTitle, $fieldToCollectForPageIds ) ); |
126 | } |
127 | $pageIDsForBacklinks = array_unique( $pageIDsForBacklinks ); |
128 | } |
129 | |
130 | $queryDisplayer = CargoQueryDisplayer::newFromSQLQuery( $sqlQuery ); |
131 | $queryDisplayer->mFormat = $format; |
132 | $queryDisplayer->mDisplayParams = $displayParams; |
133 | $queryDisplayer->mParser = $parser; |
134 | $formatter = $queryDisplayer->getFormatter( $parser->getOutput(), $parser ); |
135 | |
136 | // Let the format run the query itself, if it wants to. |
137 | if ( $formatter->isDeferred() ) { |
138 | // @TODO - fix this inefficiency. Right now a |
139 | // CargoSQLQuery object is constructed three times for |
140 | // deferred formats: the first two times here and the |
141 | // 3rd by Special:CargoExport. It's the first |
142 | // construction that involves a bunch of text |
143 | // processing, and is unneeded. |
144 | // However, this first CargoSQLQuery is passed to |
145 | // the CargoQueryDisplayer, which in turn uses it |
146 | // to figure out the formatting class, so that we |
147 | // know whether it is a deferred class or not. The |
148 | // class is based in part on the set of fields in the |
149 | // query, so in theory (though not in practice), |
150 | // whether or not it's deferred could depend on the |
151 | // fields in the query, making the first 'Query |
152 | // necessary. There has to be some better way, though. |
153 | $sqlQuery = CargoSQLQuery::newFromValues( $tablesStr, $fieldsStr, $whereStr, $joinOnStr, |
154 | $groupByStr, $havingStr, $orderByStr, $limitStr, $offsetStr ); |
155 | $text = $formatter->queryAndDisplay( [ $sqlQuery ], $displayParams ); |
156 | self::setBackLinks( $parser, $pageIDsForBacklinks ); |
157 | return [ $text, 'noparse' => true, 'isHTML' => true ]; |
158 | } |
159 | |
160 | // If the query limit was set to 0, no need to run the query - |
161 | // all we need to do is show the "more results" link, then exit. |
162 | if ( $sqlQuery->mQueryLimit == 0 ) { |
163 | $text = $queryDisplayer->viewMoreResultsLink( true ); |
164 | return [ $text, 'noparse' => true, 'isHTML' => true ]; |
165 | } |
166 | |
167 | try { |
168 | $queryResults = $sqlQuery->run(); |
169 | } catch ( Exception $e ) { |
170 | return CargoUtils::formatError( $e->getMessage() ); |
171 | } |
172 | |
173 | // Finally, do the display. |
174 | $text = $queryDisplayer->displayQueryResults( $formatter, $queryResults ); |
175 | // If there are no results, then - given that we already know |
176 | // that the limit was not set to 0 - we just need to display an |
177 | // automatic message, so there's no need for special parsing. |
178 | if ( count( $queryResults ) == 0 ) { |
179 | return $text; |
180 | } |
181 | // No errors? Let's save our reverse links. |
182 | self::setBackLinks( $parser, $pageIDsForBacklinks ); |
183 | |
184 | // The 'template' format gets special parsing, because |
185 | // it can be used to display a larger component, like a table, |
186 | // which means that everything needs to be parsed together |
187 | // instead of one instance at a time. Also, the template will |
188 | // contain wikitext, not HTML. |
189 | $displayHTML = ( !$noHTML && $format != 'template' ); |
190 | |
191 | // If there are (seemingly) more results than what we showed, |
192 | // show a "View more" link that links to Special:ViewData. |
193 | if ( count( $queryResults ) == $sqlQuery->mQueryLimit ) { |
194 | $text .= $queryDisplayer->viewMoreResultsLink( $displayHTML ); |
195 | } |
196 | |
197 | if ( $displayHTML ) { |
198 | return [ $text, 'noparse' => true, 'isHTML' => true ]; |
199 | } else { |
200 | return [ $text, 'noparse' => false ]; |
201 | } |
202 | } |
203 | |
204 | /** |
205 | * Store the list of page IDs referenced by this query in the parser output. |
206 | * @param Parser $parser |
207 | * @param int[] $backlinkPageIds List of referenced page IDs to store. |
208 | */ |
209 | private static function setBacklinks( Parser $parser, array $backlinkPageIds ): void { |
210 | $parserOutput = $parser->getOutput(); |
211 | |
212 | // MW 1.38 compatibility |
213 | if ( method_exists( $parserOutput, 'appendExtensionData' ) ) { |
214 | foreach ( $backlinkPageIds as $pageId ) { |
215 | $parserOutput->appendExtensionData( CargoBackLinks::BACKLINKS_DATA_KEY, $pageId ); |
216 | } |
217 | } else { |
218 | $backlinks = (array)$parserOutput->getExtensionData( CargoBackLinks::BACKLINKS_DATA_KEY ); |
219 | foreach ( $backlinkPageIds as $pageId ) { |
220 | $backlinks[$pageId] = true; |
221 | } |
222 | |
223 | $parserOutput->setExtensionData( |
224 | CargoBackLinks::BACKLINKS_DATA_KEY, |
225 | $backlinks |
226 | ); |
227 | } |
228 | } |
229 | |
230 | } |