diff --git a/application/Espo/Controllers/Attachment.php b/application/Espo/Controllers/Attachment.php
index 2ef614f234..c7a52e19ad 100644
--- a/application/Espo/Controllers/Attachment.php
+++ b/application/Espo/Controllers/Attachment.php
@@ -34,16 +34,5 @@ use \Espo\Core\Exceptions\BadRequest;
class Attachment extends \Espo\Core\Controllers\Record
{
- public function postActionUpload($params, $data, $request)
- {
- if (!$this->getAcl()->checkScope('Attachment', 'create')) {
- throw new Forbidden();
- }
- $attachment = $this->getRecordService()->upload($data);
-
- return array(
- 'attachmentId' => $attachment->id
- );
- }
}
diff --git a/application/Espo/Core/defaults/config.php b/application/Espo/Core/defaults/config.php
index 3a65bc1573..1eab2a8b14 100644
--- a/application/Espo/Core/defaults/config.php
+++ b/application/Espo/Core/defaults/config.php
@@ -167,6 +167,7 @@ return array (
'currencyDecimalPlaces' => null,
'aclStrictMode' => false,
'aclAllowDeleteCreated' => false,
+ 'inlineAttachmentUploadMaxSize' => 20,
'isInstalled' => false
);
diff --git a/application/Espo/Repositories/Attachment.php b/application/Espo/Repositories/Attachment.php
index afbf968743..b4bebd2a62 100644
--- a/application/Espo/Repositories/Attachment.php
+++ b/application/Espo/Repositories/Attachment.php
@@ -61,7 +61,7 @@ class Attachment extends \Espo\Core\ORM\Repositories\RDB
if ($entity->isNew()) {
if (!$entity->has('size') && $entity->has('contents')) {
- $entity->set('size', mb_strlen($entity->has('contents')));
+ $entity->set('size', mb_strlen($entity->get('contents')));
}
}
}
diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json
index 1907a4e8fc..f5d55923cb 100644
--- a/application/Espo/Resources/i18n/en_US/Admin.json
+++ b/application/Espo/Resources/i18n/en_US/Admin.json
@@ -151,7 +151,8 @@
"dynamicLogicRequired": "Conditions making field required",
"dynamicLogicOptions": "Conditional options",
"probabilityMap": "Stage Probabilities (%)",
- "readOnly": "Read-only"
+ "readOnly": "Read-only",
+ "maxFileSize": "Max File Size (Mb)"
},
"messages": {
"upgradeVersion": "EspoCRM will be upgraded to version {version}. Please be patient as this may take a while.",
diff --git a/application/Espo/Resources/i18n/en_US/FieldManager.json b/application/Espo/Resources/i18n/en_US/FieldManager.json
index eccea3233a..2fcf7401af 100644
--- a/application/Espo/Resources/i18n/en_US/FieldManager.json
+++ b/application/Espo/Resources/i18n/en_US/FieldManager.json
@@ -70,7 +70,8 @@
"maxLength": "Max acceptable length of text.",
"before": "The date value should be before the date value of the specified field.",
"after": "The date value should be after the date value of the specified field.",
- "readOnly": "Field value can't be specified by user. But can be calculated by formula."
+ "readOnly": "Field value can't be specified by user. But can be calculated by formula.",
+ "maxFileSize": "If empty or 0 then no limit."
},
"fieldParts": {
"address": {
diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json
index 33681295e6..f737dabee1 100644
--- a/application/Espo/Resources/i18n/en_US/Global.json
+++ b/application/Espo/Resources/i18n/en_US/Global.json
@@ -240,6 +240,7 @@
"fieldShouldBeLess": "{field} should be less then {value}",
"fieldShouldBeGreater": "{field} should be greater then {value}",
"fieldBadPasswordConfirm": "{field} not confirmed properly",
+ "fieldMaxFileSizeError": "File should not exceed {max} Mb",
"resetPreferencesDone": "Preferences has been reset to defaults",
"confirmation": "Are you sure?",
"unlinkAllConfirmation": "Are you sure you want to unlink all related records?",
diff --git a/application/Espo/Resources/metadata/entityDefs/Attachment.json b/application/Espo/Resources/metadata/entityDefs/Attachment.json
index ccefbea428..98ce96050d 100644
--- a/application/Espo/Resources/metadata/entityDefs/Attachment.json
+++ b/application/Espo/Resources/metadata/entityDefs/Attachment.json
@@ -26,6 +26,10 @@
"readOnly": true,
"disabled": true
},
+ "field": {
+ "type": "varchar",
+ "disabled": true
+ },
"createdAt": {
"type": "datetime",
"readOnly": true
diff --git a/application/Espo/Resources/metadata/entityDefs/Note.json b/application/Espo/Resources/metadata/entityDefs/Note.json
index 4937ac415c..0f70bc9c28 100644
--- a/application/Espo/Resources/metadata/entityDefs/Note.json
+++ b/application/Espo/Resources/metadata/entityDefs/Note.json
@@ -24,7 +24,7 @@
"readOnly": true
},
"attachments": {
- "type": "linkMultiple",
+ "type": "attachmentMultiple",
"view": "views/stream/fields/attachment-multiple"
},
"number": {
diff --git a/application/Espo/Resources/metadata/fields/attachmentMultiple.json b/application/Espo/Resources/metadata/fields/attachmentMultiple.json
index 81a04237af..c7d78d0fc9 100644
--- a/application/Espo/Resources/metadata/fields/attachmentMultiple.json
+++ b/application/Espo/Resources/metadata/fields/attachmentMultiple.json
@@ -9,6 +9,12 @@
"name": "sourceList",
"type": "multiEnum",
"view": "views/admin/field-manager/fields/source-list"
+ },
+ {
+ "name": "maxFileSize",
+ "type": "float",
+ "tooltip": true,
+ "min": 0
}
],
"actualFields":[
diff --git a/application/Espo/Resources/metadata/fields/file.json b/application/Espo/Resources/metadata/fields/file.json
index b0ef47d625..c6f2247f5c 100644
--- a/application/Espo/Resources/metadata/fields/file.json
+++ b/application/Espo/Resources/metadata/fields/file.json
@@ -10,6 +10,12 @@
"type": "multiEnum",
"view": "views/admin/field-manager/fields/source-list"
},
+ {
+ "name": "maxFileSize",
+ "type": "float",
+ "tooltip": true,
+ "min": 0
+ },
{
"name":"audited",
"type":"bool"
diff --git a/application/Espo/Resources/metadata/fields/image.json b/application/Espo/Resources/metadata/fields/image.json
index 5c8ea3b1a7..abf44bcfad 100644
--- a/application/Espo/Resources/metadata/fields/image.json
+++ b/application/Espo/Resources/metadata/fields/image.json
@@ -11,6 +11,12 @@
"default":"small",
"options": ["x-small", "small", "medium", "large"]
},
+ {
+ "name": "maxFileSize",
+ "type": "float",
+ "tooltip": true,
+ "min": 0
+ },
{
"name":"audited",
"type":"bool"
diff --git a/application/Espo/Services/Attachment.php b/application/Espo/Services/Attachment.php
index c58288707d..beba4dc524 100644
--- a/application/Espo/Services/Attachment.php
+++ b/application/Espo/Services/Attachment.php
@@ -31,10 +31,18 @@ namespace Espo\Services;
use \Espo\ORM\Entity;
+use \Espo\Core\Exceptions\BadRequest;
+use \Espo\Core\Exceptions\Forbidden;
+use \Espo\Core\Exceptions\Error;
+
class Attachment extends Record
{
protected $notFilteringAttributeList = ['contents'];
+ protected $attachmentFieldTypeList = ['file', 'image', 'attachmentMultiple'];
+
+ protected $inlineAttachmentFieldTypeList = ['text', 'wysiwyg'];
+
public function upload($fileData)
{
if (!$this->getAcl()->checkScope('Attachment', 'create')) {
@@ -59,9 +67,79 @@ class Attachment extends Record
public function createEntity($data)
{
if (!empty($data['file'])) {
- list($prefix, $contents) = explode(',', $data['file']);
+ $arr = explode(',', $data['file']);
+ $contents = '';
+ if (count($arr) > 1) {
+ $contents = $arr[1];
+ }
+
$contents = base64_decode($contents);
$data['contents'] = $contents;
+
+ $relatedEntityType = null;
+ $field = null;
+ $role = 'Attachment';
+ if (isset($data['parentType'])) {
+ $relatedEntityType = $data['parentType'];
+ } else if (isset($data['relatedType'])) {
+ $relatedEntityType = $data['relatedType'];
+ }
+ if (isset($data['field'])) {
+ $field = $data['field'];
+ }
+ if (isset($data['role'])) {
+ $role = $data['role'];
+ }
+ if (!$relatedEntityType || !$field) {
+ throw new BadRequest("Params 'field' and 'parentType' not passed along with 'file'.");
+ }
+
+ $fieldType = $this->getMetadata()->get(['entityDefs', $relatedEntityType, 'fields', $field, 'type']);
+ if (!$fieldType) {
+ throw new Error("Field '{$field}' does not exist.");
+ }
+
+ if (
+ !$this->getAcl()->checkScope($relatedEntityType, 'create')
+ &&
+ !$this->getAcl()->checkScope($relatedEntityType, 'edit')
+ ) {
+ throw new Forbidden("No access to " . $relatedEntityType . ".");
+ }
+
+ if (in_array($field, $this->getAcl()->getScopeForbiddenFieldList($relatedEntityType, 'edit'))) {
+ throw new Forbidden("No access to field '" . $field . "'.");
+ }
+
+ $size = mb_strlen($contents, '8bit');
+
+ if ($role === 'Attachment') {
+ if (!in_array($fieldType, $this->attachmentFieldTypeList)) {
+ throw new Error("Field type '{$fieldType}' is not allowed for attachment.");
+ }
+ $maxSize = $this->getMetadata()->get(['entityDefs', $relatedEntityType, 'fields', $field, 'maxFileSize']);
+ if (!$maxSize) {
+ $maxSize = $this->getConfig()->get('attachmentUploadMaxSize');
+ }
+ if ($maxSize) {
+ if ($size > $maxSize * 1024 * 1024) {
+ throw new Error("File size should not exceed {$maxSize}Mb.");
+ }
+ }
+
+ } else if ($role === 'Inline Attachment') {
+ if (!in_array($fieldType, $this->inlineAttachmentFieldTypeList)) {
+ throw new Error("Field '{$field}' is not allowed to have inline attachment.");
+ }
+ $inlineAttachmentUploadMaxSize = $this->getConfig()->get('inlineAttachmentUploadMaxSize');
+ if ($inlineAttachmentUploadMaxSize) {
+ if ($size > $inlineAttachmentUploadMaxSize * 1024 * 1024) {
+ throw new Error("File size should not exceed {$inlineAttachmentUploadMaxSize}Mb.");
+ }
+ }
+ } else {
+ throw new BadRequest("Not supported attachment role.");
+ }
}
$entity = parent::createEntity($data);
diff --git a/client/src/views/fields/attachment-multiple.js b/client/src/views/fields/attachment-multiple.js
index 960bdd7cb8..f857e96fd1 100644
--- a/client/src/views/fields/attachment-multiple.js
+++ b/client/src/views/fields/attachment-multiple.js
@@ -291,7 +291,7 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
$attachments.append($container);
if (!id) {
- var $loading = $('' + this.translate('Uploading...') + '');
+ var $loading = $('' + this.translate('Uploading...') + '');
$container.append($loading);
$att.on('ready', function () {
$loading.html(this.translate('Ready'));
@@ -303,22 +303,50 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
return $att;
},
+ showValidationMessage: function (msg, selector) {
+ var $label = this.$el.find('label');
+ var title = $label.attr('title');
+ $label.attr('title', '');
+ Dep.prototype.showValidationMessage.call(this, msg, selector);
+ $label.attr('title', title);
+ },
+
uploadFiles: function (files) {
var uploadedCount = 0;
var totalCount = 0;
+ var exceedsMaxFileSize = false;
+ var maxFileSize = this.params.maxFileSize;
+ if (maxFileSize) {
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ if (file.size > maxFileSize * 1024 * 1024) {
+ exceedsMaxFileSize = true;
+ }
+ }
+ }
+ if (exceedsMaxFileSize) {
+ var msg = this.translate('fieldMaxFileSizeError', 'messages')
+ .replace('{field}', this.translate(this.name, 'fields', this.model.name))
+ .replace('{max}', maxFileSize);
+
+ this.showValidationMessage(msg, 'label');
+ return;
+ }
+
this.getModelFactory().create('Attachment', function (model) {
var canceledList = [];
var fileList = [];
for (var i = 0; i < files.length; i++) {
fileList.push(files[i]);
+ totalCount++;
}
fileList.forEach(function (file) {
- var $att = this.addAttachmentBox(file.name, file.type);
+ var $attachmentBox = this.addAttachmentBox(file.name, file.type);
- $att.find('.remove-attachment').on('click.uploading', function () {
+ $attachmentBox.find('.remove-attachment').on('click.uploading', function () {
canceledList.push(attachment.cid);
totalCount--;
});
@@ -327,33 +355,33 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
var fileReader = new FileReader();
fileReader.onload = function (e) {
- $.ajax({
- type: 'POST',
- url: 'Attachment/action/upload',
- data: e.target.result,
- contentType: 'multipart/encrypted',
- timeout: 0,
- }).done(function (data) {
+ attachment.set('name', file.name);
+ attachment.set('type', file.type || 'text/plain');
+ attachment.set('role', 'Attachment');
+ attachment.set('size', file.size);
+ attachment.set('parentType', this.model.name);
+ attachment.set('file', e.target.result);
+ attachment.set('field', this.name);
- attachment.id = data.attachmentId;
- attachment.set('name', file.name);
- attachment.set('type', file.type || 'text/plain');
- attachment.set('role', 'Attachment');
- attachment.set('size', file.size);
- attachment.set('parentType', this.model.name);
-
- attachment.once('sync', function () {
- if (canceledList.indexOf(attachment.cid) === -1) {
- $att.trigger('ready');
- this.pushAttachment(attachment);
- $att.attr('data-id', attachment.id);
- uploadedCount++;
- if (uploadedCount == totalCount) {
- afterAttachmentsUploaded.call(this);
- }
+ attachment.save().then(function () {
+ if (canceledList.indexOf(attachment.cid) === -1) {
+ $attachmentBox.trigger('ready');
+ this.pushAttachment(attachment);
+ $attachmentBox.attr('data-id', attachment.id);
+ uploadedCount++;
+ if (uploadedCount == totalCount) {
+ this.afterAttachmentsUploaded.call(this);
}
- }, this);
- attachment.save();
+ }
+ }.bind(this)).fail(function () {
+ $attachmentBox.remove();
+ totalCount--;
+ if (!totalCount) {
+ this.$el.find('.uploading-message').remove();
+ }
+ if (uploadedCount == totalCount) {
+ this.afterAttachmentsUploaded.call(this);
+ }
}.bind(this));
}.bind(this);
fileReader.readAsDataURL(file);
@@ -522,7 +550,7 @@ Espo.define('views/fields/attachment-multiple', 'views/fields/base', function (D
if (this.isRequired()) {
if (this.model.get(this.idsName).length == 0) {
var msg = this.translate('fieldIsRequired', 'messages').replace('{field}', this.translate(this.name, 'fields', this.model.name));
- this.showValidationMessage(msg);
+ this.showValidationMessage(msg, 'label');
return true;
}
}
diff --git a/client/src/views/fields/base.js b/client/src/views/fields/base.js
index 93d19ca2ed..9ab85126bc 100644
--- a/client/src/views/fields/base.js
+++ b/client/src/views/fields/base.js
@@ -537,7 +537,7 @@ Espo.define('views/fields/base', 'view', function (Dep) {
placement: 'bottom',
container: 'body',
content: message,
- trigger: 'manual',
+ trigger: 'manual'
}).popover('show');
$el.closest('.field').one('mousedown click', function () {
diff --git a/client/src/views/fields/file.js b/client/src/views/fields/file.js
index a9ef93d054..abcc081c46 100644
--- a/client/src/views/fields/file.js
+++ b/client/src/views/fields/file.js
@@ -107,6 +107,14 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
return data;
},
+ showValidationMessage: function (msg, selector) {
+ var $label = this.$el.find('label');
+ var title = $label.attr('title');
+ $label.attr('title', '');
+ Dep.prototype.showValidationMessage.call(this, msg, selector);
+ $label.attr('title', title);
+ },
+
validateRequired: function () {
if (this.isRequired()) {
if (this.model.get(this.idName) == null) {
@@ -283,12 +291,27 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
uploadFile: function (file) {
var isCanceled = false;
+ var exceedsMaxFileSize = false;
+ var maxFileSize = this.params.maxFileSize;
+ if (maxFileSize) {
+ if (file.size > maxFileSize * 1024 * 1024) {
+ exceedsMaxFileSize = true;
+ }
+ }
+ if (exceedsMaxFileSize) {
+ var msg = this.translate('fieldMaxFileSizeError', 'messages')
+ .replace('{field}', this.translate(this.name, 'fields', this.model.name))
+ .replace('{max}', maxFileSize);
+ this.showValidationMessage(msg, '.attachment-button label');
+ return;
+ }
+
this.getModelFactory().create('Attachment', function (attachment) {
- var $att = this.addAttachmentBox(file.name, file.type);
+ var $attachmentBox = this.addAttachmentBox(file.name, file.type);
this.$el.find('.attachment-button').addClass('hidden');
- $att.find('.remove-attachment').on('click.uploading', function () {
+ $attachmentBox.find('.remove-attachment').on('click.uploading', function () {
isCanceled = true;
this.$el.find('.attachment-button').removeClass('hidden');
}.bind(this));
@@ -296,27 +319,23 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
var fileReader = new FileReader();
fileReader.onload = function (e) {
this.handleFileUpload(file, e.target.result, function (result, fileParams) {
- $.ajax({
- type: 'POST',
- url: 'Attachment/action/upload',
- data: result,
- contentType: 'multipart/encrypted',
- timeout: 0,
- }).done(function (data) {
- attachment.id = data.attachmentId;
- attachment.set('name', fileParams.name);
- attachment.set('type', fileParams.type || 'text/plain');
- attachment.set('size', fileParams.size);
- attachment.set('role', 'Attachment');
- attachment.set('relatedType', this.model.name);
+ attachment.set('name', fileParams.name);
+ attachment.set('type', fileParams.type || 'text/plain');
+ attachment.set('size', fileParams.size);
+ attachment.set('role', 'Attachment');
+ attachment.set('relatedType', this.model.name);
+ attachment.set('file', e.target.result);
+ attachment.set('field', this.name);
- attachment.once('sync', function () {
- if (!isCanceled) {
- $att.trigger('ready');
- this.setAttachment(attachment);
- }
- }, this);
- attachment.save();
+ attachment.save().then(function () {
+ if (!isCanceled) {
+ $attachmentBox.trigger('ready');
+ this.setAttachment(attachment);
+ }
+ }.bind(this)).fail(function () {
+ $attachmentBox.remove();
+ this.$el.find('.uploading-message').remove();
+ this.$el.find('.attachment-button').removeClass('hidden');
}.bind(this));
}.bind(this));
}.bind(this);
@@ -358,7 +377,7 @@ Espo.define('views/fields/file', 'views/fields/link', function (Dep) {
this.$attachment.append($container);
if (!id) {
- var $loading = $('' + this.translate('Uploading...') + '');
+ var $loading = $('' + this.translate('Uploading...') + '');
$container.append($loading);
$att.on('ready', function () {
$loading.html(self.translate('Ready'));
diff --git a/client/src/views/fields/wysiwyg.js b/client/src/views/fields/wysiwyg.js
index 1235a4440d..577f240ab3 100644
--- a/client/src/views/fields/wysiwyg.js
+++ b/client/src/views/fields/wysiwyg.js
@@ -206,28 +206,23 @@ Espo.define('views/fields/wysiwyg', ['views/fields/text', 'lib!Summernote'], fun
this.getModelFactory().create('Attachment', function (attachment) {
var fileReader = new FileReader();
fileReader.onload = function (e) {
- $.ajax({
- type: 'POST',
- url: 'Attachment/action/upload',
- data: e.target.result,
- contentType: 'multipart/encrypted',
- }).done(function (data) {
- attachment.id = data.attachmentId;
- attachment.set('name', file.name);
- attachment.set('type', file.type);
- attachment.set('role', 'Inline Attachment');
- attachment.set('global', true);
- attachment.set('size', file.size);
- attachment.once('sync', function () {
- var url = '?entryPoint=attachment&id=' + attachment.id;
- this.$summernote.summernote('insertImage', url);
- this.notify(false);
- }, this);
- attachment.save();
- }.bind(this));
+ attachment.set('name', file.name);
+ attachment.set('type', file.type);
+ attachment.set('role', 'Inline Attachment');
+ attachment.set('global', true);
+ attachment.set('size', file.size);
+ attachment.set('relatedType', this.model.name);
+ attachment.set('file', e.target.result);
+ attachment.set('field', this.name);
+
+ attachment.once('sync', function () {
+ var url = '?entryPoint=attachment&id=' + attachment.id;
+ this.$summernote.summernote('insertImage', url);
+ this.notify(false);
+ }, this);
+ attachment.save();
}.bind(this);
fileReader.readAsDataURL(file);
-
}, this);
}.bind(this),
onBlur: function () {