Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.84% covered (warning)
71.84%
74 / 103
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoQuery
71.84% covered (warning)
71.84%
74 / 103
50.00% covered (danger)
50.00%
1 / 2
70.23
0.00% covered (danger)
0.00%
0 / 1
 run
71.00% covered (warning)
71.00%
71 / 100
0.00% covered (danger)
0.00%
0 / 1
67.61
 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            } elseif ( $key == 'intro' || $key == 'outro' || $key == 'default' ) {
72                $displayParams[$key] = $value;
73            } else {
74                // We'll assume it's going to the formatter.
75                $displayParams[$key] = htmlspecialchars( $value );
76            }
77        }
78        // Special handling.
79        if ( $format == 'dynamic table' && $orderByStr != null ) {
80            $displayParams['order by'] = $orderByStr;
81        }
82
83        try {
84            $sqlQuery = CargoSQLQuery::newFromValues( $tablesStr, $fieldsStr, $whereStr, $joinOnStr,
85                $groupByStr, $havingStr, $orderByStr, $limitStr, $offsetStr );
86            // If this is a non-grouped query, make a 2nd query just
87            // for _pageID (since the original query won't always
88            // have a _pageID field) in order to populate the
89            // cargo_backlinks table.
90            // Also remove the limit from this 2nd query so that it
91            // can include all results.
92            // Fetch results title only if "cargo_backlinks" table exists
93            $dbr = CargoUtils::getMainDBForRead();
94            if ( !$wgCargoIgnoreBacklinks && !$sqlQuery->isAggregating() && $dbr->tableExists( 'cargo_backlinks', __METHOD__ ) ) {
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        foreach ( $backlinkPageIds as $pageId ) {
211            $parserOutput->appendExtensionData( CargoBackLinks::BACKLINKS_DATA_KEY, $pageId );
212        }
213    }
214
215}