Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 162 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
HeaderBlock | |
0.00% |
0 / 162 |
|
0.00% |
0 / 11 |
2550 | |
0.00% |
0 / 1 |
init | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
validate | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
validateNextRevision | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
validateFirstRevision | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
commit | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
renderApi | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
90 | |||
renderDiffviewApi | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
renderSingleViewApi | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
renderRevisionApi | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
42 | |||
renderUndoApi | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Flow\Block; |
4 | |
5 | use Flow\Container; |
6 | use Flow\Exception\FlowException; |
7 | use Flow\Exception\InvalidActionException; |
8 | use Flow\Exception\InvalidInputException; |
9 | use Flow\Formatter\FormatterRow; |
10 | use Flow\Formatter\HeaderViewQuery; |
11 | use Flow\Formatter\RevisionDiffViewFormatter; |
12 | use Flow\Formatter\RevisionViewFormatter; |
13 | use Flow\Formatter\RevisionViewQuery; |
14 | use Flow\Model\Header; |
15 | use Flow\Model\UUID; |
16 | use Flow\RevisionActionPermissions; |
17 | use Flow\UrlGenerator; |
18 | use IContextSource; |
19 | |
20 | class HeaderBlock extends AbstractBlock { |
21 | /** |
22 | * @var Header|null |
23 | */ |
24 | protected $header; |
25 | |
26 | /** |
27 | * New revision created via submission. |
28 | * |
29 | * @var Header|null |
30 | */ |
31 | protected $newRevision; |
32 | |
33 | /** |
34 | * @var array Map of data to be passed on as |
35 | * commit metadata for event handlers |
36 | */ |
37 | protected $extraCommitMetadata = []; |
38 | |
39 | /** |
40 | * @var string[] |
41 | */ |
42 | protected $supportedPostActions = [ 'edit-header', 'undo-edit-header' ]; |
43 | |
44 | /** |
45 | * @var string[] |
46 | */ |
47 | protected $supportedGetActions = [ 'view', 'compare-header-revisions', 'edit-header', 'view-header', 'undo-edit-header' ]; |
48 | |
49 | /** |
50 | * @var string[] |
51 | * @todo Fill in the template names |
52 | */ |
53 | protected $templates = [ |
54 | 'view' => '', |
55 | 'compare-header-revisions' => 'diff_view', |
56 | 'edit-header' => 'edit', |
57 | 'undo-edit-header' => 'undo_edit', |
58 | 'view-header' => 'single_view', |
59 | ]; |
60 | |
61 | /** |
62 | * @var RevisionActionPermissions Allows or denies actions to be performed |
63 | */ |
64 | protected $permissions; |
65 | |
66 | public function init( IContextSource $context, $action ) { |
67 | parent::init( $context, $action ); |
68 | |
69 | // Basic initialisation done -- now, load data if applicable |
70 | if ( $this->workflow->isNew() ) { |
71 | return; |
72 | } |
73 | |
74 | // Get the latest revision attached to this workflow |
75 | $found = $this->storage->find( |
76 | 'Header', |
77 | [ 'rev_type_id' => $this->workflow->getId() ], |
78 | [ 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ] |
79 | ); |
80 | |
81 | if ( $found ) { |
82 | $this->header = reset( $found ); |
83 | } |
84 | } |
85 | |
86 | protected function validate() { |
87 | // @todo T113902: some sort of restriction along the lines of article protection |
88 | if ( !isset( $this->submitted['content'] ) ) { |
89 | $this->addError( 'content', $this->context->msg( 'flow-error-missing-header-content' ) ); |
90 | } |
91 | |
92 | if ( $this->header ) { |
93 | $this->validateNextRevision(); |
94 | } else { |
95 | // simpler case |
96 | $this->validateFirstRevision(); |
97 | } |
98 | } |
99 | |
100 | protected function validateNextRevision() { |
101 | if ( !$this->permissions->isAllowed( $this->header, 'edit-header' ) ) { |
102 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
103 | return; |
104 | } |
105 | |
106 | if ( empty( $this->submitted['prev_revision'] ) ) { |
107 | $this->addError( 'prev_revision', $this->context->msg( 'flow-error-missing-prev-revision-identifier' ) ); |
108 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
109 | } elseif ( $this->header->getRevisionId()->getAlphadecimal() !== $this->submitted['prev_revision'] ) { |
110 | // This is a reasonably effective way to ensure prev revision matches, but for guarantees against race |
111 | // conditions there also exists a unique index on rev_prev_revision in mysql, meaning if someone else inserts against the |
112 | // parent we and the submitter think is the latest, our insert will fail. |
113 | // TODO: Catch whatever exception happens there, make sure the most recent revision is the one in the cache before |
114 | // handing user back to specific dialog indicating race condition |
115 | $this->addError( |
116 | 'prev_revision', |
117 | $this->context->msg( 'flow-error-prev-revision-mismatch' )->params( |
118 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
119 | $this->submitted['prev_revision'], |
120 | $this->header->getRevisionId()->getAlphadecimal(), |
121 | $this->context->getUser()->getName() |
122 | ), |
123 | [ 'revision_id' => $this->header->getRevisionId()->getAlphadecimal() ] // save current revision ID |
124 | ); |
125 | } |
126 | |
127 | // this isn't really part of validate, but we want the error-rendering template to see the users edited header |
128 | $this->newRevision = $this->header->newNextRevision( |
129 | $this->context->getUser(), |
130 | $this->submitted['content'] ?? '', |
131 | // default to wikitext when not specified, for old API requests |
132 | $this->submitted['format'] ?? 'wikitext', |
133 | 'edit-header', |
134 | $this->workflow->getArticleTitle() |
135 | ); |
136 | |
137 | if ( $this->newRevision->getRevisionId()->equals( $this->header->getRevisionId() ) ) { |
138 | $this->extraCommitMetadata['null-edit'] = true; |
139 | } elseif ( !$this->checkSpamFilters( $this->header, $this->newRevision ) ) { |
140 | return; |
141 | } |
142 | } |
143 | |
144 | protected function validateFirstRevision() { |
145 | if ( |
146 | !$this->permissions->isRevisionAllowed( null, 'create-header' ) || |
147 | !$this->permissions->isBoardAllowed( $this->workflow, 'create-header' ) |
148 | ) { |
149 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
150 | return; |
151 | } |
152 | if ( isset( $this->submitted['prev_revision'] ) && $this->submitted['prev_revision'] ) { |
153 | // User submitted a previous revision, but we couldn't find one. This is likely |
154 | // an internal error and not a user error, consider better handling |
155 | // is this even worth checking? |
156 | $this->addError( 'prev_revision', $this->context->msg( 'flow-error-prev-revision-does-not-exist' ) ); |
157 | return; |
158 | } |
159 | |
160 | $this->newRevision = Header::create( |
161 | $this->workflow, |
162 | $this->context->getUser(), |
163 | $this->submitted['content'] ?? '', |
164 | // default to wikitext when not specified, for old API requests |
165 | $this->submitted['format'] ?? 'wikitext', |
166 | 'create-header' |
167 | ); |
168 | |
169 | if ( !$this->checkSpamFilters( null, $this->newRevision ) ) { |
170 | return; |
171 | } |
172 | } |
173 | |
174 | public function commit() { |
175 | $metadata = $this->extraCommitMetadata; |
176 | |
177 | switch ( $this->action ) { |
178 | case 'undo-edit-header': |
179 | case 'edit-header': |
180 | // store data, unless we're dealing with a null-edit (in which case |
181 | // is storing the same thing not only pointless, it can even be |
182 | // incorrect, since listeners will run & generate notifications etc) |
183 | if ( !isset( $this->extraCommitMetadata['null-edit'] ) ) { |
184 | $this->workflow->updateLastUpdated( $this->newRevision->getRevisionId() ); |
185 | $this->storage->put( $this->workflow, $metadata ); // 'discussion' workflow |
186 | $this->storage->put( $this->newRevision, $metadata + [ |
187 | 'workflow' => $this->workflow, |
188 | ] ); |
189 | } |
190 | |
191 | // Reload $this->header for renderApi() after save |
192 | $this->header = $this->newRevision; |
193 | return [ |
194 | 'header-revision-id' => $this->newRevision->getRevisionId(), |
195 | ]; |
196 | |
197 | default: |
198 | throw new InvalidActionException( 'Unrecognized commit action', 'invalid-action' ); |
199 | } |
200 | } |
201 | |
202 | public function renderApi( array $options ) { |
203 | $output = [ |
204 | 'type' => $this->getName(), |
205 | 'editToken' => $this->getEditToken(), |
206 | ]; |
207 | |
208 | switch ( $this->action ) { |
209 | case 'view': |
210 | $format = $options['format'] ?? 'fixed-html'; |
211 | $output += $this->renderRevisionApi( $format ); |
212 | break; |
213 | |
214 | case 'edit-header': |
215 | // default to wikitext for no-JS |
216 | $format = $options['format'] ?? 'wikitext'; |
217 | $output += $this->renderRevisionApi( $format ); |
218 | break; |
219 | |
220 | case 'undo-edit-header': |
221 | $output = $this->renderUndoApi( $options ) + $output; |
222 | break; |
223 | |
224 | case 'view-header': |
225 | if ( isset( $options['revId'] ) && $options['revId'] ) { |
226 | $output += $this->renderSingleViewApi( $options['revId'] ); |
227 | } else { |
228 | $format = $options['format'] ?? 'fixed-html'; |
229 | $output += $this->renderRevisionApi( $format ); |
230 | } |
231 | break; |
232 | |
233 | case 'compare-header-revisions': |
234 | $output += $this->renderDiffviewApi( $options ); |
235 | break; |
236 | } |
237 | |
238 | $output['submitted'] = $this->wasSubmitted() ? $this->submitted : []; |
239 | $output['errors'] = $this->errors; |
240 | return $output; |
241 | } |
242 | |
243 | /** |
244 | * @todo Duplicated logic in other diff view block |
245 | * @param array $options |
246 | * @return array |
247 | */ |
248 | protected function renderDiffviewApi( array $options ) { |
249 | if ( !isset( $options['newRevision'] ) || !is_string( $options['newRevision'] ) ) { |
250 | throw new InvalidInputException( 'A valid revision must be provided for comparison', 'revision-comparison' ); |
251 | } |
252 | $oldRevision = null; |
253 | if ( isset( $options['oldRevision'] ) ) { |
254 | $oldRevision = $options['oldRevision']; |
255 | } |
256 | /** @var HeaderViewQuery $query */ |
257 | $query = Container::get( 'query.header.view' ); |
258 | [ $new, $old ] = $query->getDiffViewResult( UUID::create( $options['newRevision'] ), UUID::create( $oldRevision ) ); |
259 | /** @var RevisionDiffViewFormatter $formatter */ |
260 | $formatter = Container::get( 'formatter.revision.diff.view' ); |
261 | |
262 | return [ |
263 | 'revision' => $formatter->formatApi( $new, $old, $this->context ) |
264 | ]; |
265 | } |
266 | |
267 | /** |
268 | * @todo Duplicated logic in other single view block |
269 | * @param int $revId |
270 | * @return array |
271 | */ |
272 | protected function renderSingleViewApi( $revId ) { |
273 | /** @var HeaderViewQuery $query */ |
274 | $query = Container::get( 'query.header.view' ); |
275 | $row = $query->getSingleViewResult( $revId ); |
276 | /** @var RevisionViewFormatter $formatter */ |
277 | $formatter = Container::get( 'formatter.revisionview' ); |
278 | |
279 | if ( !$this->permissions->isAllowed( $row->revision, 'view' ) ) { |
280 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
281 | return []; |
282 | } |
283 | |
284 | return [ |
285 | 'revision' => $formatter->formatApi( $row, $this->context ) |
286 | ]; |
287 | } |
288 | |
289 | /** |
290 | * @param string $format Content format (html|wikitext) |
291 | * @return array |
292 | */ |
293 | protected function renderRevisionApi( $format ) { |
294 | $output = []; |
295 | if ( $this->header === null ) { |
296 | if ( |
297 | !$this->permissions->isRevisionAllowed( null, 'view' ) || |
298 | !$this->permissions->isBoardAllowed( $this->workflow, 'view' ) |
299 | ) { |
300 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
301 | return []; |
302 | } |
303 | |
304 | /** @var UrlGenerator $urlGenerator */ |
305 | $urlGenerator = Container::get( 'url_generator' ); |
306 | |
307 | $title = $this->workflow->getArticleTitle(); |
308 | $user = $this->context->getUser(); |
309 | |
310 | $actions = []; |
311 | |
312 | if ( $this->workflow->userCan( 'edit', $user ) ) { |
313 | $actions['edit'] = $urlGenerator |
314 | ->createHeaderAction( $this->workflow->getArticleTitle() ); |
315 | } |
316 | |
317 | $output['revision'] = [ |
318 | 'actions' => $actions, |
319 | 'links' => [ |
320 | ], |
321 | ]; |
322 | } else { |
323 | $row = new FormatterRow; |
324 | $row->workflow = $this->workflow; |
325 | $row->revision = $this->header; |
326 | $row->currentRevision = $this->header; |
327 | |
328 | if ( !$this->permissions->isAllowed( $row->revision, 'view' ) ) { |
329 | $this->addError( 'permissions', $this->context->msg( 'flow-error-not-allowed' ) ); |
330 | return []; |
331 | } |
332 | |
333 | $serializer = Container::get( 'formatter.revision.factory' )->create(); |
334 | $serializer->setContentFormat( $format ); |
335 | |
336 | // For flow-description-last-modified-at |
337 | $serializer->setIncludeHistoryProperties( true ); |
338 | |
339 | $output['revision'] = $serializer->formatApi( $row, $this->context ); |
340 | } |
341 | |
342 | $output['copyrightMessage'] = $this->context->getSkin()->getCopyright(); |
343 | |
344 | return $output; |
345 | } |
346 | |
347 | protected function renderUndoApi( array $options ) { |
348 | if ( $this->workflow->isNew() ) { |
349 | throw new FlowException( 'No header exists to undo' ); |
350 | } |
351 | |
352 | if ( !isset( $options['startId'] ) || !isset( $options['endId'] ) ) { |
353 | throw new InvalidInputException( 'Both startId and endId must be provided' ); |
354 | } |
355 | |
356 | /** @var RevisionViewQuery */ |
357 | $query = Container::get( 'query.header.view' ); |
358 | $rows = $query->getUndoDiffResult( $options['startId'], $options['endId'] ); |
359 | if ( !$rows ) { |
360 | throw new InvalidInputException( 'Could not load revision to undo' ); |
361 | } |
362 | |
363 | $serializer = Container::get( 'formatter.undoedit' ); |
364 | return $serializer->formatApi( $rows[0], $rows[1], $rows[2], $this->context ); |
365 | } |
366 | |
367 | public function getName() { |
368 | return 'header'; |
369 | } |
370 | } |