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.

424 lines
14 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. class Message extends Model
  8. {
  9. protected $primaryKey = ['user_id', 'jidfrom', 'id'];
  10. public $incrementing = false;
  11. public $mucpm; // Only used in Message Payloads to detect composer/paused PM messages
  12. protected $guarded = [];
  13. protected $with = ['reactions'];
  14. protected $attributes = [
  15. 'type' => 'chat'
  16. ];
  17. protected $casts = [
  18. 'quoted' => 'boolean',
  19. 'markable' => 'boolean'
  20. ];
  21. public function save(array $options = [])
  22. {
  23. try {
  24. parent::save($options);
  25. } catch (\Exception $e) {
  26. \Utils::error($e->getMessage());
  27. }
  28. }
  29. public function user()
  30. {
  31. return $this->belongsTo('App\User');
  32. }
  33. public function reactions()
  34. {
  35. return $this->hasMany('App\Reaction', 'message_mid', 'mid');
  36. }
  37. public function setFileAttribute(array $file)
  38. {
  39. $this->attributes['file'] = serialize($file);
  40. }
  41. public function getFileAttribute()
  42. {
  43. if (isset($this->attributes['file'])) {
  44. $file = unserialize($this->attributes['file']);
  45. if (\array_key_exists('size', $file)) {
  46. $file['cleansize'] = sizeToCleanSize($file['size']);
  47. }
  48. return $file;
  49. }
  50. return null;
  51. }
  52. public static function findByStanza($stanza)
  53. {
  54. /**
  55. * If this stanza replaces another one, we load the original message
  56. */
  57. if ($stanza->replace) {
  58. return self::firstOrNew([
  59. 'user_id' => \App\User::me()->id,
  60. 'replaceid' => (string)$stanza->replace->attributes()->id,
  61. 'jidfrom' => current(explode('/', (string)$stanza->attributes()->from))
  62. ]);
  63. }
  64. /**
  65. * If not we just create or load a message
  66. */
  67. $id = ($stanza->{'stanza-id'} && $stanza->{'stanza-id'}->attributes()->id)
  68. ? (string)$stanza->{'stanza-id'}->attributes()->id
  69. : 'm_' . generateUUID();
  70. return self::firstOrNew([
  71. 'user_id' => \App\User::me()->id,
  72. 'id' => $id,
  73. 'jidfrom' => current(explode('/', (string)$stanza->attributes()->from))
  74. ]);
  75. }
  76. public function clearUnreads()
  77. {
  78. if ($this->jidfrom == $this->user_id) {
  79. $this->user->messages()
  80. ->where('jidfrom', $this->jidto)
  81. ->where('seen', false)
  82. ->update(['seen' => true]);
  83. }
  84. }
  85. public function set($stanza, $parent = false)
  86. {
  87. $this->id = ($stanza->{'stanza-id'} && $stanza->{'stanza-id'}->attributes()->id)
  88. ? (string)$stanza->{'stanza-id'}->attributes()->id
  89. : 'm_' . generateUUID();
  90. if ($stanza->attributes()->id) {
  91. $this->replaceid = $stanza->attributes()->id;
  92. }
  93. $jid = explode('/', (string)$stanza->attributes()->from);
  94. $to = current(explode('/', (string)$stanza->attributes()->to));
  95. $this->user_id = \App\User::me()->id;
  96. if (!$this->jidto) {
  97. $this->jidto = $to;
  98. }
  99. if (!$this->jidfrom) {
  100. $this->jidfrom = $jid[0];
  101. }
  102. // If the message is from me
  103. if ($this->jidfrom == $this->user_id) {
  104. $this->seen = true;
  105. }
  106. if (isset($jid[1])) {
  107. $this->resource = $jid[1];
  108. }
  109. if ($stanza->delay) {
  110. $this->published = gmdate('Y-m-d H:i:s', strtotime($stanza->delay->attributes()->stamp));
  111. } elseif ($parent && $parent->delay) {
  112. $this->published = gmdate('Y-m-d H:i:s', strtotime($parent->delay->attributes()->stamp));
  113. } elseif (!isset($stanza->replace) || $this->published === null) {
  114. $this->published = gmdate('Y-m-d H:i:s');
  115. }
  116. $this->type = 'chat';
  117. if ($stanza->attributes()->type) {
  118. $this->type = (string)$stanza->attributes()->type;
  119. }
  120. if (isset($jid[1])
  121. && $this->type !== 'groupchat'
  122. && $stanza->x
  123. && (string)$stanza->x->attributes()->xmlns == 'http://jabber.org/protocol/muc#user') {
  124. $this->mucpm = true;
  125. $this->jidfrom = $jid[0].'/'.$jid[1];
  126. }
  127. if ($stanza->body || $stanza->subject) {
  128. /*if (isset($stanza->attributes()->id)) {
  129. $this->id = (string)$stanza->attributes()->id;
  130. }*/
  131. if ($stanza->body) {
  132. $this->body = (string)$stanza->body;
  133. }
  134. # HipChat MUC specific cards
  135. if (in_array(
  136. explodeJid($this->jidfrom)['server'],
  137. ['conf.hipchat.com', 'conf.btf.hipchat.com']
  138. )
  139. && $this->type == 'groupchat'
  140. && $stanza->x
  141. && $stanza->x->attributes()->xmlns == 'http://hipchat.com/protocol/muc#room'
  142. && $stanza->x->card) {
  143. $this->body = trim(html_entity_decode($this->body));
  144. }
  145. $this->markable = (bool)($stanza->markable);
  146. if ($stanza->subject) {
  147. $this->subject = (string)$stanza->subject;
  148. }
  149. if ($stanza->thread) {
  150. $this->thread = (string)$stanza->thread;
  151. }
  152. if ($this->type == 'groupchat') {
  153. $presence = $this->user->session->presences()
  154. ->where('jid', $this->jidfrom)
  155. ->where('mucjid', $this->user->id)
  156. ->first();
  157. if ($presence
  158. && strpos($this->body, $presence->resource) !== false
  159. && $this->resource != $presence->resource) {
  160. $this->quoted = true;
  161. }
  162. }
  163. if ($stanza->html) {
  164. $results = [];
  165. $xml = \simplexml_load_string((string)$stanza->html);
  166. if (!$xml) {
  167. $xml = \simplexml_load_string((string)$stanza->html->body);
  168. if ($xml) {
  169. $results = $xml->xpath('//img/@src');
  170. }
  171. } else {
  172. $xml->registerXPathNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
  173. $results = $xml->xpath('//xhtml:img/@src');
  174. }
  175. if (!empty($results)) {
  176. if (substr((string)$results[0], 0, 10) == 'data:image') {
  177. $str = explode('base64,', $results[0]);
  178. if (isset($str[1])) {
  179. $p = new Picture;
  180. $p->fromBase(urldecode($str[1]));
  181. $key = sha1(urldecode($str[1]));
  182. $p->set($key, 'png');
  183. $this->sticker = $key;
  184. }
  185. } else {
  186. $this->sticker = getCid((string)$results[0]);
  187. }
  188. }
  189. }
  190. if ($stanza->reference
  191. && (string)$stanza->reference->attributes()->xmlns == 'urn:xmpp:reference:0') {
  192. $filetmp = [];
  193. if ($stanza->reference->{'media-sharing'}
  194. && (string)$stanza->reference->{'media-sharing'}->attributes()->xmlns == 'urn:xmpp:sims:1') {
  195. $file = $stanza->reference->{'media-sharing'}->file;
  196. if (isset($file)) {
  197. if (preg_match('/\w+\/[-+.\w]+/', $file->{'media-type'}) == 1) {
  198. $filetmp['type'] = (string)$file->{'media-type'};
  199. }
  200. $filetmp['size'] = (int)$file->size;
  201. $filetmp['name'] = (string)$file->name;
  202. }
  203. if ($stanza->reference->{'media-sharing'}->sources) {
  204. $source = $stanza->reference->{'media-sharing'}->sources->reference;
  205. if (!filter_var((string)$source->attributes()->uri, FILTER_VALIDATE_URL) === false) {
  206. $filetmp['uri'] = (string)$source->attributes()->uri;
  207. }
  208. }
  209. if (array_key_exists('uri', $filetmp)
  210. && array_key_exists('type', $filetmp)
  211. && array_key_exists('size', $filetmp)
  212. && array_key_exists('name', $filetmp)) {
  213. if (empty($filetmp['name'])) {
  214. $filetmp['name'] =
  215. pathinfo(parse_url($filetmp['uri'], PHP_URL_PATH), PATHINFO_BASENAME)
  216. . ' ('.parse_url($filetmp['uri'], PHP_URL_HOST).')';
  217. }
  218. $this->file = $filetmp;
  219. }
  220. } elseif (\in_array($stanza->reference->attributes()->type, ['mention', 'data'])
  221. && $stanza->reference->attributes()->uri) {
  222. $uri = parse_url($stanza->reference->attributes()->uri);
  223. if ($uri['scheme'] === 'xmpp') {
  224. $begin = '<a href="' . Route::urlize('share', $stanza->reference->attributes()->uri) . '">';
  225. if ($stanza->reference->attributes()->begin && $stanza->reference->attributes()->end) {
  226. $this->html = substr_replace(
  227. $this->body,
  228. $begin,
  229. (int)$stanza->reference->attributes()->begin,
  230. 0
  231. );
  232. $this->html = substr_replace(
  233. $this->html,
  234. '</a>',
  235. (int)$stanza->reference->attributes()->end + strlen($begin),
  236. 0
  237. );
  238. } else {
  239. $this->html = $begin . $this->body . '</a>';
  240. }
  241. $this->file = [
  242. 'type' => 'xmpp',
  243. 'uri' => (string)$stanza->reference->attributes()->uri,
  244. ];
  245. }
  246. }
  247. }
  248. if ($stanza->replace
  249. && $this->user->messages()
  250. ->where('jidfrom', $this->jidfrom)
  251. ->where('replaceid', $this->replaceid)
  252. ->count() == 0
  253. ) {
  254. $message = $this->user->messages()
  255. ->where('jidfrom', $this->jidfrom)
  256. ->where('replaceid', (string)$stanza->replace->attributes()->id)
  257. ->first();
  258. $this->oldid = $message->id;
  259. /**
  260. * We prepare the existing message to be edited in the DB
  261. */
  262. try {
  263. Message::where('replaceid', (string)$stanza->replace->attributes()->id)
  264. ->where('user_id', $this->user_id)
  265. ->where('jidfrom', $this->jidfrom)
  266. ->update(['id' => $this->id]);
  267. } catch (\Exception $e) {
  268. \Utils::error($e->getMessage());
  269. }
  270. }
  271. if (isset($stanza->x->invite)) {
  272. $this->type = 'invitation';
  273. $this->subject = $this->jidfrom;
  274. $this->jidfrom = current(explode('/', (string)$stanza->x->invite->attributes()->from));
  275. }
  276. } elseif (isset($stanza->x)
  277. && $stanza->x->attributes()->xmlns == 'jabber:x:conference') {
  278. $this->type = 'invitation';
  279. $this->body = (string)$stanza->x->attributes()->reason;
  280. $this->subject = (string)$stanza->x->attributes()->jid;
  281. }
  282. # XEP-xxxx: Message Reactions
  283. elseif (isset($stanza->reactions)
  284. && $stanza->reactions->attributes()->xmlns == 'urn:xmpp:reactions:0') {
  285. $parentMessage = $this->user
  286. ->messages()
  287. ->where('replaceid', (string)$stanza->reactions->attributes()->to)
  288. ->where(function ($query) {
  289. $query->where('jidfrom', $this->jidfrom)
  290. ->orWhere('jidto', $this->jidfrom);
  291. })
  292. ->first();
  293. if ($parentMessage) {
  294. $resource = ($this->type == 'groupchat')
  295. ? $this->resource
  296. : $this->jidfrom;
  297. $parentMessage
  298. ->reactions()
  299. ->where('jidfrom', $resource)
  300. ->delete();
  301. $emojis = [];
  302. $now = \Carbon\Carbon::now();
  303. $emoji = \Movim\Emoji::getInstance();
  304. foreach ($stanza->reactions->reaction as $children) {
  305. $emoji->replace((string)$children);
  306. if ($emoji->isSingleEmoji()) {
  307. $reaction = new Reaction;
  308. $reaction->message_mid = $parentMessage->mid;
  309. $reaction->emoji = (string)$children;
  310. $reaction->jidfrom = $resource;
  311. $reaction->created_at = $now;
  312. $reaction->updated_at = $now;
  313. \array_push($emojis, $reaction->toArray());
  314. }
  315. }
  316. try {
  317. Reaction::insert($emojis);
  318. } catch (QueryException $exception) {
  319. // Duplicate ?
  320. }
  321. return $parentMessage;
  322. }
  323. }
  324. return $this;
  325. }
  326. public function isEmpty()
  327. {
  328. return (empty($this->body)
  329. && empty($this->picture)
  330. && empty($this->file)
  331. && empty($this->sticker)
  332. );
  333. }
  334. public function isSubject()
  335. {
  336. return !empty($this->subject);
  337. }
  338. public function isOTR()
  339. {
  340. return preg_match('#^\?OTR#', $this->body);
  341. }
  342. public function addUrls()
  343. {
  344. if (is_string($this->body)) {
  345. $old = $this->body;
  346. $this->body = addUrls($this->body);
  347. // TODO fix addUrls, see https://github.com/movim/movim/issues/877
  348. if (strlen($this->body) < strlen($old)) {
  349. $this->body = $old;
  350. }
  351. }
  352. }
  353. }