Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
18.56% |
62 / 334 |
|
3.12% |
1 / 32 |
CRAP | |
0.00% |
0 / 1 |
TemplateHelper | |
18.56% |
62 / 334 |
|
3.12% |
1 / 32 |
5610.47 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getTemplateFilenames | |
69.23% |
9 / 13 |
|
0.00% |
0 / 1 |
12.91 | |||
getTemplate | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
56 | |||
compile | |
90.70% |
39 / 43 |
|
0.00% |
0 / 1 |
2.00 | |||
processTemplate | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
uuidTimestamp | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
timestampHelper | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
timestamp | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
htmlHelper | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
block | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
eachPost | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
110 | |||
post | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
historyTimestamp | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
historyDescription | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
showCharacterDifference | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
progressiveEnhancement | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
oouify | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
20 | |||
l10n | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
l10nParse | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
l10nParseFlowTermsOfUse | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getSaveOrPublishMessage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
diffRevision | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
6 | |||
diffUndo | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
moderationAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
concat | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
ifAnonymous | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
addReturnTo | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
linkWithReturnTo | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
escapeContent | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
ifCond | |
54.55% |
12 / 22 |
|
0.00% |
0 / 1 |
25.52 | |||
tooltip | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
enablePatrollingLink | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Flow; |
4 | |
5 | use Closure; |
6 | use Flow\Exception\FlowException; |
7 | use Flow\Model\UUID; |
8 | use LightnCandy\LightnCandy; |
9 | use LightnCandy\SafeString; |
10 | use MediaWiki\Context\RequestContext; |
11 | use MediaWiki\Html\Html; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\Utils\MWTimestamp; |
15 | use OOUI\IconWidget; |
16 | |
17 | class TemplateHelper { |
18 | |
19 | /** |
20 | * @var string |
21 | */ |
22 | protected $templateDir; |
23 | |
24 | /** |
25 | * @var callable[] |
26 | */ |
27 | protected $renderers; |
28 | |
29 | /** |
30 | * @var bool Always compile template files |
31 | */ |
32 | protected $forceRecompile = false; |
33 | |
34 | /** |
35 | * @param string $templateDir |
36 | * @param bool $forceRecompile |
37 | */ |
38 | public function __construct( $templateDir, $forceRecompile = false ) { |
39 | $this->templateDir = $templateDir; |
40 | $this->forceRecompile = $forceRecompile; |
41 | } |
42 | |
43 | /** |
44 | * Constructs the location of the source handlebars template |
45 | * and the compiled php code that goes with it. |
46 | * |
47 | * @param string $templateName |
48 | * |
49 | * @return string[] |
50 | * @throws FlowException Disallows upwards directory traversal via $templateName |
51 | */ |
52 | public function getTemplateFilenames( $templateName ) { |
53 | // Prevent upwards directory traversal using same methods as Title::secureAndSplit, |
54 | // which is implemented in MediaWikiTitleCodec::splitTitleString. |
55 | if ( |
56 | str_contains( $templateName, '.' ) && |
57 | ( |
58 | $templateName === '.' || $templateName === '..' || |
59 | str_starts_with( $templateName, './' ) || |
60 | str_starts_with( $templateName, '../' ) || |
61 | str_contains( $templateName, '/./' ) || |
62 | str_contains( $templateName, '/../' ) || |
63 | str_ends_with( $templateName, '/.' ) || |
64 | str_ends_with( $templateName, '/..' ) |
65 | ) |
66 | ) { |
67 | throw new FlowException( "Malformed \$templateName: $templateName" ); |
68 | } |
69 | |
70 | return [ |
71 | 'template' => "{$this->templateDir}/{$templateName}.handlebars", |
72 | 'compiled' => "{$this->templateDir}/compiled/{$templateName}.handlebars.php", |
73 | ]; |
74 | } |
75 | |
76 | /** |
77 | * Returns a given template function if found, otherwise throws an exception. |
78 | * |
79 | * @param string $templateName |
80 | * |
81 | * @return callable |
82 | * @throws FlowException |
83 | * @throws \Exception |
84 | */ |
85 | public function getTemplate( $templateName ) { |
86 | if ( isset( $this->renderers[$templateName] ) ) { |
87 | return $this->renderers[$templateName]; |
88 | } |
89 | |
90 | $filenames = $this->getTemplateFilenames( $templateName ); |
91 | |
92 | if ( $this->forceRecompile ) { |
93 | if ( !file_exists( $filenames['template'] ) ) { |
94 | throw new FlowException( "Could not locate template: {$filenames['template']}" ); |
95 | } |
96 | |
97 | $code = self::compile( file_get_contents( $filenames['template'] ), $this->templateDir ); |
98 | |
99 | if ( !$code ) { |
100 | throw new FlowException( "Failed to compile template '$templateName'." ); |
101 | } |
102 | $success = file_put_contents( $filenames['compiled'], '<?php ' . $code ); |
103 | |
104 | // failed to recompile template (OS permissions?); unless the |
105 | // content hasn't changes, throw an exception! |
106 | if ( !$success && file_get_contents( $filenames['compiled'] ) !== $code ) { |
107 | throw new FlowException( "Failed to save updated compiled template '$templateName'" ); |
108 | } |
109 | } |
110 | |
111 | /** @var callable $renderer */ |
112 | $renderer = require $filenames['compiled']; |
113 | $this->renderers[$templateName] = static function ( $args, array $scopes = [] ) use ( $templateName, $renderer ) { |
114 | return $renderer( $args, $scopes ); |
115 | }; |
116 | return $this->renderers[$templateName]; |
117 | } |
118 | |
119 | /** |
120 | * @param string $code Handlebars code |
121 | * @param string $templateDir Directory templates are stored in |
122 | * |
123 | * @return string PHP code |
124 | * @suppress PhanTypeMismatchArgument |
125 | */ |
126 | public static function compile( $code, $templateDir ) { |
127 | return LightnCandy::compile( |
128 | $code, |
129 | [ |
130 | 'flags' => LightnCandy::FLAG_ERROR_EXCEPTION |
131 | | LightnCandy::FLAG_EXTHELPER |
132 | | LightnCandy::FLAG_SPVARS |
133 | | LightnCandy::FLAG_HANDLEBARS |
134 | | LightnCandy::FLAG_RUNTIMEPARTIAL, |
135 | 'partialresolver' => static function ( $context, $name ) use ( $templateDir ) { |
136 | $filename = "$templateDir/$name.partial.handlebars"; |
137 | if ( file_exists( $filename ) ) { |
138 | return file_get_contents( $filename ); |
139 | } |
140 | return null; |
141 | }, |
142 | 'helpers' => [ |
143 | 'l10n' => 'Flow\TemplateHelper::l10n', |
144 | 'uuidTimestamp' => 'Flow\TemplateHelper::uuidTimestamp', |
145 | 'timestamp' => 'Flow\TemplateHelper::timestampHelper', |
146 | 'html' => 'Flow\TemplateHelper::htmlHelper', |
147 | 'block' => 'Flow\TemplateHelper::block', |
148 | 'post' => 'Flow\TemplateHelper::post', |
149 | 'historyTimestamp' => 'Flow\TemplateHelper::historyTimestamp', |
150 | 'historyDescription' => 'Flow\TemplateHelper::historyDescription', |
151 | 'showCharacterDifference' => 'Flow\TemplateHelper::showCharacterDifference', |
152 | 'l10nParse' => 'Flow\TemplateHelper::l10nParse', |
153 | 'l10nParseFlowTermsOfUse' => 'Flow\TemplateHelper::l10nParseFlowTermsOfUse', |
154 | 'diffRevision' => 'Flow\TemplateHelper::diffRevision', |
155 | 'diffUndo' => 'Flow\TemplateHelper::diffUndo', |
156 | 'moderationAction' => 'Flow\TemplateHelper::moderationAction', |
157 | 'concat' => 'Flow\TemplateHelper::concat', |
158 | 'linkWithReturnTo' => 'Flow\TemplateHelper::linkWithReturnTo', |
159 | 'escapeContent' => 'Flow\TemplateHelper::escapeContent', |
160 | 'enablePatrollingLink' => 'Flow\TemplateHelper::enablePatrollingLink', |
161 | 'oouify' => 'Flow\TemplateHelper::oouify', |
162 | 'getSaveOrPublishMessage' => 'Flow\TemplateHelper::getSaveOrPublishMessage', |
163 | 'eachPost' => 'Flow\TemplateHelper::eachPost', |
164 | 'ifAnonymous' => 'Flow\TemplateHelper::ifAnonymous', |
165 | 'ifCond' => 'Flow\TemplateHelper::ifCond', |
166 | 'tooltip' => 'Flow\TemplateHelper::tooltip', |
167 | 'progressiveEnhancement' => 'Flow\TemplateHelper::progressiveEnhancement', |
168 | ], |
169 | ] |
170 | ); |
171 | } |
172 | |
173 | /** |
174 | * Returns HTML for a given template by calling the template function with the given args. |
175 | * |
176 | * @param string $templateName |
177 | * @param array $args |
178 | * @param array $scopes |
179 | * |
180 | * @return string |
181 | */ |
182 | public static function processTemplate( $templateName, array $args, array $scopes = [] ) { |
183 | // Undesirable, but lightncandy helpers have to be static methods |
184 | /** @var TemplateHelper $lightncandy */ |
185 | $lightncandy = Container::get( 'lightncandy' ); |
186 | $template = $lightncandy->getTemplate( $templateName ); |
187 | // @todo ugly hack...remove someday. Requires switching to newest version |
188 | // of lightncandy which supports recursive partial templates. |
189 | if ( !array_key_exists( 'rootBlock', $args ) ) { |
190 | $args['rootBlock'] = $args; |
191 | } |
192 | return $template( $args, $scopes ); |
193 | } |
194 | |
195 | // Helpers |
196 | |
197 | /** |
198 | * Generates a timestamp using the UUID, then calls the timestamp helper with it. |
199 | * |
200 | * @param string $uuid |
201 | * |
202 | * @return SafeString|null |
203 | */ |
204 | public static function uuidTimestamp( $uuid ) { |
205 | $obj = UUID::create( $uuid ); |
206 | if ( !$obj ) { |
207 | return null; |
208 | } |
209 | |
210 | // timestamp helper expects ms timestamp |
211 | $timestamp = (int)$obj->getTimestampObj()->getTimestamp() * 1000; |
212 | return self::timestamp( $timestamp ); |
213 | } |
214 | |
215 | /** |
216 | * @param string $timestamp |
217 | * |
218 | * @return SafeString|null |
219 | */ |
220 | public static function timestampHelper( $timestamp ) { |
221 | return self::timestamp( (int)$timestamp ); |
222 | } |
223 | |
224 | /** |
225 | * @param int $timestamp milliseconds since the unix epoch |
226 | * |
227 | * @return SafeString|null |
228 | */ |
229 | protected static function timestamp( $timestamp ) { |
230 | global $wgLang; |
231 | |
232 | if ( !$timestamp ) { |
233 | return null; |
234 | } |
235 | |
236 | // source timestamps are in ms |
237 | $timestamp /= 1000; |
238 | $ts = new MWTimestamp( $timestamp ); |
239 | |
240 | return new SafeString( self::processTemplate( |
241 | 'timestamp', |
242 | [ |
243 | 'time_iso' => $timestamp, |
244 | 'time_ago' => $wgLang->getHumanTimestamp( $ts ), |
245 | 'time_readable' => $wgLang->userTimeAndDate( |
246 | $timestamp, |
247 | RequestContext::getMain()->getUser() |
248 | ), |
249 | 'guid' => null, // generated client-side |
250 | ] |
251 | ) ); |
252 | } |
253 | |
254 | /** |
255 | * @param string $html |
256 | * |
257 | * @return SafeString |
258 | */ |
259 | public static function htmlHelper( $html ) { |
260 | return new SafeString( $html ?? 'undefined' ); |
261 | } |
262 | |
263 | /** |
264 | * @param array $block |
265 | * |
266 | * @return SafeString |
267 | */ |
268 | public static function block( $block ) { |
269 | $template = "flow_block_" . $block['type']; |
270 | if ( $block['block-action-template'] ) { |
271 | $template .= '_' . $block['block-action-template']; |
272 | } |
273 | return new SafeString( self::processTemplate( |
274 | $template, |
275 | $block |
276 | ) ); |
277 | } |
278 | |
279 | /** |
280 | * @param array $context The 'this' value of the calling context |
281 | * @param mixed $postIds List of ids (roots) |
282 | * @param array $options blockhelper specific invocation options |
283 | * |
284 | * @return null|string HTML |
285 | * @throws FlowException When callbacks are not Closure instances |
286 | */ |
287 | public static function eachPost( array $context, $postIds, array $options ) { |
288 | /** @var callable $inverse */ |
289 | $inverse = $options['inverse'] ?? null; |
290 | /** @var callable $fn */ |
291 | $fn = $options['fn']; |
292 | |
293 | if ( $postIds && !is_array( $postIds ) ) { |
294 | $postIds = [ $postIds ]; |
295 | } elseif ( $postIds === [] ) { |
296 | // Failure callback, if any |
297 | if ( !$inverse ) { |
298 | return null; |
299 | } |
300 | if ( !$inverse instanceof Closure ) { |
301 | throw new FlowException( 'Invalid inverse callback, expected Closure' ); |
302 | } |
303 | return $inverse( $options['cx'], [] ); |
304 | } else { |
305 | return null; |
306 | } |
307 | |
308 | if ( !$fn instanceof Closure ) { |
309 | throw new FlowException( 'Invalid callback, expected Closure' ); |
310 | } |
311 | $html = []; |
312 | foreach ( $postIds as $id ) { |
313 | $revId = $context['posts'][$id][0] ?? ''; |
314 | |
315 | if ( !$revId || !isset( $context['revisions'][$revId] ) ) { |
316 | throw new FlowException( "Revision not available: $revId. Post ID: $id" ); |
317 | } |
318 | |
319 | // $fn is always safe return value, it's the inner template content. |
320 | $html[] = $fn( $context['revisions'][$revId] ); |
321 | } |
322 | |
323 | // Return the resulting HTML |
324 | return implode( '', $html ); |
325 | } |
326 | |
327 | /** |
328 | * Required to prevent recursion loop rendering nested posts |
329 | * |
330 | * @param array $rootBlock |
331 | * @param array $revision |
332 | * |
333 | * @return SafeString |
334 | */ |
335 | public static function post( $rootBlock, $revision ) { |
336 | return new SafeString( self::processTemplate( 'flow_post', [ |
337 | 'revision' => $revision, |
338 | 'rootBlock' => $rootBlock, |
339 | ] ) ); |
340 | } |
341 | |
342 | /** |
343 | * @param array $revision |
344 | * |
345 | * @return SafeString |
346 | */ |
347 | public static function historyTimestamp( $revision ) { |
348 | $raw = false; |
349 | $formattedTime = $revision['dateFormats']['timeAndDate']; |
350 | $formattedTimeOutput = ''; |
351 | $linkKeys = [ 'header-revision', 'topic-revision', 'post-revision', 'summary-revision' ]; |
352 | foreach ( $linkKeys as $linkKey ) { |
353 | if ( isset( $revision['links'][$linkKey] ) ) { |
354 | $link = $revision['links'][$linkKey]; |
355 | $formattedTimeOutput = Html::element( |
356 | 'a', |
357 | [ |
358 | 'href' => $link['url'], |
359 | 'title' => $link['title'], |
360 | ], |
361 | $formattedTime |
362 | ); |
363 | $raw = true; |
364 | break; |
365 | } |
366 | } |
367 | |
368 | if ( !$raw ) { |
369 | $formattedTimeOutput = htmlspecialchars( $formattedTime ); |
370 | } |
371 | |
372 | $class = [ 'mw-changeslist-date' ]; |
373 | if ( $revision['isModeratedNotLocked'] ) { |
374 | $class[] = 'history-deleted'; |
375 | } |
376 | |
377 | return new SafeString( |
378 | '<span class="plainlinks">' |
379 | . Html::rawElement( 'span', [ 'class' => $class ], $formattedTimeOutput ) |
380 | . '</span>' |
381 | ); |
382 | } |
383 | |
384 | /** |
385 | * @param array $revision |
386 | * |
387 | * @return SafeString|null |
388 | */ |
389 | public static function historyDescription( $revision ) { |
390 | if ( !isset( $revision['properties']['_key'] ) ) { |
391 | return null; |
392 | } |
393 | |
394 | $i18nKey = $revision['properties']['_key']; |
395 | unset( $revision['properties']['_key'] ); |
396 | |
397 | // $revision['properties'] contains the params for the i18n message, which are named, |
398 | // so we need array_values() to strip the names. They are in the correct order because |
399 | // RevisionFormatter::getDescriptionParams() uses a foreach loop to build this array |
400 | // from the i18n-params definition in FlowActions.php. |
401 | // A variety of the i18n history messages contain wikitext and require ->parse(). |
402 | return new SafeString( wfMessage( $i18nKey, array_values( $revision['properties'] ) )->parse() ); |
403 | } |
404 | |
405 | /** |
406 | * @param string $old |
407 | * @param string $new |
408 | * |
409 | * @return SafeString |
410 | */ |
411 | public static function showCharacterDifference( $old, $new ) { |
412 | return new SafeString( \ChangesList::showCharacterDifference( (int)$old, (int)$new ) ); |
413 | } |
414 | |
415 | /** |
416 | * Creates a special script tag to be processed client-side. This contains extra template HTML, which allows |
417 | * the front-end to "progressively enhance" the page with more content which isn't needed in a non-JS state. |
418 | * |
419 | * @see FlowHandlebars.prototype.progressiveEnhancement in flow-handlebars.js for more details. |
420 | * |
421 | * @param array $options |
422 | * |
423 | * @return SafeString |
424 | */ |
425 | public static function progressiveEnhancement( array $options ) { |
426 | $fn = $options['fn']; |
427 | $input = $options['hash']; |
428 | $insertionType = empty( $input['type'] ) ? 'insert' : htmlspecialchars( $input['type'] ); |
429 | $target = empty( $input['target'] ) ? '' : 'data-target="' . htmlspecialchars( $input['target'] ) . '"'; |
430 | $sectionId = empty( $input['id'] ) ? '' : 'id="' . htmlspecialchars( $input['id'] ) . '"'; |
431 | |
432 | return new SafeString( |
433 | '<script name="handlebars-template-progressive-enhancement"' . |
434 | ' type="text/x-handlebars-template-progressive-enhancement"' . |
435 | ' data-type="' . $insertionType . '"' . |
436 | ' ' . $target . |
437 | ' ' . $sectionId . |
438 | '>' . |
439 | // Replace the nested script tag with a placeholder tag for recursive progressiveEnhancement |
440 | str_replace( '</script>', '</flowprogressivescript>', $fn() ) . |
441 | '</script>' |
442 | ); |
443 | } |
444 | |
445 | /** |
446 | * A helper to output OOUI widgets. |
447 | * |
448 | * @param array ...$args one or more arguments, i18n key and parameters |
449 | * @return \OOUI\Widget|null |
450 | */ |
451 | public static function oouify( ...$args ) { |
452 | $options = array_pop( $args ); |
453 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable Only when $args is empty |
454 | $named = $options['hash']; |
455 | |
456 | $widgetType = $named[ 'type' ]; |
457 | $data = []; |
458 | |
459 | $classes = []; |
460 | if ( isset( $named['classes'] ) ) { |
461 | $classes = explode( ' ', $named[ 'classes' ] ); |
462 | } |
463 | |
464 | // Push raw arguments |
465 | $data['args'] = $args; |
466 | $baseConfig = [ |
467 | // 'infusable' => true, |
468 | 'id' => $named[ 'name' ] ?? null, |
469 | 'classes' => $classes, |
470 | 'data' => $data |
471 | ]; |
472 | $widget = null; |
473 | switch ( $widgetType ) { |
474 | case 'BoardDescriptionWidget': |
475 | $dataArgs = [ |
476 | 'infusable' => false, |
477 | 'description' => $args[0], |
478 | 'editLink' => $args[1] |
479 | ]; |
480 | $widget = new OOUI\BoardDescriptionWidget( $baseConfig + $dataArgs ); |
481 | break; |
482 | case 'IconWidget': |
483 | $dataArgs = [ |
484 | 'icon' => $args[0], |
485 | ]; |
486 | $widget = new IconWidget( $baseConfig + $dataArgs ); |
487 | break; |
488 | } |
489 | |
490 | return $widget; |
491 | } |
492 | |
493 | /** |
494 | * @param array ...$args one or more arguments, i18n key and parameters |
495 | * |
496 | * @return string Message output, using the 'text' format |
497 | */ |
498 | public static function l10n( ...$args ) { |
499 | $options = array_pop( $args ); |
500 | // @phan-suppress-next-line PhanParamTooFewUnpack |
501 | return wfMessage( ...$args )->text(); |
502 | } |
503 | |
504 | /** |
505 | * @param array ...$args one or more arguments, i18n key and parameters |
506 | * |
507 | * @return SafeString HTML |
508 | */ |
509 | public static function l10nParse( ...$args ) { |
510 | $options = array_pop( $args ); |
511 | // @phan-suppress-next-line PhanParamTooFewUnpack |
512 | return new SafeString( wfMessage( ...$args )->parse() ); |
513 | } |
514 | |
515 | /** |
516 | * @param string $key |
517 | * |
518 | * @return SafeString HTML |
519 | */ |
520 | public static function l10nParseFlowTermsOfUse( $key ) { |
521 | $context = RequestContext::getMain(); |
522 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
523 | $messages = Hooks::getTermsOfUseMessagesParsed( $context, $config ); |
524 | return new SafeString( $messages[ $key ] ); |
525 | } |
526 | |
527 | /** |
528 | * A helper to output whether a wiki is publish wiki or not |
529 | * |
530 | * @param array $options |
531 | * @return string Translated message string for either 'save' or 'publish' |
532 | * version |
533 | */ |
534 | public static function getSaveOrPublishMessage( array $options ) { |
535 | global $wgEditSubmitButtonLabelPublish; |
536 | $named = $options['hash']; |
537 | |
538 | if ( !$named['save'] || !$named['publish'] ) { |
539 | throw new FlowException( "Missing an argument. Expected two message keys for 'save' and 'post'" ); |
540 | } |
541 | |
542 | $msg = $wgEditSubmitButtonLabelPublish ? $named['publish'] : $named['save']; |
543 | |
544 | return wfMessage( $msg )->text(); |
545 | } |
546 | |
547 | /** |
548 | * @param array $data RevisionDiffViewFormatter::formatApi return value |
549 | * |
550 | * @return SafeString |
551 | */ |
552 | public static function diffRevision( $data ) { |
553 | $differenceEngine = new \DifferenceEngine(); |
554 | $notice = ''; |
555 | if ( $data['diff_content'] === '' ) { |
556 | $notice .= '<div class="mw-diff-empty">' . |
557 | wfMessage( 'diff-empty' )->parse() . |
558 | "</div>\n"; |
559 | } |
560 | // Work around exception in DifferenceEngine::showDiffStyle() (T202454) |
561 | $out = RequestContext::getMain()->getOutput(); |
562 | $out->addModules( 'mediawiki.diff' ); |
563 | $out->addModuleStyles( 'mediawiki.diff.styles' ); |
564 | |
565 | $renderer = Container::get( 'lightncandy' )->getTemplate( 'flow_revision_diff_header' ); |
566 | |
567 | return new SafeString( $differenceEngine->addHeader( |
568 | $data['diff_content'], |
569 | $renderer( [ |
570 | 'old' => true, |
571 | 'revision' => $data['old'], |
572 | 'links' => $data['links'], |
573 | ] ), |
574 | $renderer( [ |
575 | 'new' => true, |
576 | 'revision' => $data['new'], |
577 | 'links' => $data['links'], |
578 | ] ), |
579 | // FIXME we should be passing in a multinotice for multi-rev diffs here |
580 | '', |
581 | $notice |
582 | ) ); |
583 | } |
584 | |
585 | public static function diffUndo( $diffContent ) { |
586 | $differenceEngine = new \DifferenceEngine(); |
587 | $notice = ''; |
588 | if ( $diffContent === '' ) { |
589 | $notice = '<div class="mw-diff-empty">' . |
590 | wfMessage( 'diff-empty' )->parse() . |
591 | "</div>\n"; |
592 | } |
593 | // Work around exception in DifferenceEngine::showDiffStyle() (T202454) |
594 | $out = RequestContext::getMain()->getOutput(); |
595 | $out->addModules( 'mediawiki.diff' ); |
596 | $out->addModuleStyles( 'mediawiki.diff.styles' ); |
597 | |
598 | return new SafeString( $differenceEngine->addHeader( |
599 | $diffContent, |
600 | wfMessage( 'flow-undo-latest-revision' )->parse(), |
601 | wfMessage( 'flow-undo-your-text' )->parse(), |
602 | // FIXME we should be passing in a multinotice for multi-rev diffs here |
603 | '', |
604 | $notice |
605 | ) ); |
606 | } |
607 | |
608 | /** |
609 | * @param array $actions |
610 | * @param string $moderationState |
611 | * |
612 | * @return string |
613 | */ |
614 | public static function moderationAction( $actions, $moderationState ) { |
615 | return isset( $actions[$moderationState] ) ? $actions[$moderationState]['url'] : ''; |
616 | } |
617 | |
618 | /** |
619 | * @param string ...$args Expects one or more strings to join |
620 | * |
621 | * @return string all unnamed arguments joined together |
622 | */ |
623 | public static function concat( ...$args ) { |
624 | $options = array_pop( $args ); |
625 | return implode( '', $args ); |
626 | } |
627 | |
628 | /** |
629 | * Runs a callback when user is anonymous |
630 | * |
631 | * @param array $options which must contain fn and inverse key mapping to functions. |
632 | * |
633 | * @return mixed result of callback |
634 | * @throws FlowException Fails when callbacks are not Closure instances |
635 | */ |
636 | public static function ifAnonymous( $options ) { |
637 | if ( !RequestContext::getMain()->getUser()->isRegistered() ) { |
638 | $fn = $options['fn']; |
639 | if ( !$fn instanceof Closure ) { |
640 | throw new FlowException( 'Expected callback to be Closuire instance' ); |
641 | } |
642 | } elseif ( isset( $options['inverse'] ) ) { |
643 | $fn = $options['inverse']; |
644 | if ( !$fn instanceof Closure ) { |
645 | throw new FlowException( 'Expected inverse callback to be Closuire instance' ); |
646 | } |
647 | } else { |
648 | return ''; |
649 | } |
650 | |
651 | return $fn(); |
652 | } |
653 | |
654 | /** |
655 | * Adds returnto parameter pointing to current page to existing URL |
656 | * |
657 | * @param string $url to modify |
658 | * |
659 | * @return string modified url |
660 | */ |
661 | protected static function addReturnTo( $url ) { |
662 | $ctx = RequestContext::getMain(); |
663 | $returnTo = $ctx->getTitle(); |
664 | if ( !$returnTo ) { |
665 | return $url; |
666 | } |
667 | // We can't get only the query parameters from |
668 | $returnToQuery = $ctx->getRequest()->getQueryValues(); |
669 | |
670 | unset( $returnToQuery['title'] ); |
671 | |
672 | $args = [ |
673 | 'returnto' => $returnTo->getPrefixedURL(), |
674 | ]; |
675 | if ( $returnToQuery ) { |
676 | $args['returntoquery'] = wfArrayToCgi( $returnToQuery ); |
677 | } |
678 | return wfAppendQuery( $url, wfArrayToCgi( $args ) ); |
679 | } |
680 | |
681 | /** |
682 | * Adds returnto parameter pointing to given Title to an existing URL |
683 | * |
684 | * @param string $title |
685 | * |
686 | * @return string modified url |
687 | */ |
688 | public static function linkWithReturnTo( $title ) { |
689 | $title = Title::newFromText( $title ); |
690 | if ( !$title ) { |
691 | return ''; |
692 | } |
693 | // FIXME: This should use local url to avoid redirects on mobile. See bug 66746. |
694 | $url = $title->getFullURL(); |
695 | |
696 | return self::addReturnTo( $url ); |
697 | } |
698 | |
699 | /** |
700 | * Accepts the contentType and content properties returned from the api |
701 | * for individual revisions and ensures that content is included in the |
702 | * final html page in an xss safe maner. |
703 | * |
704 | * It is expected that all content with contentType of html has been |
705 | * processed by parsoid and is safe for direct output into the document. |
706 | * |
707 | * @param string $contentType |
708 | * @param string $content |
709 | * |
710 | * @return string|SafeString |
711 | */ |
712 | public static function escapeContent( $contentType, $content ) { |
713 | return in_array( $contentType, [ 'html', 'fixed-html', 'topic-title-html' ] ) ? |
714 | new SafeString( $content ) : |
715 | $content; |
716 | } |
717 | |
718 | /** |
719 | * Only perform action when conditions match |
720 | * |
721 | * @param string $value |
722 | * @param string $operator e.g. 'or' |
723 | * @param string $value2 to compare with |
724 | * @param array $options lightncandy hbhelper options |
725 | * |
726 | * @return mixed result of callback |
727 | * @throws FlowException Fails when callbacks are not Closure instances |
728 | */ |
729 | public static function ifCond( $value, $operator, $value2, array $options ) { |
730 | $doCallback = false; |
731 | |
732 | // Perform operator |
733 | // FIXME: Rename to || to be consistent with other operators |
734 | if ( $operator === 'or' ) { |
735 | if ( $value || $value2 ) { |
736 | $doCallback = true; |
737 | } |
738 | } elseif ( $operator === '===' ) { |
739 | if ( $value === $value2 ) { |
740 | $doCallback = true; |
741 | } |
742 | } elseif ( $operator === '!==' ) { |
743 | if ( $value !== $value2 ) { |
744 | $doCallback = true; |
745 | } |
746 | } else { |
747 | return ''; |
748 | } |
749 | |
750 | if ( $doCallback ) { |
751 | $fn = $options['fn']; |
752 | if ( !$fn instanceof Closure ) { |
753 | throw new FlowException( 'Expected callback to be Closure instance' ); |
754 | } |
755 | return $fn(); |
756 | } elseif ( isset( $options['inverse'] ) ) { |
757 | $inverse = $options['inverse']; |
758 | if ( !$inverse instanceof Closure ) { |
759 | throw new FlowException( 'Expected inverse callback to be Closure instance' ); |
760 | } |
761 | return $inverse(); |
762 | } else { |
763 | return ''; |
764 | } |
765 | } |
766 | |
767 | /** |
768 | * @param array $options |
769 | * |
770 | * @return string tooltip |
771 | */ |
772 | public static function tooltip( $options ) { |
773 | $fn = $options['fn']; |
774 | $params = $options['hash']; |
775 | |
776 | return ( |
777 | self::processTemplate( 'flow_tooltip', [ |
778 | 'positionClass' => $params['positionClass'] ? 'flow-ui-tooltip-' . $params['positionClass'] : null, |
779 | 'contextClass' => $params['contextClass'] ? 'mw-ui-' . $params['contextClass'] : null, |
780 | 'extraClass' => $params['extraClass'] ?: '', |
781 | 'blockClass' => $params['isBlock'] ? 'flow-ui-tooltip-block' : null, |
782 | 'content' => $fn(), |
783 | ] ) |
784 | ); |
785 | } |
786 | |
787 | /** |
788 | * Enhance the patrolling link and protect it. |
789 | */ |
790 | public static function enablePatrollingLink() { |
791 | $outputPage = RequestContext::getMain()->getOutput(); |
792 | |
793 | // Enhance the patrol link with ajax |
794 | // FIXME: This duplicates DifferenceEngine::markPatrolledLink. |
795 | $outputPage->setPreventClickjacking( true ); |
796 | $outputPage->addModules( 'mediawiki.misc-authed-curate' ); |
797 | } |
798 | } |