Incorrect handling of GIF cover photos when rebuilding users
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / worker / UserRebuildDataWorker.class.php
1 <?php
2
3 namespace wcf\system\worker;
4
5 use wcf\data\reaction\type\ReactionTypeCache;
6 use wcf\data\user\avatar\UserAvatar;
7 use wcf\data\user\avatar\UserAvatarEditor;
8 use wcf\data\user\avatar\UserAvatarList;
9 use wcf\data\user\cover\photo\DefaultUserCoverPhoto;
10 use wcf\data\user\cover\photo\IWebpUserCoverPhoto;
11 use wcf\data\user\User;
12 use wcf\data\user\UserEditor;
13 use wcf\data\user\UserList;
14 use wcf\data\user\UserProfileAction;
15 use wcf\data\user\UserProfileList;
16 use wcf\system\bbcode\BBCodeHandler;
17 use wcf\system\database\util\PreparedStatementConditionBuilder;
18 use wcf\system\exception\SystemException;
19 use wcf\system\html\input\HtmlInputProcessor;
20 use wcf\system\image\ImageHandler;
21 use wcf\system\user\storage\UserStorageHandler;
22 use wcf\system\WCF;
23
24 /**
25 * Worker implementation for updating users.
26 *
27 * @author Marcel Werk
28 * @copyright 2001-2019 WoltLab GmbH
29 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
30 * @package WoltLabSuite\Core\System\Worker
31 *
32 * @method UserList getObjectList()
33 */
34 class UserRebuildDataWorker extends AbstractRebuildDataWorker
35 {
36 /**
37 * @inheritDoc
38 */
39 protected $objectListClassName = UserList::class;
40
41 /**
42 * @inheritDoc
43 */
44 protected $limit = 50;
45
46 /**
47 * @inheritDoc
48 */
49 protected function initObjectList()
50 {
51 parent::initObjectList();
52
53 $this->objectList->sqlSelects = 'user_option_value.userOption' . User::getUserOptionID('aboutMe') . ' AS aboutMe';
54 $this->objectList->sqlJoins = "
55 LEFT JOIN wcf" . WCF_N . "_user_option_value user_option_value
56 ON user_option_value.userID = user_table.userID";
57 $this->objectList->sqlOrderBy = 'user_table.userID';
58 }
59
60 /**
61 * @inheritDoc
62 */
63 public function execute()
64 {
65 parent::execute();
66
67 $users = $userIDs = [];
68 foreach ($this->getObjectList() as $user) {
69 $users[] = new UserEditor($user);
70 $userIDs[] = $user->userID;
71 }
72
73 // update user ranks
74 if (!empty($users)) {
75 $action = new UserProfileAction($users, 'updateUserOnlineMarking');
76 $action->executeAction();
77 }
78
79 if (!empty($userIDs)) {
80 // update article counter
81 $conditionBuilder = new PreparedStatementConditionBuilder();
82 $conditionBuilder->add('user_table.userID IN (?)', [$userIDs]);
83 $sql = "UPDATE wcf" . WCF_N . "_user user_table
84 SET articles = (
85 SELECT COUNT(*)
86 FROM wcf" . WCF_N . "_article
87 WHERE userID = user_table.userID
88 )
89 " . $conditionBuilder;
90 $statement = WCF::getDB()->prepareStatement($sql);
91 $statement->execute($conditionBuilder->getParameters());
92
93 // update like counter
94 if (MODULE_LIKE) {
95 $sql = "UPDATE wcf" . WCF_N . "_user user_table
96 SET";
97
98 $reactionTypeIDs = \array_keys(ReactionTypeCache::getInstance()->getReactionTypes());
99 if (!empty($reactionTypeIDs)) {
100 $sql .= "
101 likesReceived = (
102 SELECT COUNT(*)
103 FROM wcf" . WCF_N . "_like
104 WHERE objectUserID = user_table.userID
105 AND reactionTypeID IN (" . \implode(',', $reactionTypeIDs) . ")
106 )";
107 } else {
108 $sql .= " likesReceived = 0";
109 }
110
111 $sql .= " " . $conditionBuilder;
112 $statement = WCF::getDB()->prepareStatement($sql);
113 $statement->execute($conditionBuilder->getParameters());
114 }
115
116 // update trophy points
117 if (MODULE_TROPHY) {
118 $sql = "UPDATE wcf" . WCF_N . "_user user_table
119 SET trophyPoints = (
120 SELECT COUNT(*)
121 FROM wcf" . WCF_N . "_user_trophy user_trophy
122 LEFT JOIN wcf" . WCF_N . "_trophy trophy
123 ON user_trophy.trophyID = trophy.trophyID
124 LEFT JOIN wcf" . WCF_N . "_category trophy_category
125 ON trophy.categoryID = trophy_category.categoryID
126 WHERE user_trophy.userID = user_table.userID
127 AND trophy.isDisabled = 0
128 AND trophy_category.isDisabled = 0
129 )
130 " . $conditionBuilder;
131 $statement = WCF::getDB()->prepareStatement($sql);
132 $statement->execute($conditionBuilder->getParameters());
133 }
134
135 // update signatures and about me
136 $sql = "UPDATE wcf" . WCF_N . "_user_option_value
137 SET userOption" . User::getUserOptionID('aboutMe') . " = ?
138 WHERE userID = ?";
139 $statement = WCF::getDB()->prepareStatement($sql);
140
141 // retrieve permissions
142 $userIDs = [];
143 foreach ($users as $user) {
144 $userIDs[] = $user->userID;
145 }
146 $userPermissions = $this->getBulkUserPermissions(
147 $userIDs,
148 ['user.message.disallowedBBCodes', 'user.signature.disallowedBBCodes']
149 );
150
151 $htmlInputProcessor = new HtmlInputProcessor();
152 WCF::getDB()->beginTransaction();
153 /** @var UserEditor $user */
154 foreach ($users as $user) {
155 BBCodeHandler::getInstance()->setDisallowedBBCodes(\explode(
156 ',',
157 $this->getBulkUserPermissionValue(
158 $userPermissions,
159 $user->userID,
160 'user.signature.disallowedBBCodes'
161 )
162 ));
163
164 if (!$user->signatureEnableHtml) {
165 $htmlInputProcessor->process(
166 $user->signature,
167 'com.woltlab.wcf.user.signature',
168 $user->userID,
169 true
170 );
171
172 $user->update([
173 'signature' => $htmlInputProcessor->getHtml(),
174 'signatureEnableHtml' => 1,
175 ]);
176 } else {
177 $htmlInputProcessor->reprocess($user->signature, 'com.woltlab.wcf.user.signature', $user->userID);
178 $user->update(['signature' => $htmlInputProcessor->getHtml()]);
179 }
180
181 if ($user->aboutMe) {
182 BBCodeHandler::getInstance()->setDisallowedBBCodes(\explode(
183 ',',
184 $this->getBulkUserPermissionValue(
185 $userPermissions,
186 $user->userID,
187 'user.message.disallowedBBCodes'
188 )
189 ));
190
191 if (!$user->signatureEnableHtml) {
192 $htmlInputProcessor->process(
193 $user->aboutMe,
194 'com.woltlab.wcf.user.aboutMe',
195 $user->userID,
196 true
197 );
198 } else {
199 $htmlInputProcessor->reprocess($user->aboutMe, 'com.woltlab.wcf.user.aboutMe', $user->userID);
200 }
201
202 $html = $htmlInputProcessor->getHtml();
203 // MySQL's TEXT type allows for 65,535 bytes, hence we need to count
204 // the bytes rather than the actual amount of characters
205 if (\strlen($html) > 65535) {
206 // content does not fit the available space, and any
207 // attempts to truncate it will yield awkward results
208 $html = '';
209 }
210
211 $statement->execute([$html, $user->userID]);
212 }
213 }
214 WCF::getDB()->commitTransaction();
215
216 // update old/imported avatars
217 $avatarList = new UserAvatarList();
218 $avatarList->getConditionBuilder()->add('user_avatar.userID IN (?)', [$userIDs]);
219 $avatarList->getConditionBuilder()->add(
220 '(
221 (user_avatar.width <> ? OR user_avatar.height <> ?)
222 OR (user_avatar.hasWebP = ? AND user_avatar.avatarExtension <> ?)
223 )',
224 [
225 UserAvatar::AVATAR_SIZE,
226 UserAvatar::AVATAR_SIZE,
227 0,
228 "gif",
229 ]
230 );
231 $avatarList->readObjects();
232 $resetAvatarCache = [];
233 foreach ($avatarList as $avatar) {
234 $resetAvatarCache[] = $avatar->userID;
235
236 $editor = new UserAvatarEditor($avatar);
237 if (!\file_exists($avatar->getLocation()) || @\getimagesize($avatar->getLocation()) === false) {
238 // delete avatars that are missing or broken
239 $editor->delete();
240 continue;
241 }
242
243 $width = $avatar->width;
244 $height = $avatar->height;
245 if ($width != $height) {
246 // make avatar quadratic
247 $width = $height = \min($width, $height, UserAvatar::AVATAR_SIZE);
248 $adapter = ImageHandler::getInstance()->getAdapter();
249
250 try {
251 $adapter->loadFile($avatar->getLocation());
252 } catch (SystemException $e) {
253 // broken image
254 $editor->delete();
255 continue;
256 }
257
258 $thumbnail = $adapter->createThumbnail($width, $height, false);
259 $adapter->writeImage($thumbnail, $avatar->getLocation());
260 // Clear thumbnail as soon as possible to free up the memory.
261 $thumbnail = null;
262 }
263
264 if ($width != UserAvatar::AVATAR_SIZE || $height != UserAvatar::AVATAR_SIZE) {
265 // resize avatar
266 $adapter = ImageHandler::getInstance()->getAdapter();
267
268 try {
269 $adapter->loadFile($avatar->getLocation());
270 } catch (SystemException $e) {
271 // broken image
272 $editor->delete();
273 continue;
274 }
275
276 $adapter->resize(0, 0, $width, $height, UserAvatar::AVATAR_SIZE, UserAvatar::AVATAR_SIZE);
277 $adapter->writeImage($adapter->getImage(), $avatar->getLocation());
278 $width = $height = UserAvatar::AVATAR_SIZE;
279 }
280
281 $editor->deleteLegacyThumbnails();
282 $editor->createAvatarVariant();
283
284 $editor->update([
285 'width' => $width,
286 'height' => $height,
287 ]);
288 }
289
290 // Reset the avatar cache for all avatars that had been processed.
291 if (!empty($resetAvatarCache)) {
292 UserStorageHandler::getInstance()->reset($resetAvatarCache, 'avatar');
293 }
294
295 // Create WebP variants of existing cover photos.
296 $userProfiles = new UserProfileList();
297 $userProfiles->getConditionBuilder()->add("user_table.userID IN (?)", [$userIDs]);
298 $userProfiles->getConditionBuilder()->add("user_table.coverPhotoHash IS NOT NULL");
299 $userProfiles->getConditionBuilder()->add("user_table.coverPhotoHasWebP = ?", [0]);
300 $userProfiles->readObjects();
301 foreach ($userProfiles as $userProfile) {
302 $editor = new UserEditor($userProfile->getDecoratedObject());
303 $coverPhoto = $userProfile->getCoverPhoto(true);
304 if ($coverPhoto instanceof DefaultUserCoverPhoto) {
305 // The default cover photo can be returned if the user has a
306 // cover photo, but it has been disabled by an administrator.
307 continue;
308 }
309
310 // If neither the regular, nor the WebP variant is readable then the
311 // cover photo is missing and we must clear the database information.
312 if (
313 !\is_readable($coverPhoto->getLocation(false))
314 && !\is_readable($coverPhoto->getLocation(true))
315 ) {
316 $editor->update([
317 'coverPhotoHash' => null,
318 'coverPhotoExtension' => '',
319 ]);
320
321 continue;
322 }
323
324 if ($coverPhoto instanceof IWebpUserCoverPhoto) {
325 $result = $coverPhoto->createWebpVariant();
326 if ($result !== null) {
327 $data['coverPhotoHasWebP'] = 1;
328
329 // A fallback jpeg image was just created.
330 if ($result === false) {
331 $data['coverPhotoExtension'] = 'jpg';
332 }
333
334 $editor->update($data);
335 }
336 }
337 }
338 }
339 }
340 }