Cleanup and proper embedded object handling
[GitHub/WoltLab/com.woltlab.wcf.conversation.git] / files / lib / data / conversation / message / ConversationMessageAction.class.php
1 <?php
2 namespace wcf\data\conversation\message;
3 use wcf\data\conversation\Conversation;
4 use wcf\data\conversation\ConversationAction;
5 use wcf\data\conversation\ConversationEditor;
6 use wcf\data\smiley\SmileyCache;
7 use wcf\data\AbstractDatabaseObjectAction;
8 use wcf\data\DatabaseObject;
9 use wcf\data\IAttachmentMessageQuickReplyAction;
10 use wcf\data\IMessageInlineEditorAction;
11 use wcf\data\IMessageQuoteAction;
12 use wcf\system\attachment\AttachmentHandler;
13 use wcf\system\bbcode\BBCodeHandler;
14 use wcf\system\bbcode\BBCodeParser;
15 use wcf\system\bbcode\PreParser;
16 use wcf\system\exception\PermissionDeniedException;
17 use wcf\system\exception\UserInputException;
18 use wcf\system\html\input\HtmlInputProcessor;
19 use wcf\system\message\censorship\Censorship;
20 use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
21 use wcf\system\message\quote\MessageQuoteManager;
22 use wcf\system\message\QuickReplyManager;
23 use wcf\system\moderation\queue\ModerationQueueManager;
24 use wcf\system\request\LinkHandler;
25 use wcf\system\search\SearchIndexManager;
26 use wcf\system\user\notification\object\ConversationMessageUserNotificationObject;
27 use wcf\system\user\notification\UserNotificationHandler;
28 use wcf\system\user\storage\UserStorageHandler;
29 use wcf\system\WCF;
30 use wcf\util\StringUtil;
31
32 /**
33 * Executes conversation message-related actions.
34 *
35 * @author Marcel Werk
36 * @copyright 2001-2016 WoltLab GmbH
37 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
38 * @package WoltLabSuite\Core\Data\Conversation\Message
39 *
40 * @method ConversationMessageEditor[] getObjects()
41 * @method ConversationMessageEditor getSingleObject()
42 */
43 class ConversationMessageAction extends AbstractDatabaseObjectAction implements IAttachmentMessageQuickReplyAction, IMessageInlineEditorAction, IMessageQuoteAction {
44 /**
45 * @inheritDoc
46 */
47 protected $className = ConversationMessageEditor::class;
48
49 /**
50 * conversation object
51 * @var Conversation
52 */
53 public $conversation;
54
55 /**
56 * @var HtmlInputProcessor
57 */
58 public $htmlInputProcessor;
59
60 /**
61 * conversation message object
62 * @var ConversationMessage
63 */
64 public $message;
65
66 /**
67 * @inheritDoc
68 * @return ConversationMessage
69 */
70 public function create() {
71 if (!isset($this->parameters['data']['enableHtml'])) $this->parameters['data']['enableHtml'] = 1;
72
73 // count attachments
74 if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
75 $this->parameters['data']['attachments'] = count($this->parameters['attachmentHandler']);
76 }
77
78 if (LOG_IP_ADDRESS) {
79 // add ip address
80 if (!isset($this->parameters['data']['ipAddress'])) {
81 $this->parameters['data']['ipAddress'] = WCF::getSession()->ipAddress;
82 }
83 }
84 else {
85 // do not track ip address
86 if (isset($this->parameters['data']['ipAddress'])) {
87 unset($this->parameters['data']['ipAddress']);
88 }
89 }
90
91 if (!empty($this->parameters['htmlInputProcessor'])) {
92 /** @noinspection PhpUndefinedMethodInspection */
93 $this->parameters['data']['message'] = $this->parameters['htmlInputProcessor']->getHtml();
94 }
95
96 // create message
97 /** @var ConversationMessage $message */
98 $message = parent::create();
99 $messageEditor = new ConversationMessageEditor($message);
100
101 // get conversation
102 $conversation = (isset($this->parameters['converation']) ? $this->parameters['converation'] : new Conversation($message->conversationID));
103 $conversationEditor = new ConversationEditor($conversation);
104
105 if (empty($this->parameters['isFirstPost'])) {
106 // update last message
107 $conversationEditor->addMessage($message);
108
109 // fire notification event
110 if (!$conversation->isDraft) {
111 $notificationRecipients = array_diff($conversation->getParticipantIDs(true), [$message->userID]); // don't notify message author
112 if (!empty($notificationRecipients)) {
113 UserNotificationHandler::getInstance()->fireEvent('conversationMessage', 'com.woltlab.wcf.conversation.message.notification', new ConversationMessageUserNotificationObject($message), $notificationRecipients);
114 }
115 }
116
117 $userConversation = Conversation::getUserConversation($conversation->conversationID, $message->userID);
118 if ($userConversation !== null && $userConversation->isInvisible) {
119 // make invisible participant visible
120 $sql = "UPDATE wcf".WCF_N."_conversation_to_user
121 SET isInvisible = 0
122 WHERE participantID = ?
123 AND conversationID = ?";
124 $statement = WCF::getDB()->prepareStatement($sql);
125 $statement->execute([$message->userID, $conversation->conversationID]);
126
127 $conversationEditor->updateParticipantSummary();
128 $conversationEditor->updateParticipantCount();
129 }
130
131 // reset visibility if it was hidden but not left
132 $sql = "UPDATE wcf".WCF_N."_conversation_to_user
133 SET hideConversation = ?
134 WHERE conversationID = ?
135 AND hideConversation = ?";
136 $statement = WCF::getDB()->prepareStatement($sql);
137 $statement->execute([
138 Conversation::STATE_DEFAULT,
139 $conversation->conversationID,
140 Conversation::STATE_HIDDEN
141 ]);
142 }
143
144 // reset storage
145 UserStorageHandler::getInstance()->reset($conversation->getParticipantIDs(), 'unreadConversationCount');
146
147 // update search index
148 SearchIndexManager::getInstance()->set('com.woltlab.wcf.conversation.message', $message->messageID, $message->message, (!empty($this->parameters['isFirstPost']) ? $conversation->subject : ''), $message->time, $message->userID, $message->username);
149
150 // update attachments
151 if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
152 /** @noinspection PhpUndefinedMethodInspection */
153 $this->parameters['attachmentHandler']->updateObjectID($message->messageID);
154 }
155
156 // save embedded objects
157 if (!empty($this->parameters['htmlInputProcessor'])) {
158 /** @noinspection PhpUndefinedMethodInspection */
159 $this->parameters['htmlInputProcessor']->setObjectID($message->messageID);
160
161 if (MessageEmbeddedObjectManager::getInstance()->registerObjects($this->parameters['htmlInputProcessor'])) {
162 $messageEditor->update(['hasEmbeddedObjects' => 1]);
163 }
164 }
165
166 // clear quotes
167 if (isset($this->parameters['removeQuoteIDs']) && !empty($this->parameters['removeQuoteIDs'])) {
168 MessageQuoteManager::getInstance()->markQuotesForRemoval($this->parameters['removeQuoteIDs']);
169 }
170 MessageQuoteManager::getInstance()->removeMarkedQuotes();
171
172 // return new message
173 return $message;
174 }
175
176 /**
177 * @inheritDoc
178 */
179 public function update() {
180 // count attachments
181 if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
182 $this->parameters['data']['attachments'] = count($this->parameters['attachmentHandler']);
183 }
184
185 if (!empty($this->parameters['htmlInputProcessor'])) {
186 /** @noinspection PhpUndefinedMethodInspection */
187 $this->parameters['data']['message'] = $this->parameters['htmlInputProcessor']->getHtml();
188 }
189
190 parent::update();
191
192 // update search index / embedded objects
193 if (isset($this->parameters['data']) && isset($this->parameters['data']['message'])) {
194 foreach ($this->getObjects() as $message) {
195 $conversation = $message->getConversation();
196 SearchIndexManager::getInstance()->set('com.woltlab.wcf.conversation.message', $message->messageID, $this->parameters['data']['message'], ($conversation->firstMessageID == $message->messageID ? $conversation->subject : ''), $message->time, $message->userID, $message->username);
197
198 if (!empty($this->parameters['htmlInputProcessor'])) {
199 /** @noinspection PhpUndefinedMethodInspection */
200 $this->parameters['htmlInputProcessor']->setObjectID($message->messageID);
201
202 if ($message->hasEmbeddedObjects != MessageEmbeddedObjectManager::getInstance()->registerObjects($this->parameters['htmlInputProcessor'])) {
203 $message->update(['hasEmbeddedObjects' => ($message->hasEmbeddedObjects ? 0 : 1)]);
204 }
205 }
206 }
207 }
208 }
209
210 /**
211 * @inheritDoc
212 */
213 public function delete() {
214 $count = parent::delete();
215
216 $attachmentMessageIDs = $conversationIDs = [];
217 foreach ($this->getObjects() as $message) {
218 if (!in_array($message->conversationID, $conversationIDs)) {
219 $conversationIDs[] = $message->conversationID;
220 }
221
222 if ($message->attachments) {
223 $attachmentMessageIDs[] = $message->messageID;
224 }
225 }
226
227 // rebuild conversations
228 if (!empty($conversationIDs)) {
229 $conversationAction = new ConversationAction($conversationIDs, 'rebuild');
230 $conversationAction->executeAction();
231 }
232
233 if (!empty($this->objectIDs)) {
234 // delete notifications
235 UserNotificationHandler::getInstance()->deleteNotifications('conversationMessage', 'com.woltlab.wcf.conversation.message.notification', [], $this->objectIDs);
236
237 // update search index
238 SearchIndexManager::getInstance()->delete('com.woltlab.wcf.conversation.message', $this->objectIDs);
239
240 // update embedded objects
241 MessageEmbeddedObjectManager::getInstance()->removeObjects('com.woltlab.wcf.conversation.message', $this->objectIDs);
242
243 // remove moderation queues
244 ModerationQueueManager::getInstance()->removeQueues('com.woltlab.wcf.conversation.message', $this->objectIDs);
245 }
246
247 // remove attachments
248 if (!empty($attachmentMessageIDs)) {
249 AttachmentHandler::removeAttachments('com.woltlab.wcf.conversation.message', $attachmentMessageIDs);
250 }
251
252 return $count;
253 }
254
255 /**
256 * @inheritDoc
257 */
258 public function validateQuickReply() {
259 QuickReplyManager::getInstance()->setAllowedBBCodes(explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
260 QuickReplyManager::getInstance()->validateParameters($this, $this->parameters, Conversation::class);
261 }
262
263 /**
264 * @inheritDoc
265 */
266 public function quickReply() {
267 return QuickReplyManager::getInstance()->createMessage(
268 $this,
269 $this->parameters,
270 ConversationAction::class,
271 CONVERSATION_LIST_DEFAULT_SORT_ORDER,
272 'conversationMessageList'
273 );
274 }
275
276 /**
277 * @inheritDoc
278 */
279 public function validateJumpToExtended() {
280 $this->readInteger('containerID');
281 $this->readString('message', true);
282 $this->readString('tmpHash', true);
283
284 $this->conversation = new Conversation($this->parameters['containerID']);
285 if (!$this->conversation->conversationID) {
286 throw new UserInputException('containerID');
287 }
288 else if ($this->conversation->isClosed || !Conversation::isParticipant([$this->conversation->conversationID])) {
289 throw new PermissionDeniedException();
290 }
291
292 // editing existing message
293 if (isset($this->parameters['messageID'])) {
294 $this->message = new ConversationMessage(intval($this->parameters['messageID']));
295 if (!$this->message->messageID || ($this->message->conversationID != $this->conversation->conversationID)) {
296 throw new UserInputException('messageID');
297 }
298
299 if (!$this->message->canEdit()) {
300 throw new PermissionDeniedException();
301 }
302 }
303 }
304
305 /**
306 * @inheritDoc
307 */
308 public function jumpToExtended() {
309 // quick reply
310 if ($this->message === null) {
311 QuickReplyManager::getInstance()->setMessage('conversation', $this->conversation->conversationID, $this->parameters['message']);
312 $url = LinkHandler::getInstance()->getLink('ConversationMessageAdd', ['id' => $this->conversation->conversationID]);
313 }
314 else {
315 // editing message
316 QuickReplyManager::getInstance()->setMessage('conversationMessage', $this->message->messageID, $this->parameters['message']);
317 $url = LinkHandler::getInstance()->getLink('ConversationMessageEdit', ['id' => $this->message->messageID]);
318 }
319
320 if (!empty($this->parameters['tmpHash'])) {
321 QuickReplyManager::getInstance()->setTmpHash($this->parameters['tmpHash']);
322 }
323
324 // redirect
325 return [
326 'url' => $url
327 ];
328 }
329
330 /**
331 * @inheritDoc
332 */
333 public function validateBeginEdit() {
334 $this->readInteger('containerID');
335 $this->readInteger('objectID');
336
337 $this->conversation = new Conversation($this->parameters['containerID']);
338 if (!$this->conversation->conversationID) {
339 throw new UserInputException('containerID');
340 }
341
342 if ($this->conversation->isClosed || !Conversation::isParticipant([$this->conversation->conversationID])) {
343 throw new PermissionDeniedException();
344 }
345
346 $this->message = new ConversationMessage($this->parameters['objectID']);
347 if (!$this->message->messageID) {
348 throw new UserInputException('objectID');
349 }
350
351 if (!$this->message->canEdit()) {
352 throw new PermissionDeniedException();
353 }
354 }
355
356 /**
357 * @inheritDoc
358 */
359 public function beginEdit() {
360 BBCodeHandler::getInstance()->setAllowedBBCodes(explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
361
362 WCF::getTPL()->assign([
363 'defaultSmilies' => SmileyCache::getInstance()->getCategorySmilies(),
364 'message' => $this->message,
365 'permissionCanUseSmilies' => 'user.message.canUseSmilies',
366 'wysiwygSelector' => 'messageEditor'.$this->message->messageID
367 ]);
368
369 if (MODULE_ATTACHMENT) {
370 $tmpHash = StringUtil::getRandomID();
371 $attachmentHandler = new AttachmentHandler('com.woltlab.wcf.conversation.message', $this->message->messageID, $tmpHash);
372 $attachmentList = $attachmentHandler->getAttachmentList();
373
374 WCF::getTPL()->assign([
375 'attachmentHandler' => $attachmentHandler,
376 'attachmentList' => $attachmentList->getObjects(),
377 'attachmentObjectID' => $this->message->messageID,
378 'attachmentObjectType' => 'com.woltlab.wcf.conversation.message',
379 'attachmentParentObjectID' => 0,
380 'tmpHash' => $tmpHash
381 ]);
382 }
383
384 return [
385 'actionName' => 'beginEdit',
386 'template' => WCF::getTPL()->fetch('conversationMessageInlineEditor')
387 ];
388 }
389
390 /**
391 * @inheritDoc
392 */
393 public function validateSave() {
394 $this->readString('message', true, 'data');
395
396 if (empty($this->parameters['data']['message'])) {
397 throw new UserInputException('message', WCF::getLanguage()->get('wcf.global.form.error.empty'));
398 }
399
400 $this->validateBeginEdit();
401
402 $this->validateMessage($this->conversation, $this->getHtmlInputProcessor($this->parameters['data']['message'], $this->message->messageID));
403 }
404
405 /**
406 * @inheritDoc
407 */
408 public function save() {
409 $data = [];
410
411 if (!$this->message->getConversation()->isDraft) {
412 $data['lastEditTime'] = TIME_NOW;
413 $data['editCount'] = $this->message->editCount + 1;
414 }
415 // execute update action
416 $action = new ConversationMessageAction([$this->message], 'update', [
417 'data' => $data,
418 'htmlInputProcessor' => $this->getHtmlInputProcessor()
419 ]);
420 $action->executeAction();
421
422 // load new message
423 $this->message = new ConversationMessage($this->message->messageID);
424 $this->message->getAttachments();
425
426 $attachmentList = null;
427 if (MODULE_ATTACHMENT) {
428 $attachmentList = $this->message->getAttachments(true);
429 $count = 0;
430 if ($attachmentList !== null) {
431 // set permissions
432 $attachmentList->setPermissions([
433 'canDownload' => true,
434 'canViewPreview' => true
435 ]);
436
437 $count = count($attachmentList);
438 }
439
440 // update count to reflect number of attachments after edit
441 if ($count != $this->message->attachments) {
442 $messageEditor = new ConversationMessageEditor($this->message);
443 $messageEditor->update(['attachments' => $count]);
444 }
445 }
446
447 // load embedded objects
448 MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.conversation.message', [$this->message->messageID]);
449
450 $data = [
451 'actionName' => 'save',
452 'message' => $this->message->getFormattedMessage()
453 ];
454
455 if (MODULE_ATTACHMENT) {
456 WCF::getTPL()->assign([
457 'attachmentList' => $attachmentList,
458 'objectID' => $this->message->messageID
459 ]);
460 $data['attachmentList'] = WCF::getTPL()->fetch('attachments');
461 }
462
463 return $data;
464 }
465
466 /**
467 * @inheritDoc
468 */
469 public function validateContainer(DatabaseObject $conversation) {
470 /** @var Conversation $conversation */
471
472 if (!$conversation->conversationID) {
473 throw new UserInputException('objectID');
474 }
475 if ($conversation->isClosed) {
476 throw new PermissionDeniedException();
477 }
478 $conversation->loadUserParticipation();
479 if (!$conversation->canRead()) {
480 throw new PermissionDeniedException();
481 }
482 }
483
484 /**
485 * @inheritDoc
486 */
487 public function validateMessage(DatabaseObject $container, HtmlInputProcessor $htmlInputProcessor) {
488 /*if (mb_strlen($message) > WCF::getSession()->getPermission('user.conversation.maxLength')) {
489 throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.tooLong', ['maxTextLength' => WCF::getSession()->getPermission('user.conversation.maxLength')]));
490 }
491
492 // search for disallowed bbcodes
493 $disallowedBBCodes = BBCodeParser::getInstance()->validateBBCodes($message, explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
494 if (!empty($disallowedBBCodes)) {
495 throw new UserInputException('text', WCF::getLanguage()->getDynamicVariable('wcf.message.error.disallowedBBCodes', ['disallowedBBCodes' => $disallowedBBCodes]));
496 }
497
498 // search for censored words
499 if (ENABLE_CENSORSHIP) {
500 $result = Censorship::getInstance()->test($message);
501 if ($result) {
502 throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.censoredWordsFound', ['censoredWords' => $result]));
503 }
504 }*/
505 }
506
507 /**
508 * @inheritDoc
509 */
510 public function getMessageList(DatabaseObject $conversation, $lastMessageTime) {
511 /** @var Conversation $conversation */
512
513 $messageList = new ViewableConversationMessageList();
514 $messageList->setConversation($conversation);
515 $messageList->getConditionBuilder()->add("conversation_message.conversationID = ?", [$conversation->conversationID]);
516 $messageList->getConditionBuilder()->add("conversation_message.time > ?", [$lastMessageTime]);
517 $messageList->sqlOrderBy = "conversation_message.time ".CONVERSATION_LIST_DEFAULT_SORT_ORDER;
518 $messageList->readObjects();
519
520 return $messageList;
521 }
522
523 /**
524 * @inheritDoc
525 */
526 public function getPageNo(DatabaseObject $conversation) {
527 /** @var Conversation $conversation */
528
529 $sql = "SELECT COUNT(*) AS count
530 FROM wcf".WCF_N."_conversation_message
531 WHERE conversationID = ?";
532 $statement = WCF::getDB()->prepareStatement($sql);
533 $statement->execute([$conversation->conversationID]);
534 $count = $statement->fetchArray();
535
536 return [intval(ceil($count['count'] / CONVERSATION_MESSAGES_PER_PAGE)), $count['count']];
537 }
538
539 /**
540 * @inheritDoc
541 */
542 public function getRedirectUrl(DatabaseObject $conversation, DatabaseObject $message) {
543 /** @var ConversationMessage $message */
544 return LinkHandler::getInstance()->getLink('Conversation', [
545 'object' => $conversation,
546 'messageID' => $message->messageID
547 ]).'#message'.$message->messageID;
548 }
549
550 /**
551 * @inheritDoc
552 */
553 public function validateSaveFullQuote() {
554 $this->message = $this->getSingleObject();
555
556 if (!Conversation::isParticipant([$this->message->conversationID])) {
557 throw new PermissionDeniedException();
558 }
559 }
560
561 /**
562 * @inheritDoc
563 */
564 public function saveFullQuote() {
565 $quoteID = MessageQuoteManager::getInstance()->addQuote(
566 'com.woltlab.wcf.conversation.message',
567 $this->message->conversationID,
568 $this->message->messageID,
569 $this->message->getExcerpt(),
570 $this->message->getMessage()
571 );
572
573 if ($quoteID === false) {
574 $removeQuoteID = MessageQuoteManager::getInstance()->getQuoteID('com.woltlab.wcf.conversation.message', $this->message->messageID, $this->message->getExcerpt(), $this->message->getMessage());
575 MessageQuoteManager::getInstance()->removeQuote($removeQuoteID);
576 }
577
578 $returnValues = [
579 'count' => MessageQuoteManager::getInstance()->countQuotes(),
580 'fullQuoteMessageIDs' => MessageQuoteManager::getInstance()->getFullQuoteObjectIDs(['com.woltlab.wcf.conversation.message'])
581 ];
582
583 if ($quoteID) {
584 $returnValues['renderedQuote'] = MessageQuoteManager::getInstance()->getQuoteComponents($quoteID);
585 }
586
587 return $returnValues;
588 }
589
590 /**
591 * @inheritDoc
592 */
593 public function validateSaveQuote() {
594 $this->readString('message');
595 $this->readBoolean('renderQuote', true);
596 $this->message = $this->getSingleObject();
597
598 if (!Conversation::isParticipant([$this->message->conversationID])) {
599 throw new PermissionDeniedException();
600 }
601 }
602
603 /**
604 * @inheritDoc
605 */
606 public function saveQuote() {
607 $quoteID = MessageQuoteManager::getInstance()->addQuote('com.woltlab.wcf.conversation.message', $this->message->conversationID, $this->message->messageID, $this->parameters['message'], false);
608
609 $returnValues = [
610 'count' => MessageQuoteManager::getInstance()->countQuotes(),
611 'fullQuoteMessageIDs' => MessageQuoteManager::getInstance()->getFullQuoteObjectIDs(['com.woltlab.wcf.conversation.message'])
612 ];
613
614 if ($this->parameters['renderQuote']) {
615 $returnValues['renderedQuote'] = MessageQuoteManager::getInstance()->getQuoteComponents($quoteID);
616 }
617
618 return $returnValues;
619 }
620
621 /**
622 * @inheritDoc
623 */
624 public function validateGetRenderedQuotes() {
625 $this->readInteger('parentObjectID');
626
627 $this->conversation = new Conversation($this->parameters['parentObjectID']);
628 if (!$this->conversation->conversationID) {
629 throw new UserInputException('parentObjectID');
630 }
631 }
632
633 /**
634 * @inheritDoc
635 */
636 public function getRenderedQuotes() {
637 $quotes = MessageQuoteManager::getInstance()->getQuotesByParentObjectID('com.woltlab.wcf.conversation.message', $this->conversation->conversationID);
638
639 return [
640 'template' => implode("\n\n", $quotes)
641 ];
642 }
643
644 /**
645 * @inheritDoc
646 */
647 public function getAttachmentHandler(DatabaseObject $conversation) {
648 return new AttachmentHandler('com.woltlab.wcf.conversation.message', 0, $this->parameters['tmpHash']);
649 }
650
651 /**
652 * @inheritDoc
653 */
654 public function getHtmlInputProcessor($message = null, $objectID = 0) {
655 if ($message === null) {
656 return $this->htmlInputProcessor;
657 }
658
659 $this->htmlInputProcessor = new HtmlInputProcessor();
660 $this->htmlInputProcessor->process($message, 'com.woltlab.wcf.conversation.message', $objectID);
661
662 return $this->htmlInputProcessor;
663 }
664 }