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