Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
TranslateSandbox.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TranslatorSandbox;
5
6use InvalidArgumentException;
7use JobQueueGroup;
8use MailAddress;
9use MediaWiki\Auth\AuthenticationRequest;
10use MediaWiki\Auth\AuthenticationResponse;
11use MediaWiki\Auth\AuthManager;
12use MediaWiki\Config\ServiceOptions;
16use MediaWiki\MediaWikiServices;
17use MediaWiki\Permissions\PermissionManager;
18use MediaWiki\SpecialPage\SpecialPage;
19use MediaWiki\User\ActorStore;
20use MediaWiki\User\User;
21use MediaWiki\User\UserArray;
22use MediaWiki\User\UserFactory;
23use MediaWiki\User\UserGroupManager;
24use MediaWiki\User\UserOptionsManager;
25use RuntimeException;
26use SiteStatsUpdate;
27use UnexpectedValueException;
28use Wikimedia\Rdbms\ILoadBalancer;
29use Wikimedia\ScopedCallback;
30
39 public const CONSTRUCTOR_OPTIONS = [
40 'EmergencyContact',
41 'TranslateSandboxPromotedGroup',
42 ];
43
44 private UserFactory $userFactory;
45 private ILoadBalancer $loadBalancer;
46 private PermissionManager $permissionManager;
47 private AuthManager $authManager;
48 private UserGroupManager $userGroupManager;
49 private ActorStore $actorStore;
50 private UserOptionsManager $userOptionsManager;
51 private JobQueueGroup $jobQueueGroup;
52 private HookRunner $hookRunner;
53 private ServiceOptions $options;
54
55 public function __construct(
56 UserFactory $userFactory,
57 ILoadBalancer $loadBalancer,
58 PermissionManager $permissionManager,
59 AuthManager $authManager,
60 UserGroupManager $userGroupManager,
61 ActorStore $actorStore,
62 UserOptionsManager $userOptionsManager,
63 JobQueueGroup $jobQueueGroup,
64 HookRunner $hookRunner,
65 ServiceOptions $options
66 ) {
67 $this->userFactory = $userFactory;
68 $this->loadBalancer = $loadBalancer;
69 $this->permissionManager = $permissionManager;
70 $this->authManager = $authManager;
71 $this->userGroupManager = $userGroupManager;
72 $this->actorStore = $actorStore;
73 $this->userOptionsManager = $userOptionsManager;
74 $this->jobQueueGroup = $jobQueueGroup;
75 $this->hookRunner = $hookRunner;
76 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
77 $this->options = $options;
78 }
79
84 public const USER_CREATION_FAILURE = 56739;
85
87 public function addUser( string $name, string $email, string $password ): User {
88 $user = $this->userFactory->newFromName( $name, UserFactory::RIGOR_CREATABLE );
89
90 if ( !$user ) {
91 throw new InvalidArgumentException( 'Invalid user name' );
92 }
93
94 $data = [
95 'username' => $user->getName(),
96 'password' => $password,
97 'retype' => $password,
98 'email' => $email,
99 'realname' => '',
100 ];
101
102 $creator = TranslateUserManager::getUser();
103 $guard = $this->permissionManager->addTemporaryUserRights( $creator, 'createaccount' );
104
105 $reqs = $this->authManager->getAuthenticationRequests( AuthManager::ACTION_CREATE );
106 $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
107 $res = $this->authManager->beginAccountCreation( $creator, $reqs, 'null:' );
108
109 ScopedCallback::consume( $guard );
110
111 switch ( $res->status ) {
112 case AuthenticationResponse::PASS:
113 break;
114 case AuthenticationResponse::FAIL:
115 // Unless things are misconfigured, this will handle errors such as username taken,
116 // invalid user name or too short password. The WebAPI is prechecking these to
117 // provide nicer error messages.
118 $reason = $res->message->inLanguage( 'en' )->useDatabase( false )->text();
119 throw new RuntimeException(
120 "Account creation failed: $reason",
121 self::USER_CREATION_FAILURE
122 );
123 default:
124 // A provider requested further user input. Abort but clean up first if it was a
125 // secondary provider (in which case the user was created).
126 if ( $user->getId() ) {
127 $this->deleteUser( $user, 'force' );
128 }
129
130 throw new RuntimeException(
131 'AuthManager does not support such simplified account creation'
132 );
133 }
134
135 // group-translate-sandboxed group-translate-sandboxed-member
136 $this->userGroupManager->addUserToGroup( $user, 'translate-sandboxed' );
137
138 return $user;
139 }
140
148 public function deleteUser( User $user, string $force = '' ): void {
149 $uid = $user->getId();
150 $actorId = $user->getActorId();
151
152 if ( $force !== 'force' && !self::isSandboxed( $user ) ) {
153 throw new UserNotSandboxedException();
154 }
155
156 // Delete from database
157 $dbw = $this->loadBalancer->getConnection( DB_PRIMARY );
158 $dbw->delete( 'user', [ 'user_id' => $uid ], __METHOD__ );
159 $dbw->delete( 'user_groups', [ 'ug_user' => $uid ], __METHOD__ );
160 $dbw->delete( 'user_properties', [ 'up_user' => $uid ], __METHOD__ );
161
162 $this->actorStore->deleteActor( $user, $dbw );
163
164 // Assume no joins are needed for logging or recentchanges
165 $dbw->delete( 'logging', [ 'log_actor' => $actorId ], __METHOD__ );
166 $dbw->delete( 'recentchanges', [ 'rc_actor' => $actorId ], __METHOD__ );
167
168 // Update the site stats
169 $statsUpdate = SiteStatsUpdate::factory( [ 'users' => -1 ] );
170 $statsUpdate->doUpdate();
171
172 // If someone tries to access still object still, they will get anon user
173 // data.
174 $user->clearInstanceCache( 'defaults' );
175
176 // Nobody should access the user by id anymore, but in case they do, purge
177 // the cache so they wont get stale data
178 $user->invalidateCache();
179 }
180
182 public function getUsers(): UserArray {
183 $dbr = Utilities::getSafeReadDB();
184 $query = User::newQueryBuilder( $dbr );
185
186 $res = $query->join( 'user_groups', null, 'ug_user = user_id' )
187 ->where( [ 'ug_group' => 'translate-sandboxed' ] )
188 ->caller( __METHOD__ )
189 ->fetchResultSet();
190
191 return UserArray::newFromResult( $res );
192 }
193
198 public function promoteUser( User $user ): void {
199 $translateSandboxPromotedGroup = $this->options->get( 'TranslateSandboxPromotedGroup' );
200
201 if ( !self::isSandboxed( $user ) ) {
202 throw new UserNotSandboxedException();
203 }
204
205 $this->userGroupManager->removeUserFromGroup( $user, 'translate-sandboxed' );
206 if ( $translateSandboxPromotedGroup ) {
207 $this->userGroupManager->addUserToGroup( $user, $translateSandboxPromotedGroup );
208 }
209
210 $this->userOptionsManager->setOption( $user, 'translate-sandbox-reminders', null );
211 $this->userOptionsManager->saveOptions( $user );
212
213 $this->hookRunner->onTranslate_TranslatorSandbox_UserPromoted( $user );
214 }
215
223 public function sendEmail( User $sender, User $target, string $type ): void {
224 $emergencyContact = $this->options->get( 'EmergencyContact' );
225
226 $targetLang = $this->userOptionsManager->getOption( $target, 'language' );
227
228 switch ( $type ) {
229 case 'reminder':
230 if ( !self::isSandboxed( $target ) ) {
231 throw new UserNotSandboxedException();
232 }
233
234 $subjectMsg = 'tsb-reminder-title-generic';
235 $bodyMsg = 'tsb-reminder-content-generic';
236 $targetSpecialPage = 'TranslationStash';
237
238 break;
239 case 'promotion':
240 $subjectMsg = 'tsb-email-promoted-subject';
241 $bodyMsg = 'tsb-email-promoted-body';
242 $targetSpecialPage = 'Translate';
243
244 break;
245 case 'rejection':
246 $subjectMsg = 'tsb-email-rejected-subject';
247 $bodyMsg = 'tsb-email-rejected-body';
248 $targetSpecialPage = 'TwnMainPage';
249
250 break;
251 default:
252 throw new UnexpectedValueException( "'$type' is an invalid type of translate sandbox email" );
253 }
254
255 $subject = wfMessage( $subjectMsg )->inLanguage( $targetLang )->text();
256 $body = wfMessage(
257 $bodyMsg,
258 $target->getName(),
259 SpecialPage::getTitleFor( $targetSpecialPage )->getCanonicalURL(),
260 $sender->getName()
261 )->inLanguage( $targetLang )->text();
262
263 $params = [
264 'user' => $target->getId(),
265 'to' => MailAddress::newFromUser( $target ),
266 'from' => new MailAddress( $emergencyContact ),
267 'replyto' => new MailAddress( $emergencyContact ),
268 'subj' => $subject,
269 'body' => $body,
270 'emailType' => $type,
271 ];
272
273 $reminders = $this->userOptionsManager->getOption( $target, 'translate-sandbox-reminders' );
274 $reminders = $reminders ? explode( '|', $reminders ) : [];
275 $reminders[] = wfTimestamp();
276
277 $this->userOptionsManager->setOption( $target, 'translate-sandbox-reminders', implode( '|', $reminders ) );
278 $this->userOptionsManager->saveOptions( $target );
279
280 $this->jobQueueGroup->push( TranslateSandboxEmailJob::newJob( $params ) );
281 }
282
284 public static function isSandboxed( User $user ): bool {
285 $userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
286 return in_array( 'translate-sandboxed', $userGroupManager->getUserGroups( $user ), true );
287 }
288}
Hook runner for the Translate extension.
Utility class for the sandbox feature of Translate.
const USER_CREATION_FAILURE
Custom exception code used when user creation fails in order to differentiate between other exception...
deleteUser(User $user, string $force='')
Deletes a sandboxed user without doing much validation.
promoteUser(User $user)
Removes the user from the sandbox.
sendEmail(User $sender, User $target, string $type)
Sends a reminder to the user.
static isSandboxed(User $user)
Shortcut for checking if given user is in the sandbox.
addUser(string $name, string $email, string $password)
Adds a new user without doing much validation.
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31