data privacy

This commit is contained in:
yuri
2018-05-14 11:49:25 +03:00
parent 8ad2c425bf
commit 40a13c16eb
58 changed files with 1247 additions and 127 deletions
+63
View File
@@ -0,0 +1,63 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Acl;
use \Espo\Entities\User as EntityUser;
use \Espo\ORM\Entity;
class EmailAddress extends \Espo\Core\Acl\Base
{
public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity)
{
$id = $entity->id;
$isFobidden = false;
$repository = $this->getEntityManager()->getRepository('EmailAddress');
if (!$user->isAdmin()) {
$entityWithSameAddressList = $repository->getEntityListByAddressId($id, $excludeEntity);
foreach ($entityWithSameAddressList as $e) {
if (!$this->getAclManager()->check($user, $e, 'edit')) {
$isFobidden = true;
if (
$e->get('isPortalUser') && $excludeEntity->getEntityType() === 'Contact' &&
$e->get('contactId') === $excludeEntity->id
) {
$isFobidden = false;
}
if ($isFobidden) break;
}
}
}
return !$isFobidden;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Acl;
use \Espo\Entities\User as EntityUser;
use \Espo\ORM\Entity;
class PhoneNumber extends \Espo\Core\Acl\Base
{
public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity)
{
$id = $entity->id;
$isFobidden = false;
$repository = $this->getEntityManager()->getRepository('PhoneNumber');
if (!$user->isAdmin()) {
$entityWithSameNumberList = $repository->getEntityListByPhoneNumberId($id, $excludeEntity);
foreach ($entityWithSameNumberList as $e) {
if (!$this->getAclManager()->check($user, $e, 'edit')) {
$isFobidden = true;
if (
$e->get('isPortalUser') && $excludeEntity->getEntityType() === 'Contact' &&
$e->get('contactId') === $excludeEntity->id
) {
$isFobidden = false;
}
if ($isFobidden) break;
}
}
}
return !$isFobidden;
}
}
@@ -0,0 +1,56 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\AclPortal;
use \Espo\Entities\User as EntityUser;
use \Espo\ORM\Entity;
class EmailAddress extends \Espo\Core\AclPortal\Base
{
public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity)
{
$id = $entity->id;
$isFobidden = false;
$repository = $this->getEntityManager()->getRepository('EmailAddress');
if (!$user->isAdmin()) {
$entityWithSameAddressList = $repository->getEntityListByAddressId($id, $excludeEntity);
foreach ($entityWithSameAddressList as $e) {
if (!$this->getAclManager()->check($user, $e, 'edit')) {
$isFobidden = true;
break;
}
}
}
return !$isFobidden;
}
}
@@ -0,0 +1,57 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\AclPortal;
use \Espo\Entities\User as EntityUser;
use \Espo\ORM\Entity;
class PhoneNumber extends \Espo\Core\AclPortal\Base
{
public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity)
{
$id = $entity->id;
$isFobidden = false;
$repository = $this->getEntityManager()->getRepository('PhoneNumber');
if (!$user->isAdmin()) {
$entityWithSameNumberList = $repository->getEntityListByPhoneNumberId($id, $excludeEntity);
foreach ($entityWithSameNumberList as $e) {
if (!$this->getAclManager()->check($user, $e, 'edit')) {
$isFobidden = true;
break;
}
}
}
return !$isFobidden;
}
}
@@ -0,0 +1,55 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\BadRequest;
class DataPrivacy extends \Espo\Core\Controllers\Base
{
protected function checkControllerAccess()
{
if ($this->getAcl()->get('dataPrivacyPermission') === 'no') {
throw new Forbidden();
}
}
public function postActionErase($params, $data)
{
if (empty($data->entityType) || empty($data->id) || empty($data->fieldList) || !is_array($data->fieldList)) {
throw new BadRequest();
}
return $this->getServiceFactory()->create('DataPrivacy')->erase($data->entityType, $data->id, $data->fieldList);
}
}
@@ -1,7 +1,8 @@
{
"fields": {
"name": {
"type": "personName"
"type": "personName",
"isPersonalData": true
},
"salutationName": {
"type": "enum",
@@ -24,15 +25,18 @@
"type": "text"
},
"emailAddress": {
"type": "email"
"type": "email",
"isPersonalData": true
},
"phoneNumber": {
"type": "phone",
"typeList": ["Mobile", "Office", "Home", "Fax", "Other"],
"defaultType": "Mobile"
"defaultType": "Mobile",
"isPersonalData": true
},
"address": {
"type": "address"
"type": "address",
"isPersonalData": true
},
"addressStreet": {
"type": "text",
@@ -7,5 +7,6 @@
"aclPortalLevelList": ["all", "account", "contact", "own", "no"],
"customizable": true,
"importable": true,
"notifications": true
"notifications": true,
"hasPersonalData": true
}
@@ -10,7 +10,8 @@
"type": "url"
},
"emailAddress": {
"type": "email"
"type": "email",
"isPersonalData": true
},
"phoneNumber": {
"type": "phone",
@@ -1,7 +1,8 @@
{
"fields": {
"name": {
"type": "personName"
"type": "personName",
"isPersonalData": true
},
"salutationName": {
"type": "enum",
@@ -71,18 +72,21 @@
"type": "text"
},
"emailAddress": {
"type": "email"
"type": "email",
"isPersonalData": true
},
"phoneNumber": {
"type": "phone",
"typeList": ["Mobile", "Office", "Home", "Fax", "Other"],
"defaultType": "Mobile"
"defaultType": "Mobile",
"isPersonalData": true
},
"doNotCall": {
"type": "bool"
},
"address": {
"type": "address"
"type": "address",
"isPersonalData": true
},
"addressStreet": {
"type": "text",
@@ -1,7 +1,8 @@
{
"fields": {
"name": {
"type": "personName"
"type": "personName",
"isPersonalData": true
},
"salutationName": {
"type": "enum",
@@ -59,7 +60,8 @@
"type": "url"
},
"address": {
"type": "address"
"type": "address",
"isPersonalData": true
},
"addressStreet": {
"type": "text",
@@ -83,12 +85,14 @@
"trim": true
},
"emailAddress": {
"type": "email"
"type": "email",
"isPersonalData": true
},
"phoneNumber": {
"type": "phone",
"typeList": ["Mobile", "Office", "Home", "Fax", "Other"],
"defaultType": "Mobile"
"defaultType": "Mobile",
"isPersonalData": true
},
"doNotCall": {
"type": "bool"
@@ -9,5 +9,6 @@
"stream": true,
"importable": true,
"notifications": true,
"object": true
"object": true,
"hasPersonalData": true
}
@@ -9,5 +9,6 @@
"stream": true,
"importable": true,
"notifications": true,
"object": true
"object": true,
"hasPersonalData": true
}
@@ -10,5 +10,6 @@
"importable": true,
"notifications": true,
"object": true,
"statusField": "status"
"statusField": "status",
"hasPersonalData": true
}
@@ -222,6 +222,7 @@ class MassEmail extends \Espo\Services\Record
foreach ($entityList as $target) {
$emailAddress = $target->get('emailAddress');
if (!$target->get('emailAddress')) continue;
if (strpos($emailAddress, 'ERASED:') === 0) continue;
$emailAddressRecord = $this->getEntityManager()->getRepository('EmailAddress')->getByAddress($emailAddress);
if ($emailAddressRecord) {
if ($emailAddressRecord->get('invalid') || $emailAddressRecord->get('optOut')) {
+111 -37
View File
@@ -41,32 +41,43 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
{
parent::init();
$this->addDependency('user');
$this->addDependency('acl');
$this->addDependency('aclManager');
}
public function getIdListFormAddressList(array $arr = [])
protected function getAcl()
{
return $this->getIds($arr);
return $this->getInjection('acl');
}
public function getIds(array $arr = [])
public function getIdListFormAddressList(array $addressList = [])
{
return $this->getIds($addressList);
}
public function getIds(array $addressList = [])
{
$ids = array();
if (!empty($arr)) {
$a = array_map(function ($item) {
return strtolower($item);
}, $arr);
$eas = $this->where(array(
'lower' => array_map(function ($item) {
return strtolower($item);
}, $arr)
))->find();
if (!empty($addressList)) {
$lowerAddressList = [];
foreach ($addressList as $address) {
$lowerAddressList[] = trim(strtolower($address));
}
$eaCollection = $this->where([
[
'lower' => $lowerAddressList
]
])->find();
$ids = array();
$exist = array();
foreach ($eas as $ea) {
foreach ($eaCollection as $ea) {
$ids[] = $ea->id;
$exist[] = $ea->get('lower');
}
foreach ($arr as $address) {
foreach ($addressList as $address) {
$address = trim($address);
if (empty($address) || !filter_var($address, FILTER_VALIDATE_EMAIL)) {
continue;
}
@@ -118,6 +129,42 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
return $this->where(array('lower' => strtolower($address)))->findOne();
}
public function getEntityListByAddressId($emailAddressId, $exceptionEntity = null)
{
$entityList = [];
$pdo = $this->getEntityManager()->getPDO();
$sql = "
SELECT entity_email_address.entity_type AS 'entityType', entity_email_address.entity_id AS 'entityId'
FROM entity_email_address
WHERE
entity_email_address.email_address_id = ".$pdo->quote($emailAddressId)." AND
entity_email_address.deleted = 0
";
if ($exceptionEntity) {
$sql .= "
AND (
entity_email_address.entity_type <> " .$pdo->quote($exceptionEntity->getEntityType()) . "
OR
entity_email_address.entity_id <> " .$pdo->quote($exceptionEntity->id) . "
)
";
}
$sth = $pdo->prepare($sql);
$sth->execute();
while ($row = $sth->fetch()) {
if (empty($row['entityType']) || empty($row['entityId'])) continue;
if (!$this->getEntityManager()->hasRepository($row['entityType'])) continue;
$entity = $this->getEntityManager()->getEntity($row['entityType'], $row['entityId']);
if ($entity) {
$entityList[] = $entity;
}
}
return $entityList;
}
public function getEntityByAddressId($emailAddressId, $entityType = null, $onlyName = false)
{
$pdo = $this->getEntityManager()->getPDO();
@@ -219,14 +266,14 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
$hash = array();
foreach ($emailAddressData as $row) {
$key = $row->emailAddress;
$key = trim($row->emailAddress);
if (!empty($key)) {
$key = strtolower($key);
$hash[$key] = [
'primary' => !empty($row->primary) ? true : false,
'optOut' => !empty($row->optOut) ? true : false,
'invalid' => !empty($row->invalid) ? true : false,
'emailAddress' => $row->emailAddress
'emailAddress' => trim($row->emailAddress)
];
}
}
@@ -249,6 +296,7 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
$toUpdate = array();
$toRemove = array();
$revertData = [];
foreach ($hash as $key => $data) {
$new = true;
@@ -261,9 +309,10 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
if (array_key_exists($key, $hashPrev)) {
$new = false;
$changed =
$hash[$key]['optOut'] != $hashPrev[$key]['optOut'] ||
$hash[$key]['invalid'] != $hashPrev[$key]['invalid'] ||
$hash[$key]['emailAddress'] !== $hashPrev[$key]['emailAddress'];
$hash[$key]['optOut'] != $hashPrev[$key]['optOut'] ||
$hash[$key]['invalid'] != $hashPrev[$key]['invalid'] ||
$hash[$key]['emailAddress'] !== $hashPrev[$key]['emailAddress'];
if ($hash[$key]['primary']) {
if ($hash[$key]['primary'] == $hashPrev[$key]['primary']) {
$primary = false;
@@ -303,12 +352,7 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
foreach ($toUpdate as $address) {
$emailAddress = $this->getByAddress($address);
if ($emailAddress) {
$skipSave = false;
if (!$this->getInjection('user')->isAdmin()) {
if ($this->getEntityByAddressId($emailAddress->id, 'User', true)) {
$skipSave = true;
}
}
$skipSave = $this->checkChangeIsForbidden($emailAddress, $entity);
if (!$skipSave) {
$emailAddress->set(array(
'optOut' => $hash[$address]['optOut'],
@@ -316,6 +360,11 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
'name' => $hash[$address]['emailAddress']
));
$this->save($emailAddress);
} else {
$revertData[$address] = [
'optOut' => $emailAddress->get('optOut'),
'invalid' => $emailAddress->get('invalid')
];
}
}
}
@@ -332,17 +381,25 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
));
$this->save($emailAddress);
} else {
if (
$emailAddress->get('optOut') != $hash[$address]['optOut'] ||
$emailAddress->get('invalid') != $hash[$address]['invalid'] ||
$emailAddress->get('emailAddress') != $hash[$address]['emailAddress']
) {
$emailAddress->set(array(
'optOut' => $hash[$address]['optOut'],
'invalid' => $hash[$address]['invalid'],
'name' => $hash[$address]['emailAddress']
));
$this->save($emailAddress);
$skipSave = $this->checkChangeIsForbidden($emailAddress, $entity);
if (!$skipSave) {
if (
$emailAddress->get('optOut') != $hash[$address]['optOut'] ||
$emailAddress->get('invalid') != $hash[$address]['invalid'] ||
$emailAddress->get('emailAddress') != $hash[$address]['emailAddress']
) {
$emailAddress->set(array(
'optOut' => $hash[$address]['optOut'],
'invalid' => $hash[$address]['invalid'],
'name' => $hash[$address]['emailAddress']
));
$this->save($emailAddress);
}
} else {
$revertData[$address] = [
'optOut' => $emailAddress->get('optOut'),
'invalid' => $emailAddress->get('invalid')
];
}
}
@@ -391,7 +448,20 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
}
}
if (!empty($revertData)) {
foreach ($emailAddressData as $row) {
if (!empty($revertData[$row->emailAddress])) {
$row->optOut = $revertData[$row->emailAddress]['optOut'];
$row->invalid = $revertData[$row->emailAddress]['invalid'];
}
}
$entity->set('emailAddressData', $emailAddressData);
}
} else {
if (!$entity->has('emailAddress')) {
return;
}
$entityRepository = $this->getEntityManager()->getRepository($entity->getEntityName());
if (!empty($emailAddressValue)) {
if ($emailAddressValue != $entity->getFetched('emailAddress')) {
@@ -436,5 +506,9 @@ class EmailAddress extends \Espo\Core\ORM\Repositories\RDB
}
}
}
}
protected function checkChangeIsForbidden($entity, $excudeEntity)
{
return !$this->getInjection('aclManager')->getImplementation('EmailAddress')->checkEditInEntity($this->getInjection('user'), $entity, $excudeEntity);
}
}
+95 -28
View File
@@ -41,33 +41,40 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
{
parent::init();
$this->addDependency('user');
$this->addDependency('acl');
$this->addDependency('aclManager');
}
public function getIds($arr = array())
protected function getAcl()
{
return $this->getInjection('acl');
}
public function getIds($numberList = [])
{
$ids = array();
if (!empty($arr)) {
$a = array_map(function ($item) {
return $item;
}, $arr);
$phoneNumbers = $this->where(array(
'name' => array_map(function ($item) {
return $item;
}, $arr)
))->find();
if (!empty($numberList)) {
$phoneNumbers = $this->where([
[
'name' => $numberList,
'hash' => null
]
])->find();
$ids = array();
$exist = array();
foreach ($phoneNumbers as $phoneNumber) {
$ids[] = $phoneNumber->id;
$exist[] = $phoneNumber->get('name');
}
foreach ($arr as $phone) {
if (empty($phone)) {
foreach ($numberList as $number) {
$number = trim($number);
if (empty($number)) {
continue;
}
if (!in_array($phone, $exist)) {
if (!in_array($number, $exist)) {
$phoneNumber = $this->get();
$phoneNumber->set('name', $phone);
$phoneNumber->set('name', $number);
$this->save($phoneNumber);
$ids[] = $phoneNumber->id;
}
@@ -112,6 +119,42 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
return $this->where(array('name' => $number))->findOne();
}
public function getEntityListByPhoneNumberId($phoneNumberId, $exceptionEntity = null)
{
$entityList = [];
$pdo = $this->getEntityManager()->getPDO();
$sql = "
SELECT entity_phone_number.entity_type AS 'entityType', entity_phone_number.entity_id AS 'entityId'
FROM entity_phone_number
WHERE
entity_phone_number.phone_number_id = ".$pdo->quote($phoneNumberId)." AND
entity_phone_number.deleted = 0
";
if ($exceptionEntity) {
$sql .= "
AND (
entity_phone_number.entity_type <> " .$pdo->quote($exceptionEntity->getEntityType()) . "
OR
entity_phone_number.entity_id <> " .$pdo->quote($exceptionEntity->id) . "
)
";
}
$sth = $pdo->prepare($sql);
$sth->execute();
while ($row = $sth->fetch()) {
if (empty($row['entityType']) || empty($row['entityId'])) continue;
if (!$this->getEntityManager()->hasRepository($row['entityType'])) continue;
$entity = $this->getEntityManager()->getEntity($row['entityType'], $row['entityId']);
if ($entity) {
$entityList[] = $entity;
}
}
return $entityList;
}
public function getEntityByPhoneNumberId($phoneNumberId, $entityType = null)
{
$pdo = $this->getEntityManager()->getPDO();
@@ -167,7 +210,7 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
$hash = array();
foreach ($phoneNumberData as $row) {
$key = $row->phoneNumber;
$key = trim($row->phoneNumber);
if (!empty($key)) {
$hash[$key] = array(
'primary' => $row->primary ? true : false,
@@ -182,7 +225,7 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
if (!empty($key)) {
$hashPrev[$key] = array(
'primary' => $row->primary ? true : false,
'type' => $row->type,
'type' => $row->type
);
}
}
@@ -192,6 +235,7 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
$toUpdate = array();
$toRemove = array();
$revertData = [];
foreach ($hash as $key => $data) {
$new = true;
@@ -243,17 +287,16 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
foreach ($toUpdate as $number) {
$phoneNumber = $this->getByNumber($number);
if ($phoneNumber) {
$skipSave = false;
if (!$this->getInjection('user')->isAdmin()) {
if ($this->getEntityByPhoneNumberId($phoneNumber->id, 'User')) {
$skipSave = true;
}
}
$skipSave = $this->checkChangeIsForbidden($phoneNumber, $entity);
if (!$skipSave) {
$phoneNumber->set(array(
'type' => $hash[$number]['type'],
));
$this->save($phoneNumber);
} else {
$revertData[$number] = [
'type' => $phoneNumber->get('type')
];
}
}
}
@@ -269,11 +312,18 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
));
$this->save($phoneNumber);
} else {
if ($phoneNumber->get('type') != $hash[$number]['type']) {
$phoneNumber->set(array(
'type' => $hash[$number]['type'],
));
$this->save($phoneNumber);
$skipSave = $this->checkChangeIsForbidden($phoneNumber, $entity);
if (!$skipSave) {
if ($phoneNumber->get('type') != $hash[$number]['type']) {
$phoneNumber->set(array(
'type' => $hash[$number]['type'],
));
$this->save($phoneNumber);
}
} else {
$revertData[$number] = [
'type' => $phoneNumber->get('type')
];
}
}
@@ -321,7 +371,20 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
$sth->execute();
}
}
if (!empty($revertData)) {
foreach ($phoneNumberData as $row) {
if (!empty($revertData[$row->phoneNumber])) {
$row->type = $revertData[$row->phoneNumber]['type'];
}
}
$entity->set('phoneNumberData', $phoneNumberData);
}
} else {
if (!$entity->has('phoneNumber')) {
return;
}
$entityRepository = $this->getEntityManager()->getRepository($entity->getEntityName());
if (!empty($phoneNumberValue)) {
if ($phoneNumberValue !== $entity->getFetched('phoneNumber')) {
@@ -370,5 +433,9 @@ class PhoneNumber extends \Espo\Core\ORM\Repositories\RDB
}
}
}
}
protected function checkChangeIsForbidden($entity, $excudeEntity)
{
return !$this->getInjection('aclManager')->getImplementation('PhoneNumber')->checkEditInEntity($this->getInjection('user'), $entity, $excudeEntity);
}
}
@@ -160,7 +160,8 @@
"dynamicLogicOptions": "Conditional options",
"probabilityMap": "Stage Probabilities (%)",
"readOnly": "Read-only",
"maxFileSize": "Max File Size (Mb)"
"maxFileSize": "Max File Size (Mb)",
"isPersonalData": "Is Personal Data"
},
"messages": {
"upgradeVersion": "EspoCRM will be upgraded to version <strong>{version}</strong>. Please be patient as this may take a while.",
@@ -227,7 +227,10 @@
"New notifications": "New notifications",
"Manage Categories": "Manage Categories",
"Manage Folders": "Manage Folders",
"Convert to": "Convert to"
"Convert to": "Convert to",
"View Personal Data": "View Personal Data",
"Personal Data": "Personal Data",
"Erase": "Erase"
},
"messages": {
"pleaseWait": "Please wait...",
@@ -283,7 +286,8 @@
"massFollowResultSingle": "{count} record now is followed",
"massUnfollowResultSingle": "{count} record now is not followed",
"massFollowZeroResult": "Nothing got followed",
"massUnfollowZeroResult": "Nothing got unfollowed"
"massUnfollowZeroResult": "Nothing got unfollowed",
"erasePersonalDataConfirmation": "Checked fields will be erased permanently. Are you sure?"
},
"boolFilters": {
"onlyMy": "Only My",
@@ -6,7 +6,8 @@
"userPermission": "User Permission",
"portalPermission": "Portal Permission",
"groupEmailAccountPermission": "Group Email Account Permission",
"exportPermission": "Export Permission"
"exportPermission": "Export Permission",
"dataPrivacyPermission": "Data Privacy Permission"
},
"links": {
"users": "Users",
@@ -17,7 +18,8 @@
"userPermission": "Allows to restrict an ability for users to view activities, calendar and stream of other users.\n\nall - can view all\n\nteam - can view activities of teammates only\n\nno - can't view",
"portalPermission": "Defines an access to portal information, ability to post messages to portal users.",
"groupEmailAccountPermission": "Defines an access to group email accounts, an ability to send emails from group SMTP.",
"exportPermission": "Defines wheter users have an ability to export records."
"exportPermission": "Defines wheter users have an ability to export records.",
"dataPrivacyPermission": "Allows to view and erase personal data."
},
"labels": {
"Access": "Access",
@@ -97,7 +97,8 @@
"authTokenPreventConcurrent": "Only one auth token per user",
"scopeColorsDisabled": "Disable scope colors",
"tabColorsDisabled": "Disable tab colors",
"tabIconsDisabled": "Disable tab icons"
"tabIconsDisabled": "Disable tab icons",
"emailAddressIsOptedOutByDefault": "Mark new email addresses as opted-out"
},
"options": {
"weekStart": {
@@ -150,7 +151,8 @@
"aclAllowDeleteCreated": "Users will be able to remove records they created even if they don't have a delete access.",
"textFilterUseContainsForVarchar": "If not checked then 'starts with' operator is used. You can use the wildcard '%'.",
"streamEmailNotificationsEntityList": "Email notifications about stream updates of followed records. Users will receive email notifications only for specified entity types.",
"authTokenPreventConcurrent": "Users won't be able to be logged in on multiple devices simultaneously."
"authTokenPreventConcurrent": "Users won't be able to be logged in on multiple devices simultaneously.",
"emailAddressIsOptedOutByDefault": "When creating new record email addess will be marked as opted-out."
},
"labels": {
"System": "System",
@@ -3,13 +3,22 @@
"rows": [
[
{"name": "name"},
false,
false
]
]
},
{
"rows": [
[
{"name": "exportPermission"},
{"name": "userPermission"}
{"name": "userPermission"},
{"name": "assignmentPermission"}
],
[
{"name": "assignmentPermission"},
{"name": "portalPermission"},
{"name": "groupEmailAccountPermission"}
{"name": "groupEmailAccountPermission"},
{"name": "dataPrivacyPermission"}
]
]
}
@@ -6,7 +6,7 @@
[{"name": "exportDisabled"}, {"name": "globalSearchEntityList"}],
[{"name": "followCreatedEntities"}, {"name": "b2cMode"}],
[{"name": "aclStrictMode"}, {"name": "aclAllowDeleteCreated"}],
[{"name": "textFilterUseContainsForVarchar"}, false]
[{"name": "textFilterUseContainsForVarchar"}, {"name": "emailAddressIsOptedOutByDefault"}]
]
},
{
@@ -112,28 +112,32 @@
"userPermission",
"portalPermission",
"groupEmailAccountPermission",
"exportPermission"
"exportPermission",
"dataPrivacyPermission"
],
"valuePermissionHighestLevels": {
"assignmentPermission": "all",
"userPermission": "all",
"portalPermission": "yes",
"groupEmailAccountPermission": "all",
"exportPermission": "yes"
"exportPermission": "yes",
"dataPrivacyPermission": "yes"
},
"permissionsDefaults": {
"assignmentPermission": "all",
"userPermission": "all",
"portalPermission": "no",
"groupEmailAccountPermission": "no",
"exportPermission": "yes"
"exportPermission": "yes",
"dataPrivacyPermission": "no"
},
"permissionsStrictDefaults": {
"assignmentPermission": "no",
"userPermission": "no",
"portalPermission": "no",
"groupEmailAccountPermission": "no",
"exportPermission": "no"
"exportPermission": "no",
"dataPrivacyPermission": "no"
},
"scopeLevelTypesDefaults": {
"boolean": true,
@@ -41,6 +41,13 @@
"tooltip": true,
"translation": "Role.options.levelList"
},
"dataPrivacyPermission": {
"type": "enum",
"options": ["not-set", "yes", "no"],
"default": "not-set",
"tooltip": true,
"translation": "Role.options.levelList"
},
"data": {
"type": "jsonObject"
},
@@ -465,6 +465,10 @@
},
"tabIconsDisabled": {
"type": "bool"
},
"emailAddressIsOptedOutByDefault": {
"type": "bool",
"tooltip": true
}
}
}
@@ -40,5 +40,6 @@
"notMergeable":true,
"notCreatable": false,
"filter": true,
"skipOrmDefs": true
"skipOrmDefs": true,
"personalData": true
}
@@ -34,5 +34,6 @@
"fieldDefs":{
"type":"jsonArray"
},
"translatedOptions": true
"translatedOptions": true,
"personalData": true
}
@@ -34,5 +34,6 @@
"fieldDefs": {
"layoutListDisabled": true
},
"hookClassName": "\\Espo\\Core\\Utils\\FieldManager\\Hooks\\AttachmentMultipleType"
"hookClassName": "\\Espo\\Core\\Utils\\FieldManager\\Hooks\\AttachmentMultipleType",
"personalData": true
}
@@ -36,5 +36,6 @@
"readOnly": true
}
},
"filter": true
"filter": true,
"personalData": true
}
@@ -60,5 +60,6 @@
"filter": true,
"fieldDefs":{
"notNull":false
}
},
"personalData": true
}
@@ -57,5 +57,6 @@
"filter": true,
"fieldDefs":{
"notNull":false
}
},
"personalData": true
}
@@ -70,5 +70,6 @@
"type":"datetime",
"notNull":false
},
"view": "views/fields/datetime-optional"
"view": "views/fields/datetime-optional",
"personalData": true
}
@@ -13,5 +13,6 @@
"filter": true,
"fieldDefs":{
"notStorable":true
}
},
"personalData": true
}
@@ -37,5 +37,6 @@
"fieldDefs":{
"type":"varchar"
},
"translatedOptions": true
"translatedOptions": true,
"personalData": true
}
@@ -32,5 +32,6 @@
"type": "belongsTo",
"entity": "Attachment",
"skipOrmDefs": true
}
},
"personalData": true
}
@@ -33,5 +33,6 @@
"type": "belongsTo",
"entity": "Attachment",
"skipOrmDefs": true
}
},
"personalData": true
}
@@ -30,5 +30,6 @@
"type":"bool"
}
],
"filter": true
"filter": true,
"personalData": true
}
@@ -29,5 +29,6 @@
"fieldDefs":{
"type":"jsonArray"
},
"translatedOptions": true
"translatedOptions": true,
"personalData": true
}
@@ -28,5 +28,6 @@
"notMergeable":true,
"notCreatable":true,
"filter":true,
"skipOrmDefs": true
"skipOrmDefs": true,
"personalData": true
}
@@ -25,5 +25,6 @@
"fieldDefs":{
"notStorable":true
},
"translatedOptions": true
"translatedOptions": true,
"personalData": true
}
@@ -36,5 +36,6 @@
"type":"bool"
}
],
"filter": true
"filter": true,
"personalData": true
}
@@ -25,5 +25,6 @@
"filter": true,
"fieldDefs":{
"type":"varchar"
}
},
"personalData": true
}
@@ -27,5 +27,6 @@
"type":"bool"
}
],
"filter": true
"filter": true,
"personalData": true
}
@@ -25,5 +25,6 @@
"filter": true,
"fieldDefs":{
"type":"text"
}
},
"personalData": true
}
+215
View File
@@ -0,0 +1,215 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Services;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\NotFound;
use Espo\ORM\Entity;
use \Espo\Core\Htmlizer\Htmlizer;
class DataPrivacy extends \Espo\Core\Services\Base
{
protected function init()
{
$this->addDependency('fileManager');
$this->addDependency('acl');
$this->addDependency('aclManager');
$this->addDependency('metadata');
$this->addDependency('serviceFactory');
$this->addDependency('dateTime');
$this->addDependency('number');
$this->addDependency('entityManager');
$this->addDependency('defaultLanguage');
$this->addDependency('fieldManagerUtil');
$this->addDependency('user');
}
protected function getAcl()
{
return $this->getInjection('acl');
}
protected function getMetadata()
{
return $this->getInjection('metadata');
}
protected function getServiceFactory()
{
return $this->getInjection('serviceFactory');
}
protected function getFileManager()
{
return $this->getInjection('fileManager');
}
protected function getEntityManager()
{
return $this->getInjection('entityManager');
}
public function erase($entityType, $id, array $fieldList)
{
if ($this->getAcl()->get('dataPrivacyPermission') === 'no') {
throw new Forbidden();
}
if ($this->getServiceFactory()->checkExists($entityType)) {
$service = $this->getServiceFactory()->create($entityType);
} else {
$service = $this->getServiceFactory()->create('Record');
$service->setEntityType($entityType);
}
$entity = $this->getEntityManager()->getEntity($entityType, $id);
if (!$entity) {
throw new NotFound();
}
if (!$this->getAcl()->check($entity, 'edit')) {
throw new Forbidden("No edit access.");
}
$forbiddenFieldList = $this->getAcl()->getScopeForbiddenFieldList($entityType, 'edit');
foreach ($fieldList as $field) {
if (in_array($field, $forbiddenFieldList)) {
throw new Forbidden("Field '{$field}' is forbidden to edit.");
}
}
$service->loadAdditionalFields($entity);
$filedManager = $this->getInjection('fieldManagerUtil');
foreach ($fieldList as $field) {
$type = $this->getMetadata()->get(['entityDefs', $entityType, 'fields', $field, 'type']);
$attributeList = $filedManager->getActualAttributeList($entityType, $field);
if ($type === 'email') {
$emailAddressList = $entity->get('emailAddresses');
foreach ($emailAddressList as $emailAddress) {
if (
$this
->getInjection('aclManager')
->getImplementation('EmailAddress')
->checkEditInEntity($this->getInjection('user'), $emailAddress, $entity)
) {
$hash = $this->getEntityManager()->getRepository('EmailAddress')->hashAddress($emailAddress->get('name'));
$emailAddress->set('hash', $hash);
$emailAddress->set('name', 'ERASED:' . $emailAddress->id);
$emailAddress->set('optOut', true);
$this->getEntityManager()->saveEntity($emailAddress);
}
}
$entity->clear($field);
$entity->clear($field . 'Data');
continue;
}
else if ($type === 'phone') {
$phoneNumberList = $entity->get('phoneNumbers');
foreach ($phoneNumberList as $phoneNumber) {
if (
$this
->getInjection('aclManager')
->getImplementation('PhoneNumber')
->checkEditInEntity($this->getInjection('user'), $phoneNumber, $entity)
) {
$hash = $this->getEntityManager()->getRepository('PhoneNumber')->hashNumber($phoneNumber->get('name'));
$phoneNumber->set('hash', $hash);
$phoneNumber->set('name', 'ERASED:' . $phoneNumber->id);
$this->getEntityManager()->saveEntity($phoneNumber);
}
}
$entity->clear($field);
$entity->clear($field . 'Data');
continue;
}
else if ($type === 'file' || $type === 'image') {
$attachmentId = $entity->get($field . 'Id');
if ($attachmentId) {
$attachment = $this->getEntityManager()->getEntity('Attachment', $attachmentId);
$this->getEntityManager()->removeEntity($attachment);
}
}
else if ($type === 'attachmentMultiple') {
$attachmentList = $entity->get($field);
foreach ($attachmentList as $attachment) {
$this->getEntityManager()->removeEntity($attachment);
}
}
foreach ($attributeList as $attribute) {
if (in_array($entity->getAttributeType($attribute), [$entity::VARCHAR, $entity::TEXT]) && $entity->get($attribute)) {
$entity->set($attribute, null);
} else {
$entity->set($attribute, null);
}
}
}
$this->getEntityManager()->saveEntity($entity);
return true;
}
public function exportPdf()
{
$htmlizer = new Htmlizer(
$this->getFileManager(),
$this->getInjection('dateTime'),
$this->getInjection('number'),
$this->getAcl(),
$this->getInjection('entityManager'),
$this->getInjection('metadata'),
$this->getInjection('defaultLanguage')
);
$pdf = new \Espo\Core\Pdf\Tcpdf();
$fontFace = $this->getConfig()->get('pdfFontFace', $this->fontFace);
$pdf->setFont($fontFace, '', $this->fontSize, '', true);
$pdf->setPrintHeader(false);
}
}
@@ -2,10 +2,14 @@
{{#each emailAddressData}}
<div>
{{#unless invalid}}
{{#unless erased}}
<a href="javascript:" data-email-address="{{emailAddress}}" data-action="mailTo">
{{/unless}}
{{/unless}}
<span {{#if invalid}}style="text-decoration: line-through;"{{/if}}{{#if optOut}}style="text-decoration: line-through;"{{/if}}>{{emailAddress}}</span>
{{#unless invalid}}
{{#unless erased}}
{{/unless}}
</a>
{{/unless}}
</div>
+1 -1
View File
@@ -1 +1 @@
<a href="javascript:" data-email-address="{{value}}" data-action="mailTo" title="{{value}}"{{#if isOptedOut}} style="text-decoration: line-through;"{{/if}}>{{value}}</a>
{{#unless isErased}}<a href="javascript:" data-email-address="{{value}}" data-action="mailTo" title="{{value}}"{{#if isOptedOut}} style="text-decoration: line-through;"{{/if}}>{{/unless}}{{value}}{{#unless isErased}}</a>{{/unless}}
+9 -1
View File
@@ -1,7 +1,15 @@
{{#if phoneNumberData}}
{{#each phoneNumberData}}
<div>
{{#if ../doNotCall}}<s>{{/if}}<a href="tel:{{phoneNumber}}" data-phone-number="{{phoneNumber}}" data-action="dial">{{phoneNumber}}</a>{{#if ../doNotCall}}</s>{{/if}}
{{#if ../doNotCall}}<s>{{/if}}
{{#unless erased}}
<a href="tel:{{phoneNumber}}" data-phone-number="{{phoneNumber}}" data-action="dial">
{{/unless}}
{{phoneNumber}}
{{#unless erased}}
</a>
{{/unless}}
{{#if ../doNotCall}}</s>{{/if}}
<span class="text-muted small">({{translateOption type scope=../../scope field=../../name}})</span>
</div>
{{/each}}
+1 -1
View File
@@ -1 +1 @@
{{#if doNotCall}}<s>{{/if}}<a href="tel:{{value}}" data-phone-number="{{value}}" data-action="dial" title="{{value}}">{{value}}</a>{{#if doNotCall}}</s>{{/if}}
{{#if doNotCall}}<s>{{/if}}{{#unless isErased}}<a href="tel:{{value}}" data-phone-number="{{value}}" data-action="dial" title="{{value}}">{{/unless}}{{value}}{{#unless isErased}}</a>{{/unless}}{{#if doNotCall}}</s>{{/if}}
@@ -0,0 +1 @@
<div class="record">{{{record}}}</div>
@@ -0,0 +1,22 @@
{{#if fieldDataList.length}}
<table class="table table-bordered">
{{#if editAccess}}
<tr>
<th width="20"><input type="checkbox" class="checkbox-all"></th>
<th width="30%"></th>
<th></th>
</tr>
{{/if}}
{{#each fieldDataList}}
<tr>
{{#if ../editAccess}}<td>{{#if editAccess}}<input type="checkbox" class="checkbox" data-name="{{name}}">{{/if}}</td>{{/if}}
<td width="30%">{{translate name category='fields' scope=../scope}}</td>
<td>
<div class="field" data-name="{{name}}">{{{var key ../this}}}</div>
</td>
</tr>
{{/each}}
</table>
{{else}}
{{translate 'No Data'}}
{{/if}}
+28 -2
View File
@@ -32,6 +32,8 @@ Espo.define('email-helper', [], function () {
this.language = language;
this.user = user;
this.dateTime = dateTime;
this.erasedPlaceholder = 'ERASED:';
}
_.extend(EmailHelper.prototype, {
@@ -120,15 +122,39 @@ Espo.define('email-helper', [], function () {
item = item.trim();
if (item != this.getUser().get('emailAddress')) {
if (isReplyOnSent) {
attributes.to += ';' + item;
if (attributes.to) {
attributes.to += ';'
}
attributes.to += item;
} else {
attributes.cc += ';' + item;
if (attributes.cc) {
attributes.cc += ';'
}
attributes.cc += item;
}
}
}, this);
attributes.cc = attributes.cc.replace(/^(\; )/,"");
}
if (attributes.to) {
var toList = attributes.to.split(';');
toList = toList.filter(function (item) {
if (item.indexOf(this.erasedPlaceholder) === 0) return false;
return true;
}, this);
attributes.to = toList.join(';');
}
if (attributes.cc) {
var ccList = attributes.cc.split(';');
ccList = ccList.filter(function (item) {
if (item.indexOf(this.erasedPlaceholder) === 0) return false;
return true;
}, this);
attributes.cc = ccList.join(';');
}
if (model.get('parentId')) {
attributes['parentId'] = model.get('parentId');
attributes['parentName'] = model.get('parentName');
@@ -102,6 +102,14 @@ Espo.define('views/admin/field-manager/edit', ['view', 'model'], function (Dep,
this.type = model.getFieldType(this.field);
}
if (
this.getMetadata().get(['scopes', this.scope, 'hasPersonalData'])
&&
this.getMetadata().get(['fields', this.type, 'personalData'])
) {
this.hasPersonalData = true;
}
Promise.race([
new Promise(function (resolve) {
if (this.isNew) {
@@ -137,6 +145,13 @@ Espo.define('views/admin/field-manager/edit', ['view', 'model'], function (Dep,
this.paramList.push(o);
}, this);
if (this.hasPersonalData) {
this.paramList.push({
name: 'isPersonalData',
type: 'bool'
});
}
this.paramList.forEach(function (o) {
this.model.defs.fields[o.name] = o;
}, this);
@@ -159,7 +174,14 @@ Espo.define('views/admin/field-manager/edit', ['view', 'model'], function (Dep,
rows: 1
});
if (this.hasPersonalData) {
this.createFieldView('bool', 'isPersonalData', null, {});
}
this.createFieldView('text', 'tooltipText', null, {
trim: true,
rows: 1
});
this.hasDynamicLogicPanel = false;
if (
+25 -5
View File
@@ -47,7 +47,7 @@ Espo.define('views/fields/email', 'views/fields/varchar', function (Dep) {
var notValid = false;
data.forEach(function (row, i) {
var emailAddress = row.emailAddress;
if (!re.test(emailAddress)) {
if (!re.test(emailAddress) && emailAddress.indexOf(this.erasedPlaceholder) !== 0) {
var msg = this.translate('fieldShouldBeEmail', 'messages').replace('{field}', this.getLabelText());
this.showValidationMessage(msg, 'div.email-address-block:nth-child(' + (i + 1).toString() + ') input');
notValid = true;
@@ -76,10 +76,16 @@ Espo.define('views/fields/email', 'views/fields/varchar', function (Dep) {
if (this.model.isNew() || !this.model.get(this.name)) {
if (!emailAddressData || !emailAddressData.length) {
emailAddressData = [{
var optOut = false;
if (this.model.isNew()) {
optOut = this.emailAddressOptedOutByDefault;
} else {
optOut = this.model.get(this.isOptedOutFieldName)
}
emailAddressData = [{
emailAddress: this.model.get(this.name) || '',
primary: true,
optOut: false,
optOut: optOut,
invalid: false
}];
}
@@ -90,7 +96,7 @@ Espo.define('views/fields/email', 'views/fields/varchar', function (Dep) {
}
if ((!emailAddressData || emailAddressData.length === 0) && this.model.get(this.name)) {
emailAddressData = [{
emailAddressData = [{
emailAddress: this.model.get(this.name),
primary: true,
optOut: false,
@@ -98,12 +104,22 @@ Espo.define('views/fields/email', 'views/fields/varchar', function (Dep) {
}];
}
if (emailAddressData) {
emailAddressData = Espo.Utils.clone(emailAddressData);
emailAddressData.forEach(function (item) {
item.erased = item.emailAddress.indexOf(this.erasedPlaceholder) === 0
}, this);
}
var data = _.extend({
emailAddressData: emailAddressData
}, Dep.prototype.data.call(this));
if (this.mode === 'list' || this.mode === 'detail') {
data.isOptedOut = this.model.get(this.isOptedOutFieldName);
if (this.model.get(this.name)) {
data.isErased = this.model.get(this.name).indexOf(this.erasedPlaceholder) === 0
}
}
return data;
@@ -177,7 +193,7 @@ Espo.define('views/fields/email', 'views/fields/varchar', function (Dep) {
o = {
emailAddress: '',
primary: data.length ? false : true,
optOut: false,
optOut: this.emailAddressOptedOutByDefault,
invalid: false,
lower: ''
};
@@ -346,6 +362,10 @@ Espo.define('views/fields/email', 'views/fields/varchar', function (Dep) {
setup: function () {
this.dataFieldName = this.name + 'Data';
this.isOptedOutFieldName = this.name + 'IsOptedOut';
this.erasedPlaceholder = 'ERASED:';
this.emailAddressOptedOutByDefault = this.getConfig().get('emailAddressIsOptedOutByDefault');
},
fetchEmailAddressData: function () {
+18 -1
View File
@@ -68,6 +68,13 @@ Espo.define('views/fields/phone', 'views/fields/varchar', function (Dep) {
phoneNumberData = this.model.get(this.dataFieldName) || false;
}
if (phoneNumberData) {
phoneNumberData = Espo.Utils.clone(phoneNumberData);
phoneNumberData.forEach(function (item) {
item.erased = item.phoneNumber.indexOf(this.erasedPlaceholder) === 0
}, this);
}
if ((!phoneNumberData || phoneNumberData.length === 0) && this.model.get(this.name)) {
phoneNumberData = [{
phoneNumber: this.model.get(this.name),
@@ -77,10 +84,18 @@ Espo.define('views/fields/phone', 'views/fields/varchar', function (Dep) {
}];
}
return _.extend({
var data = _.extend({
phoneNumberData: phoneNumberData,
doNotCall: this.model.get('doNotCall')
}, Dep.prototype.data.call(this));
if (this.mode === 'detail' || this.mode === 'list') {
if (this.model.get(this.name)) {
data.isErased = this.model.get(this.name).indexOf(this.erasedPlaceholder) === 0
}
}
return data;
},
events: {
@@ -220,6 +235,8 @@ Espo.define('views/fields/phone', 'views/fields/varchar', function (Dep) {
this.reRender();
}, this);
}
this.erasedPlaceholder = 'ERASED:';
},
fetchPhoneNumberData: function () {
@@ -0,0 +1,105 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/personal-data/modals/personal-data', ['views/modal'], function (Dep) {
return Dep.extend({
className: 'dialog dialog-record',
template: 'personal-data/modals/personal-data',
backdrop: true,
setup: function () {
Dep.prototype.setup.call(this);
this.buttonList = [
{
name: 'cancel',
label: 'Close'
}
];
this.header = this.getLanguage().translate('Personal Data');
this.header += ': ' + Handlebars.Utils.escapeExpression(this.model.get('name'));
if (this.getAcl().check(this.model, 'edit')) {
this.buttonList.unshift({
name: 'erase',
label: 'Erase',
style: 'danger',
disabled: true
});
}
this.fieldList = [];
this.scope = this.model.name;
this.createView('record', 'views/personal-data/record/record', {
el: this.getSelector() + ' .record',
model: this.model
}, function (view) {
this.listenTo(view, 'check', function (fieldList) {
this.fieldList = fieldList;
if (fieldList.length) {
this.enableButton('erase');
} else {
this.disableButton('erase');
}
});
if (!view.fieldList.length) {
this.disableButton('export');
}
});
},
actionErase: function () {
this.confirm({
message: this.translate('erasePersonalDataConfirmation', 'messages'),
confirmText: this.translate('Erase')
}, function () {
this.disableButton('erase');
this.ajaxPostRequest('DataPrivacy/action/erase', {
fieldList: this.fieldList,
entityType: this.scope,
id: this.model.id
}).then(function () {
Espo.Ui.success(this.translate('Done'));
this.trigger('erase');
}.bind(this)).fail(function () {
this.enableButton('erase');
}.bind(this));
}.bind(this));
}
});
});
@@ -0,0 +1,153 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
Espo.define('views/personal-data/record/record', 'views/record/base', function (Dep) {
return Dep.extend({
template: 'personal-data/record/record',
events: _.extend({
'click .checkbox': function (e) {
var name = $(e.currentTarget).data('name');
if (e.currentTarget.checked) {
if (!~this.checkedFieldList.indexOf(name)) {
this.checkedFieldList.push(name);
}
if (this.checkedFieldList.length == this.fieldList.length) {
this.$el.find('.checkbox-all').prop('checked', true);
} else {
this.$el.find('.checkbox-all').prop('checked', false);
}
} else {
var index = this.checkedFieldList.indexOf(name);
if (~index) {
this.checkedFieldList.splice(index, 1);
}
this.$el.find('.checkbox-all').prop('checked', false);
}
this.trigger('check', this.checkedFieldList);
},
'click .checkbox-all': function (e) {
if (e.currentTarget.checked) {
this.checkedFieldList = Espo.Utils.clone(this.fieldList);
this.$el.find('.checkbox').prop('checked', true);
} else {
this.checkedFieldList = [];
this.$el.find('.checkbox').prop('checked', false);
}
this.trigger('check', this.checkedFieldList);
},
}, Dep.prototype.events),
data: function () {
var data = {};
data.fieldDataList = this.getFieldDataList();
data.scope = this.scope;
data.editAccess = this.editAccess;
return data;
},
setup: function () {
Dep.prototype.setup.call(this);
this.scope = this.model.name;
this.fieldList = [];
this.checkedFieldList = [];
this.editAccess = this.getAcl().check(this.model, 'edit');
var fieldDefs = this.getMetadata().get(['entityDefs', this.scope, 'fields']) || {};
var fieldList = [];
for (var field in fieldDefs) {
var defs = fieldDefs[field];
if (defs.isPersonalData) {
fieldList.push(field);
}
}
fieldList.forEach(function (field) {
var type = fieldDefs[field].type;
var attributeList = this.getFieldManager().getActualAttributeList(type, field);
var isNotEmpty = false;
attributeList.forEach(function (attribute) {
var value = this.model.get(attribute);
if (value) {
if (Object.prototype.toString.call(value) === '[object Array]') {
if (value.length) {
return;
}
}
isNotEmpty = true;
}
}, this);
var hasAccess = !~this.getAcl().getScopeForbiddenFieldList(this.scope, 'view').indexOf(field);
if (isNotEmpty && hasAccess) {
this.fieldList.push(field);
}
}, this);
this.fieldList = this.fieldList.sort(function (v1, v2) {
return this.translate(v1, 'fields', this.scope).localeCompare(this.translate(v2, 'fields', this.scope));
}.bind(this));
this.fieldList.forEach(function (field) {
this.createField(field, null, null, 'detail', true);
}, this);
},
getFieldDataList: function () {
var forbiddenList = this.getAcl().getScopeForbiddenFieldList(this.scope, 'edit');
var list = [];
this.fieldList.forEach(function (field) {
list.push({
name: field,
key: field + 'Field',
editAccess: this.editAccess && !~forbiddenList.indexOf(field)
});
}, this);
return list;
}
});
});
+22
View File
@@ -245,6 +245,15 @@ Espo.define('views/record/detail', ['views/record/base', 'view-record-helper'],
});
}
}
if (this.type === 'detail' && this.getMetadata().get(['scopes', this.scope, 'hasPersonalData'])) {
if (this.getAcl().get('dataPrivacyPermission') !== 'no') {
this.dropdownItemList.push({
'label': 'View Personal Data',
'name': 'viewPersonalData'
});
}
}
},
hideActionItem: function (name) {
@@ -874,6 +883,19 @@ Espo.define('views/record/detail', ['views/record/base', 'view-record-helper'],
}
},
actionViewPersonalData: function () {
this.createView('viewPersonalData', 'views/personal-data/modals/personal-data', {
model: this.model
}, function (view) {
view.render();
this.listenToOnce(view, 'erase', function () {
this.clearView('viewPersonalData');
this.model.fetch();
}, this);
});
},
actionPrintPdf: function () {
this.createView('pdfTemplate', 'views/modals/select-template', {
entityType: this.model.name