Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 63 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
ChartRenderer | |
0.00% |
0 / 63 |
|
0.00% |
0 / 4 |
72 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
renderSVG | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
renderWithService | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
6 | |||
renderWithCli | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Chart; |
4 | |
5 | use MediaWiki\Config\ServiceOptions; |
6 | use MediaWiki\Context\RequestContext; |
7 | use MediaWiki\Http\HttpRequestFactory; |
8 | use MediaWiki\Language\FormatterFactory; |
9 | use MediaWiki\Shell\Shell; |
10 | use MediaWiki\Status\Status; |
11 | use MWCryptHash; |
12 | use Psr\Log\LoggerInterface; |
13 | use stdclass; |
14 | |
15 | class ChartRenderer { |
16 | |
17 | private ServiceOptions $options; |
18 | private HttpRequestFactory $httpRequestFactory; |
19 | private FormatterFactory $formatterFactory; |
20 | private LoggerInterface $logger; |
21 | |
22 | /** |
23 | * @internal For use by ServiceWiring |
24 | */ |
25 | public const CONSTRUCTOR_OPTIONS = [ |
26 | 'ChartServiceUrl', |
27 | 'ChartCliPath' |
28 | ]; |
29 | |
30 | /** |
31 | * @param ServiceOptions $options |
32 | * @param HttpRequestFactory $httpRequestFactory |
33 | * @param FormatterFactory $formatterFactory |
34 | * @param LoggerInterface $logger |
35 | */ |
36 | public function __construct( |
37 | ServiceOptions $options, |
38 | HttpRequestFactory $httpRequestFactory, |
39 | FormatterFactory $formatterFactory, |
40 | LoggerInterface $logger |
41 | ) { |
42 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
43 | $this->options = $options; |
44 | $this->httpRequestFactory = $httpRequestFactory; |
45 | $this->formatterFactory = $formatterFactory; |
46 | $this->logger = $logger; |
47 | } |
48 | |
49 | /** |
50 | * Render a chart from a definition object and a tabular data object. |
51 | * |
52 | * @param stdclass $chartDef Chart definition, obtained from JCChartContent::getContent() |
53 | * @param stdclass $tabularData Tabular data, obtained from JCTabularContent::getContent() |
54 | * @param array{width?:string,height?:string} $options Additional rendering options: |
55 | * 'width': Width of the chart, in pixels. Overrides width specified in the chart definition |
56 | * 'height': Height of the chart, in pixels. Overrides height specified in the chart definition. |
57 | * @return Status A Status object wrapping an SVG string or an error |
58 | */ |
59 | public function renderSVG( stdclass $chartDef, stdclass $tabularData, array $options = [] ): Status { |
60 | // Prefix for IDs in the SVG. This has to be unique between charts on the same page, to |
61 | // prevent ID collisions (T371558). If the same chart with the same data is displayed twice |
62 | // on the same page, this gives them the same ID prefixes and causes their IDs to collide, |
63 | // but that doesn't seem to cause a problem in practice. |
64 | $definitionForHash = json_encode( [ 'format' => $chartDef, 'source' => $tabularData ] ); |
65 | $chartDef = clone $chartDef; |
66 | $chartDef->idPrefix = 'mw-chart-' . MWCryptHash::hash( $definitionForHash, false ); |
67 | |
68 | if ( $this->options->get( 'ChartServiceUrl' ) !== null ) { |
69 | return $this->renderWithService( $chartDef, $tabularData, $options ); |
70 | } |
71 | |
72 | return $this->renderWithCli( $chartDef, $tabularData, $options ); |
73 | } |
74 | |
75 | private function renderWithService( stdclass $chartDef, stdclass $tabularData, array $options ): Status { |
76 | $requestData = array_merge( [ |
77 | 'definition' => $chartDef, |
78 | 'data' => $tabularData, |
79 | ], $options ); |
80 | |
81 | $requestOptions = [ |
82 | 'method' => 'POST', |
83 | 'postData' => json_encode( $requestData ) |
84 | ]; |
85 | $request = $this->httpRequestFactory->create( |
86 | $this->options->get( 'ChartServiceUrl' ), |
87 | $requestOptions, |
88 | __METHOD__ |
89 | ); |
90 | $request->setHeader( 'Content-Type', 'application/json' ); |
91 | |
92 | $status = $request->execute(); |
93 | if ( !$status->isOK() ) { |
94 | [ $message, $context ] = $this->formatterFactory->getStatusFormatter( RequestContext::getMain() ) |
95 | ->getPsr3MessageAndContext( $status ); |
96 | $this->logger->error( |
97 | 'Chart service request returned error: {error}', |
98 | [ 'error' => $message ] + $context |
99 | ); |
100 | return Status::newFatal( 'chart-error-rendering-error' ); |
101 | } |
102 | $response = $request->getContent(); |
103 | return Status::newGood( $response ); |
104 | } |
105 | |
106 | private function renderWithCli( stdclass $chartDef, stdclass $tabularData, array $options ): Status { |
107 | if ( Shell::isDisabled() ) { |
108 | return Status::newFatal( 'chart-error-shell-disabled' ); |
109 | } |
110 | |
111 | $dataPath = tempnam( \wfTempDir(), 'data-json' ); |
112 | file_put_contents( $dataPath, json_encode( [ |
113 | 'definition' => $chartDef, |
114 | 'data' => $tabularData, |
115 | ...$options |
116 | ] ) ); |
117 | |
118 | $result = Shell::command( |
119 | 'node', |
120 | $this->options->get( 'ChartCliPath' ), |
121 | $dataPath, |
122 | '-' |
123 | ) |
124 | ->execute(); |
125 | |
126 | $error = $result->getStderr(); |
127 | if ( $error ) { |
128 | $this->logger->error( |
129 | 'Chart shell command returned error: {error}', |
130 | [ 'error' => $error ] |
131 | ); |
132 | |
133 | // @todo tracking category |
134 | $status = Status::newFatal( 'chart-error-rendering-error' ); |
135 | } else { |
136 | $svg = $result->getStdout(); |
137 | $status = Status::newGood( $svg ); |
138 | } |
139 | |
140 | unlink( $dataPath ); |
141 | return $status; |
142 | } |
143 | } |