From 7b65cba5e4009c62b3753009ea3bb690cfb14078 Mon Sep 17 00:00:00 2001 From: yuri Date: Wed, 26 Sep 2018 16:12:03 +0300 Subject: [PATCH] stream image link inserting --- application/Espo/Controllers/Attachment.php | 14 +++ application/Espo/Services/Attachment.php | 109 +++++++++++++++++++- client/res/templates/stream/panel.tpl | 2 +- client/src/views/note/fields/post.js | 88 +++++++++++++++- client/src/views/stream/panel.js | 39 +++---- 5 files changed, 227 insertions(+), 25 deletions(-) diff --git a/application/Espo/Controllers/Attachment.php b/application/Espo/Controllers/Attachment.php index 8d9e3d5750..63184c5630 100644 --- a/application/Espo/Controllers/Attachment.php +++ b/application/Espo/Controllers/Attachment.php @@ -41,4 +41,18 @@ class Attachment extends \Espo\Core\Controllers\Record } return parent::actionList($params, $data, $request); } + + public function postActionGetAttachmentFromImageUrl($params, $data) + { + if (empty($data->url)) throw new BadRequest(); + + return $this->getRecordService()->getAttachmentFromImageUrl($data->url)->getValueMap(); + } + + public function postActionGetCopiedAttachment($params, $data) + { + if (empty($data->id)) throw new BadRequest(); + + return $this->getRecordService()->getCopiedAttachment($data->id)->getValueMap(); + } } diff --git a/application/Espo/Services/Attachment.php b/application/Espo/Services/Attachment.php index 061dea94c0..597a967e9c 100644 --- a/application/Espo/Services/Attachment.php +++ b/application/Espo/Services/Attachment.php @@ -34,6 +34,7 @@ use \Espo\ORM\Entity; use \Espo\Core\Exceptions\BadRequest; use \Espo\Core\Exceptions\Forbidden; use \Espo\Core\Exceptions\Error; +use \Espo\Core\Exceptions\NotFound; class Attachment extends Record { @@ -166,5 +167,111 @@ class Attachment extends Record $entity->clear('storage'); } } -} + public function getCopiedAttachment($id) + { + $attachment = $this->getEntity($id); + if (!$attachment) throw new NotFound(); + + $copied = $this->getRepository()->getCopiedAttachment($attachment); + + return $copied; + } + + public function getAttachmentFromImageUrl($url) + { + $attachment = $this->getEntity(); + + $data = $this->getImageDataByUrl($url); + if (!$data) throw new Error('Attachment::getAttachmentFromImageUrl: Bad image data.'); + + $type = $data['type']; + $contents = $data['contents']; + + $attachment->set([ + 'name' => $url, + 'type' => $type, + 'contents' => $contents, + 'role' => 'Attachment' + ]); + + $this->getRepository()->save($attachment); + + $attachment->clear('contents'); + + return $attachment; + } + + protected function getImageDataByUrl($url) + { + $type = null; + + if (function_exists('curl_init')) { + $opts = []; + $httpHeaders = []; + $httpHeaders[] = 'Expect:'; + $opts[\CURLOPT_URL] = $url; + $opts[\CURLOPT_HTTPHEADER] = $httpHeaders; + $opts[\CURLOPT_CONNECTTIMEOUT] = 10; + $opts[\CURLOPT_TIMEOUT] = 10; + $opts[\CURLOPT_HEADER] = true; + $opts[\CURLOPT_BINARYTRANSFER] = true; + $opts[\CURLOPT_VERBOSE] = true; + $opts[\CURLOPT_SSL_VERIFYPEER] = false; + $opts[\CURLOPT_SSL_VERIFYHOST] = 2; + $opts[\CURLOPT_RETURNTRANSFER] = true; + $opts[\CURLOPT_FOLLOWLOCATION] = true; + $opts[\CURLOPT_MAXREDIRS] = 2; + $opts[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; + + $ch = curl_init(); + curl_setopt_array($ch, $opts); + $response = curl_exec($ch); + + $headerSize = curl_getinfo($ch, \CURLINFO_HEADER_SIZE); + + $header = substr($response, 0, $headerSize); + $body = substr($response, $headerSize); + + $headLineList = explode("\n", $header); + foreach ($headLineList as $i => $line) { + if ($i === 0) continue; + if (strpos(strtolower($line), strtolower('Content-Type:')) === 0) { + $part = trim(substr($line, 13)); + if ($part) { + $type = trim(explode(";", $part)[0]); + } + } + } + + if (!$type) { + $extTypeMap = [ + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif' + ]; + + $extension = preg_replace('#\?.*#', '', pathinfo($url, \PATHINFO_EXTENSION)); + + if (isset($extTypeMap[$extension])) { + $type = $extTypeMap[$extension]; + } + } + + if (!$type) return; + + if (!in_array($type, ['image/png', 'image/jpeg', 'image/gif'])) { + return; + } + + return [ + 'type' => $type, + 'contents' => $body + ]; + + curl_close($ch); + } + return null; + } +} diff --git a/client/res/templates/stream/panel.tpl b/client/res/templates/stream/panel.tpl index 0d131ada8a..35a7ebf870 100644 --- a/client/res/templates/stream/panel.tpl +++ b/client/res/templates/stream/panel.tpl @@ -1,5 +1,5 @@
- +
{{{postField}}}
diff --git a/client/src/views/note/fields/post.js b/client/src/views/note/fields/post.js index 96dc7e0d9c..2243cb28d8 100644 --- a/client/src/views/note/fields/post.js +++ b/client/src/views/note/fields/post.js @@ -38,10 +38,20 @@ Espo.define('views/note/fields/post', ['views/fields/text', 'lib!Textcomplete'], 'input textarea': function (e) { this.controlTextareaHeight(); }, + 'paste textarea': function (e) { + if (!e.originalEvent.clipboardData) return; + var text = e.originalEvent.clipboardData.getData('text/plain'); + if (!text) return; + text = text.trim(); + if (!text) return; + this.handlePastedText(text); + } }, Dep.prototype.events), setup: function () { Dep.prototype.setup.call(this); + + this.insertedImagesData = {}; }, controlTextareaHeight: function (lastHeight) { @@ -62,7 +72,8 @@ Espo.define('views/note/fields/post', ['views/fields/text', 'lib!Textcomplete'], afterRender: function () { Dep.prototype.afterRender.call(this); - this.$element.attr('placeholder', this.translate('writeMessage', 'messages', 'Note')); + var placeholderText = this.options.placeholderText || this.translate('writeMessage', 'messages', 'Note'); + this.$element.attr('placeholder', placeholderText); this.$textarea = this.$element; var $textarea = this.$textarea; @@ -143,6 +154,81 @@ Espo.define('views/note/fields/post', ['views/fields/text', 'lib!Textcomplete'], return Dep.prototype.validateRequired.call(this); }, + handlePastedText: function (text) { + if (/^http(s){0,1}\:\/\//.test(text)) { + var imageExtensionList = ['jpg', 'jpeg', 'png', 'gif']; + var regExpString = '.+\\.(' + imageExtensionList.join('|') + ')(/?.*){0,1}$'; + var regExp = new RegExp(regExpString, 'i'); + var url = text; + var siteUrl = this.getConfig().get('siteUrl').replace(/\/$/, ''); + + var attachmentIdList = this.model.get('attachmentsIds') || []; + + if (regExp.test(text)) { + var insertedId = this.insertedImagesData[url]; + if (insertedId) { + if (~attachmentIdList.indexOf(insertedId)) return; + } + + this.ajaxPostRequest('Attachment/action/getAttachmentFromImageUrl', { + url: url + }).then(function (attachment) { + var attachmentIdList = Espo.Utils.clone(this.model.get('attachmentsIds') || []); + var attachmentNames = Espo.Utils.clone(this.model.get('attachmentsNames') || {}); + var attachmentTypes = Espo.Utils.clone(this.model.get('attachmentsTypes') || {}); + + attachmentIdList.push(attachment.id); + attachmentNames[attachment.id] = attachment.name; + attachmentTypes[attachment.id] = attachment.type; + + this.insertedImagesData[url] = attachment.id; + + this.model.set({ + attachmentsIds: attachmentIdList, + attachmentsNames: attachmentNames, + attachmentsTypes: attachmentTypes + }); + }.bind(this)).fail(function (xhr) { + xhr.errorIsHandled = true; + }); + + } else if (/\?entryPoint\=image\&/.test(text) && text.indexOf(siteUrl) === 0) { + url = text.replace(/[\&]{0,1}size\=[a-z\-]*/, ''); + + var match = /\&{0,1}id\=([a-z0-9A-Z]*)/g.exec(text) + if (match.length === 2) { + var id = match[1]; + if (~attachmentIdList.indexOf(id)) return; + var insertedId = this.insertedImagesData[id]; + if (insertedId) { + if (~attachmentIdList.indexOf(insertedId)) return; + } + + this.ajaxPostRequest('Attachment/action/getCopiedAttachment', { + id: id + }).then(function (attachment) { + var attachmentIdList = Espo.Utils.clone(this.model.get('attachmentsIds') || []); + var attachmentNames = Espo.Utils.clone(this.model.get('attachmentsNames') || {}); + var attachmentTypes = Espo.Utils.clone(this.model.get('attachmentsTypes') || {}); + + attachmentIdList.push(attachment.id); + attachmentNames[attachment.id] = attachment.name; + attachmentTypes[attachment.id] = attachment.type; + + this.insertedImagesData[id] = attachment.id; + + this.model.set({ + attachmentsIds: attachmentIdList, + attachmentsNames: attachmentNames, + attachmentsTypes: attachmentTypes + }); + }.bind(this)).fail(function (xhr) { + xhr.errorIsHandled = true; + }); + } + } + } + } }); diff --git a/client/src/views/stream/panel.js b/client/src/views/stream/panel.js index 875bc1d03e..5273f5b670 100644 --- a/client/src/views/stream/panel.js +++ b/client/src/views/stream/panel.js @@ -37,7 +37,7 @@ Espo.define('views/stream/panel', ['views/record/panels/relationship', 'lib!Text postDisabled: false, events: _.extend({ - 'focus textarea.note': function (e) { + 'focus textarea[name="post"]': function (e) { this.enablePostingMode(); }, 'click button.post': function () { @@ -55,7 +55,7 @@ Espo.define('views/stream/panel', ['views/record/panels/relationship', 'lib!Text } }, - 'keypress textarea.note': function (e) { + 'keypress textarea[name="post"]': function (e) { if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) { this.post(); } else if (e.keyCode == 9) { @@ -64,10 +64,7 @@ Espo.define('views/stream/panel', ['views/record/panels/relationship', 'lib!Text this.disablePostingMode(); } } - }, - 'input textarea.note': function (e) { - this.controlTextareaHeight(); - }, + } }, Dep.prototype.events), data: function () { @@ -78,26 +75,12 @@ Espo.define('views/stream/panel', ['views/record/panels/relationship', 'lib!Text return data; }, - controlTextareaHeight: function (lastHeight) { - var scrollHeight = this.$textarea.prop('scrollHeight'); - var clientHeight = this.$textarea.prop('clientHeight'); - - if (clientHeight === lastHeight) return; - if (scrollHeight > clientHeight + 1) { - this.$textarea.attr('rows', this.$textarea.prop('rows') + 1); - this.controlTextareaHeight(clientHeight); - } - if (this.$textarea.val().length === 0) { - this.$textarea.attr('rows', 1); - } - }, - enablePostingMode: function () { this.$el.find('.buttons-panel').removeClass('hide'); if (!this.postingMode) { if (this.$textarea.val() && this.$textarea.val().length) { - this.controlTextareaHeight(); + this.getView('postField').controlTextareaHeight(); } $('body').on('click.stream-panel', function (e) { var $target = $(e.target); @@ -169,6 +152,18 @@ Espo.define('views/stream/panel', ['views/record/panels/relationship', 'lib!Text attachmentsNames: storedAttachments.names }); } + + this.createView('postField', 'views/note/fields/post', { + el: this.getSelector() + ' .textarea-container', + name: 'post', + mode: 'edit', + params: { + required: true, + rows: 1 + }, + model: this.seed, + placeholderText: this.placeholderText + }); this.createCollection(function () { this.wait(false); }, this); @@ -213,7 +208,7 @@ Espo.define('views/stream/panel', ['views/record/panels/relationship', 'lib!Text }, afterRender: function () { - this.$textarea = this.$el.find('textarea.note'); + this.$textarea = this.$el.find('textarea[name="post"]'); this.$attachments = this.$el.find('div.attachments'); this.$postContainer = this.$el.find('.post-container');