1<?php 2/** 3 * 2007-2016 PrestaShop 4 * 5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA 6 * Copyright (C) 2017-2018 thirty bees 7 * 8 * NOTICE OF LICENSE 9 * 10 * This source file is subject to the Open Software License (OSL 3.0) 11 * that is bundled with this package in the file LICENSE.txt. 12 * It is also available through the world-wide-web at this URL: 13 * http://opensource.org/licenses/osl-3.0.php 14 * If you did not receive a copy of the license and are unable to 15 * obtain it through the world-wide-web, please send an email 16 * to license@thirtybees.com so we can send you a copy immediately. 17 * 18 * DISCLAIMER 19 * 20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer 21 * versions in the future. If you wish to customize PrestaShop for your 22 * needs please refer to https://www.thirtybees.com for more information. 23 * 24 * @author thirty bees <contact@thirtybees.com> 25 * @author PrestaShop SA <contact@prestashop.com> 26 * @copyright 2017-2018 thirty bees 27 * @copyright 2007-2016 PrestaShop SA 28 * @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) 29 * PrestaShop is an internationally registered trademark & property of PrestaShop SA 30 */ 31 32/** 33 * Class AdminCustomerThreadsControllerCore 34 * 35 * @since 1.0.0 36 */ 37class AdminCustomerThreadsControllerCore extends AdminController 38{ 39 /** 40 * AdminCustomerThreadsControllerCore constructor. 41 * 42 * @since 1.0.0 43 */ 44 public function __construct() 45 { 46 $this->bootstrap = true; 47 $this->context = Context::getContext(); 48 $this->table = 'customer_thread'; 49 $this->className = 'CustomerThread'; 50 $this->lang = false; 51 52 $contactArray = []; 53 $contacts = Contact::getContacts($this->context->language->id); 54 55 foreach ($contacts as $contact) { 56 $contactArray[$contact['id_contact']] = $contact['name']; 57 } 58 59 $languageArray = []; 60 $languages = Language::getLanguages(); 61 foreach ($languages as $language) { 62 $languageArray[$language['id_lang']] = $language['name']; 63 } 64 65 $iconArray = [ 66 'open' => ['class' => 'icon-circle text-success', 'alt' => $this->l('Open')], 67 'closed' => ['class' => 'icon-circle text-danger', 'alt' => $this->l('Closed')], 68 'pending1' => ['class' => 'icon-circle text-warning', 'alt' => $this->l('Pending 1')], 69 'pending2' => ['class' => 'icon-circle text-warning', 'alt' => $this->l('Pending 2')], 70 ]; 71 72 $statusArray = []; 73 foreach ($iconArray as $k => $v) { 74 $statusArray[$k] = $v['alt']; 75 } 76 77 $this->fields_list = [ 78 'id_customer_thread' => [ 79 'title' => $this->l('ID'), 80 'align' => 'center', 81 'class' => 'fixed-width-xs', 82 ], 83 'customer' => [ 84 'title' => $this->l('Customer'), 85 'filter_key' => 'customer', 86 'tmpTableFilter' => true, 87 ], 88 'email' => [ 89 'title' => $this->l('Email'), 90 'filter_key' => 'a!email', 91 ], 92 'contact' => [ 93 'title' => $this->l('Type'), 94 'type' => 'select', 95 'list' => $contactArray, 96 'filter_key' => 'cl!id_contact', 97 'filter_type' => 'int', 98 ], 99 'language' => [ 100 'title' => $this->l('Language'), 101 'type' => 'select', 102 'list' => $languageArray, 103 'filter_key' => 'l!id_lang', 104 'filter_type' => 'int', 105 ], 106 'status' => [ 107 'title' => $this->l('Status'), 108 'type' => 'select', 109 'list' => $statusArray, 110 'icon' => $iconArray, 111 'align' => 'center', 112 'filter_key' => 'a!status', 113 'filter_type' => 'string', 114 ], 115 'employee' => [ 116 'title' => $this->l('Employee'), 117 'filter_key' => 'employee', 118 'tmpTableFilter' => true, 119 ], 120 'messages' => [ 121 'title' => $this->l('Messages'), 122 'filter_key' => 'messages', 123 'tmpTableFilter' => true, 124 'maxlength' => 40, 125 ], 126 'private' => [ 127 'title' => $this->l('Private'), 128 'type' => 'select', 129 'filter_key' => 'private', 130 'align' => 'center', 131 'cast' => 'intval', 132 'callback' => 'printOptinIcon', 133 'list' => [ 134 '0' => $this->l('No'), 135 '1' => $this->l('Yes'), 136 ], 137 ], 138 'date_upd' => [ 139 'title' => $this->l('Last message'), 140 'havingFilter' => true, 141 'type' => 'datetime', 142 ], 143 ]; 144 145 $this->bulk_actions = [ 146 'delete' => [ 147 'text' => $this->l('Delete selected'), 148 'confirm' => $this->l('Delete selected items?'), 149 'icon' => 'icon-trash', 150 ], 151 ]; 152 153 $this->shopLinkType = 'shop'; 154 155 $this->fields_options = [ 156 'contact' => [ 157 'title' => $this->l('Contact options'), 158 'fields' => [ 159 'PS_CUSTOMER_SERVICE_FILE_UPLOAD' => [ 160 'title' => $this->l('Allow file uploading'), 161 'hint' => $this->l('Allow customers to upload files using the contact page.'), 162 'type' => 'bool', 163 ], 164 'PS_CUSTOMER_SERVICE_SIGNATURE' => [ 165 'title' => $this->l('Default message'), 166 'hint' => $this->l('Please fill out the message fields that appear by default when you answer a thread on the customer service page.'), 167 'type' => 'textareaLang', 168 'lang' => true, 169 ], 170 ], 171 'submit' => ['title' => $this->l('Save')], 172 ], 173 'general' => [ 174 'title' => $this->l('Customer service options'), 175 'fields' => [ 176 'PS_SAV_IMAP_URL' => [ 177 'title' => $this->l('IMAP URL'), 178 'hint' => $this->l('URL for your IMAP server (ie.: mail.server.com).'), 179 'type' => 'text', 180 ], 181 'PS_SAV_IMAP_PORT' => [ 182 'title' => $this->l('IMAP port'), 183 'hint' => $this->l('Port to use to connect to your IMAP server.'), 184 'type' => 'text', 185 'defaultValue' => 143, 186 ], 187 'PS_SAV_IMAP_USER' => [ 188 'title' => $this->l('IMAP user'), 189 'hint' => $this->l('User to use to connect to your IMAP server.'), 190 'type' => 'text', 191 ], 192 'PS_SAV_IMAP_PWD' => [ 193 'title' => $this->l('IMAP password'), 194 'hint' => $this->l('Password to use to connect your IMAP server.'), 195 'type' => 'text', 196 ], 197 'PS_SAV_IMAP_DELETE_MSG' => [ 198 'title' => $this->l('Delete messages'), 199 'hint' => $this->l('Delete messages after synchronization. If you do not enable this option, the synchronization will take more time.'), 200 'type' => 'bool', 201 ], 202 'PS_SAV_IMAP_CREATE_THREADS' => [ 203 'title' => $this->l('Create new threads'), 204 'hint' => $this->l('Create new threads for unrecognized emails.'), 205 'type' => 'bool', 206 ], 207 'PS_SAV_IMAP_OPT_NORSH' => [ 208 'title' => $this->l('IMAP options').' (/norsh)', 209 'type' => 'bool', 210 'hint' => $this->l('Do not use RSH or SSH to establish a preauthenticated IMAP sessions.'), 211 ], 212 'PS_SAV_IMAP_OPT_SSL' => [ 213 'title' => $this->l('IMAP options').' (/ssl)', 214 'type' => 'bool', 215 'hint' => $this->l('Use the Secure Socket Layer (TLS/SSL) to encrypt the session.'), 216 ], 217 'PS_SAV_IMAP_OPT_VALIDATE-CERT' => [ 218 'title' => $this->l('IMAP options').' (/validate-cert)', 219 'type' => 'bool', 220 'hint' => $this->l('Validate certificates from the TLS/SSL server.'), 221 ], 222 'PS_SAV_IMAP_OPT_NOVALIDATE-CERT' => [ 223 'title' => $this->l('IMAP options').' (/novalidate-cert)', 224 'type' => 'bool', 225 'hint' => $this->l('Do not validate certificates from the TLS/SSL server. This is only needed if a server uses self-signed certificates.'), 226 ], 227 'PS_SAV_IMAP_OPT_TLS' => [ 228 'title' => $this->l('IMAP options').' (/tls)', 229 'type' => 'bool', 230 'hint' => $this->l('Force use of start-TLS to encrypt the session, and reject connection to servers that do not support it.'), 231 ], 232 'PS_SAV_IMAP_OPT_NOTLS' => [ 233 'title' => $this->l('IMAP options').' (/notls)', 234 'type' => 'bool', 235 'hint' => $this->l('Do not use start-TLS to encrypt the session, even with servers that support it.'), 236 ], 237 ], 238 'submit' => ['title' => $this->l('Save')], 239 ], 240 ]; 241 242 parent::__construct(); 243 } 244 245 /** 246 * Render list 247 * 248 * @return string 249 * 250 * @since 1.0.0 251 */ 252 public function renderList() 253 { 254 // Check the new IMAP messages before rendering the list 255 $this->renderProcessSyncImap(); 256 257 $this->addRowAction('view'); 258 $this->addRowAction('delete'); 259 260 $this->_select = ' 261 CONCAT(c.`firstname`," ",c.`lastname`) as customer, cl.`name` as contact, l.`name` as language, group_concat(message) as messages, cm.private, 262 ( 263 SELECT IFNULL(CONCAT(LEFT(e.`firstname`, 1),". ",e.`lastname`), "--") 264 FROM `'._DB_PREFIX_.'customer_message` cm2 265 INNER JOIN '._DB_PREFIX_.'employee e 266 ON e.`id_employee` = cm2.`id_employee` 267 WHERE cm2.id_employee > 0 268 AND cm2.`id_customer_thread` = a.`id_customer_thread` 269 ORDER BY cm2.`date_add` DESC LIMIT 1 270 ) as employee'; 271 272 $this->_join = ' 273 LEFT JOIN `'._DB_PREFIX_.'customer` c 274 ON c.`id_customer` = a.`id_customer` 275 LEFT JOIN `'._DB_PREFIX_.'customer_message` cm 276 ON cm.`id_customer_thread` = a.`id_customer_thread` 277 LEFT JOIN `'._DB_PREFIX_.'lang` l 278 ON l.`id_lang` = a.`id_lang` 279 LEFT JOIN `'._DB_PREFIX_.'contact_lang` cl 280 ON (cl.`id_contact` = a.`id_contact` AND cl.`id_lang` = '.(int) $this->context->language->id.')'; 281 282 if ($idOrder = Tools::getValue('id_order')) { 283 $this->_where .= ' AND id_order = '.(int) $idOrder; 284 } 285 286 $this->_group = 'GROUP BY cm.id_customer_thread'; 287 $this->_orderBy = 'date_upd'; 288 $this->_orderWay = 'DESC'; 289 290 $contacts = CustomerThread::getContacts(); 291 292 $categories = Contact::getCategoriesContacts(); 293 294 $params = [ 295 $this->l('Total threads') => $all = CustomerThread::getTotalCustomerThreads(), 296 $this->l('Threads pending') => $pending = CustomerThread::getTotalCustomerThreads('status LIKE "%pending%"'), 297 $this->l('Total number of customer messages') => CustomerMessage::getTotalCustomerMessages('id_employee = 0'), 298 $this->l('Total number of employee messages') => CustomerMessage::getTotalCustomerMessages('id_employee != 0'), 299 $this->l('Unread threads') => $unread = CustomerThread::getTotalCustomerThreads('status = "open"'), 300 $this->l('Closed threads') => $all - ($unread + $pending), 301 ]; 302 303 $this->tpl_list_vars = [ 304 'contacts' => $contacts, 305 'categories' => $categories, 306 'params' => $params, 307 ]; 308 309 return parent::renderList(); 310 } 311 312 /** 313 * Call the IMAP synchronization during the render process. 314 */ 315 public function renderProcessSyncImap() 316 { 317 // To avoid an error if the IMAP isn't configured, we check the configuration here, like during 318 // the synchronization. All parameters will exists. 319 if (!(Configuration::get('PS_SAV_IMAP_URL') 320 || Configuration::get('PS_SAV_IMAP_PORT') 321 || Configuration::get('PS_SAV_IMAP_USER') 322 || Configuration::get('PS_SAV_IMAP_PWD')) 323 ) { 324 return; 325 } 326 327 // Executes the IMAP synchronization. 328 $syncErrors = $this->syncImap(); 329 330 // Show the errors. 331 if (isset($syncErrors['hasError']) && $syncErrors['hasError']) { 332 if (isset($syncErrors['errors'])) { 333 foreach ($syncErrors['errors'] as &$error) { 334 $this->displayWarning($error); 335 } 336 } 337 } 338 } 339 340 /** 341 * Imap synchronization method. 342 * 343 * @return array Errors list. 344 */ 345 public function syncImap() 346 { 347 if (!($url = Configuration::get('PS_SAV_IMAP_URL')) 348 || !($port = Configuration::get('PS_SAV_IMAP_PORT')) 349 || !($user = Configuration::get('PS_SAV_IMAP_USER')) 350 || !($password = Configuration::get('PS_SAV_IMAP_PWD')) 351 ) { 352 return ['hasError' => true, 'errors' => ['IMAP configuration is not correct']]; 353 } 354 355 $conf = Configuration::getMultiple( 356 [ 357 'PS_SAV_IMAP_OPT_NORSH', 358 'PS_SAV_IMAP_OPT_SSL', 359 'PS_SAV_IMAP_OPT_VALIDATE-CERT', 360 'PS_SAV_IMAP_OPT_NOVALIDATE-CERT', 361 'PS_SAV_IMAP_OPT_TLS', 362 'PS_SAV_IMAP_OPT_NOTLS', 363 ] 364 ); 365 366 $confStr = ''; 367 if ($conf['PS_SAV_IMAP_OPT_NORSH']) { 368 $confStr .= '/norsh'; 369 } 370 if ($conf['PS_SAV_IMAP_OPT_SSL']) { 371 $confStr .= '/ssl'; 372 } 373 if ($conf['PS_SAV_IMAP_OPT_VALIDATE-CERT']) { 374 $confStr .= '/validate-cert'; 375 } 376 if ($conf['PS_SAV_IMAP_OPT_NOVALIDATE-CERT']) { 377 $confStr .= '/novalidate-cert'; 378 } 379 if ($conf['PS_SAV_IMAP_OPT_TLS']) { 380 $confStr .= '/tls'; 381 } 382 if ($conf['PS_SAV_IMAP_OPT_NOTLS']) { 383 $confStr .= '/notls'; 384 } 385 386 if (!function_exists('imap_open')) { 387 return ['hasError' => true, 'errors' => ['imap is not installed on this server']]; 388 } 389 390 $mbox = @imap_open('{'.$url.':'.$port.$confStr.'}', $user, $password); 391 392 //checks if there is no error when connecting imap server 393 $errors = imap_errors(); 394 if (is_array($errors)) { 395 $errors = array_unique($errors); 396 } 397 $strErrors = ''; 398 $strErrorDelete = ''; 399 400 if (count($errors) && is_array($errors)) { 401 $strErrors = ''; 402 foreach ($errors as $error) { 403 $strErrors .= $error.', '; 404 } 405 $strErrors = rtrim(trim($strErrors), ','); 406 } 407 //checks if imap connexion is active 408 if (!$mbox) { 409 return ['hasError' => true, 'errors' => ['Cannot connect to the mailbox :<br />'.($strErrors)]]; 410 } 411 412 //Returns information about the current mailbox. Returns FALSE on failure. 413 $check = imap_check($mbox); 414 if (!$check) { 415 return ['hasError' => true, 'errors' => ['Fail to get information about the current mailbox']]; 416 } 417 418 if ($check->Nmsgs == 0) { 419 return ['hasError' => true, 'errors' => ['NO message to sync']]; 420 } 421 422 $result = imap_fetch_overview($mbox, "1:{$check->Nmsgs}", 0); 423 foreach ($result as $overview) { 424 //check if message exist in database 425 if (isset($overview->subject)) { 426 $subject = $overview->subject; 427 } else { 428 $subject = ''; 429 } 430 //Creating an md5 to check if message has been allready processed 431 $md5 = md5($overview->date.$overview->from.$subject.$overview->msgno); 432 $exist = Db::getInstance()->getValue( 433 (new DbQuery()) 434 ->select('`md5_header`') 435 ->from('customer_message_sync_imap') 436 ->where('`md5_header` = \''.pSQL($md5).'\'') 437 ); 438 if ($exist) { 439 if (Configuration::get('PS_SAV_IMAP_DELETE_MSG')) { 440 if (!imap_delete($mbox, $overview->msgno)) { 441 $strErrorDelete = ', Fail to delete message'; 442 } 443 } 444 } else { 445 //check if subject has id_order 446 preg_match('/\#ct([0-9]*)/', $subject, $matches1); 447 preg_match('/\#tc([0-9-a-z-A-Z]*)/', $subject, $matches2); 448 $matchFound = false; 449 if (isset($matches1[1]) && isset($matches2[1])) { 450 $matchFound = true; 451 } 452 453 $newCt = (Configuration::get('PS_SAV_IMAP_CREATE_THREADS') && !$matchFound && (strpos($subject, '[no_sync]') == false)); 454 455 if ($matchFound || $newCt) { 456 if ($newCt) { 457 if (!preg_match('/<('.Tools::cleanNonUnicodeSupport('[a-z\p{L}0-9!#$%&\'*+\/=?^`{}|~_-]+[.a-z\p{L}0-9!#$%&\'*+\/=?^`{}|~_-]*@[a-z\p{L}0-9]+[._a-z\p{L}0-9-]*\.[a-z0-9]+').')>/', $overview->from, $result) 458 || !Validate::isEmail($from = Tools::convertEmailToIdn($result[1])) 459 ) { 460 continue; 461 } 462 463 // we want to assign unrecognized mails to the right contact category 464 $contacts = Contact::getContacts($this->context->language->id); 465 if (!$contacts) { 466 continue; 467 } 468 469 foreach ($contacts as $contact) { 470 if (strpos($overview->to, $contact['email']) !== false) { 471 $idContact = $contact['id_contact']; 472 } 473 } 474 475 if (!isset($idContact)) { // if not use the default contact category 476 $idContact = $contacts[0]['id_contact']; 477 } 478 479 $customer = new Customer(); 480 $client = $customer->getByEmail($from); //check if we already have a customer with this email 481 $ct = new CustomerThread(); 482 if (isset($client->id)) { //if mail is owned by a customer assign to him 483 $ct->id_customer = $client->id; 484 } 485 $ct->email = $from; 486 $ct->id_contact = $idContact; 487 $ct->id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); 488 $ct->id_shop = $this->context->shop->id; //new customer threads for unrecognized mails are not shown without shop id 489 $ct->status = 'open'; 490 $ct->token = Tools::passwdGen(12); 491 $ct->add(); 492 } else { 493 $ct = new CustomerThread((int) $matches1[1]); 494 } //check if order exist in database 495 496 if (Validate::isLoadedObject($ct) && ((isset($matches2[1]) && $ct->token == $matches2[1]) || $newCt)) { 497 $message = imap_fetchbody($mbox, $overview->msgno, 1); 498 if (base64_encode(base64_decode($message)) === $message) { 499 $message = base64_decode($message); 500 } 501 $message = quoted_printable_decode($message); 502 $message = utf8_encode($message); 503 $message = quoted_printable_decode($message); 504 $message = nl2br($message); 505 $message = mb_substr($message, 0, (int) CustomerMessage::$definition['fields']['message']['size']); 506 507 $cm = new CustomerMessage(); 508 $cm->id_customer_thread = $ct->id; 509 if (empty($message) || !Validate::isCleanHtml($message)) { 510 $strErrors .= Tools::displayError(sprintf('Invalid Message Content for subject: %1s', $subject)); 511 } else { 512 $cm->message = $message; 513 $cm->add(); 514 } 515 } 516 } 517 Db::getInstance()->insert( 518 'customer_message_sync_imap', 519 [ 520 'md5_header' => pSQL($md5), 521 ] 522 ); 523 } 524 } 525 imap_expunge($mbox); 526 imap_close($mbox); 527 if ($strErrors.$strErrorDelete) { 528 return ['hasError' => true, 'errors' => [$strErrors.$strErrorDelete]]; 529 } else { 530 return ['hasError' => false, 'errors' => '']; 531 } 532 } 533 534 /** 535 * @return void 536 * 537 * @since 1.0.0 538 */ 539 public function initToolbar() 540 { 541 parent::initToolbar(); 542 unset($this->toolbar_btn['new']); 543 } 544 545 /** 546 * @param mixed $value 547 * @param Customer $customer 548 * 549 * @return string 550 * 551 * @since 1.0.0 552 */ 553 public function printOptinIcon($value, $customer) 554 { 555 return ($value ? '<i class="icon-check"></i>' : '<i class="icon-remove"></i>'); 556 } 557 558 /** 559 * @return bool 560 * 561 * @since 1.0.0 562 */ 563 public function postProcess() 564 { 565 if ($idCustomerThread = (int) Tools::getValue('id_customer_thread')) { 566 if (($idContact = (int) Tools::getValue('id_contact'))) { 567 Db::getInstance()->execute( 568 ' 569 UPDATE '._DB_PREFIX_.'customer_thread 570 SET id_contact = '.(int) $idContact.' 571 WHERE id_customer_thread = '.(int) $idCustomerThread 572 ); 573 } 574 if ($idStatus = (int) Tools::getValue('setstatus')) { 575 $statusArray = [1 => 'open', 2 => 'closed', 3 => 'pending1', 4 => 'pending2']; 576 Db::getInstance()->execute( 577 ' 578 UPDATE '._DB_PREFIX_.'customer_thread 579 SET status = "'.$statusArray[$idStatus].'" 580 WHERE id_customer_thread = '.(int) $idCustomerThread.' LIMIT 1 581 ' 582 ); 583 } 584 if (isset($_POST['id_employee_forward'])) { 585 $messages = Db::getInstance()->getRow( 586 ' 587 SELECT ct.*, cm.*, cl.name subject, CONCAT(e.firstname, \' \', e.lastname) employee_name, 588 CONCAT(c.firstname, \' \', c.lastname) customer_name, c.firstname 589 FROM '._DB_PREFIX_.'customer_thread ct 590 LEFT JOIN '._DB_PREFIX_.'customer_message cm 591 ON (ct.id_customer_thread = cm.id_customer_thread) 592 LEFT JOIN '._DB_PREFIX_.'contact_lang cl 593 ON (cl.id_contact = ct.id_contact AND cl.id_lang = '.(int) $this->context->language->id.') 594 LEFT OUTER JOIN '._DB_PREFIX_.'employee e 595 ON e.id_employee = cm.id_employee 596 LEFT OUTER JOIN '._DB_PREFIX_.'customer c 597 ON (c.email = ct.email) 598 WHERE ct.id_customer_thread = '.(int) Tools::getValue('id_customer_thread').' 599 ORDER BY cm.date_add DESC 600 ' 601 ); 602 $output = $this->displayMessage($messages, true, (int) Tools::getValue('id_employee_forward')); 603 $cm = new CustomerMessage(); 604 $cm->id_employee = (int) $this->context->employee->id; 605 $cm->id_customer_thread = (int) Tools::getValue('id_customer_thread'); 606 $cm->ip_address = (int) ip2long(Tools::getRemoteAddr()); 607 $currentEmployee = $this->context->employee; 608 $idEmployee = (int) Tools::getValue('id_employee_forward'); 609 $employee = new Employee($idEmployee); 610 $email = Tools::convertEmailToIdn(Tools::getValue('email')); 611 $message = Tools::getValue('message_forward'); 612 if (($error = $cm->validateField('message', $message, null, [], true)) !== true) { 613 $this->errors[] = $error; 614 } elseif ($idEmployee && $employee && Validate::isLoadedObject($employee)) { 615 $params = [ 616 '{messages}' => stripslashes($output), 617 '{employee}' => $currentEmployee->firstname.' '.$currentEmployee->lastname, 618 '{comment}' => stripslashes(Tools::nl2br($_POST['message_forward'])), 619 '{firstname}' => $employee->firstname, 620 '{lastname}' => $employee->lastname, 621 ]; 622 623 if (Mail::Send( 624 $this->context->language->id, 625 'forward_msg', 626 Mail::l('Fwd: Customer message', $this->context->language->id), 627 $params, 628 $employee->email, 629 $employee->firstname.' '.$employee->lastname, 630 $currentEmployee->email, 631 $currentEmployee->firstname.' '.$currentEmployee->lastname, 632 null, 633 null, 634 _PS_MAIL_DIR_, 635 true 636 )) { 637 $cm->private = 1; 638 $cm->message = $this->l('Message forwarded to').' '.$employee->firstname.' '.$employee->lastname."\n".$this->l('Comment:').' '.$message; 639 $cm->add(); 640 } 641 } elseif ($email && Validate::isEmail($email)) { 642 $params = [ 643 '{messages}' => Tools::nl2br(stripslashes($output)), 644 '{employee}' => $currentEmployee->firstname.' '.$currentEmployee->lastname, 645 '{comment}' => stripslashes($_POST['message_forward']), 646 '{firstname}' => '', 647 '{lastname}' => '', 648 ]; 649 650 if (Mail::Send( 651 $this->context->language->id, 652 'forward_msg', 653 Mail::l('Fwd: Customer message', $this->context->language->id), 654 $params, 655 $email, 656 null, 657 $currentEmployee->email, 658 $currentEmployee->firstname.' '.$currentEmployee->lastname, 659 null, 660 null, 661 _PS_MAIL_DIR_, 662 true 663 )) { 664 $cm->message = $this->l('Message forwarded to').' '.Tools::convertEmailFromIdn($email)."\n".$this->l('Comment:').' '.$message; 665 $cm->add(); 666 } 667 } else { 668 $this->errors[] = '<div class="alert error">'.Tools::displayError('The email address is invalid.').'</div>'; 669 } 670 } 671 if (Tools::isSubmit('submitReply')) { 672 $ct = new CustomerThread($idCustomerThread); 673 674 ShopUrl::cacheMainDomainForShop((int) $ct->id_shop); 675 676 $cm = new CustomerMessage(); 677 $cm->id_employee = (int) $this->context->employee->id; 678 $cm->id_customer_thread = $ct->id; 679 $cm->ip_address = (int) ip2long(Tools::getRemoteAddr()); 680 $cm->message = Tools::getValue('reply_message'); 681 if (($error = $cm->validateField('message', $cm->message, null, [], true)) !== true) { 682 $this->errors[] = $error; 683 } elseif (isset($_FILES) && !empty($_FILES['joinFile']['name']) && $_FILES['joinFile']['error'] != 0) { 684 $this->errors[] = Tools::displayError('An error occurred during the file upload process.'); 685 } elseif ($cm->add()) { 686 $fileAttachment = null; 687 if (!empty($_FILES['joinFile']['name'])) { 688 $fileAttachment['content'] = file_get_contents($_FILES['joinFile']['tmp_name']); 689 $fileAttachment['name'] = $_FILES['joinFile']['name']; 690 $fileAttachment['mime'] = $_FILES['joinFile']['type']; 691 } 692 $customer = new Customer($ct->id_customer); 693 $params = [ 694 '{reply}' => Tools::nl2br(Tools::getValue('reply_message')), 695 '{link}' => Tools::url( 696 $this->context->link->getPageLink('contact', true, null, null, false, $ct->id_shop), 697 'id_customer_thread='.(int) $ct->id.'&token='.$ct->token 698 ), 699 '{firstname}' => $customer->firstname, 700 '{lastname}' => $customer->lastname, 701 ]; 702 //#ct == id_customer_thread #tc == token of thread <== used in the synchronization imap 703 $contact = new Contact((int) $ct->id_contact, (int) $ct->id_lang); 704 705 if (Validate::isLoadedObject($contact)) { 706 $fromName = $contact->name; 707 $fromEmail = $contact->email; 708 } else { 709 $fromName = null; 710 $fromEmail = null; 711 } 712 713 if (Mail::Send( 714 (int) $ct->id_lang, 715 'reply_msg', 716 sprintf(Mail::l('An answer to your message is available #ct%1$s #tc%2$s', $ct->id_lang), $ct->id, $ct->token), 717 $params, 718 Tools::getValue('msg_email'), 719 null, 720 Tools::convertEmailToIdn($fromEmail), 721 $fromName, 722 $fileAttachment, 723 null, 724 _PS_MAIL_DIR_, 725 true, 726 $ct->id_shop 727 )) { 728 $ct->status = 'closed'; 729 $ct->update(); 730 } 731 Tools::redirectAdmin( 732 static::$currentIndex.'&id_customer_thread='.(int) $idCustomerThread.'&viewcustomer_thread&token='.Tools::getValue('token') 733 ); 734 } else { 735 $this->errors[] = Tools::displayError('An error occurred. Your message was not sent. Please contact your system administrator.'); 736 } 737 } 738 } 739 740 return parent::postProcess(); 741 } 742 743 /** 744 * @param $message 745 * @param bool $email 746 * @param null $idEmployee 747 * 748 * @return string 749 * 750 * @since 1.0. 751 */ 752 protected function displayMessage($message, $email = false, $idEmployee = null) 753 { 754 $tpl = $this->createTemplate('message.tpl'); 755 756 $contacts = Contact::getContacts($this->context->language->id); 757 $contactArray = []; 758 foreach ($contacts as $contact) { 759 $contactArray[$contact['id_contact']] = ['id_contact' => $contact['id_contact'], 'name' => $contact['name']]; 760 } 761 $contacts = $contactArray; 762 763 if (!$email) { 764 if (!empty($message['id_product']) && empty($message['employee_name'])) { 765 $idOrderProduct = Order::getIdOrderProduct((int) $message['id_customer'], (int) $message['id_product']); 766 } 767 } 768 $message['date_add'] = Tools::displayDate($message['date_add'], null, true); 769 $message['user_agent'] = strip_tags($message['user_agent']); 770 $message['message'] = preg_replace( 771 '/(https?:\/\/[a-z0-9#%&_=\(\)\.\? \+\-@\/]{6,1000})([\s\n<])/Uui', 772 '<a href="\1">\1</a>\2', 773 html_entity_decode( 774 $message['message'], 775 ENT_QUOTES, 776 'UTF-8' 777 ) 778 ); 779 780 $isValidOrderId = true; 781 $order = new Order((int) $message['id_order']); 782 783 if (!Validate::isLoadedObject($order)) { 784 $isValidOrderId = false; 785 } 786 787 $tpl->assign( 788 [ 789 'thread_url' => Tools::getAdminUrl(basename(_PS_ADMIN_DIR_).'/'.$this->context->link->getAdminLink('AdminCustomerThreads').'&id_customer_thread='.(int) $message['id_customer_thread'].'&viewcustomer_thread=1'), 790 'link' => $this->context->link, 791 'current' => static::$currentIndex, 792 'token' => $this->token, 793 'message' => $message, 794 'id_order_product' => isset($idOrderProduct) ? $idOrderProduct : null, 795 'email' => Tools::convertEmailFromIdn($email), 796 'id_employee' => $idEmployee, 797 'PS_SHOP_NAME' => Configuration::get('PS_SHOP_NAME'), 798 'file_name' => file_exists(_PS_UPLOAD_DIR_.$message['file_name']), 799 'contacts' => $contacts, 800 'is_valid_order_id' => $isValidOrderId, 801 ] 802 ); 803 804 return $tpl->fetch(); 805 } 806 807 /** 808 * Initialize content 809 * 810 * @return void 811 * 812 * @since 1.0.0 813 */ 814 public function initContent() 815 { 816 if (isset($_GET['filename']) && file_exists(_PS_UPLOAD_DIR_.$_GET['filename']) && Validate::isFileName($_GET['filename'])) { 817 static::openUploadedFile(); 818 } 819 820 parent::initContent(); 821 } 822 823 /** 824 * Render KPIs 825 * 826 * @return mixed 827 * 828 * @since 1.0.0 829 */ 830 public function renderKpis() 831 { 832 $time = time(); 833 $kpis = []; 834 835 /* The data generation is located in AdminStatsControllerCore */ 836 837 $helper = new HelperKpi(); 838 $helper->id = 'box-pending-messages'; 839 $helper->icon = 'icon-envelope'; 840 $helper->color = 'color1'; 841 $helper->href = $this->context->link->getAdminLink('AdminCustomerThreads'); 842 $helper->title = $this->l('Pending Discussion Threads', null, null, false); 843 if (ConfigurationKPI::get('PENDING_MESSAGES') !== false) { 844 $helper->value = ConfigurationKPI::get('PENDING_MESSAGES'); 845 } 846 $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=pending_messages'; 847 $helper->refresh = (bool) (ConfigurationKPI::get('PENDING_MESSAGES_EXPIRE') < $time); 848 $kpis[] = $helper->generate(); 849 850 $helper = new HelperKpi(); 851 $helper->id = 'box-age'; 852 $helper->icon = 'icon-time'; 853 $helper->color = 'color2'; 854 $helper->title = $this->l('Average Response Time', null, null, false); 855 $helper->subtitle = $this->l('30 days', null, null, false); 856 if (ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME') !== false) { 857 $helper->value = ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME'); 858 } 859 $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=avg_msg_response_time'; 860 $helper->refresh = (bool) (ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME_EXPIRE') < $time); 861 $kpis[] = $helper->generate(); 862 863 $helper = new HelperKpi(); 864 $helper->id = 'box-messages-per-thread'; 865 $helper->icon = 'icon-copy'; 866 $helper->color = 'color3'; 867 $helper->title = $this->l('Messages per Thread', null, null, false); 868 $helper->subtitle = $this->l('30 day', null, null, false); 869 if (ConfigurationKPI::get('MESSAGES_PER_THREAD') !== false) { 870 $helper->value = ConfigurationKPI::get('MESSAGES_PER_THREAD'); 871 } 872 $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=messages_per_thread'; 873 $helper->refresh = (bool) (ConfigurationKPI::get('MESSAGES_PER_THREAD_EXPIRE') < $time); 874 $kpis[] = $helper->generate(); 875 876 $helper = new HelperKpiRow(); 877 $helper->kpis = $kpis; 878 879 return $helper->generate(); 880 } 881 882 /** 883 * Render view 884 * 885 * @return string 886 * 887 * @since 1.0.0 888 */ 889 public function renderView() 890 { 891 if (!$idCustomerThread = (int) Tools::getValue('id_customer_thread')) { 892 return ''; 893 } 894 895 if (!($thread = $this->loadObject())) { 896 return ''; 897 } 898 $this->context->cookie->{'customer_threadFilter_cl!id_contact'} = $thread->id_contact; 899 900 $employees = Employee::getEmployees(); 901 902 $messages = CustomerThread::getMessageCustomerThreads($idCustomerThread); 903 904 foreach ($messages as $key => $mess) { 905 if ($mess['id_employee']) { 906 $employee = new Employee($mess['id_employee']); 907 $messages[$key]['employee_image'] = $employee->getImage(); 908 } 909 if (isset($mess['file_name']) && $mess['file_name'] != '') { 910 $messages[$key]['file_name'] = _THEME_PROD_PIC_DIR_.$mess['file_name']; 911 } else { 912 unset($messages[$key]['file_name']); 913 } 914 915 if ($mess['id_product']) { 916 $product = new Product((int) $mess['id_product'], false, $this->context->language->id); 917 if (Validate::isLoadedObject($product)) { 918 $messages[$key]['product_name'] = $product->name; 919 $messages[$key]['product_link'] = $this->context->link->getAdminLink('AdminProducts').'&updateproduct&id_product='.(int) $product->id; 920 } 921 } 922 } 923 924 $nextThread = CustomerThread::getNextThread((int) $thread->id); 925 926 $contacts = Contact::getContacts($this->context->language->id); 927 928 $actions = []; 929 930 if ($nextThread) { 931 $nextThread = [ 932 'href' => static::$currentIndex.'&id_customer_thread='.(int) $nextThread.'&viewcustomer_thread&token='.$this->token, 933 'name' => $this->l('Reply to the next unanswered message in this thread'), 934 ]; 935 } 936 937 if ($thread->status != 'closed') { 938 $actions['closed'] = [ 939 'href' => static::$currentIndex.'&viewcustomer_thread&setstatus=2&id_customer_thread='.(int) Tools::getValue('id_customer_thread').'&viewmsg&token='.$this->token, 940 'label' => $this->l('Mark as "handled"'), 941 'name' => 'setstatus', 942 'value' => 2, 943 ]; 944 } else { 945 $actions['open'] = [ 946 'href' => static::$currentIndex.'&viewcustomer_thread&setstatus=1&id_customer_thread='.(int) Tools::getValue('id_customer_thread').'&viewmsg&token='.$this->token, 947 'label' => $this->l('Re-open'), 948 'name' => 'setstatus', 949 'value' => 1, 950 ]; 951 } 952 953 if ($thread->status != 'pending1') { 954 $actions['pending1'] = [ 955 'href' => static::$currentIndex.'&viewcustomer_thread&setstatus=3&id_customer_thread='.(int) Tools::getValue('id_customer_thread').'&viewmsg&token='.$this->token, 956 'label' => $this->l('Mark as "pending 1" (will be answered later)'), 957 'name' => 'setstatus', 958 'value' => 3, 959 ]; 960 } else { 961 $actions['pending1'] = [ 962 'href' => static::$currentIndex.'&viewcustomer_thread&setstatus=1&id_customer_thread='.(int) Tools::getValue('id_customer_thread').'&viewmsg&token='.$this->token, 963 'label' => $this->l('Disable pending status'), 964 'name' => 'setstatus', 965 'value' => 1, 966 ]; 967 } 968 969 if ($thread->status != 'pending2') { 970 $actions['pending2'] = [ 971 'href' => static::$currentIndex.'&viewcustomer_thread&setstatus=4&id_customer_thread='.(int) Tools::getValue('id_customer_thread').'&viewmsg&token='.$this->token, 972 'label' => $this->l('Mark as "pending 2" (will be answered later)'), 973 'name' => 'setstatus', 974 'value' => 4, 975 ]; 976 } else { 977 $actions['pending2'] = [ 978 'href' => static::$currentIndex.'&viewcustomer_thread&setstatus=1&id_customer_thread='.(int) Tools::getValue('id_customer_thread').'&viewmsg&token='.$this->token, 979 'label' => $this->l('Disable pending status'), 980 'name' => 'setstatus', 981 'value' => 1, 982 ]; 983 } 984 985 if ($thread->id_customer) { 986 $customer = new Customer($thread->id_customer); 987 $orders = Order::getCustomerOrders($customer->id); 988 if ($orders && count($orders)) { 989 $totalOk = 0; 990 $ordersOk = []; 991 foreach ($orders as $key => $order) { 992 if ($order['valid']) { 993 $ordersOk[] = $order; 994 $totalOk += $order['total_paid_real'] / $order['conversion_rate']; 995 } 996 $orders[$key]['date_add'] = Tools::displayDate($order['date_add']); 997 $orders[$key]['total_paid_real'] = Tools::displayPrice($order['total_paid_real'], new Currency((int) $order['id_currency'])); 998 } 999 } 1000 1001 $products = $customer->getBoughtProducts(); 1002 if ($products && count($products)) { 1003 foreach ($products as $key => $product) { 1004 $products[$key]['date_add'] = Tools::displayDate($product['date_add'], null, true); 1005 } 1006 } 1007 } 1008 $timelineItems = $this->getTimeline($messages, $thread->id_order); 1009 $firstMessage = $messages[0]; 1010 1011 if (!$messages[0]['id_employee']) { 1012 unset($messages[0]); 1013 } 1014 1015 $contact = ''; 1016 foreach ($contacts as $c) { 1017 if ($c['id_contact'] == $thread->id_contact) { 1018 $contact = $c['name']; 1019 } 1020 } 1021 1022 $this->tpl_view_vars = [ 1023 'id_customer_thread' => $idCustomerThread, 1024 'thread' => $thread, 1025 'actions' => $actions, 1026 'employees' => $employees, 1027 'current_employee' => $this->context->employee, 1028 'messages' => $messages, 1029 'first_message' => $firstMessage, 1030 'contact' => $contact, 1031 'next_thread' => $nextThread, 1032 'orders' => isset($orders) ? $orders : false, 1033 'customer' => isset($customer) ? $customer : false, 1034 'products' => isset($products) ? $products : false, 1035 'total_ok' => isset($totalOk) ? Tools::displayPrice($totalOk, $this->context->currency) : false, 1036 'orders_ok' => isset($ordersOk) ? $ordersOk : false, 1037 'count_ok' => isset($ordersOk) ? count($ordersOk) : false, 1038 'PS_CUSTOMER_SERVICE_SIGNATURE' => str_replace('\r\n', "\n", Configuration::get('PS_CUSTOMER_SERVICE_SIGNATURE', (int) $thread->id_lang)), 1039 'timeline_items' => $timelineItems, 1040 ]; 1041 1042 if ($nextThread) { 1043 $this->tpl_view_vars['next_thread'] = $nextThread; 1044 } 1045 1046 return parent::renderView(); 1047 } 1048 1049 /** 1050 * Get timeline 1051 * 1052 * @param $messages 1053 * @param $idOrder 1054 * 1055 * @return array 1056 * 1057 * @since 1.0.0 1058 */ 1059 public function getTimeline($messages, $idOrder) 1060 { 1061 $timeline = []; 1062 foreach ($messages as $message) { 1063 $product = new Product((int) $message['id_product'], false, $this->context->language->id); 1064 1065 $content = ''; 1066 if (!$message['private']) { 1067 $content .= $this->l('Message to: ').' <span class="badge">'.(!$message['id_employee'] ? $message['subject'] : $message['customer_name']).'</span><br/>'; 1068 } 1069 if (Validate::isLoadedObject($product)) { 1070 $content .= '<br/>'.$this->l('Product: ').'<span class="label label-info">'.$product->name.'</span><br/><br/>'; 1071 } 1072 $content .= Tools::safeOutput($message['message']); 1073 1074 $timeline[$message['date_add']][] = [ 1075 'arrow' => 'left', 1076 'background_color' => '', 1077 'icon' => 'icon-envelope', 1078 'content' => $content, 1079 'date' => $message['date_add'], 1080 ]; 1081 } 1082 1083 $order = new Order((int) $idOrder); 1084 if (Validate::isLoadedObject($order)) { 1085 $orderHistory = $order->getHistory($this->context->language->id); 1086 foreach ($orderHistory as $history) { 1087 $linkOrder = $this->context->link->getAdminLink('AdminOrders').'&vieworder&id_order='.(int) $order->id; 1088 1089 $content = '<a class="badge" target="_blank" href="'.Tools::safeOutput($linkOrder).'">'.$this->l('Order').' #'.(int) $order->id.'</a><br/><br/>'; 1090 1091 $content .= '<span>'.$this->l('Status:').' '.$history['ostate_name'].'</span>'; 1092 1093 $timeline[$history['date_add']][] = [ 1094 'arrow' => 'right', 1095 'alt' => true, 1096 'background_color' => $history['color'], 1097 'icon' => 'icon-credit-card', 1098 'content' => $content, 1099 'date' => $history['date_add'], 1100 'see_more_link' => $linkOrder, 1101 ]; 1102 } 1103 } 1104 krsort($timeline); 1105 1106 return $timeline; 1107 } 1108 1109 /** 1110 * Render options 1111 * 1112 * @return string 1113 * 1114 * @since 1.0.0 1115 */ 1116 public function renderOptions() 1117 { 1118 if (Configuration::get('PS_SAV_IMAP_URL') 1119 && Configuration::get('PS_SAV_IMAP_PORT') 1120 && Configuration::get('PS_SAV_IMAP_USER') 1121 && Configuration::get('PS_SAV_IMAP_PWD') 1122 ) { 1123 $this->tpl_option_vars['use_sync'] = true; 1124 } else { 1125 $this->tpl_option_vars['use_sync'] = false; 1126 } 1127 1128 return parent::renderOptions(); 1129 } 1130 1131 /** 1132 * AdminController::getList() override 1133 * 1134 * @see AdminController::getList() 1135 * 1136 * @param int $idLang 1137 * @param string|null $orderBy 1138 * @param string|null $orderWay 1139 * @param int $start 1140 * @param int|null $limit 1141 * @param int|bool $idLangShop 1142 * 1143 * @throws PrestaShopException 1144 * 1145 * @since 1.0.0 1146 */ 1147 public function getList($idLang, $orderBy = null, $orderWay = null, $start = 0, $limit = null, $idLangShop = false) 1148 { 1149 parent::getList($idLang, $orderBy, $orderWay, $start, $limit, $idLangShop); 1150 1151 $nbItems = count($this->_list); 1152 for ($i = 0; $i < $nbItems; ++$i) { 1153 if (isset($this->_list[$i]['messages'])) { 1154 $this->_list[$i]['messages'] = Tools::htmlentitiesDecodeUTF8($this->_list[$i]['messages']); 1155 } 1156 if (isset($this->_list[$i]['email'])) { 1157 $this->_list[$i]['email'] = Tools::convertEmailFromIdn($this->_list[$i]['email']); 1158 } 1159 } 1160 } 1161 1162 /** 1163 * @param $value 1164 * 1165 * @throws PrestaShopException 1166 * 1167 * @since 1.0.0 1168 */ 1169 public function updateOptionPsSavImapOpt($value) 1170 { 1171 if ($this->tabAccess['edit'] != '1') { 1172 throw new PrestaShopException(Tools::displayError('You do not have permission to edit this.')); 1173 } 1174 1175 if (!$this->errors && $value) { 1176 Configuration::updateValue('PS_SAV_IMAP_OPT', implode('', $value)); 1177 } 1178 } 1179 1180 /** 1181 * @throws PrestaShopException 1182 * 1183 * @since 1.0.0 1184 */ 1185 public function ajaxProcessMarkAsRead() 1186 { 1187 if ($this->tabAccess['edit'] != '1') { 1188 throw new PrestaShopException(Tools::displayError('You do not have permission to edit this.')); 1189 } 1190 1191 $idThread = Tools::getValue('id_thread'); 1192 $messages = CustomerThread::getMessageCustomerThreads($idThread); 1193 if (count($messages)) { 1194 Db::getInstance()->execute('UPDATE `'._DB_PREFIX_.'customer_message` set `read` = 1 WHERE `id_employee` = '.(int) $this->context->employee->id.' AND `id_customer_thread` = '.(int) $idThread); 1195 } 1196 } 1197 1198 /** 1199 * Call the IMAP synchronization during an AJAX process. 1200 * 1201 * @throws PrestaShopException 1202 * 1203 * @since 1.0.0 1204 */ 1205 public function ajaxProcessSyncImap() 1206 { 1207 if ($this->tabAccess['edit'] != '1') { 1208 throw new PrestaShopException(Tools::displayError('You do not have permission to edit this.')); 1209 } 1210 1211 if (Tools::isSubmit('syncImapMail')) { 1212 $this->ajaxDie(json_encode($this->syncImap())); 1213 } 1214 } 1215 1216 protected function openUploadedFile() 1217 { 1218 $filename = $_GET['filename']; 1219 1220 $extensions = [ 1221 '.txt' => 'text/plain', 1222 '.rtf' => 'application/rtf', 1223 '.doc' => 'application/msword', 1224 '.docx' => 'application/msword', 1225 '.pdf' => 'application/pdf', 1226 '.zip' => 'multipart/x-zip', 1227 '.png' => 'image/png', 1228 '.jpeg' => 'image/jpeg', 1229 '.gif' => 'image/gif', 1230 '.jpg' => 'image/jpeg', 1231 ]; 1232 1233 $extension = false; 1234 foreach ($extensions as $key => $val) { 1235 if (substr(mb_strtolower($filename), -4) == $key || substr(mb_strtolower($filename), -5) == $key) { 1236 $extension = $val; 1237 break; 1238 } 1239 } 1240 1241 if (!$extension || !Validate::isFileName($filename)) { 1242 die(Tools::displayError()); 1243 } 1244 1245 if (ob_get_level() && ob_get_length() > 0) { 1246 ob_end_clean(); 1247 } 1248 header('Content-Type: '.$extension); 1249 header('Content-Disposition:attachment;filename="'.$filename.'"'); 1250 readfile(_PS_UPLOAD_DIR_.$filename); 1251 die; 1252 } 1253 1254 /** 1255 * @param $content 1256 * 1257 * @return string 1258 * 1259 * @since 1.0.0 1260 */ 1261 protected function displayButton($content) 1262 { 1263 return '<div><p>'.$content.'</p></div>'; 1264 } 1265} 1266