Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiGraph
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 6
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 preprocess
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getGraphSpec
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 *
4 * @license MIT
5 * @file
6 *
7 * @author Yuri Astrakhan
8 */
9
10namespace Graph;
11
12use MediaWiki\Api\ApiBase;
13use MediaWiki\Api\ApiMain;
14use MediaWiki\Json\FormatJson;
15use MediaWiki\Page\WikiPageFactory;
16use MediaWiki\Parser\ParserFactory;
17use MediaWiki\Parser\ParserOptions;
18use MediaWiki\Title\Title;
19use Wikimedia\ObjectCache\WANObjectCache;
20use Wikimedia\ParamValidator\ParamValidator;
21
22/**
23 * This class implements action=graph api, allowing client-side graphs to get the spec,
24 * regardless of how it is stored (page-props or other storage)
25 * @package Graph
26 */
27class ApiGraph extends ApiBase {
28    private ParserFactory $parserFactory;
29    private WANObjectCache $cache;
30    private WikiPageFactory $wikiPageFactory;
31
32    public function __construct(
33        ApiMain $main,
34        string $action,
35        ParserFactory $parserFactory,
36        WANObjectCache $cache,
37        WikiPageFactory $wikiPageFactory
38    ) {
39        parent::__construct( $main, $action );
40        $this->parserFactory = $parserFactory;
41        $this->cache = $cache;
42        $this->wikiPageFactory = $wikiPageFactory;
43    }
44
45    public function execute() {
46        $params = $this->extractRequestParams();
47
48        $this->requireOnlyOneParameter( $params, 'title', 'text' );
49
50        if ( $params['title'] !== null ) {
51            if ( $params['hash'] === null ) {
52                $this->dieWithError( [ 'apierror-invalidparammix-mustusewith', 'title', 'hash' ],
53                    'missingparam' );
54            }
55            $graph = $this->getGraphSpec( $params['title'], $params['oldid'], $params['hash'] );
56        } else {
57            if ( !$this->getRequest()->wasPosted() ) {
58                $this->dieWithError( 'apierror-graph-mustposttext', 'mustposttext' );
59            }
60            if ( $params['hash'] !== null ) {
61                $this->dieWithError( [ 'apierror-invalidparammix-cannotusewith', 'hash', 'text' ],
62                    'invalidparammix' );
63            }
64            $graph = $this->preprocess( $params['text'] );
65        }
66
67        $this->getMain()->setCacheMode( 'public' );
68        $this->getResult()->addValue( null, $this->getModuleName(), $graph );
69    }
70
71    /**
72     * @inheritDoc
73     */
74    public function getAllowedParams() {
75        return [
76            'hash' => [
77                ParamValidator::PARAM_TYPE => 'string',
78            ],
79            'title' => [
80                ParamValidator::PARAM_TYPE => 'string',
81            ],
82            'text' => [
83                ParamValidator::PARAM_TYPE => 'string',
84            ],
85            'oldid' => [
86                ParamValidator::PARAM_TYPE => 'integer',
87                ParamValidator::PARAM_DEFAULT => 0
88            ],
89        ];
90    }
91
92    /**
93     * @inheritDoc
94     */
95    protected function getExamplesMessages() {
96        return [
97            'formatversion=2&action=graph&title=Extension%3AGraph%2FDemo' .
98                '&hash=1533aaad45c733dcc7e07614b54cbae4119a6747' => 'apihelp-graph-example',
99        ];
100    }
101
102    /**
103     * Parse graph definition that may contain wiki markup into pure json
104     * @param string $text
105     * @return string
106     */
107    private function preprocess( $text ) {
108        $title = Title::makeTitle( NS_SPECIAL, Sandbox::PAGENAME )->fixSpecialName();
109        $text = $this->parserFactory->getInstance()
110            ->preprocess( $text, $title, new ParserOptions( $this->getUser() ) );
111        $st = FormatJson::parse( $text, FormatJson::TRY_FIXING | FormatJson::STRIP_COMMENTS );
112        if ( !$st->isOK() ) {
113            // Sometimes we get <graph ...> {...} </graph> as input. Try to strip <graph> tags
114            $count = 0;
115            $text = preg_replace( '/^\s*<graph[^>]*>(.*)<\/graph>\s*$/s', '$1', $text, 1, $count );
116            if ( $count === 1 ) {
117                $st = FormatJson::parse( $text );
118            }
119            if ( !$st->isOK() ) {
120                $this->dieWithError( 'apierror-graph-invalid', 'invalidtext' );
121            }
122        }
123        return $st->getValue();
124    }
125
126    /**
127     * Get graph definition with title and hash
128     * @param string $titleText
129     * @param int $revId
130     * @param string $hash
131     * @return mixed Decoded graph spec from the DB or the stash
132     */
133    private function getGraphSpec( $titleText, $revId, $hash ) {
134        $title = Title::newFromText( $titleText );
135        if ( !$title ) {
136            $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $titleText ) ] );
137        }
138
139        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
140        $page = $this->wikiPageFactory->newFromTitle( $title );
141        if ( !$page->exists() ) {
142            $this->dieWithError( 'apierror-missingtitle' );
143        }
144
145        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
146        $this->checkTitleUserPermissions( $title, 'read' );
147
148        // Use caching to avoid parses for old revisions and I/O for current revisions
149        $graph = $this->cache->getWithSetCallback(
150            $this->cache->makeKey( 'graph-data', $hash, $page->getTouched() ),
151            $this->cache::TTL_DAY,
152            static function ( $oldValue, &$ttl ) use ( $page, $revId, $hash ) {
153                $value = false;
154                $parserOptions = ParserOptions::newFromAnon();
155                $parserOutput = $page->getParserOutput( $parserOptions, $revId );
156
157                if ( $parserOutput !== false ) {
158                    $allGraphs = $parserOutput->getExtensionData( 'graph_specs_index' );
159                    if ( is_array( $allGraphs ) && array_key_exists( $hash, $allGraphs ) ) {
160                        $value = $parserOutput->getExtensionData( 'graph_specs[' . $hash . ']' );
161                    }
162                }
163
164                return $value;
165            }
166        );
167
168        if ( !$graph ) {
169            $this->dieWithError( 'apierror-graph-missing', 'invalidhash' );
170        }
171
172        return $graph;
173    }
174}