Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 234 |
|
0.00% |
0 / 19 |
CRAP | |
0.00% |
0 / 1 |
SpecialGlobalVanishRequest | |
0.00% |
0 / 234 |
|
0.00% |
0 / 19 |
4160 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
onSubmit | |
0.00% |
0 / 54 |
|
0.00% |
0 / 1 |
210 | |||
onSuccess | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
execute | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
110 | |||
getFormFields | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
requiresUnblock | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
userCanExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
displayRestrictionError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
alterForm | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getDisplayFormat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
preHtml | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGlobalUser | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
generateUsername | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
eligibleForAutomaticVanish | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
sendVanishingSuccessfulEmail | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
30 | |||
getUserBlockAppealSitelinks | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
110 | |||
queryWikidata | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * @section LICENSE |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License along |
15 | * with this program; if not, write to the Free Software Foundation, Inc., |
16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * http://www.gnu.org/copyleft/gpl.html |
18 | * |
19 | * @file |
20 | */ |
21 | |
22 | namespace MediaWiki\Extension\CentralAuth\Special; |
23 | |
24 | use MailAddress; |
25 | use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames; |
26 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameDenylist; |
27 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameFactory; |
28 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequest; |
29 | use MediaWiki\Extension\CentralAuth\GlobalRename\GlobalRenameRequestStore; |
30 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
31 | use MediaWiki\Html\Html; |
32 | use MediaWiki\HTMLForm\HTMLForm; |
33 | use MediaWiki\Http\HttpRequestFactory; |
34 | use MediaWiki\Json\FormatJson; |
35 | use MediaWiki\Logger\LoggerFactory; |
36 | use MediaWiki\Parser\Parser; |
37 | use MediaWiki\SpecialPage\FormSpecialPage; |
38 | use MediaWiki\Status\Status; |
39 | use MediaWiki\User\User; |
40 | use MediaWiki\User\UserIdentityLookup; |
41 | use PermissionsError; |
42 | use Psr\Log\LoggerInterface; |
43 | use RuntimeException; |
44 | use UserMailer; |
45 | use Wikimedia\Rdbms\IDBAccessObject; |
46 | |
47 | /** |
48 | * Request an account vanish. |
49 | */ |
50 | class SpecialGlobalVanishRequest extends FormSpecialPage { |
51 | |
52 | private LoggerInterface $logger; |
53 | private GlobalRenameDenylist $globalRenameDenylist; |
54 | private GlobalRenameRequestStore $globalRenameRequestStore; |
55 | private GlobalRenameFactory $globalRenameFactory; |
56 | private HttpRequestFactory $httpRequestFactory; |
57 | private UserIdentityLookup $userIdentityLookup; |
58 | |
59 | public function __construct( |
60 | GlobalRenameDenylist $globalRenameDenylist, |
61 | GlobalRenameRequestStore $globalRenameRequestStore, |
62 | GlobalRenameFactory $globalRenameFactory, |
63 | HttpRequestFactory $httpRequestFactory, |
64 | UserIdentityLookup $userIdentityLookup |
65 | ) { |
66 | parent::__construct( 'GlobalVanishRequest' ); |
67 | |
68 | $this->logger = LoggerFactory::getInstance( 'CentralAuth' ); |
69 | $this->globalRenameDenylist = $globalRenameDenylist; |
70 | $this->globalRenameRequestStore = $globalRenameRequestStore; |
71 | $this->globalRenameFactory = $globalRenameFactory; |
72 | $this->httpRequestFactory = $httpRequestFactory; |
73 | $this->userIdentityLookup = $userIdentityLookup; |
74 | } |
75 | |
76 | /** @inheritDoc */ |
77 | public function onSubmit( array $data ): Status { |
78 | $newUsername = $this->generateUsername(); |
79 | if ( !$newUsername ) { |
80 | return Status::newFatal( $this->msg( 'globalvanishrequest-save-error' ) ); |
81 | } |
82 | |
83 | // Verify that the user is a global user. |
84 | $causer = $this->getGlobalUser(); |
85 | if ( !$causer ) { |
86 | return Status::newFatal( $this->msg( 'globalvanishrequest-globaluser-error' ) ); |
87 | } |
88 | |
89 | // Disallow for users that have blocks on any connected wikis. |
90 | if ( $causer->isBlocked() ) { |
91 | return Status::newFatal( $this->msg( 'globalvanishrequest-blocked-error' ) ); |
92 | } |
93 | |
94 | // Disallow duplicate rename / vanish requests. |
95 | $username = $this->getUser()->getName(); |
96 | if ( $this->globalRenameRequestStore->currentNameHasPendingRequest( $username ) ) { |
97 | return Status::newFatal( $this->msg( 'globalvanishrequest-pending-request-error' ) ); |
98 | } |
99 | |
100 | $request = $this->globalRenameRequestStore |
101 | ->newBlankRequest() |
102 | ->setName( $username ) |
103 | ->setNewName( $newUsername ) |
104 | ->setReason( $data['reason'] ?? null ) |
105 | ->setType( GlobalRenameRequest::VANISH ); |
106 | |
107 | $automaticVanishPerformerName = $this->getConfig() |
108 | ->get( CAMainConfigNames::CentralAuthAutomaticVanishPerformer ); |
109 | $automaticVanishPerformer = $automaticVanishPerformerName !== null |
110 | ? CentralAuthUser::getInstanceByName( $automaticVanishPerformerName ) |
111 | : null; |
112 | $localAutomaticVanishPerformer = $this->userIdentityLookup |
113 | ->getUserIdentityByName( $automaticVanishPerformerName ); |
114 | |
115 | // Immediately start the vanish if we already know that the user is |
116 | // eligible for approval without a review. |
117 | if ( |
118 | $automaticVanishPerformer !== null && |
119 | $automaticVanishPerformer->exists() && |
120 | $automaticVanishPerformer->isAttached() && |
121 | $localAutomaticVanishPerformer !== null && |
122 | $this->eligibleForAutomaticVanish() |
123 | ) { |
124 | $request |
125 | ->setStatus( GlobalRenameRequest::APPROVED ) |
126 | ->setPerformer( $automaticVanishPerformer->getId() ) |
127 | ->setComments( |
128 | $this->msg( 'globalvanishrequest-autoapprove-public-note' ) |
129 | ->inContentLanguage()->text() ); |
130 | |
131 | // Save the rename request to the queue before initiating the job. |
132 | if ( !$this->globalRenameRequestStore->save( $request ) ) { |
133 | return Status::newFatal( $this->msg( 'globalvanishrequest-save-error' ) ); |
134 | } |
135 | |
136 | // Scrub the "reason". In the context of GlobalRenameUser, reason |
137 | // is the public log entry, not the private reason stated by the |
138 | // user. That value should never be logged. |
139 | $request->setReason( |
140 | $this->msg( 'globalvanishrequest-autoapprove-public-note' ) |
141 | ->inContentLanguage()->text() ); |
142 | |
143 | $requestArray = $request->toArray(); |
144 | |
145 | // We need to add this two fields that are usually being provided by the Form |
146 | $requestArray['movepages'] = true; |
147 | $requestArray['suppressredirects'] = true; |
148 | |
149 | $renameResult = $this->globalRenameFactory |
150 | ->newGlobalRenameUser( $localAutomaticVanishPerformer, $causer, $request->getNewName() ) |
151 | ->withSession( $this->getContext()->exportSession() ) |
152 | ->withLockPerformingUser( $localAutomaticVanishPerformer ) |
153 | ->rename( $requestArray ); |
154 | |
155 | if ( !$renameResult->isGood() ) { |
156 | return $renameResult; |
157 | } |
158 | |
159 | $this->sendVanishingSuccessfulEmail( $request ); |
160 | } else { |
161 | // Save the request to the database for it to be processed later. |
162 | if ( !$this->globalRenameRequestStore->save( $request ) ) { |
163 | return Status::newFatal( $this->msg( 'globalvanishrequest-save-error' ) ); |
164 | } |
165 | } |
166 | |
167 | return Status::newGood(); |
168 | } |
169 | |
170 | /** @inheritDoc */ |
171 | public function onSuccess(): void { |
172 | $isVanished = $this->globalRenameRequestStore |
173 | ->currentNameHasApprovedVanish( |
174 | $this->getUser()->getName(), IDBAccessObject::READ_LATEST ); |
175 | |
176 | $destination = $isVanished ? 'vanished' : 'status'; |
177 | |
178 | $this->getOutput()->redirect( |
179 | $this->getPageTitle( $destination )->getFullURL(), '303' |
180 | ); |
181 | } |
182 | |
183 | /** @inheritDoc */ |
184 | public function execute( $subPage ): void { |
185 | $out = $this->getOutput(); |
186 | |
187 | if ( $subPage === 'vanished' ) { |
188 | $out->setPageTitleMsg( $this->msg( 'globalvanishrequest-vanished-title' ) ); |
189 | $out->addWikiMsg( 'globalvanishrequest-vanished-text' ); |
190 | return; |
191 | } |
192 | |
193 | $this->requireNamedUser(); |
194 | $username = $this->getUser()->getName(); |
195 | $hasPending = $this->globalRenameRequestStore->currentNameHasPendingRequest( $username ); |
196 | |
197 | if ( $subPage === 'status' ) { |
198 | if ( !$hasPending ) { |
199 | $out->redirect( $this->getPageTitle()->getFullURL(), '303' ); |
200 | return; |
201 | } |
202 | |
203 | $out->setPageTitleMsg( $this->msg( 'globalvanishrequest-status-title' ) ); |
204 | $out->addWikiMsg( 'globalvanishrequest-status-text' ); |
205 | return; |
206 | } |
207 | |
208 | // Preemptively check if the user has any blocks, and if so prevent the |
209 | // form from rendering and give them a link to appeal. |
210 | $causer = $this->getGlobalUser(); |
211 | if ( $causer ) { |
212 | $blockedWikiIds = []; |
213 | foreach ( $causer->getBlocks() as $wikiId => $blocks ) { |
214 | if ( count( $blocks ) > 0 ) { |
215 | $blockedWikiIds[] = $wikiId; |
216 | } |
217 | } |
218 | |
219 | if ( count( $blockedWikiIds ) > 0 ) { |
220 | $out->setPageTitleMsg( $this->msg( 'globalvanishrequest-blocked-title' ) ); |
221 | |
222 | $sitelinks = $this->getUserBlockAppealSitelinks( $blockedWikiIds ); |
223 | if ( count( $sitelinks ) > 0 ) { |
224 | $out->addWikiMsg( 'globalvanishrequest-blocked-text' ); |
225 | } else { |
226 | $out->addWikiMsg( 'globalvanishrequest-blocked-text-minimal' ); |
227 | } |
228 | |
229 | // Create an unordered list of appeal links that are relevant to |
230 | // the user. For each wiki that the user is blocked in, the |
231 | // relevant appeal page on that wiki is added. |
232 | $appealListItems = array_map( |
233 | fn ( $sitelink ) => Html::rawElement( 'li', [], |
234 | Parser::stripOuterParagraph( $out->parseAsContent( $sitelink ) ) |
235 | ), |
236 | $sitelinks |
237 | ); |
238 | $out->addHTML( Html::rawElement( 'ul', [], implode( '', $appealListItems ) ) ); |
239 | |
240 | return; |
241 | } |
242 | } |
243 | |
244 | if ( $hasPending ) { |
245 | $out = $this->getOutput(); |
246 | $out->redirect( $this->getPageTitle( 'status' )->getFullURL(), '303' ); |
247 | return; |
248 | } |
249 | |
250 | $out->addModules( 'ext.centralauth.globalvanishrequest' ); |
251 | |
252 | parent::execute( $subPage ); |
253 | } |
254 | |
255 | /** @inheritDoc */ |
256 | public function getFormFields(): array { |
257 | return [ |
258 | 'username' => [ |
259 | 'cssclass' => 'mw-globalvanishrequest-field', |
260 | 'default' => $this->getUser()->getName(), |
261 | 'label-message' => 'globalvanishrequest-username-label', |
262 | 'required' => true, |
263 | 'type' => 'text', |
264 | 'disabled' => true, |
265 | ], |
266 | 'reason' => [ |
267 | 'cssclass' => 'mw-globalvanishrequest-field', |
268 | 'id' => 'mw-vanishrequest-reason', |
269 | 'label-message' => 'globalvanishrequest-reason-label', |
270 | 'name' => 'reason', |
271 | 'rows' => 3, |
272 | 'type' => 'textarea', |
273 | ], |
274 | ]; |
275 | } |
276 | |
277 | /** @inheritDoc */ |
278 | public function doesWrites(): bool { |
279 | return true; |
280 | } |
281 | |
282 | /** |
283 | * Blocked users should not be able to request a vanish. |
284 | * @return bool |
285 | */ |
286 | public function requiresUnblock(): bool { |
287 | return true; |
288 | } |
289 | |
290 | /** @inheritDoc */ |
291 | public function userCanExecute( User $user ): bool { |
292 | return $this->globalRenameDenylist->checkUser( $user->getName() ); |
293 | } |
294 | |
295 | /** @inheritDoc */ |
296 | public function displayRestrictionError(): void { |
297 | throw new PermissionsError( null, [ 'centralauth-badaccess-blacklisted' ] ); |
298 | } |
299 | |
300 | /** @inheritDoc */ |
301 | protected function alterForm( HTMLForm $form ): void { |
302 | $form |
303 | ->setSubmitTextMsg( 'globalvanishrequest-submit-text' ) |
304 | ->setSubmitID( 'mw-vanishrequest-submit' ); |
305 | } |
306 | |
307 | /** @inheritDoc */ |
308 | protected function getDisplayFormat(): string { |
309 | return 'ooui'; |
310 | } |
311 | |
312 | /** @inheritDoc */ |
313 | protected function preHtml(): string { |
314 | return $this->msg( 'globalvanishrequest-pretext' )->parse(); |
315 | } |
316 | |
317 | /** @inheritDoc */ |
318 | protected function getGroupName(): string { |
319 | return 'login'; |
320 | } |
321 | |
322 | /** |
323 | * Return the global user if the authenticated user has a global account. |
324 | * @return CentralAuthUser|false |
325 | */ |
326 | private function getGlobalUser() { |
327 | $user = $this->getUser(); |
328 | $causer = CentralAuthUser::getInstance( $user ); |
329 | |
330 | if ( $causer->exists() && $causer->isAttached() ) { |
331 | return $causer; |
332 | } |
333 | return false; |
334 | } |
335 | |
336 | /** |
337 | * Generate a random username that the user requesting a vanish would be |
338 | * renamed to if the request is accepted. |
339 | * |
340 | * @return string|false contains a string if successful |
341 | */ |
342 | private function generateUsername() { |
343 | $attempts = 0; |
344 | |
345 | do { |
346 | $random = wfRandomString(); |
347 | $candidate = "Renamed user {$random}"; |
348 | if ( GlobalRenameRequest::isNameAvailable( $candidate, IDBAccessObject::READ_NORMAL )->isOK() ) { |
349 | return $candidate; |
350 | } |
351 | $attempts++; |
352 | } while ( $attempts < 5 ); |
353 | |
354 | return false; |
355 | } |
356 | |
357 | /** |
358 | * Checks if the currently authenticated user is eligible for automatic vanishing. |
359 | * @return bool |
360 | */ |
361 | private function eligibleForAutomaticVanish(): bool { |
362 | $causer = $this->getGlobalUser(); |
363 | if ( !$causer ) { |
364 | return false; |
365 | } |
366 | |
367 | return $causer->getGlobalEditCount() === 0 && |
368 | !$causer->isBlocked() && |
369 | !$causer->hasPublicLogs(); |
370 | } |
371 | |
372 | /** |
373 | * Attempt to send a success email to the user whose vanish was fulfilled. |
374 | * TODO: https://phabricator.wikimedia.org/T369134 - refactor email sending |
375 | * @param GlobalRenameRequest $request |
376 | * @return void |
377 | */ |
378 | private function sendVanishingSuccessfulEmail( GlobalRenameRequest $request ): void { |
379 | $causer = $this->getGlobalUser(); |
380 | if ( !$causer ) { |
381 | return; |
382 | } |
383 | |
384 | $bodyKey = $request->getComments() === '' |
385 | ? 'globalrenamequeue-vanish-email-body-approved' |
386 | : 'globalrenamequeue-vanish-email-body-approved-with-note'; |
387 | |
388 | $subject = $this->msg( 'globalrenamequeue-vanish-email-subject-approved' ) |
389 | ->inContentLanguage() |
390 | ->text(); |
391 | $body = $this->msg( $bodyKey, [ $request->getName(), $request->getComments() ] ) |
392 | ->inContentLanguage() |
393 | ->text(); |
394 | |
395 | $from = new MailAddress( |
396 | $this->getConfig()->get( 'PasswordSender' ), |
397 | $this->msg( 'emailsender' )->inContentLanguage()->text() |
398 | ); |
399 | $to = new MailAddress( $causer->getEmail(), $causer->getName(), '' ); |
400 | |
401 | // Users don't always have email addresses. Since this is acceptable |
402 | // and expected behavior, bail out with a warning if there isn't one. |
403 | if ( !$to->address ) { |
404 | $this->logger->info( |
405 | "Unable to sent approval email to User:{oldName} as there is no email address to send to.", |
406 | [ 'oldName' => $request->getName(), 'component' => 'GlobalVanish' ] |
407 | ); |
408 | return; |
409 | } |
410 | |
411 | $this->logger->info( 'Send approval email to User:{oldName}', [ |
412 | 'oldName' => $request->getName(), |
413 | 'component' => 'GlobalVanish', |
414 | ] ); |
415 | |
416 | // Attempt to send the email, and log an error if this fails. |
417 | $emailSendResult = UserMailer::send( $to, $from, $subject, $body ); |
418 | if ( !$emailSendResult->isOK() ) { |
419 | $this->logger->error( $emailSendResult->getValue() ); |
420 | } |
421 | } |
422 | |
423 | /** |
424 | * Retrieve the block appeal links most relevant for the user. |
425 | * |
426 | * For each wiki the user is blocked on, the appeal link for that wiki will |
427 | * be returned. If that page is not available, then fallback pages will be |
428 | * attempted as well if configured. |
429 | * |
430 | * @param array $wikiIds a list of wikis to fetch links from |
431 | * @return array sitelinks to the most relevant appeal pages for the user |
432 | */ |
433 | private function getUserBlockAppealSitelinks( array $wikiIds ): array { |
434 | $sitelinks = []; |
435 | |
436 | $entityIds = $this->getConfig()->get( CAMainConfigNames::CentralAuthBlockAppealWikidataIds ); |
437 | if ( $entityIds !== null && count( $entityIds ) > 0 ) { |
438 | // Fetch block appeal and block policy pages from the Wikidata API. |
439 | $parameters = [ |
440 | "action" => "wbgetentities", |
441 | "format" => "json", |
442 | "ids" => implode( '|', $entityIds ), |
443 | "props" => "sitelinks|sitelinks/urls", |
444 | "formatversion" => "2", |
445 | ]; |
446 | $wikidataResult = $this->queryWikidata( $parameters ); |
447 | if ( !$wikidataResult->isGood() ) { |
448 | return []; |
449 | } |
450 | $wikidataResponse = $wikidataResult->getValue(); |
451 | $entities = $wikidataResponse['entities']; |
452 | |
453 | // Iterate through every wiki with blocks and find the most |
454 | // relevant page for appealing blocks on an account. |
455 | foreach ( $wikiIds as $wikiId ) { |
456 | foreach ( $entityIds as $entityId ) { |
457 | if ( isset( $entities[$entityId]['sitelinks'][$wikiId]['url'] ) ) { |
458 | $sitelink = $entities[$entityId]['sitelinks'][$wikiId]; |
459 | $sitelinks[] = "[{$sitelink['url']} {$sitelink['title']}]"; |
460 | break; |
461 | } |
462 | } |
463 | } |
464 | } |
465 | |
466 | // Fallback to showing a fallback URL (if configured) in the event that |
467 | // no appeal links were able to be found from the Wikidata API. |
468 | if ( count( $sitelinks ) === 0 ) { |
469 | $appealUrl = $this->getConfig()->get( CAMainConfigNames::CentralAuthFallbackAppealUrl ); |
470 | $appealTitle = $this->getConfig()->get( CAMainConfigNames::CentralAuthFallbackAppealTitle ); |
471 | |
472 | if ( $appealUrl !== null && $appealTitle !== null ) { |
473 | $sitelinks[] = "[{$appealUrl} {$appealTitle}]"; |
474 | } |
475 | } |
476 | |
477 | return $sitelinks; |
478 | } |
479 | |
480 | /** |
481 | * Retrieve entity data from the Wikidata API. |
482 | * |
483 | * @param array $parameters |
484 | * @return Status |
485 | */ |
486 | private function queryWikidata( array $parameters ): Status { |
487 | $options = [ |
488 | 'method' => 'GET', |
489 | 'userAgent' => "{$this->httpRequestFactory->getUserAgent()} CentralAuth", |
490 | ]; |
491 | $url = $this->getConfig()->get( CAMainConfigNames::CentralAuthWikidataApiUrl ); |
492 | if ( $url === null ) { |
493 | return Status::newFatal( |
494 | 'Cannot make Wikidata request for entities as $wgCentralAuthWikidataApiUrl is unset.' |
495 | ); |
496 | } |
497 | $encodedParameters = wfArrayToCgi( $parameters ); |
498 | $request = $this->httpRequestFactory->create( "{$url}?{$encodedParameters}", $options, __METHOD__ ); |
499 | |
500 | $httpResult = $request->execute(); |
501 | if ( $httpResult->isOK() ) { |
502 | $httpResult->merge( FormatJson::parse( $request->getContent(), FormatJson::FORCE_ASSOC ), true ); |
503 | } |
504 | |
505 | [ $errorsOnlyStatus, $warningsOnlyStatus ] = $httpResult->splitByErrorType(); |
506 | if ( !$warningsOnlyStatus->isGood() ) { |
507 | LoggerFactory::getInstance( 'CentralAuth' )->warning( |
508 | $warningsOnlyStatus->getWikiText( false, false, 'en' ), |
509 | [ 'exception' => new RuntimeException ] |
510 | ); |
511 | } |
512 | return $errorsOnlyStatus; |
513 | } |
514 | } |