Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
89.00% |
186 / 209 |
|
66.67% |
6 / 9 |
CRAP | |
0.00% |
0 / 1 |
RollbackPage | |
89.00% |
186 / 209 |
|
66.67% |
6 / 9 |
53.33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
setSummary | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
markAsBot | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
setChangeTags | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
authorizeRollback | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
rollbackIfAllowed | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
rollback | |
85.42% |
82 / 96 |
|
0.00% |
0 / 1 |
19.00 | |||
updateRecentChange | |
85.71% |
42 / 49 |
|
0.00% |
0 / 1 |
12.42 | |||
getSummary | |
93.55% |
29 / 31 |
|
0.00% |
0 / 1 |
8.02 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Page; |
22 | |
23 | use IDBAccessObject; |
24 | use ManualLogEntry; |
25 | use MediaWiki\CommentStore\CommentStoreComment; |
26 | use MediaWiki\Config\ServiceOptions; |
27 | use MediaWiki\HookContainer\HookContainer; |
28 | use MediaWiki\HookContainer\HookRunner; |
29 | use MediaWiki\Language\RawMessage; |
30 | use MediaWiki\MainConfigNames; |
31 | use MediaWiki\Message\Converter; |
32 | use MediaWiki\Message\Message; |
33 | use MediaWiki\Permissions\Authority; |
34 | use MediaWiki\Permissions\PermissionStatus; |
35 | use MediaWiki\Revision\RevisionRecord; |
36 | use MediaWiki\Revision\RevisionStore; |
37 | use MediaWiki\Revision\SlotRecord; |
38 | use MediaWiki\Storage\EditResult; |
39 | use MediaWiki\Title\TitleFormatter; |
40 | use MediaWiki\Title\TitleValue; |
41 | use MediaWiki\User\ActorMigration; |
42 | use MediaWiki\User\ActorNormalization; |
43 | use MediaWiki\User\UserFactory; |
44 | use MediaWiki\User\UserIdentity; |
45 | use RecentChange; |
46 | use StatusValue; |
47 | use Wikimedia\Message\MessageValue; |
48 | use Wikimedia\Rdbms\IConnectionProvider; |
49 | use Wikimedia\Rdbms\IDatabase; |
50 | use Wikimedia\Rdbms\ReadOnlyMode; |
51 | use Wikimedia\Rdbms\SelectQueryBuilder; |
52 | |
53 | /** |
54 | * Backend logic for performing a page rollback action. |
55 | * |
56 | * @since 1.37 |
57 | */ |
58 | class RollbackPage { |
59 | |
60 | /** |
61 | * @internal For use in PageCommandFactory only |
62 | * @var array |
63 | */ |
64 | public const CONSTRUCTOR_OPTIONS = [ |
65 | MainConfigNames::UseRCPatrol, |
66 | MainConfigNames::DisableAnonTalk, |
67 | ]; |
68 | |
69 | /** @var ServiceOptions */ |
70 | private $options; |
71 | |
72 | /** @var IConnectionProvider */ |
73 | private $dbProvider; |
74 | |
75 | /** @var UserFactory */ |
76 | private $userFactory; |
77 | |
78 | /** @var ReadOnlyMode */ |
79 | private $readOnlyMode; |
80 | |
81 | /** @var TitleFormatter */ |
82 | private $titleFormatter; |
83 | |
84 | /** @var RevisionStore */ |
85 | private $revisionStore; |
86 | |
87 | /** @var HookRunner */ |
88 | private $hookRunner; |
89 | |
90 | /** @var WikiPageFactory */ |
91 | private $wikiPageFactory; |
92 | |
93 | /** @var ActorMigration */ |
94 | private $actorMigration; |
95 | |
96 | /** @var ActorNormalization */ |
97 | private $actorNormalization; |
98 | |
99 | /** @var PageIdentity */ |
100 | private $page; |
101 | |
102 | /** @var Authority */ |
103 | private $performer; |
104 | |
105 | /** @var UserIdentity who made the edits we are rolling back */ |
106 | private $byUser; |
107 | |
108 | /** @var string */ |
109 | private $summary = ''; |
110 | |
111 | /** @var bool */ |
112 | private $bot = false; |
113 | |
114 | /** @var string[] */ |
115 | private $tags = []; |
116 | |
117 | /** |
118 | * @internal Create via the RollbackPageFactory service. |
119 | * @param ServiceOptions $options |
120 | * @param IConnectionProvider $dbProvider |
121 | * @param UserFactory $userFactory |
122 | * @param ReadOnlyMode $readOnlyMode |
123 | * @param RevisionStore $revisionStore |
124 | * @param TitleFormatter $titleFormatter |
125 | * @param HookContainer $hookContainer |
126 | * @param WikiPageFactory $wikiPageFactory |
127 | * @param ActorMigration $actorMigration |
128 | * @param ActorNormalization $actorNormalization |
129 | * @param PageIdentity $page |
130 | * @param Authority $performer |
131 | * @param UserIdentity $byUser who made the edits we are rolling back |
132 | */ |
133 | public function __construct( |
134 | ServiceOptions $options, |
135 | IConnectionProvider $dbProvider, |
136 | UserFactory $userFactory, |
137 | ReadOnlyMode $readOnlyMode, |
138 | RevisionStore $revisionStore, |
139 | TitleFormatter $titleFormatter, |
140 | HookContainer $hookContainer, |
141 | WikiPageFactory $wikiPageFactory, |
142 | ActorMigration $actorMigration, |
143 | ActorNormalization $actorNormalization, |
144 | PageIdentity $page, |
145 | Authority $performer, |
146 | UserIdentity $byUser |
147 | ) { |
148 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
149 | $this->options = $options; |
150 | $this->dbProvider = $dbProvider; |
151 | $this->userFactory = $userFactory; |
152 | $this->readOnlyMode = $readOnlyMode; |
153 | $this->revisionStore = $revisionStore; |
154 | $this->titleFormatter = $titleFormatter; |
155 | $this->hookRunner = new HookRunner( $hookContainer ); |
156 | $this->wikiPageFactory = $wikiPageFactory; |
157 | $this->actorMigration = $actorMigration; |
158 | $this->actorNormalization = $actorNormalization; |
159 | |
160 | $this->page = $page; |
161 | $this->performer = $performer; |
162 | $this->byUser = $byUser; |
163 | } |
164 | |
165 | /** |
166 | * Set custom edit summary. |
167 | * |
168 | * @param string|null $summary |
169 | * @return $this |
170 | */ |
171 | public function setSummary( ?string $summary ): self { |
172 | $this->summary = $summary ?? ''; |
173 | return $this; |
174 | } |
175 | |
176 | /** |
177 | * Mark all reverted edits as bot. |
178 | * |
179 | * @param bool|null $bot |
180 | * @return $this |
181 | */ |
182 | public function markAsBot( ?bool $bot ): self { |
183 | if ( $bot && $this->performer->isAllowedAny( 'markbotedits', 'bot' ) ) { |
184 | $this->bot = true; |
185 | } elseif ( !$bot ) { |
186 | $this->bot = false; |
187 | } |
188 | return $this; |
189 | } |
190 | |
191 | /** |
192 | * Change tags to apply to the rollback. |
193 | * |
194 | * @note Callers are responsible for permission checks (with ChangeTags::canAddTagsAccompanyingChange) |
195 | * |
196 | * @param string[]|null $tags |
197 | * @return $this |
198 | */ |
199 | public function setChangeTags( ?array $tags ): self { |
200 | $this->tags = $tags ?: []; |
201 | return $this; |
202 | } |
203 | |
204 | /** |
205 | * Authorize the rollback. |
206 | * |
207 | * @return PermissionStatus |
208 | */ |
209 | public function authorizeRollback(): PermissionStatus { |
210 | $permissionStatus = PermissionStatus::newEmpty(); |
211 | $this->performer->authorizeWrite( 'edit', $this->page, $permissionStatus ); |
212 | $this->performer->authorizeWrite( 'rollback', $this->page, $permissionStatus ); |
213 | |
214 | if ( $this->readOnlyMode->isReadOnly() ) { |
215 | $permissionStatus->fatal( 'readonlytext' ); |
216 | } |
217 | |
218 | return $permissionStatus; |
219 | } |
220 | |
221 | /** |
222 | * Rollback the most recent consecutive set of edits to a page |
223 | * from the same user; fails if there are no eligible edits to |
224 | * roll back to, e.g. user is the sole contributor. This function |
225 | * performs permissions checks and executes ::rollback. |
226 | * |
227 | * @return StatusValue see ::rollback for return value documentation. |
228 | * In case the rollback is not allowed, PermissionStatus is returned. |
229 | */ |
230 | public function rollbackIfAllowed(): StatusValue { |
231 | $permissionStatus = $this->authorizeRollback(); |
232 | if ( !$permissionStatus->isGood() ) { |
233 | return $permissionStatus; |
234 | } |
235 | return $this->rollback(); |
236 | } |
237 | |
238 | /** |
239 | * Backend implementation of rollbackIfAllowed(). |
240 | * |
241 | * @note This function does NOT check ANY permissions, it just commits the |
242 | * rollback to the DB. Therefore, you should only call this function directly |
243 | * if you want to use custom permissions checks. If you don't, use |
244 | * ::rollbackIfAllowed() instead. |
245 | * |
246 | * @return StatusValue On success, wrapping the array with the following keys: |
247 | * 'summary' - rollback edit summary |
248 | * 'current-revision-record' - revision record that was current before rollback |
249 | * 'target-revision-record' - revision record we are rolling back to |
250 | * 'newid' => the id of the rollback revision |
251 | * 'tags' => the tags applied to the rollback |
252 | */ |
253 | public function rollback() { |
254 | // Begin revision creation cycle by creating a PageUpdater. |
255 | // If the page is changed concurrently after grabParentRevision(), the rollback will fail. |
256 | // TODO: move PageUpdater to PageStore or PageUpdaterFactory or something? |
257 | $updater = $this->wikiPageFactory->newFromTitle( $this->page )->newPageUpdater( $this->performer ); |
258 | $currentRevision = $updater->grabParentRevision(); |
259 | |
260 | if ( !$currentRevision ) { |
261 | // Something wrong... no page? |
262 | return StatusValue::newFatal( 'notanarticle' ); |
263 | } |
264 | |
265 | $currentEditor = $currentRevision->getUser( RevisionRecord::RAW ); |
266 | $currentEditorForPublic = $currentRevision->getUser( RevisionRecord::FOR_PUBLIC ); |
267 | // User name given should match up with the top revision. |
268 | if ( !$this->byUser->equals( $currentEditor ) ) { |
269 | $result = StatusValue::newGood( [ |
270 | 'current-revision-record' => $currentRevision |
271 | ] ); |
272 | $result->fatal( |
273 | 'alreadyrolled', |
274 | htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ), |
275 | htmlspecialchars( $this->byUser->getName() ), |
276 | htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) |
277 | ); |
278 | return $result; |
279 | } |
280 | |
281 | $dbw = $this->dbProvider->getPrimaryDatabase(); |
282 | |
283 | // Get the last edit not by this person... |
284 | // Note: these may not be public values |
285 | $actorWhere = $this->actorMigration->getWhere( $dbw, 'rev_user', $currentEditor ); |
286 | $queryBuilder = $this->revisionStore->newSelectQueryBuilder( $dbw ) |
287 | ->where( [ 'rev_page' => $currentRevision->getPageId(), 'NOT(' . $actorWhere['conds'] . ')' ] ) |
288 | ->useIndex( [ 'revision' => 'rev_page_timestamp' ] ) |
289 | ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC ); |
290 | $targetRevisionRow = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
291 | |
292 | if ( $targetRevisionRow === false ) { |
293 | // No one else ever edited this page |
294 | return StatusValue::newFatal( 'cantrollback' ); |
295 | } elseif ( $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_TEXT |
296 | || $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_USER |
297 | ) { |
298 | // Only admins can see this text |
299 | return StatusValue::newFatal( 'notvisiblerev' ); |
300 | } |
301 | |
302 | // Generate the edit summary if necessary |
303 | $targetRevision = $this->revisionStore |
304 | ->getRevisionById( $targetRevisionRow->rev_id, IDBAccessObject::READ_LATEST ); |
305 | |
306 | // Save |
307 | $flags = EDIT_UPDATE | EDIT_INTERNAL; |
308 | |
309 | if ( $this->performer->isAllowed( 'minoredit' ) ) { |
310 | $flags |= EDIT_MINOR; |
311 | } |
312 | |
313 | if ( $this->bot ) { |
314 | $flags |= EDIT_FORCE_BOT; |
315 | } |
316 | |
317 | // TODO: MCR: also log model changes in other slots, in case that becomes possible! |
318 | $currentContent = $currentRevision->getContent( SlotRecord::MAIN ); |
319 | $targetContent = $targetRevision->getContent( SlotRecord::MAIN ); |
320 | $changingContentModel = $targetContent->getModel() !== $currentContent->getModel(); |
321 | |
322 | // Build rollback revision: |
323 | // Restore old content |
324 | // TODO: MCR: test this once we can store multiple slots |
325 | foreach ( $targetRevision->getSlots()->getSlots() as $slot ) { |
326 | $updater->inheritSlot( $slot ); |
327 | } |
328 | |
329 | // Remove extra slots |
330 | // TODO: MCR: test this once we can store multiple slots |
331 | foreach ( $currentRevision->getSlotRoles() as $role ) { |
332 | if ( !$targetRevision->hasSlot( $role ) ) { |
333 | $updater->removeSlot( $role ); |
334 | } |
335 | } |
336 | |
337 | $updater->markAsRevert( |
338 | EditResult::REVERT_ROLLBACK, |
339 | $currentRevision->getId(), |
340 | $targetRevision->getId() |
341 | ); |
342 | |
343 | // TODO: this logic should not be in the storage layer, it's here for compatibility |
344 | // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same |
345 | // place the 'bot' right is handled, which is currently in EditPage::attemptSave. |
346 | if ( $this->options->get( MainConfigNames::UseRCPatrol ) && |
347 | $this->performer->authorizeWrite( 'autopatrol', $this->page ) |
348 | ) { |
349 | $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED ); |
350 | } |
351 | |
352 | $summary = $this->getSummary( $currentRevision, $targetRevision ); |
353 | |
354 | // Actually store the rollback |
355 | $rev = $updater->addTags( $this->tags )->saveRevision( |
356 | CommentStoreComment::newUnsavedComment( $summary ), |
357 | $flags |
358 | ); |
359 | |
360 | // This is done even on edit failure to have patrolling in that case (T64157). |
361 | $this->updateRecentChange( $dbw, $currentRevision, $targetRevision ); |
362 | |
363 | if ( !$updater->wasSuccessful() ) { |
364 | return $updater->getStatus(); |
365 | } |
366 | |
367 | // Report if the edit was not created because it did not change the content. |
368 | if ( !$updater->wasRevisionCreated() ) { |
369 | $result = StatusValue::newGood( [ |
370 | 'current-revision-record' => $currentRevision |
371 | ] ); |
372 | $result->fatal( |
373 | 'alreadyrolled', |
374 | htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ), |
375 | htmlspecialchars( $this->byUser->getName() ), |
376 | htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) |
377 | ); |
378 | return $result; |
379 | } |
380 | |
381 | if ( $changingContentModel ) { |
382 | // If the content model changed during the rollback, |
383 | // make sure it gets logged to Special:Log/contentmodel |
384 | $log = new ManualLogEntry( 'contentmodel', 'change' ); |
385 | $log->setPerformer( $this->performer->getUser() ); |
386 | $log->setTarget( new TitleValue( $this->page->getNamespace(), $this->page->getDBkey() ) ); |
387 | $log->setComment( $summary ); |
388 | $log->setParameters( [ |
389 | '4::oldmodel' => $currentContent->getModel(), |
390 | '5::newmodel' => $targetContent->getModel(), |
391 | ] ); |
392 | |
393 | $logId = $log->insert( $dbw ); |
394 | $log->publish( $logId ); |
395 | } |
396 | |
397 | $wikiPage = $this->wikiPageFactory->newFromTitle( $this->page ); |
398 | |
399 | $this->hookRunner->onRollbackComplete( |
400 | $wikiPage, |
401 | $this->performer->getUser(), |
402 | $targetRevision, |
403 | $currentRevision |
404 | ); |
405 | |
406 | return StatusValue::newGood( [ |
407 | 'summary' => $summary, |
408 | 'current-revision-record' => $currentRevision, |
409 | 'target-revision-record' => $targetRevision, |
410 | 'newid' => $rev->getId(), |
411 | 'tags' => array_merge( $this->tags, $updater->getEditResult()->getRevertTags() ) |
412 | ] ); |
413 | } |
414 | |
415 | /** |
416 | * Set patrolling and bot flag on the edits which get rolled back. |
417 | * |
418 | * @param IDatabase $dbw |
419 | * @param RevisionRecord $current |
420 | * @param RevisionRecord $target |
421 | */ |
422 | private function updateRecentChange( |
423 | IDatabase $dbw, |
424 | RevisionRecord $current, |
425 | RevisionRecord $target |
426 | ) { |
427 | $useRCPatrol = $this->options->get( MainConfigNames::UseRCPatrol ); |
428 | if ( !$this->bot && !$useRCPatrol ) { |
429 | return; |
430 | } |
431 | |
432 | $actorId = $this->actorNormalization |
433 | ->acquireActorId( $current->getUser( RevisionRecord::RAW ), $dbw ); |
434 | $timestamp = $dbw->timestamp( $target->getTimestamp() ); |
435 | $rows = $dbw->newSelectQueryBuilder() |
436 | ->select( [ 'rc_id', 'rc_patrolled' ] ) |
437 | ->from( 'recentchanges' ) |
438 | ->where( [ 'rc_cur_id' => $current->getPageId(), 'rc_actor' => $actorId, ] ) |
439 | ->andWhere( $dbw->buildComparison( '>', [ |
440 | 'rc_timestamp' => $timestamp, |
441 | 'rc_this_oldid' => $target->getId(), |
442 | ] ) ) |
443 | ->caller( __METHOD__ )->fetchResultSet(); |
444 | |
445 | $all = []; |
446 | $patrolled = []; |
447 | $unpatrolled = []; |
448 | foreach ( $rows as $row ) { |
449 | $all[] = (int)$row->rc_id; |
450 | if ( $row->rc_patrolled ) { |
451 | $patrolled[] = (int)$row->rc_id; |
452 | } else { |
453 | $unpatrolled[] = (int)$row->rc_id; |
454 | } |
455 | } |
456 | |
457 | if ( $useRCPatrol && $this->bot ) { |
458 | // Mark all reverted edits as if they were made by a bot |
459 | // Also mark only unpatrolled reverted edits as patrolled |
460 | if ( $unpatrolled ) { |
461 | $dbw->newUpdateQueryBuilder() |
462 | ->update( 'recentchanges' ) |
463 | ->set( [ 'rc_bot' => 1, 'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ] ) |
464 | ->where( [ 'rc_id' => $unpatrolled ] ) |
465 | ->caller( __METHOD__ )->execute(); |
466 | } |
467 | if ( $patrolled ) { |
468 | $dbw->newUpdateQueryBuilder() |
469 | ->update( 'recentchanges' ) |
470 | ->set( [ 'rc_bot' => 1 ] ) |
471 | ->where( [ 'rc_id' => $patrolled ] ) |
472 | ->caller( __METHOD__ )->execute(); |
473 | } |
474 | } elseif ( $useRCPatrol ) { |
475 | // Mark only unpatrolled reverted edits as patrolled |
476 | if ( $unpatrolled ) { |
477 | $dbw->newUpdateQueryBuilder() |
478 | ->update( 'recentchanges' ) |
479 | ->set( [ 'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ] ) |
480 | ->where( [ 'rc_id' => $unpatrolled ] ) |
481 | ->caller( __METHOD__ )->execute(); |
482 | } |
483 | } else { |
484 | // Edit is from a bot |
485 | if ( $all ) { |
486 | $dbw->newUpdateQueryBuilder() |
487 | ->update( 'recentchanges' ) |
488 | ->set( [ 'rc_bot' => 1 ] ) |
489 | ->where( [ 'rc_id' => $all ] ) |
490 | ->caller( __METHOD__ )->execute(); |
491 | } |
492 | } |
493 | } |
494 | |
495 | /** |
496 | * Generate and format summary for the rollback. |
497 | * |
498 | * @param RevisionRecord $current |
499 | * @param RevisionRecord $target |
500 | * @return string |
501 | */ |
502 | private function getSummary( RevisionRecord $current, RevisionRecord $target ): string { |
503 | $revisionsBetween = $this->revisionStore->countRevisionsBetween( |
504 | $current->getPageId(), |
505 | $target, |
506 | $current, |
507 | 1000, |
508 | RevisionStore::INCLUDE_NEW |
509 | ); |
510 | $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC ); |
511 | if ( $this->summary === '' ) { |
512 | if ( !$currentEditorForPublic ) { // no public user name |
513 | $summary = MessageValue::new( 'revertpage-nouser' ); |
514 | } elseif ( $this->options->get( MainConfigNames::DisableAnonTalk ) && |
515 | !$currentEditorForPublic->isRegistered() ) { |
516 | $summary = MessageValue::new( 'revertpage-anon' ); |
517 | } else { |
518 | $summary = MessageValue::new( 'revertpage' ); |
519 | } |
520 | } else { |
521 | $summary = $this->summary; |
522 | } |
523 | |
524 | $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC ); |
525 | // Allow the custom summary to use the same args as the default message |
526 | $args = [ |
527 | $targetEditorForPublic ? $targetEditorForPublic->getName() : null, |
528 | $currentEditorForPublic ? $currentEditorForPublic->getName() : null, |
529 | $target->getId(), |
530 | Message::dateTimeParam( $target->getTimestamp() ), |
531 | $current->getId(), |
532 | Message::dateTimeParam( $current->getTimestamp() ), |
533 | $revisionsBetween, |
534 | ]; |
535 | if ( $summary instanceof MessageValue ) { |
536 | $summary = ( new Converter() )->convertMessageValue( $summary ); |
537 | $summary = $summary->params( $args )->inContentLanguage()->text(); |
538 | } else { |
539 | $summary = ( new RawMessage( $summary, $args ) )->inContentLanguage()->plain(); |
540 | } |
541 | |
542 | // Trim spaces on user supplied text |
543 | return trim( $summary ); |
544 | } |
545 | } |