Commit | Line | Data |
---|---|---|
320f4a6d | 1 | <?php |
a9229942 | 2 | |
320f4a6d | 3 | namespace wcf\system\cronjob; |
a9229942 | 4 | |
c5fc8e59 | 5 | use ParagonIE\ConstantTime\Hex; |
320f4a6d MW |
6 | use wcf\data\cronjob\Cronjob; |
7 | use wcf\data\user\notification\event\UserNotificationEventList; | |
707ac913 AE |
8 | use wcf\data\user\notification\UserNotification; |
9 | use wcf\data\user\User; | |
320f4a6d MW |
10 | use wcf\data\user\UserEditor; |
11 | use wcf\data\user\UserList; | |
12 | use wcf\data\user\UserProfile; | |
9d375181 | 13 | use wcf\form\NotificationUnsubscribeForm; |
a8c5936e | 14 | use wcf\system\cache\runtime\UserProfileRuntimeCache; |
320f4a6d | 15 | use wcf\system\database\util\PreparedStatementConditionBuilder; |
a9229942 | 16 | use wcf\system\email\Email; |
bc1a4c3a TD |
17 | use wcf\system\email\mime\MimePartFacade; |
18 | use wcf\system\email\mime\RecipientAwareTextMimePart; | |
bc1a4c3a | 19 | use wcf\system\email\UserMailbox; |
9d375181 | 20 | use wcf\system\request\LinkHandler; |
e4499881 | 21 | use wcf\system\user\notification\event\IUserNotificationEvent; |
320f4a6d MW |
22 | use wcf\system\user\notification\UserNotificationHandler; |
23 | use wcf\system\WCF; | |
320f4a6d MW |
24 | |
25 | /** | |
26 | * Sends daily mail notifications. | |
a9229942 TD |
27 | * |
28 | * @author Marcel Werk | |
29 | * @copyright 2001-2019 WoltLab GmbH | |
30 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
320f4a6d | 31 | */ |
a9229942 TD |
32 | class DailyMailNotificationCronjob extends AbstractCronjob |
33 | { | |
34 | /** | |
35 | * @inheritDoc | |
36 | */ | |
37 | public function execute(Cronjob $cronjob) | |
38 | { | |
39 | parent::execute($cronjob); | |
40 | ||
41 | // get user ids | |
42 | $sql = "SELECT DISTINCT userID | |
43 | FROM wcf" . WCF_N . "_user_notification | |
44 | WHERE mailNotified = ? | |
45 | AND time < ? | |
46 | AND confirmTime = ?"; | |
47 | $statement = WCF::getDB()->prepareStatement($sql); | |
48 | $statement->execute([ | |
49 | 0, | |
50 | TIME_NOW - 3600 * 23, | |
51 | 0, | |
52 | ]); | |
53 | $userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); | |
54 | if (empty($userIDs)) { | |
55 | return; | |
56 | } | |
57 | ||
58 | // get users | |
59 | $userList = new UserList(); | |
60 | $userList->setObjectIDs($userIDs); | |
61 | $userList->readObjects(); | |
62 | $users = $userList->getObjects(); | |
63 | ||
64 | // get notifications | |
65 | $conditions = new PreparedStatementConditionBuilder(); | |
66 | $conditions->add("notification.userID IN (?)", [$userIDs]); | |
67 | $conditions->add("notification.mailNotified = ?", [0]); | |
68 | $conditions->add("notification.confirmTime = ?", [0]); | |
69 | ||
70 | $sql = "SELECT notification.*, notification_event.eventID, object_type.objectType | |
71 | FROM wcf" . WCF_N . "_user_notification notification | |
72 | LEFT JOIN wcf" . WCF_N . "_user_notification_event notification_event | |
c240c98a | 73 | ON notification_event.eventID = notification.eventID |
a9229942 | 74 | LEFT JOIN wcf" . WCF_N . "_object_type object_type |
c240c98a | 75 | ON object_type.objectTypeID = notification_event.objectTypeID |
a9229942 TD |
76 | " . $conditions . " |
77 | ORDER BY notification.time"; | |
78 | $statement = WCF::getDB()->prepareStatement($sql); | |
79 | $statement->execute($conditions->getParameters()); | |
80 | ||
81 | // mark notifications as done | |
82 | $conditions = new PreparedStatementConditionBuilder(); | |
83 | $conditions->add("userID IN (?)", [$userIDs]); | |
84 | $conditions->add("mailNotified = ?", [0]); | |
85 | $sql = "UPDATE wcf" . WCF_N . "_user_notification | |
86 | SET mailNotified = 1 | |
87 | " . $conditions; | |
88 | $statement2 = WCF::getDB()->prepareStatement($sql); | |
89 | $statement2->execute($conditions->getParameters()); | |
90 | ||
91 | // collect data | |
92 | $eventsToUser = $objectTypes = $eventIDs = $notificationObjects = []; | |
93 | $availableObjectTypes = UserNotificationHandler::getInstance()->getAvailableObjectTypes(); | |
94 | while ($row = $statement->fetchArray()) { | |
95 | if (!isset($eventsToUser[$row['userID']])) { | |
96 | $eventsToUser[$row['userID']] = []; | |
97 | } | |
98 | $eventsToUser[$row['userID']][] = $row['notificationID']; | |
99 | ||
100 | // cache object types | |
101 | if (!isset($objectTypes[$row['objectType']])) { | |
102 | $objectTypes[$row['objectType']] = [ | |
103 | 'objectType' => $availableObjectTypes[$row['objectType']], | |
104 | 'objectIDs' => [], | |
105 | 'objects' => [], | |
106 | ]; | |
107 | } | |
108 | ||
109 | $objectTypes[$row['objectType']]['objectIDs'][] = $row['objectID']; | |
110 | $eventIDs[] = $row['eventID']; | |
111 | ||
112 | $notificationObjects[$row['notificationID']] = new UserNotification(null, $row); | |
113 | } | |
114 | ||
115 | // load authors | |
116 | $conditions = new PreparedStatementConditionBuilder(); | |
117 | $conditions->add("notificationID IN (?)", [\array_keys($notificationObjects)]); | |
118 | $sql = "SELECT notificationID, authorID | |
119 | FROM wcf" . WCF_N . "_user_notification_author | |
120 | " . $conditions . " | |
121 | ORDER BY time ASC"; | |
122 | $statement = WCF::getDB()->prepareStatement($sql); | |
123 | $statement->execute($conditions->getParameters()); | |
124 | $authorIDs = $authorToNotification = []; | |
125 | while ($row = $statement->fetchArray()) { | |
126 | if ($row['authorID']) { | |
127 | $authorIDs[] = $row['authorID']; | |
128 | } | |
129 | ||
130 | if (!isset($authorToNotification[$row['notificationID']])) { | |
131 | $authorToNotification[$row['notificationID']] = []; | |
132 | } | |
133 | ||
134 | $authorToNotification[$row['notificationID']][] = $row['authorID']; | |
135 | } | |
136 | ||
137 | // load authors | |
138 | $authors = UserProfileRuntimeCache::getInstance()->getObjects($authorIDs); | |
139 | $unknownAuthor = new UserProfile(new User( | |
140 | null, | |
141 | ['userID' => null, 'username' => WCF::getLanguage()->get('wcf.user.guest')] | |
142 | )); | |
143 | ||
144 | // load objects associated with each object type | |
145 | foreach ($objectTypes as $objectType => $objectData) { | |
146 | /** @noinspection PhpUndefinedMethodInspection */ | |
147 | $objectTypes[$objectType]['objects'] = $objectData['objectType']->getObjectsByIDs($objectData['objectIDs']); | |
148 | } | |
149 | ||
150 | // load required events | |
151 | $eventList = new UserNotificationEventList(); | |
152 | $eventList->getConditionBuilder()->add("user_notification_event.eventID IN (?)", [$eventIDs]); | |
153 | $eventList->readObjects(); | |
154 | $eventObjects = $eventList->getObjects(); | |
155 | ||
156 | foreach ($eventsToUser as $userID => $notificationIDs) { | |
157 | if (!isset($users[$userID])) { | |
158 | continue; | |
159 | } | |
160 | $user = $users[$userID]; | |
161 | ||
162 | // no notifications for disabled or banned users | |
163 | if (!$user->isEmailConfirmed()) { | |
164 | continue; | |
165 | } | |
166 | if ($user->banned) { | |
167 | continue; | |
168 | } | |
169 | ||
170 | $notifications = \array_map(static function ($notificationID) use ( | |
171 | $notificationObjects, | |
172 | $eventObjects, | |
173 | $user, | |
174 | $objectTypes, | |
175 | $authors, | |
176 | $authorToNotification, | |
177 | $unknownAuthor | |
178 | ) { | |
179 | $notification = $notificationObjects[$notificationID]; | |
180 | ||
181 | $className = $eventObjects[$notification->eventID]->className; | |
182 | ||
183 | /** @var IUserNotificationEvent $class */ | |
184 | $class = new $className($eventObjects[$notification->eventID]); | |
185 | $class->setObject( | |
186 | $notification, | |
187 | $objectTypes[$notification->objectType]['objects'][$notification->objectID], | |
188 | ($authors[$notification->authorID] ?? $unknownAuthor), | |
189 | $notification->additionalData | |
190 | ); | |
191 | $class->setLanguage($user->getLanguage()); | |
192 | ||
193 | if (isset($authorToNotification[$notification->notificationID])) { | |
194 | $eventAuthors = []; | |
195 | foreach ($authorToNotification[$notification->notificationID] as $userID) { | |
196 | if (!$userID) { | |
197 | $eventAuthors[0] = $unknownAuthor; | |
198 | } elseif (isset($authors[$userID])) { | |
199 | $eventAuthors[$userID] = $authors[$userID]; | |
200 | } | |
201 | } | |
202 | if (!empty($eventAuthors)) { | |
203 | $class->setAuthors($eventAuthors); | |
204 | } | |
205 | } | |
206 | ||
207 | $message = $class->getEmailMessage('daily'); | |
208 | if (\is_array($message)) { | |
209 | if (!isset($message['variables'])) { | |
210 | $message['variables'] = []; | |
211 | } | |
212 | ||
213 | return \array_merge($message['variables'], [ | |
214 | 'notificationContent' => $message, | |
215 | 'event' => $class, | |
216 | 'notificationType' => 'daily', | |
217 | 'variables' => $message['variables'], // deprecated, but is kept for backwards compatibility | |
218 | ]); | |
219 | } else { | |
220 | return [ | |
221 | 'notificationContent' => $message, | |
222 | 'event' => $class, | |
223 | 'notificationType' => 'daily', | |
224 | ]; | |
225 | } | |
226 | }, $notificationIDs); | |
227 | ||
228 | // generate token if not present | |
229 | if (!$user->notificationMailToken) { | |
230 | $token = Hex::encode(\random_bytes(10)); | |
231 | $editor = new UserEditor($user); | |
232 | $editor->update(['notificationMailToken' => $token]); | |
233 | ||
234 | // reload user | |
235 | $user = new User($user->userID); | |
236 | } | |
237 | ||
238 | $email = new Email(); | |
239 | $email->setSubject($user->getLanguage()->getDynamicVariable( | |
240 | 'wcf.user.notification.mail.daily.subject', | |
241 | ['count' => \count($notifications)] | |
242 | )); | |
243 | $email->addRecipient(new UserMailbox($user)); | |
244 | $email->setListID('daily.notification'); | |
245 | $email->setListUnsubscribe( | |
246 | LinkHandler::getInstance()->getControllerLink( | |
247 | NotificationUnsubscribeForm::class, | |
248 | [ | |
249 | 'userID' => $user->userID, | |
250 | 'token' => $user->notificationMailToken, | |
251 | ] | |
252 | ), | |
253 | true | |
254 | ); | |
255 | ||
bc0e1f3f | 256 | $maximumNotificationCount = 7; |
257 | $notificationCount = \count($notifications); | |
258 | if ($notificationCount === $maximumNotificationCount + 1) { | |
259 | $maximumNotificationCount++; | |
260 | } | |
261 | $remainingNotificationCount = $notificationCount - $maximumNotificationCount; | |
262 | $notifications = \array_slice($notifications, 0, $maximumNotificationCount); | |
263 | ||
a9229942 TD |
264 | $html = new RecipientAwareTextMimePart( |
265 | 'text/html', | |
266 | 'email_dailyNotification', | |
267 | 'wcf', | |
bc0e1f3f | 268 | [ |
269 | 'notifications' => $notifications, | |
270 | 'remaining' => $remainingNotificationCount, | |
271 | 'maximum' => $maximumNotificationCount, | |
272 | 'notificationCount' => $notificationCount, | |
273 | ] | |
a9229942 TD |
274 | ); |
275 | $plainText = new RecipientAwareTextMimePart( | |
276 | 'text/plain', | |
277 | 'email_dailyNotification', | |
278 | 'wcf', | |
bc0e1f3f | 279 | [ |
280 | 'notifications' => $notifications, | |
281 | 'remaining' => $remainingNotificationCount, | |
282 | 'maximum' => $maximumNotificationCount, | |
283 | 'notificationCount' => $notificationCount, | |
284 | ] | |
a9229942 TD |
285 | ); |
286 | $email->setBody(new MimePartFacade([$html, $plainText])); | |
287 | ||
288 | $email->send(); | |
289 | } | |
290 | } | |
320f4a6d | 291 | } |