1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace ST; |
4
|
|
|
|
5
|
|
|
use Content; |
6
|
|
|
use ContentHandler; |
7
|
|
|
use Exception; |
8
|
|
|
use IContextSource; |
9
|
|
|
use Language; |
10
|
|
|
use MediaWiki\Diff\ComplexityException; |
11
|
|
|
use MWException; |
12
|
|
|
use ParserOutput; |
13
|
|
|
use SMW\ApplicationFactory; |
14
|
|
|
use SMW\DIWikiPage; |
15
|
|
|
use SMWDataItem; |
16
|
|
|
use SMWPrintRequest; |
17
|
|
|
use Title; |
18
|
|
|
use User; |
19
|
|
|
use WikiPage; |
20
|
|
|
|
21
|
|
|
if ( !defined( 'MEDIAWIKI' ) ) { |
22
|
|
|
echo 'Not a valid entry point'; |
23
|
|
|
exit( 1 ); |
24
|
|
|
} |
25
|
|
|
|
26
|
|
|
if ( !defined( 'SMW_VERSION' ) ) { |
27
|
|
|
echo 'This extension requires Semantic MediaWiki to be installed.'; |
28
|
|
|
exit( 1 ); |
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
// constants for message type |
32
|
|
|
if ( !defined( 'ST_NEWTASK' ) ) { |
33
|
|
|
define( 'ST_NEWTASK', 0 ); |
34
|
|
|
define( 'ST_UPDATE', 1 ); |
35
|
|
|
define( 'ST_ASSIGNED', 2 ); |
36
|
|
|
define( 'ST_CLOSED', 3 ); |
37
|
|
|
define( 'ST_UNASSIGNED', 4 ); |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* This class handles the creation and sending of notification emails. |
42
|
|
|
*/ |
43
|
|
|
class SemanticTasksMailer { |
44
|
|
|
|
45
|
|
|
private static $user_mailer; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Mails the assignees when the task is modified |
49
|
|
|
* |
50
|
|
|
* @param Assignees $assignees |
51
|
|
|
* @param WikiPage $article |
52
|
|
|
* @param User $current_user |
53
|
|
|
* @param Content $text |
54
|
|
|
* @param string $summary Unused |
55
|
|
|
* @param bool $minoredit |
56
|
|
|
* @param null $watchthis Unused |
57
|
|
|
* @param null $sectionanchor Unused |
58
|
|
|
* @param $flags |
59
|
|
|
* @return boolean |
60
|
|
|
* @throws ComplexityException |
61
|
|
|
* @throws MWException |
62
|
|
|
*/ |
63
|
|
|
public static function mailAssigneesUpdatedTask( Assignees $assignees, WikiPage $article, User $current_user, $text, |
64
|
|
|
$summary, $minoredit, $watchthis, $sectionanchor, $flags, $revision ) { |
|
|
|
|
65
|
|
|
if ( $minoredit ) { |
66
|
|
|
return true; |
67
|
|
|
} |
68
|
|
|
$status = ST_UPDATE; |
69
|
|
|
if ( ( $flags & EDIT_NEW ) && !$article->getTitle()->isTalkPage() ) { |
70
|
|
|
$status = ST_NEWTASK; |
71
|
|
|
} |
72
|
|
|
$assignedToParserOutput = self::getAssignedUsersFromParserOutput($article, $revision, $current_user); |
73
|
|
|
|
74
|
|
|
return self::mailAssignees( $article, $text, $current_user, $status, $assignees, $assignedToParserOutput ); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
// todo: this could replace Assignees->getAssignees(...). |
78
|
|
|
public static function getAssignedUsersFromParserOutput(WikiPage $article, $revision, User $current_user) { |
79
|
|
|
if ( $revision === null) { |
80
|
|
|
return []; |
81
|
|
|
} |
82
|
|
|
$smwFactory = ApplicationFactory::getInstance(); |
83
|
|
|
$mwCollaboratorFactory = $smwFactory->newMwCollaboratorFactory(); |
84
|
|
|
if ( version_compare( SMW_VERSION, '3.1', '<' ) ) { |
85
|
|
|
$editInfo = $mwCollaboratorFactory->newEditInfoProvider( |
86
|
|
|
$article, |
87
|
|
|
$revision, |
88
|
|
|
$current_user |
89
|
|
|
); |
90
|
|
|
} else { |
91
|
|
|
$editInfo = $mwCollaboratorFactory->newEditInfo( |
92
|
|
|
$article, |
93
|
|
|
$revision, |
94
|
|
|
$current_user |
95
|
|
|
); |
96
|
|
|
} |
97
|
|
|
$editInfo->fetchEditInfo(); |
98
|
|
|
$parserOutput = $editInfo->getOutput(); |
99
|
|
|
|
100
|
|
|
if ( !$parserOutput instanceof ParserOutput ) { |
|
|
|
|
101
|
|
|
return []; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
global $stgPropertyAssignedTo; |
105
|
|
|
$stgPropertyAssignedToString = str_replace(' ', '_', $stgPropertyAssignedTo); |
106
|
|
|
$property = new \SMW\DIProperty($stgPropertyAssignedToString, false); |
107
|
|
|
|
108
|
|
|
/** @var $semanticData \SMW\SemanticData */ |
109
|
|
|
$semanticData = $parserOutput->getExtensionData('smwdata'); |
110
|
|
|
$assigneesPropValues = $semanticData->getPropertyValues($property); |
111
|
|
|
// todo: maybe there should be a check if these pages are userpages. |
112
|
|
|
$assigneeList = array_map(function(DIWikiPage $assignePropVal) { |
113
|
|
|
return $assignePropVal->getTitle()->getText(); |
114
|
|
|
}, $assigneesPropValues); |
115
|
|
|
|
116
|
|
|
|
117
|
|
|
return $assigneeList; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* |
122
|
|
|
* @param WikiPage $article |
123
|
|
|
* @param Content $content |
124
|
|
|
* @param User $user |
125
|
|
|
* @param integer $status |
126
|
|
|
* @param Assignees $assignees |
127
|
|
|
* @return boolean |
128
|
|
|
* @throws ComplexityException |
129
|
|
|
* @throws MWException |
130
|
|
|
* @global boolean $wgSemanticTasksNotifyIfUnassigned |
131
|
|
|
*/ |
132
|
|
|
static function mailAssignees( WikiPage $article, Content $content, User $user, $status, Assignees $assignees, |
|
|
|
|
133
|
|
|
$assignedToParserOutput ) { |
134
|
|
|
$text = ContentHandler::getContentText( $content ); |
135
|
|
|
$title = $article->getTitle(); |
136
|
|
|
|
137
|
|
|
// Notify those unassigned from this task |
138
|
|
|
global $wgSemanticTasksNotifyIfUnassigned; |
139
|
|
|
if ( $wgSemanticTasksNotifyIfUnassigned ) { |
140
|
|
|
$removedAssignees = $assignees->getRemovedAssignees( $article ); |
141
|
|
|
$removedAssignees = Assignees::getAssigneeAddresses( $removedAssignees ); |
142
|
|
|
self::mailNotification( $removedAssignees, $text, $title, $user, ST_UNASSIGNED ); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
// Send notification of an assigned task to assignees |
146
|
|
|
// Treat task as new |
147
|
|
|
$newAssignees = $assignees->getNewAssignees( $article ); |
148
|
|
|
$newAssignees = Assignees::getAssigneeAddresses( $newAssignees ); |
149
|
|
|
self::mailNotification( $newAssignees, $text, $title, $user, ST_ASSIGNED ); |
150
|
|
|
|
151
|
|
|
$copies = $assignees->getCurrentCarbonCopy( $article ); |
152
|
|
|
$currentStatus = $assignees->getCurrentStatus( $article ); |
153
|
|
|
$oldStatus = $assignees->getSavedStatus(); |
154
|
|
|
if ( $currentStatus === "Closed" && $oldStatus !== "Closed" ) { |
155
|
|
|
$close_mailto = Assignees::getAssigneeAddresses( $copies ); |
156
|
|
|
self::mailNotification( $close_mailto, $text, $title, $user, ST_CLOSED ); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$currentAssignees = $assignees->getCurrentAssignees( $article ); |
160
|
|
|
|
161
|
|
|
// Only send group notifications on new tasks |
162
|
|
|
$groups = array(); |
163
|
|
|
if ( $status === ST_NEWTASK ) { |
164
|
|
|
$groups = $assignees->getGroupAssignees( $article ); |
165
|
|
|
|
166
|
|
|
// if this is a new task and there are no $currentAssignees but there are $assignedToParserOutput |
167
|
|
|
// then this is probably a new page and sending ST_ASSIGNED notifications didn't work and needs to be retried. |
168
|
|
|
if ( empty( $currentAssignees ) ) { |
169
|
|
|
$mails = Assignees::getAssigneeAddresses( $assignedToParserOutput ); |
170
|
|
|
self::mailNotification( $mails, $text, $title, $user, ST_ASSIGNED ); |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
$mailto = array_merge( $currentAssignees, $copies, $groups ); |
175
|
|
|
$mailto = array_unique( $mailto ); |
176
|
|
|
$mailto = Assignees::getAssigneeAddresses( $mailto ); |
177
|
|
|
|
178
|
|
|
// Send notifications to assignees, ccs, and groups |
179
|
|
|
self::mailNotification( $mailto, $text, $title, $user, $status ); |
180
|
|
|
|
181
|
|
|
return true; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* Sends mail notifications |
186
|
|
|
* |
187
|
|
|
* @param array $assignees |
188
|
|
|
* @param string $text |
189
|
|
|
* @param Title $title |
190
|
|
|
* @param User $user |
191
|
|
|
* @param integer $status |
192
|
|
|
* @throws MWException |
193
|
|
|
* @throws ComplexityException |
194
|
|
|
* @global string $wgSitename |
195
|
|
|
*/ |
196
|
|
|
static function mailNotification( array $assignees, $text, Title $title, User $user, $status ) { |
|
|
|
|
197
|
|
|
global $wgSitename; |
198
|
|
|
|
199
|
|
|
if ( empty( $assignees ) ) { |
200
|
|
|
return; |
201
|
|
|
} |
202
|
|
|
$title_text = $title->getFullText(); |
203
|
|
|
$from = new \MailAddress( $user->getEmail(), $user->getName() ); |
204
|
|
|
$link = htmlspecialchars( $title->getFullURL() ); |
205
|
|
|
|
206
|
|
|
/** @todo This should probably be refactored */ |
207
|
|
|
if ( $status == ST_NEWTASK ) { |
208
|
|
|
$subject = '[' . $wgSitename . '] ' . wfMessage( 'semantictasks-newtask' )->text() . ' ' . |
209
|
|
|
$title_text; |
210
|
|
|
$message = 'semantictasks-newtask-msg'; |
211
|
|
|
$body = wfMessage( $message, $title_text )->text() . " " . $link; |
212
|
|
|
$body .= "\n \n" . wfMessage( 'semantictasks-text-message' )->text() . "\n" . $text; |
213
|
|
|
} elseif ( $status == ST_UPDATE ) { |
214
|
|
|
$subject = '[' . $wgSitename . '] ' . wfMessage( 'semantictasks-taskupdated' )->text() . ' ' . |
215
|
|
|
$title_text; |
216
|
|
|
$message = 'semantictasks-updatedtoyou-msg2'; |
217
|
|
|
$body = wfMessage( $message, $title_text )->text() . " " . $link; |
218
|
|
|
$body .= "\n \n" . wfMessage( 'semantictasks-diff-message' )->text() . "\n" . |
219
|
|
|
self::generateDiffBodyTxt( $title ); |
220
|
|
|
} elseif ( $status == ST_CLOSED ) { |
221
|
|
|
$subject = '[' . $wgSitename . '] ' . wfMessage( 'semantictasks-taskclosed' )->text() . ' ' . |
222
|
|
|
$title_text; |
223
|
|
|
$message = 'semantictasks-taskclosed-msg'; |
224
|
|
|
$body = wfMessage( $message, $title_text )->text() . " " . $link; |
225
|
|
|
$body .= "\n \n" . wfMessage( 'semantictasks-text-message' )->text() . "\n" . $text; |
226
|
|
|
} elseif ( $status == ST_UNASSIGNED ) { |
227
|
|
|
$subject = '[' . $wgSitename . '] ' . wfMessage( 'semantictasks-taskunassigned' )->text() . ' ' . |
228
|
|
|
$title_text; |
229
|
|
|
$message = 'semantictasks-unassignedtoyou-msg2'; |
230
|
|
|
$body = wfMessage( $message, $title_text )->text() . " " . $link; |
231
|
|
|
$body .= "\n \n" . wfMessage( 'semantictasks-text-message' )->text() . "\n" . $text; |
232
|
|
|
} else { |
233
|
|
|
// status == ASSIGNED |
234
|
|
|
$subject = '[' . $wgSitename . '] ' . wfMessage( 'semantictasks-taskassigned' )->text() . ' ' . |
235
|
|
|
$title_text; |
236
|
|
|
$message = 'semantictasks-assignedtoyou-msg2'; |
237
|
|
|
$body = wfMessage( $message, $title_text )->text() . " " . $link; |
238
|
|
|
$body .= "\n \n" . wfMessage( 'semantictasks-text-message' )->text() . "\n" . $text; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
if (!self::$user_mailer) { |
242
|
|
|
self::$user_mailer = new \ST\UserMailer(new \UserMailer()); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
self::$user_mailer->send( $assignees, $from, $subject, $body ); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
static function setUserMailer(\ST\UserMailer $user_mailer) { |
|
|
|
|
249
|
|
|
self::$user_mailer = $user_mailer; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Generates a diff txt |
254
|
|
|
* |
255
|
|
|
* Code is similar to DifferenceEngine::generateTextDiffBody |
256
|
|
|
* @param Title $title |
257
|
|
|
* @param IContextSource $context |
258
|
|
|
* @return string |
259
|
|
|
* @throws ComplexityException |
260
|
|
|
* @throws MWException |
261
|
|
|
*/ |
262
|
|
|
static function generateDiffBodyTxt( Title $title, IContextSource $context = null) { |
|
|
|
|
263
|
|
|
$revision = \Revision::newFromTitle( $title, 0 ); |
264
|
|
|
if ($revision === null) { |
265
|
|
|
return ''; |
266
|
|
|
} |
267
|
|
|
/** @todo The first parameter should be a Context. */ |
268
|
|
|
$diff = new \DifferenceEngine( $context, $revision->getId(), 'prev' ); |
269
|
|
|
// The DifferenceEngine::getDiffBody() method generates html, |
270
|
|
|
// so let's generate the txt diff manually: |
271
|
|
|
global $wgContLang; |
272
|
|
|
$diff->loadText(); |
273
|
|
|
$otext = ''; |
274
|
|
|
$ntext = ''; |
275
|
|
|
if ( version_compare( MW_VERSION, '1.32', '<' ) ) { |
276
|
|
|
$otext = str_replace( "\r\n", "\n", \ContentHandler::getContentText( $diff->mOldContent ) ); |
277
|
|
|
$ntext = str_replace( "\r\n", "\n", \ContentHandler::getContentText( $diff->mNewContent ) ); |
278
|
|
|
} else { |
279
|
|
|
if ($diff->getOldRevision()) { |
280
|
|
|
$otext = str_replace( "\r\n", "\n", ContentHandler::getContentText( $diff->getOldRevision()->getContent( 'main' ) ) ); |
281
|
|
|
} |
282
|
|
|
if ($diff->getNewRevision()) { |
283
|
|
|
$ntext = str_replace( "\r\n", "\n", ContentHandler::getContentText( $diff->getNewRevision()->getContent( 'main' ) ) ); |
284
|
|
|
} |
285
|
|
|
} |
286
|
|
|
$ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); |
287
|
|
|
$nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); |
288
|
|
|
// We use here the php diff engine included in MediaWiki |
289
|
|
|
$diffs = new \Diff( $ota, $nta ); |
290
|
|
|
// And we ask for a txt formatted diff |
291
|
|
|
$formatter = new \UnifiedDiffFormatter(); |
292
|
|
|
$diff_text = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ); |
293
|
|
|
return $diff_text; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Run by the maintenance script to remind the assignees |
298
|
|
|
* |
299
|
|
|
* @return boolean |
300
|
|
|
* @throws Exception |
301
|
|
|
* @global string $wgSitename |
302
|
|
|
* @global Language $wgLang |
303
|
|
|
*/ |
304
|
|
|
static function remindAssignees() { |
|
|
|
|
305
|
|
|
global $wgSitename; |
306
|
|
|
global $stgPropertyReminderAt; |
307
|
|
|
global $stgPropertyAssignedTo; |
308
|
|
|
global $stgPropertyTargetDate; |
309
|
|
|
global $stgPropertyStatus; |
310
|
|
|
|
311
|
|
|
# Make this equal to midnight. Rational is that if users set today as the Target date with |
312
|
|
|
# reminders set to "0" so that the reminder happens on the deadline, the reminders will go |
313
|
|
|
# out even though now it is after the beginning of today and technically past the |
314
|
|
|
# target date. |
315
|
|
|
$today = wfTimestamp( TS_ISO_8601, strtotime( 'today midnight' ) ); |
316
|
|
|
|
317
|
|
|
# Get tasks where a reminder is called for, whose status is either new or in progress, and |
318
|
|
|
# whose target date is in the future. |
319
|
|
|
$query_string = "[[$stgPropertyReminderAt::+]][[$stgPropertyStatus::New||In Progress]][[$stgPropertyTargetDate::≥ $today]]"; |
320
|
|
|
$properties_to_display = array( $stgPropertyReminderAt, $stgPropertyAssignedTo, $stgPropertyTargetDate ); |
321
|
|
|
|
322
|
|
|
$results = Query::getQueryResults( $query_string, $properties_to_display, true ); |
323
|
|
|
if ( empty( $results ) ) { |
324
|
|
|
return false; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
while ( $row = $results->getNext() ) { |
328
|
|
|
$task_name = $row[0]->getNextObject()->getTitle(); |
329
|
|
|
$subject = '[' . $wgSitename . '] ' . wfMessage( 'semantictasks-reminder' )->text() . $task_name; |
330
|
|
|
// The following doesn't work, maybe because we use a cron job. |
331
|
|
|
// $link = $task_name->getFullURL(); |
332
|
|
|
// So let's do it manually |
333
|
|
|
//$link = $wiki_url . $task_name->getPartialURL(); |
334
|
|
|
// You know what? Let's try it again. |
335
|
|
|
$link = $task_name->getFullURL(); |
336
|
|
|
|
337
|
|
|
$target_date = $row[3]->getNextObject(); |
338
|
|
|
$tg_date = new DateTime( $target_date->getShortHTMLText() ); |
339
|
|
|
|
340
|
|
|
while ( $reminder = $row[1]->getNextObject() ) { |
341
|
|
|
$remind_me_in = $reminder->getShortHTMLText(); |
342
|
|
|
$date = new DateTime( 'today midnight' ); |
343
|
|
|
$date->modify( "+$remind_me_in day" ); |
344
|
|
|
|
345
|
|
|
if ( $tg_date === $date ) { |
346
|
|
|
global $wgLang; |
347
|
|
|
while ( $task_assignee = $row[2]->getNextObject() ) { |
348
|
|
|
$assignee_username = $task_assignee->getTitle()->getText(); |
349
|
|
|
$assignee = User::newFromName( $assignee_username ); |
350
|
|
|
|
351
|
|
|
$body = wfMessage( 'semantictasks-reminder-message2', $task_name, |
352
|
|
|
$wgLang->formatNum( $remind_me_in ), $link )->text(); |
353
|
|
|
$assignee->sendMail( $subject, $body ); |
354
|
|
|
} |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
} |
358
|
|
|
return true; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* Prints debugging information. $debugText is what you want to print, $debugVal |
363
|
|
|
* is the level at which you want to print the information. |
364
|
|
|
* |
365
|
|
|
* @global boolean $wgSemanticTasksDebug |
366
|
|
|
* @param string $debugText |
367
|
|
|
* @param string $debugArr |
368
|
|
|
* @access private |
369
|
|
|
*/ |
370
|
|
|
static function printDebug( $debugText, $debugArr = null ) { |
|
|
|
|
371
|
|
|
global $wgSemanticTasksDebug; |
372
|
|
|
|
373
|
|
|
if ( $wgSemanticTasksDebug ) { |
374
|
|
|
if ( isset( $debugArr ) ) { |
375
|
|
|
$text = $debugText . ' ' . implode( '::', $debugArr ); |
376
|
|
|
wfDebugLog( 'semantic-tasks', $text, false ); |
377
|
|
|
} else { |
378
|
|
|
wfDebugLog( 'semantic-tasks', $debugText, false ); |
379
|
|
|
} |
380
|
|
|
} |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
} |
384
|
|
|
|
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.