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;
13use MediaWiki\Deferred\SiteStatsUpdate;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Permissions\PermissionManager;
19use MediaWiki\SpecialPage\SpecialPage;
20use MediaWiki\User\ActorStore;
21use MediaWiki\User\Options\UserOptionsManager;
22use MediaWiki\User\User;
23use MediaWiki\User\UserArray;
24use MediaWiki\User\UserFactory;
25use MediaWiki\User\UserGroupManager;
26use RuntimeException;
27use UnexpectedValueException;
28use Wikimedia\Rdbms\IConnectionProvider;
29use Wikimedia\ScopedCallback;
30
39 public const CONSTRUCTOR_OPTIONS = [
40 'EmergencyContact',
41 'TranslateSandboxPromotedGroup',
42 ];
43
44 private UserFactory $userFactory;
45 private IConnectionProvider $dbProvider;
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 IConnectionProvider $dbProvider,
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->dbProvider = $dbProvider;
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->dbProvider->getPrimaryDatabase();
158 $dbw->newDeleteQueryBuilder()
159 ->deleteFrom( 'user' )
160 ->where( [ 'user_id' => $uid ] )
161 ->caller( __METHOD__ )
162 ->execute();
163 $dbw->newDeleteQueryBuilder()
164 ->deleteFrom( 'user_groups' )
165 ->where( [ 'ug_user' => $uid ] )
166 ->caller( __METHOD__ )
167 ->execute();
168 $dbw->newDeleteQueryBuilder()
169 ->deleteFrom( 'user_properties' )
170 ->where( [ 'up_user' => $uid ] )
171 ->caller( __METHOD__ )
172 ->execute();
173
174 $this->actorStore->deleteActor( $user, $dbw );
175
176 // Assume no joins are needed for logging or recentchanges
177 $dbw->newDeleteQueryBuilder()
178 ->deleteFrom( 'logging' )
179 ->where( [ 'log_actor' => $actorId ] )
180 ->caller( __METHOD__ )
181 ->execute();
182 $dbw->newDeleteQueryBuilder()
183 ->deleteFrom( 'recentchanges' )
184 ->where( [ 'rc_actor' => $actorId ] )
185 ->caller( __METHOD__ )
186 ->execute();
187
188 // Update the site stats
189 $statsUpdate = SiteStatsUpdate::factory( [ 'users' => -1 ] );
190 $statsUpdate->doUpdate();
191
192 // If someone tries to access still object still, they will get anon user
193 // data.
194 $user->clearInstanceCache( 'defaults' );
195
196 // Nobody should access the user by id anymore, but in case they do, purge
197 // the cache so they wont get stale data
198 $user->invalidateCache();
199 }
200
202 public function getUsers(): UserArray {
203 $dbr = Utilities::getSafeReadDB();
204 $query = User::newQueryBuilder( $dbr );
205
206 $res = $query->join( 'user_groups', null, 'ug_user = user_id' )
207 ->where( [ 'ug_group' => 'translate-sandboxed' ] )
208 ->caller( __METHOD__ )
209 ->fetchResultSet();
210
211 return UserArray::newFromResult( $res );
212 }
213
218 public function promoteUser( User $user ): void {
219 $translateSandboxPromotedGroup = $this->options->get( 'TranslateSandboxPromotedGroup' );
220
221 if ( !self::isSandboxed( $user ) ) {
222 throw new UserNotSandboxedException();
223 }
224
225 $this->userGroupManager->removeUserFromGroup( $user, 'translate-sandboxed' );
226 if ( $translateSandboxPromotedGroup ) {
227 $this->userGroupManager->addUserToGroup( $user, $translateSandboxPromotedGroup );
228 }
229
230 $this->userOptionsManager->setOption( $user, 'translate-sandbox-reminders', null );
231 $this->userOptionsManager->saveOptions( $user );
232
233 $this->hookRunner->onTranslate_TranslatorSandbox_UserPromoted( $user );
234 }
235
243 public function sendEmail( User $sender, User $target, string $type ): void {
244 $emergencyContact = $this->options->get( 'EmergencyContact' );
245
246 $targetLang = $this->userOptionsManager->getOption( $target, 'language' );
247
248 switch ( $type ) {
249 case 'reminder':
250 if ( !self::isSandboxed( $target ) ) {
251 throw new UserNotSandboxedException();
252 }
253
254 $subjectMsg = 'tsb-reminder-title-generic';
255 $bodyMsg = 'tsb-reminder-content-generic';
256 $targetSpecialPage = 'TranslationStash';
257
258 break;
259 case 'promotion':
260 $subjectMsg = 'tsb-email-promoted-subject';
261 $bodyMsg = 'tsb-email-promoted-body';
262 $targetSpecialPage = 'Translate';
263
264 break;
265 case 'rejection':
266 $subjectMsg = 'tsb-email-rejected-subject';
267 $bodyMsg = 'tsb-email-rejected-body';
268 $targetSpecialPage = 'TwnMainPage';
269
270 break;
271 default:
272 throw new UnexpectedValueException( "'$type' is an invalid type of translate sandbox email" );
273 }
274
275 $subject = wfMessage( $subjectMsg )->inLanguage( $targetLang )->text();
276 $body = wfMessage(
277 $bodyMsg,
278 $target->getName(),
279 SpecialPage::getTitleFor( $targetSpecialPage )->getCanonicalURL(),
280 $sender->getName()
281 )->inLanguage( $targetLang )->text();
282
283 $params = [
284 'user' => $target->getId(),
285 'to' => MailAddress::newFromUser( $target ),
286 'from' => new MailAddress( $emergencyContact ),
287 'replyto' => new MailAddress( $emergencyContact ),
288 'subj' => $subject,
289 'body' => $body,
290 'emailType' => $type,
291 ];
292
293 $reminders = $this->userOptionsManager->getOption( $target, 'translate-sandbox-reminders' );
294 $reminders = $reminders ? explode( '|', $reminders ) : [];
295 $reminders[] = wfTimestamp();
296
297 $this->userOptionsManager->setOption( $target, 'translate-sandbox-reminders', implode( '|', $reminders ) );
298 $this->userOptionsManager->saveOptions( $target );
299
300 $this->jobQueueGroup->push( TranslateSandboxEmailJob::newJob( $params ) );
301 }
302
304 public static function isSandboxed( User $user ): bool {
305 $userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager();
306 return in_array( 'translate-sandboxed', $userGroupManager->getUserGroups( $user ), true );
307 }
308}
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