You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

586 lines
21 KiB

7 years ago
7 years ago
7 years ago
  1. <?php
  2. namespace App;
  3. use Movim\Model;
  4. use Movim\Picture;
  5. use Movim\Route;
  6. use Illuminate\Database\QueryException;
  7. use Illuminate\Database\Capsule\Manager as DB;
  8. class Message extends Model
  9. {
  10. protected $primaryKey = ['user_id', 'jidfrom', 'id'];
  11. public $incrementing = false;
  12. public $mucpm; // Only used in Message Payloads to detect composer/paused PM messages
  13. protected $guarded = [];
  14. protected $with = ['reactions', 'parent.from', 'resolvedUrl'];
  15. protected $attributes = [
  16. 'type' => 'chat'
  17. ];
  18. protected $casts = [
  19. 'quoted' => 'boolean',
  20. 'markable' => 'boolean'
  21. ];
  22. public function save(array $options = [])
  23. {
  24. try {
  25. parent::save($options);
  26. } catch (\Exception $e) {
  27. \Utils::error($e->getMessage());
  28. }
  29. }
  30. public function parent()
  31. {
  32. return $this->belongsTo('App\Message', 'parentmid', 'mid');
  33. }
  34. public function resolvedUrl()
  35. {
  36. return $this->belongsTo('App\Url', 'urlid', 'id');
  37. }
  38. public function from()
  39. {
  40. return $this->belongsTo('App\Contact', 'jidfrom', 'id');
  41. }
  42. public function user()
  43. {
  44. return $this->belongsTo('App\User');
  45. }
  46. public function scopeJid($query, string $jid)
  47. {
  48. $jidFromToMessages = DB::table('messages')
  49. ->where('user_id', \App\User::me()->id)
  50. ->where('jidfrom', $jid)
  51. ->unionAll(DB::table('messages')
  52. ->where('user_id', \App\User::me()->id)
  53. ->where('jidto', $jid)
  54. );
  55. return $query->select('*')->from(
  56. $jidFromToMessages,
  57. 'messages'
  58. )->where('user_id', \App\User::me()->id);
  59. }
  60. public function reactions()
  61. {
  62. return $this->hasMany('App\Reaction', 'message_mid', 'mid');
  63. }
  64. public function setFileAttribute(array $file)
  65. {
  66. $this->resolved = true;
  67. $this->picture = typeIsPicture($file['type']);
  68. $this->attributes['file'] = serialize($file);
  69. }
  70. public function getFileAttribute()
  71. {
  72. if (isset($this->attributes['file'])) {
  73. $file = unserialize($this->attributes['file']);
  74. if (\array_key_exists('size', $file)) {
  75. $file['cleansize'] = sizeToCleanSize($file['size']);
  76. }
  77. return $file;
  78. }
  79. return null;
  80. }
  81. public function getJidfromAttribute()
  82. {
  83. return \unechap($this->attributes['jidfrom']);
  84. }
  85. public static function findByStanza($stanza)
  86. {
  87. $jidfrom = current(explode('/', (string)$stanza->attributes()->from));
  88. /**
  89. * If this stanza replaces another one, we load the original message
  90. */
  91. if ($stanza->replace) {
  92. return self::firstOrNew([
  93. 'user_id' => \App\User::me()->id,
  94. 'replaceid' => (string)$stanza->replace->attributes()->id,
  95. 'jidfrom' => $jidfrom
  96. ]);
  97. }
  98. /**
  99. * If not we just create or load a message
  100. */
  101. $id = ($stanza->{'stanza-id'} && $stanza->{'stanza-id'}->attributes()->id)
  102. ? (string)$stanza->{'stanza-id'}->attributes()->id
  103. : 'm_' . generateUUID();
  104. return self::firstOrNew([
  105. 'user_id' => \App\User::me()->id,
  106. 'id' => $id,
  107. 'jidfrom' => $jidfrom
  108. ]);
  109. }
  110. public function clearUnreads()
  111. {
  112. if ($this->jidfrom == $this->user_id) {
  113. $this->user->messages()
  114. ->where('jidfrom', $this->jidto)
  115. ->where('seen', false)
  116. ->update(['seen' => true]);
  117. }
  118. }
  119. public function set($stanza, $parent = false)
  120. {
  121. // We reset the URL resolution to refresh it once the message is displayed
  122. $this->resolved = false;
  123. $this->id = ($stanza->{'stanza-id'} && $stanza->{'stanza-id'}->attributes()->id)
  124. ? (string)$stanza->{'stanza-id'}->attributes()->id
  125. : 'm_' . generateUUID();
  126. if ($stanza->attributes()->id) {
  127. $this->replaceid = $stanza->attributes()->id;
  128. }
  129. $from = explode('/', (string)$stanza->attributes()->from);
  130. $to = current(explode('/', (string)$stanza->attributes()->to));
  131. $this->user_id = \App\User::me()->id;
  132. if (!$this->jidto) {
  133. $this->jidto = $to;
  134. }
  135. if (!$this->jidfrom) {
  136. $this->jidfrom = $from[0];
  137. }
  138. // If the message is from me
  139. if ($this->jidfrom == $this->user_id) {
  140. $this->seen = true;
  141. }
  142. if (isset($from[1])) {
  143. $this->resource = $from[1];
  144. }
  145. if ($stanza->delay) {
  146. $this->published = gmdate('Y-m-d H:i:s', strtotime($stanza->delay->attributes()->stamp));
  147. } elseif ($parent && $parent->delay) {
  148. $this->published = gmdate('Y-m-d H:i:s', strtotime($parent->delay->attributes()->stamp));
  149. } elseif (!isset($stanza->replace) || $this->published === null) {
  150. $this->published = gmdate('Y-m-d H:i:s');
  151. }
  152. $this->type = 'chat';
  153. if ($stanza->attributes()->type) {
  154. $this->type = (string)$stanza->attributes()->type;
  155. }
  156. // If it's a MUC message, we assume that the server already handled it
  157. if ($this->type == 'groupchat') {
  158. $this->delivered = gmdate('Y-m-d H:i:s');
  159. }
  160. if ($this->type !== 'groupchat'
  161. && $stanza->x
  162. && (string)$stanza->x->attributes()->xmlns == 'http://jabber.org/protocol/muc#user') {
  163. $this->mucpm = true;
  164. if ($parent && (string)$parent->attributes()->xmlns == 'urn:xmpp:forward:0') {
  165. $this->jidto = (string)$stanza->attributes()->to;
  166. } elseif (isset($from[1])) {
  167. $this->jidfrom = $from[0].'/'.$from[1];
  168. }
  169. }
  170. if ($stanza->body || $stanza->subject) {
  171. /*if (isset($stanza->attributes()->id)) {
  172. $this->id = (string)$stanza->attributes()->id;
  173. }*/
  174. if ($stanza->body) {
  175. $this->body = (string)$stanza->body;
  176. }
  177. # HipChat MUC specific cards
  178. if (in_array(
  179. explodeJid($this->jidfrom)['server'],
  180. ['conf.hipchat.com', 'conf.btf.hipchat.com']
  181. )
  182. && $this->type == 'groupchat'
  183. && $stanza->x
  184. && $stanza->x->attributes()->xmlns == 'http://hipchat.com/protocol/muc#room'
  185. && $stanza->x->card) {
  186. $this->body = trim(html_entity_decode($this->body));
  187. }
  188. $this->markable = (bool)($stanza->markable);
  189. if ($stanza->subject) {
  190. $this->subject = (string)$stanza->subject;
  191. }
  192. if ($stanza->thread) {
  193. $this->thread = (string)$stanza->thread;
  194. // Resolve the parent message if it exists
  195. $parent = $this->user->messages()
  196. ->jid($this->jidfrom)
  197. ->where('thread', $this->thread)
  198. ->orderBy('published', 'asc')
  199. ->first();
  200. if ($parent && $parent->mid != $this->mid
  201. && $parent->replaceid != $this->replaceid) {
  202. $this->parentmid = $parent->mid;
  203. }
  204. }
  205. if ($this->type == 'groupchat') {
  206. $presence = $this->user->session->presences()
  207. ->where('jid', $this->jidfrom)
  208. ->where('mucjid', $this->user->id)
  209. ->first();
  210. if ($presence
  211. && strpos($this->body, $presence->resource) !== false
  212. && $this->resource != $presence->resource) {
  213. $this->quoted = true;
  214. }
  215. }
  216. if ($stanza->html) {
  217. $results = [];
  218. $xml = \simplexml_load_string((string)$stanza->html);
  219. if (!$xml) {
  220. $xml = \simplexml_load_string((string)$stanza->html->body);
  221. if ($xml) {
  222. $results = $xml->xpath('//img/@src');
  223. }
  224. } else {
  225. $xml->registerXPathNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
  226. $results = $xml->xpath('//xhtml:img/@src');
  227. }
  228. if (!empty($results)) {
  229. if (substr((string)$results[0], 0, 10) == 'data:image') {
  230. $str = explode('base64,', $results[0]);
  231. if (isset($str[1])) {
  232. $p = new Picture;
  233. $p->fromBase(urldecode($str[1]));
  234. $key = sha1(urldecode($str[1]));
  235. $p->set($key, 'png');
  236. $this->sticker = $key;
  237. }
  238. } else {
  239. $this->sticker = getCid((string)$results[0]);
  240. }
  241. }
  242. }
  243. if ($stanza->reference
  244. && (string)$stanza->reference->attributes()->xmlns == 'urn:xmpp:reference:0') {
  245. $filetmp = [];
  246. if ($stanza->reference->{'media-sharing'}
  247. && (string)$stanza->reference->{'media-sharing'}->attributes()->xmlns == 'urn:xmpp:sims:1') {
  248. $file = $stanza->reference->{'media-sharing'}->file;
  249. if (isset($file)) {
  250. if (preg_match('/\w+\/[-+.\w]+/', $file->{'media-type'}) == 1) {
  251. $filetmp['type'] = (string)$file->{'media-type'};
  252. }
  253. $filetmp['size'] = (int)$file->size;
  254. $filetmp['name'] = (string)$file->name;
  255. }
  256. if ($stanza->reference->{'media-sharing'}->sources) {
  257. $source = $stanza->reference->{'media-sharing'}->sources->reference;
  258. if (!filter_var((string)$source->attributes()->uri, FILTER_VALIDATE_URL) === false) {
  259. $filetmp['uri'] = (string)$source->attributes()->uri;
  260. }
  261. }
  262. if ($stanza->reference->{'media-sharing'}->file->thumbnail
  263. && (string)$stanza->reference->{'media-sharing'}->file->thumbnail->attributes()->xmlns == 'urn:xmpp:thumbs:1') {
  264. $thumbnailAttributes = $stanza->reference->{'media-sharing'}->file->thumbnail->attributes();
  265. if (!filter_var((string)$thumbnailAttributes->uri, FILTER_VALIDATE_URL) === false) {
  266. $thumbnail = [
  267. 'width' => (int)$thumbnailAttributes->width,
  268. 'height' => (int)$thumbnailAttributes->height,
  269. 'type' => (string)$thumbnailAttributes->{'media-type'},
  270. 'uri' => (string)$thumbnailAttributes->uri
  271. ];
  272. $filetmp['thumbnail'] = $thumbnail;
  273. }
  274. }
  275. if (array_key_exists('uri', $filetmp)
  276. && array_key_exists('type', $filetmp)
  277. && array_key_exists('size', $filetmp)
  278. && array_key_exists('name', $filetmp)) {
  279. if (empty($filetmp['name'])) {
  280. $filetmp['name'] =
  281. pathinfo(parse_url($filetmp['uri'], PHP_URL_PATH), PATHINFO_BASENAME)
  282. . ' ('.parse_url($filetmp['uri'], PHP_URL_HOST).')';
  283. }
  284. $this->file = $filetmp;
  285. }
  286. } elseif (\in_array($stanza->reference->attributes()->type, ['mention', 'data'])
  287. && $stanza->reference->attributes()->uri) {
  288. $uri = parse_url($stanza->reference->attributes()->uri);
  289. if ($uri['scheme'] === 'xmpp') {
  290. $begin = '<a href="' . Route::urlize('share', $stanza->reference->attributes()->uri) . '">';
  291. if ($stanza->reference->attributes()->begin && $stanza->reference->attributes()->end) {
  292. $this->html = substr_replace(
  293. $this->body,
  294. $begin,
  295. (int)$stanza->reference->attributes()->begin,
  296. 0
  297. );
  298. $this->html = substr_replace(
  299. $this->html,
  300. '</a>',
  301. (int)$stanza->reference->attributes()->end + strlen($begin),
  302. 0
  303. );
  304. } else {
  305. $this->html = $begin . $this->body . '</a>';
  306. }
  307. $this->file = [
  308. 'type' => 'xmpp',
  309. 'uri' => (string)$stanza->reference->attributes()->uri,
  310. ];
  311. }
  312. }
  313. }
  314. if ($stanza->encryption
  315. && (string)$stanza->encryption->attributes()->xmlns == 'urn:xmpp:eme:0') {
  316. $this->encrypted = true;
  317. }
  318. if ($stanza->{'origin-id'}
  319. && (string)$stanza->{'origin-id'}->attributes()->xmlns == 'urn:xmpp:sid:0') {
  320. $this->originid = (string)$stanza->{'origin-id'}->attributes()->id;
  321. }
  322. if ($stanza->replace
  323. && $this->user->messages()
  324. ->where('jidfrom', $this->jidfrom)
  325. ->where('replaceid', $this->replaceid)
  326. ->count() == 0
  327. ) {
  328. $message = $this->user->messages()
  329. ->where('jidfrom', $this->jidfrom)
  330. ->where('replaceid', (string)$stanza->replace->attributes()->id)
  331. ->first();
  332. if ($message) {
  333. $this->oldid = $message->id;
  334. }
  335. /**
  336. * We prepare the existing message to be edited in the DB
  337. */
  338. try {
  339. Message::where('replaceid', (string)$stanza->replace->attributes()->id)
  340. ->where('user_id', $this->user_id)
  341. ->where('jidfrom', $this->jidfrom)
  342. ->update(['id' => $this->id]);
  343. } catch (\Exception $e) {
  344. \Utils::error($e->getMessage());
  345. }
  346. }
  347. if (isset($stanza->x->invite)) {
  348. $this->type = 'invitation';
  349. $this->subject = $this->jidfrom;
  350. $this->jidfrom = current(explode('/', (string)$stanza->x->invite->attributes()->from));
  351. }
  352. } elseif (isset($stanza->x)
  353. && $stanza->x->attributes()->xmlns == 'jabber:x:conference') {
  354. $this->type = 'invitation';
  355. $this->body = (string)$stanza->x->attributes()->reason;
  356. $this->subject = (string)$stanza->x->attributes()->jid;
  357. }
  358. # XEP-xxxx: Message Reactions
  359. elseif (isset($stanza->reactions)
  360. && $stanza->reactions->attributes()->xmlns == 'urn:xmpp:reactions:0') {
  361. $parentMessage = \App\Message::jid($this->jidfrom)
  362. ->where('replaceid', (string)$stanza->reactions->attributes()->to)
  363. ->first();
  364. if ($parentMessage) {
  365. $resource = ($this->type == 'groupchat')
  366. ? $this->resource
  367. : $this->jidfrom;
  368. $parentMessage
  369. ->reactions()
  370. ->where('jidfrom', $resource)
  371. ->delete();
  372. $emojis = [];
  373. $now = \Carbon\Carbon::now();
  374. $emoji = \Movim\Emoji::getInstance();
  375. foreach ($stanza->reactions->reaction as $children) {
  376. $emoji->replace((string)$children);
  377. if ($emoji->isSingleEmoji()) {
  378. $reaction = new Reaction;
  379. $reaction->message_mid = $parentMessage->mid;
  380. $reaction->emoji = (string)$children;
  381. $reaction->jidfrom = $resource;
  382. $reaction->created_at = $now;
  383. $reaction->updated_at = $now;
  384. \array_push($emojis, $reaction->toArray());
  385. }
  386. }
  387. try {
  388. Reaction::insert($emojis);
  389. } catch (QueryException $exception) {
  390. // Duplicate ?
  391. }
  392. return $parentMessage;
  393. }
  394. }
  395. return $this;
  396. }
  397. public function isEmpty()
  398. {
  399. return (empty($this->body)
  400. && empty($this->file)
  401. && empty($this->sticker)
  402. );
  403. }
  404. public function isSubject()
  405. {
  406. return !empty($this->subject);
  407. }
  408. public function retract()
  409. {
  410. $this->retracted = true;
  411. $this->oldid = null;
  412. $this->body = $this->html = 'retracted';
  413. }
  414. public function addUrls()
  415. {
  416. if (is_string($this->body)) {
  417. $old = $this->body;
  418. $this->body = addUrls($this->body);
  419. // TODO fix addUrls, see https://github.com/movim/movim/issues/877
  420. if (strlen($this->body) < strlen($old)) {
  421. $this->body = $old;
  422. }
  423. }
  424. }
  425. public function resolveColor()
  426. {
  427. $this->color = stringToColor(
  428. $this->session_id . $this->resource . $this->type
  429. );
  430. return $this->color;
  431. }
  432. public function valid()
  433. {
  434. return
  435. strlen($this->attributes['jidto']) < 256
  436. && strlen($this->attributes['jidfrom']) < 256
  437. && (!isset($this->attributes['resource']) || strlen($this->attributes['resource']) < 256)
  438. && (!isset($this->attributes['thread']) || strlen($this->attributes['thread']) < 128)
  439. && (!isset($this->attributes['replaceid']) || strlen($this->attributes['replaceid']) < 64)
  440. && (!isset($this->attributes['originid']) || strlen($this->attributes['originid']) < 255)
  441. && (!isset($this->attributes['id']) || strlen($this->attributes['id']) < 64)
  442. && (!isset($this->attributes['oldid']) || strlen($this->attributes['oldid']) < 64);
  443. }
  444. // toArray is already used
  445. public function toRawArray()
  446. {
  447. $array = [
  448. 'user_id' => $this->attributes['user_id'] ?? null,
  449. 'id' => $this->attributes['id'] ?? null,
  450. 'oldid' => $this->attributes['oldid'] ?? null,
  451. 'jidto' => $this->attributes['jidto'] ?? null,
  452. 'jidfrom' => $this->attributes['jidfrom'] ?? null,
  453. 'resource' => $this->attributes['resource'] ?? null,
  454. 'type' => $this->attributes['type'] ?? null,
  455. 'subject' => $this->attributes['subject'] ?? null,
  456. 'thread' => $this->attributes['thread'] ?? null,
  457. 'body' => $this->attributes['body'] ?? null,
  458. 'html' => $this->attributes['html'] ?? null,
  459. 'published' => $this->attributes['published'] ?? null,
  460. 'delivered' => $this->attributes['deliver'] ?? null,
  461. 'displayed' => $this->attributes['displayed'] ?? null,
  462. 'quoted' => $this->attributes['quoted'] ?? false,
  463. 'markable' => $this->attributes['markable'] ?? false,
  464. 'sticker' => $this->attributes['sticker'] ?? null,
  465. 'file' => isset($this->attributes['file']) ? $this->attributes['file'] : null,
  466. 'created_at' => $this->attributes['created_at'] ?? null,
  467. 'updated_at' => $this->attributes['updated_at'] ?? null,
  468. 'replaceid' => $this->attributes['replaceid'] ?? null,
  469. 'seen' => $this->attributes['seen'] ?? false,
  470. 'encrypted' => $this->attributes['encrypted'] ?? false,
  471. 'originid' => $this->attributes['originid'] ?? null,
  472. 'retracted' => $this->attributes['retracted'] ?? false,
  473. 'resolved' => $this->attributes['resolved'] ?? false,
  474. 'picture' => $this->attributes['picture'] ?? false,
  475. 'parentmid' => $this->attributes['parentmid'] ?? null,
  476. ];
  477. // Generate a proper mid
  478. if (empty($this->attributes['mid'])
  479. || ($this->attributes['mid'] && $this->attributes['mid'] == 1)) {
  480. $array['mid'] = $this->getNextStatementId();
  481. } else {
  482. $array['mid'] = $this->attributes['mid'];
  483. }
  484. return $array;
  485. }
  486. public function getNextStatementId()
  487. {
  488. $next_id = DB::select("select nextval('messages_mid_seq'::regclass)");
  489. return intval($next_id['0']->nextval);
  490. }
  491. }