Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
69.64% covered (warning)
69.64%
78 / 112
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoQuery
69.64% covered (warning)
69.64%
78 / 112
0.00% covered (danger)
0.00%
0 / 2
75.30
0.00% covered (danger)
0.00%
0 / 1
 run
73.27% covered (warning)
73.27%
74 / 101
0.00% covered (danger)
0.00%
0 / 1
53.80
 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 ( $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}