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