|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Implements Special:Emailuser |
|
4
|
|
|
* |
|
5
|
|
|
* This program is free software; you can redistribute it and/or modify |
|
6
|
|
|
* it under the terms of the GNU General Public License as published by |
|
7
|
|
|
* the Free Software Foundation; either version 2 of the License, or |
|
8
|
|
|
* (at your option) any later version. |
|
9
|
|
|
* |
|
10
|
|
|
* This program is distributed in the hope that it will be useful, |
|
11
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13
|
|
|
* GNU General Public License for more details. |
|
14
|
|
|
* |
|
15
|
|
|
* You should have received a copy of the GNU General Public License along |
|
16
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc., |
|
17
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
18
|
|
|
* http://www.gnu.org/copyleft/gpl.html |
|
19
|
|
|
* |
|
20
|
|
|
* @file |
|
21
|
|
|
* @ingroup SpecialPage |
|
22
|
|
|
*/ |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* A special page that allows users to send e-mails to other users |
|
26
|
|
|
* |
|
27
|
|
|
* @ingroup SpecialPage |
|
28
|
|
|
*/ |
|
29
|
|
|
class SpecialEmailUser extends UnlistedSpecialPage { |
|
30
|
|
|
protected $mTarget; |
|
31
|
|
|
|
|
32
|
|
|
/** |
|
33
|
|
|
* @var User|string $mTargetObj |
|
34
|
|
|
*/ |
|
35
|
|
|
protected $mTargetObj; |
|
36
|
|
|
|
|
37
|
|
|
public function __construct() { |
|
38
|
|
|
parent::__construct( 'Emailuser' ); |
|
39
|
|
|
} |
|
40
|
|
|
|
|
41
|
|
|
public function doesWrites() { |
|
42
|
|
|
return true; |
|
43
|
|
|
} |
|
44
|
|
|
|
|
45
|
|
|
public function getDescription() { |
|
46
|
|
|
$target = self::getTarget( $this->mTarget ); |
|
47
|
|
|
if ( !$target instanceof User ) { |
|
48
|
|
|
return $this->msg( 'emailuser-title-notarget' )->text(); |
|
49
|
|
|
} |
|
50
|
|
|
|
|
51
|
|
|
return $this->msg( 'emailuser-title-target', $target->getName() )->text(); |
|
52
|
|
|
} |
|
53
|
|
|
|
|
54
|
|
|
protected function getFormFields() { |
|
55
|
|
|
return [ |
|
56
|
|
|
'From' => [ |
|
57
|
|
|
'type' => 'info', |
|
58
|
|
|
'raw' => 1, |
|
59
|
|
|
'default' => Linker::link( |
|
60
|
|
|
$this->getUser()->getUserPage(), |
|
61
|
|
|
htmlspecialchars( $this->getUser()->getName() ) |
|
62
|
|
|
), |
|
63
|
|
|
'label-message' => 'emailfrom', |
|
64
|
|
|
'id' => 'mw-emailuser-sender', |
|
65
|
|
|
], |
|
66
|
|
|
'To' => [ |
|
67
|
|
|
'type' => 'info', |
|
68
|
|
|
'raw' => 1, |
|
69
|
|
|
'default' => Linker::link( |
|
70
|
|
|
$this->mTargetObj->getUserPage(), |
|
71
|
|
|
htmlspecialchars( $this->mTargetObj->getName() ) |
|
72
|
|
|
), |
|
73
|
|
|
'label-message' => 'emailto', |
|
74
|
|
|
'id' => 'mw-emailuser-recipient', |
|
75
|
|
|
], |
|
76
|
|
|
'Target' => [ |
|
77
|
|
|
'type' => 'hidden', |
|
78
|
|
|
'default' => $this->mTargetObj->getName(), |
|
79
|
|
|
], |
|
80
|
|
|
'Subject' => [ |
|
81
|
|
|
'type' => 'text', |
|
82
|
|
|
'default' => $this->msg( 'defemailsubject', |
|
83
|
|
|
$this->getUser()->getName() )->inContentLanguage()->text(), |
|
84
|
|
|
'label-message' => 'emailsubject', |
|
85
|
|
|
'maxlength' => 200, |
|
86
|
|
|
'size' => 60, |
|
87
|
|
|
'required' => true, |
|
88
|
|
|
], |
|
89
|
|
|
'Text' => [ |
|
90
|
|
|
'type' => 'textarea', |
|
91
|
|
|
'rows' => 20, |
|
92
|
|
|
'cols' => 80, |
|
93
|
|
|
'label-message' => 'emailmessage', |
|
94
|
|
|
'required' => true, |
|
95
|
|
|
], |
|
96
|
|
|
'CCMe' => [ |
|
97
|
|
|
'type' => 'check', |
|
98
|
|
|
'label-message' => 'emailccme', |
|
99
|
|
|
'default' => $this->getUser()->getBoolOption( 'ccmeonemails' ), |
|
100
|
|
|
], |
|
101
|
|
|
]; |
|
102
|
|
|
} |
|
103
|
|
|
|
|
104
|
|
|
public function execute( $par ) { |
|
105
|
|
|
$out = $this->getOutput(); |
|
106
|
|
|
$out->addModuleStyles( 'mediawiki.special' ); |
|
107
|
|
|
|
|
108
|
|
|
$this->mTarget = is_null( $par ) |
|
109
|
|
|
? $this->getRequest()->getVal( 'wpTarget', $this->getRequest()->getVal( 'target', '' ) ) |
|
110
|
|
|
: $par; |
|
111
|
|
|
|
|
112
|
|
|
// This needs to be below assignment of $this->mTarget because |
|
113
|
|
|
// getDescription() needs it to determine the correct page title. |
|
114
|
|
|
$this->setHeaders(); |
|
115
|
|
|
$this->outputHeader(); |
|
116
|
|
|
|
|
117
|
|
|
// error out if sending user cannot do this |
|
118
|
|
|
$error = self::getPermissionsError( |
|
119
|
|
|
$this->getUser(), |
|
120
|
|
|
$this->getRequest()->getVal( 'wpEditToken' ), |
|
121
|
|
|
$this->getConfig() |
|
122
|
|
|
); |
|
123
|
|
|
|
|
124
|
|
|
switch ( $error ) { |
|
125
|
|
|
case null: |
|
|
|
|
|
|
126
|
|
|
# Wahey! |
|
127
|
|
|
break; |
|
128
|
|
|
case 'badaccess': |
|
129
|
|
|
throw new PermissionsError( 'sendemail' ); |
|
130
|
|
|
case 'blockedemailuser': |
|
131
|
|
|
throw new UserBlockedError( $this->getUser()->mBlock ); |
|
132
|
|
|
case 'actionthrottledtext': |
|
133
|
|
|
throw new ThrottledError; |
|
134
|
|
|
case 'mailnologin': |
|
135
|
|
|
case 'usermaildisabled': |
|
136
|
|
|
throw new ErrorPageError( $error, "{$error}text" ); |
|
137
|
|
|
default: |
|
138
|
|
|
# It's a hook error |
|
139
|
|
|
list( $title, $msg, $params ) = $error; |
|
140
|
|
|
throw new ErrorPageError( $title, $msg, $params ); |
|
141
|
|
|
} |
|
142
|
|
|
// Got a valid target user name? Else ask for one. |
|
143
|
|
|
$ret = self::getTarget( $this->mTarget ); |
|
144
|
|
|
if ( !$ret instanceof User ) { |
|
145
|
|
|
if ( $this->mTarget != '' ) { |
|
146
|
|
|
// Messages used here: notargettext, noemailtext, nowikiemailtext |
|
147
|
|
|
$ret = ( $ret == 'notarget' ) ? 'emailnotarget' : ( $ret . 'text' ); |
|
148
|
|
|
$out->wrapWikiMsg( "<p class='error'>$1</p>", $ret ); |
|
149
|
|
|
} |
|
150
|
|
|
$out->addHTML( $this->userForm( $this->mTarget ) ); |
|
151
|
|
|
|
|
152
|
|
|
return; |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
$this->mTargetObj = $ret; |
|
156
|
|
|
|
|
157
|
|
|
// Set the 'relevant user' in the skin, so it displays links like Contributions, |
|
158
|
|
|
// User logs, UserRights, etc. |
|
159
|
|
|
$this->getSkin()->setRelevantUser( $this->mTargetObj ); |
|
160
|
|
|
|
|
161
|
|
|
$context = new DerivativeContext( $this->getContext() ); |
|
162
|
|
|
$context->setTitle( $this->getPageTitle() ); // Remove subpage |
|
163
|
|
|
$form = new HTMLForm( $this->getFormFields(), $context ); |
|
164
|
|
|
// By now we are supposed to be sure that $this->mTarget is a user name |
|
165
|
|
|
$form->addPreText( $this->msg( 'emailpagetext', $this->mTarget )->parse() ); |
|
166
|
|
|
$form->setSubmitTextMsg( 'emailsend' ); |
|
167
|
|
|
$form->setSubmitCallback( [ __CLASS__, 'uiSubmit' ] ); |
|
168
|
|
|
$form->setWrapperLegendMsg( 'email-legend' ); |
|
169
|
|
|
$form->loadData(); |
|
170
|
|
|
|
|
171
|
|
|
if ( !Hooks::run( 'EmailUserForm', [ &$form ] ) ) { |
|
172
|
|
|
return; |
|
173
|
|
|
} |
|
174
|
|
|
|
|
175
|
|
|
$result = $form->show(); |
|
176
|
|
|
|
|
177
|
|
|
if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) { |
|
178
|
|
|
$out->setPageTitle( $this->msg( 'emailsent' ) ); |
|
179
|
|
|
$out->addWikiMsg( 'emailsenttext', $this->mTarget ); |
|
180
|
|
|
$out->returnToMain( false, $this->mTargetObj->getUserPage() ); |
|
181
|
|
|
} |
|
182
|
|
|
} |
|
183
|
|
|
|
|
184
|
|
|
/** |
|
185
|
|
|
* Validate target User |
|
186
|
|
|
* |
|
187
|
|
|
* @param string $target Target user name |
|
188
|
|
|
* @return User User object on success or a string on error |
|
189
|
|
|
*/ |
|
190
|
|
|
public static function getTarget( $target ) { |
|
191
|
|
|
if ( $target == '' ) { |
|
192
|
|
|
wfDebug( "Target is empty.\n" ); |
|
193
|
|
|
|
|
194
|
|
|
return 'notarget'; |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
$nu = User::newFromName( $target ); |
|
198
|
|
|
if ( !$nu instanceof User || !$nu->getId() ) { |
|
199
|
|
|
wfDebug( "Target is invalid user.\n" ); |
|
200
|
|
|
|
|
201
|
|
|
return 'notarget'; |
|
202
|
|
|
} elseif ( !$nu->isEmailConfirmed() ) { |
|
203
|
|
|
wfDebug( "User has no valid email.\n" ); |
|
204
|
|
|
|
|
205
|
|
|
return 'noemail'; |
|
206
|
|
|
} elseif ( !$nu->canReceiveEmail() ) { |
|
207
|
|
|
wfDebug( "User does not allow user emails.\n" ); |
|
208
|
|
|
|
|
209
|
|
|
return 'nowikiemail'; |
|
210
|
|
|
} |
|
211
|
|
|
|
|
212
|
|
|
return $nu; |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
/** |
|
216
|
|
|
* Check whether a user is allowed to send email |
|
217
|
|
|
* |
|
218
|
|
|
* @param User $user |
|
219
|
|
|
* @param string $editToken Edit token |
|
220
|
|
|
* @param Config $config optional for backwards compatibility |
|
221
|
|
|
* @return string|null Null on success or string on error |
|
222
|
|
|
*/ |
|
223
|
|
|
public static function getPermissionsError( $user, $editToken, Config $config = null ) { |
|
224
|
|
View Code Duplication |
if ( $config === null ) { |
|
225
|
|
|
wfDebug( __METHOD__ . ' called without a Config instance passed to it' ); |
|
226
|
|
|
$config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); |
|
227
|
|
|
} |
|
228
|
|
|
if ( !$config->get( 'EnableEmail' ) || !$config->get( 'EnableUserEmail' ) ) { |
|
229
|
|
|
return 'usermaildisabled'; |
|
230
|
|
|
} |
|
231
|
|
|
|
|
232
|
|
|
if ( !$user->isAllowed( 'sendemail' ) ) { |
|
233
|
|
|
return 'badaccess'; |
|
234
|
|
|
} |
|
235
|
|
|
|
|
236
|
|
|
if ( !$user->isEmailConfirmed() ) { |
|
237
|
|
|
return 'mailnologin'; |
|
238
|
|
|
} |
|
239
|
|
|
|
|
240
|
|
|
if ( $user->isBlockedFromEmailuser() ) { |
|
241
|
|
|
wfDebug( "User is blocked from sending e-mail.\n" ); |
|
242
|
|
|
|
|
243
|
|
|
return "blockedemailuser"; |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
if ( $user->pingLimiter( 'emailuser' ) ) { |
|
247
|
|
|
wfDebug( "Ping limiter triggered.\n" ); |
|
248
|
|
|
|
|
249
|
|
|
return 'actionthrottledtext'; |
|
250
|
|
|
} |
|
251
|
|
|
|
|
252
|
|
|
$hookErr = false; |
|
253
|
|
|
|
|
254
|
|
|
Hooks::run( 'UserCanSendEmail', [ &$user, &$hookErr ] ); |
|
255
|
|
|
Hooks::run( 'EmailUserPermissionsErrors', [ $user, $editToken, &$hookErr ] ); |
|
256
|
|
|
|
|
257
|
|
|
if ( $hookErr ) { |
|
258
|
|
|
return $hookErr; |
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
return null; |
|
262
|
|
|
} |
|
263
|
|
|
|
|
264
|
|
|
/** |
|
265
|
|
|
* Form to ask for target user name. |
|
266
|
|
|
* |
|
267
|
|
|
* @param string $name User name submitted. |
|
268
|
|
|
* @return string Form asking for user name. |
|
269
|
|
|
*/ |
|
270
|
|
|
protected function userForm( $name ) { |
|
271
|
|
|
$this->getOutput()->addModules( 'mediawiki.userSuggest' ); |
|
272
|
|
|
$string = Html::openElement( |
|
273
|
|
|
'form', |
|
274
|
|
|
[ 'method' => 'get', 'action' => wfScript(), 'id' => 'askusername' ] |
|
275
|
|
|
) . |
|
276
|
|
|
Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . |
|
277
|
|
|
Html::openElement( 'fieldset' ) . |
|
278
|
|
|
Html::rawElement( 'legend', null, $this->msg( 'emailtarget' )->parse() ) . |
|
279
|
|
|
Html::label( |
|
280
|
|
|
$this->msg( 'emailusername' )->text(), |
|
281
|
|
|
'emailusertarget' |
|
282
|
|
|
) . ' ' . |
|
283
|
|
|
Html::input( |
|
284
|
|
|
'target', |
|
285
|
|
|
$name, |
|
286
|
|
|
'text', |
|
287
|
|
|
[ |
|
288
|
|
|
'id' => 'emailusertarget', |
|
289
|
|
|
'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest |
|
290
|
|
|
'autofocus' => true, |
|
291
|
|
|
'size' => 30, |
|
292
|
|
|
] |
|
293
|
|
|
) . |
|
294
|
|
|
' ' . |
|
295
|
|
|
Html::submitButton( $this->msg( 'emailusernamesubmit' )->text(), [] ) . |
|
296
|
|
|
Html::closeElement( 'fieldset' ) . |
|
297
|
|
|
Html::closeElement( 'form' ) . "\n"; |
|
298
|
|
|
|
|
299
|
|
|
return $string; |
|
300
|
|
|
} |
|
301
|
|
|
|
|
302
|
|
|
/** |
|
303
|
|
|
* Submit callback for an HTMLForm object, will simply call submit(). |
|
304
|
|
|
* |
|
305
|
|
|
* @since 1.20 |
|
306
|
|
|
* @param array $data |
|
307
|
|
|
* @param HTMLForm $form |
|
308
|
|
|
* @return Status|string|bool |
|
309
|
|
|
*/ |
|
310
|
|
|
public static function uiSubmit( array $data, HTMLForm $form ) { |
|
311
|
|
|
return self::submit( $data, $form->getContext() ); |
|
312
|
|
|
} |
|
313
|
|
|
|
|
314
|
|
|
/** |
|
315
|
|
|
* Really send a mail. Permissions should have been checked using |
|
316
|
|
|
* getPermissionsError(). It is probably also a good |
|
317
|
|
|
* idea to check the edit token and ping limiter in advance. |
|
318
|
|
|
* |
|
319
|
|
|
* @param array $data |
|
320
|
|
|
* @param IContextSource $context |
|
321
|
|
|
* @return Status|string|bool Status object, or potentially a String on error |
|
322
|
|
|
* or maybe even true on success if anything uses the EmailUser hook. |
|
323
|
|
|
*/ |
|
324
|
|
|
public static function submit( array $data, IContextSource $context ) { |
|
325
|
|
|
$config = $context->getConfig(); |
|
326
|
|
|
|
|
327
|
|
|
$target = self::getTarget( $data['Target'] ); |
|
328
|
|
|
if ( !$target instanceof User ) { |
|
329
|
|
|
// Messages used here: notargettext, noemailtext, nowikiemailtext |
|
330
|
|
|
return $context->msg( $target . 'text' )->parseAsBlock(); |
|
331
|
|
|
} |
|
332
|
|
|
|
|
333
|
|
|
$to = MailAddress::newFromUser( $target ); |
|
334
|
|
|
$from = MailAddress::newFromUser( $context->getUser() ); |
|
335
|
|
|
$subject = $data['Subject']; |
|
336
|
|
|
$text = $data['Text']; |
|
337
|
|
|
|
|
338
|
|
|
// Add a standard footer and trim up trailing newlines |
|
339
|
|
|
$text = rtrim( $text ) . "\n\n-- \n"; |
|
340
|
|
|
$text .= $context->msg( 'emailuserfooter', |
|
341
|
|
|
$from->name, $to->name )->inContentLanguage()->text(); |
|
342
|
|
|
|
|
343
|
|
|
$error = ''; |
|
344
|
|
|
if ( !Hooks::run( 'EmailUser', [ &$to, &$from, &$subject, &$text, &$error ] ) ) { |
|
345
|
|
|
return $error; |
|
346
|
|
|
} |
|
347
|
|
|
|
|
348
|
|
|
if ( $config->get( 'UserEmailUseReplyTo' ) ) { |
|
349
|
|
|
/** |
|
350
|
|
|
* Put the generic wiki autogenerated address in the From: |
|
351
|
|
|
* header and reserve the user for Reply-To. |
|
352
|
|
|
* |
|
353
|
|
|
* This is a bit ugly, but will serve to differentiate |
|
354
|
|
|
* wiki-borne mails from direct mails and protects against |
|
355
|
|
|
* SPF and bounce problems with some mailers (see below). |
|
356
|
|
|
*/ |
|
357
|
|
|
$mailFrom = new MailAddress( $config->get( 'PasswordSender' ), |
|
358
|
|
|
wfMessage( 'emailsender' )->inContentLanguage()->text() ); |
|
359
|
|
|
$replyTo = $from; |
|
360
|
|
|
} else { |
|
361
|
|
|
/** |
|
362
|
|
|
* Put the sending user's e-mail address in the From: header. |
|
363
|
|
|
* |
|
364
|
|
|
* This is clean-looking and convenient, but has issues. |
|
365
|
|
|
* One is that it doesn't as clearly differentiate the wiki mail |
|
366
|
|
|
* from "directly" sent mails. |
|
367
|
|
|
* |
|
368
|
|
|
* Another is that some mailers (like sSMTP) will use the From |
|
369
|
|
|
* address as the envelope sender as well. For open sites this |
|
370
|
|
|
* can cause mails to be flunked for SPF violations (since the |
|
371
|
|
|
* wiki server isn't an authorized sender for various users' |
|
372
|
|
|
* domains) as well as creating a privacy issue as bounces |
|
373
|
|
|
* containing the recipient's e-mail address may get sent to |
|
374
|
|
|
* the sending user. |
|
375
|
|
|
*/ |
|
376
|
|
|
$mailFrom = $from; |
|
377
|
|
|
$replyTo = null; |
|
378
|
|
|
} |
|
379
|
|
|
|
|
380
|
|
|
$status = UserMailer::send( $to, $mailFrom, $subject, $text, [ |
|
381
|
|
|
'replyTo' => $replyTo, |
|
382
|
|
|
] ); |
|
383
|
|
|
|
|
384
|
|
|
if ( !$status->isGood() ) { |
|
385
|
|
|
return $status; |
|
386
|
|
|
} else { |
|
387
|
|
|
// if the user requested a copy of this mail, do this now, |
|
388
|
|
|
// unless they are emailing themselves, in which case one |
|
389
|
|
|
// copy of the message is sufficient. |
|
390
|
|
|
if ( $data['CCMe'] && $to != $from ) { |
|
391
|
|
|
$cc_subject = $context->msg( 'emailccsubject' )->rawParams( |
|
392
|
|
|
$target->getName(), $subject )->text(); |
|
393
|
|
|
|
|
394
|
|
|
// target and sender are equal, because this is the CC for the sender |
|
395
|
|
|
Hooks::run( 'EmailUserCC', [ &$from, &$from, &$cc_subject, &$text ] ); |
|
396
|
|
|
|
|
397
|
|
|
$ccStatus = UserMailer::send( $from, $from, $cc_subject, $text ); |
|
398
|
|
|
$status->merge( $ccStatus ); |
|
399
|
|
|
} |
|
400
|
|
|
|
|
401
|
|
|
Hooks::run( 'EmailUserComplete', [ $to, $from, $subject, $text ] ); |
|
402
|
|
|
|
|
403
|
|
|
return $status; |
|
404
|
|
|
} |
|
405
|
|
|
} |
|
406
|
|
|
|
|
407
|
|
|
/** |
|
408
|
|
|
* Return an array of subpages beginning with $search that this special page will accept. |
|
409
|
|
|
* |
|
410
|
|
|
* @param string $search Prefix to search for |
|
411
|
|
|
* @param int $limit Maximum number of results to return (usually 10) |
|
412
|
|
|
* @param int $offset Number of results to skip (usually 0) |
|
413
|
|
|
* @return string[] Matching subpages |
|
414
|
|
|
*/ |
|
415
|
|
View Code Duplication |
public function prefixSearchSubpages( $search, $limit, $offset ) { |
|
416
|
|
|
$user = User::newFromName( $search ); |
|
417
|
|
|
if ( !$user ) { |
|
418
|
|
|
// No prefix suggestion for invalid user |
|
419
|
|
|
return []; |
|
420
|
|
|
} |
|
421
|
|
|
// Autocomplete subpage as user list - public to allow caching |
|
422
|
|
|
return UserNamePrefixSearch::search( 'public', $search, $limit, $offset ); |
|
423
|
|
|
} |
|
424
|
|
|
|
|
425
|
|
|
protected function getGroupName() { |
|
426
|
|
|
return 'users'; |
|
427
|
|
|
} |
|
428
|
|
|
} |
|
429
|
|
|
|