From 1bdd2d507a4bf57958751f9f8018954750c45fc9 Mon Sep 17 00:00:00 2001 From: Jerry Padgett <sjpadgett@gmail.com> Date: Mon, 15 Jul 2024 15:42:27 -0400 Subject: [PATCH] Add back RingCentral to our FaxSMS Module (#7542) * Add back RingCentral to our FaxSMS Module - need RC SDK * Files clean up Twillio fax is gone ans so is fax server and good riddence! refactors RC forms * Modify FileUtils class adding methods for mime management. * author * finish file utility * more RC integration remove unneeded 2FA redirect file rcauth. * add mass deletes to RC and etherFAX working on UI list * add email client add email notification to cron script cleanup * update twilio version * escaping and error alerts * review fixws * add user alert to email setuop to use standard SMPT setup --- composer.json | 3 +- composer.lock | 857 +++++++++++- .../oe-module-faxsms/composer.json | 3 - .../oe-module-faxsms/contact.php | 26 +- .../library/rc_sms_notification.php | 125 +- .../library/setup_services.php | 89 +- .../oe-module-faxsms/messageUI.php | 469 +++++-- .../oe-module-faxsms/moduleConfig.php | 1 - .../oe-module-faxsms/openemr.bootstrap.php | 29 +- .../custom_modules/oe-module-faxsms/setup.php | 47 + .../oe-module-faxsms/setup_email.php | 163 +++ .../oe-module-faxsms/setup_rc.php | 198 +++ .../oe-module-faxsms/src/BootstrapService.php | 4 +- .../src/Controller/AppDispatch.php | 99 +- .../src/Controller/EmailClient.php | 55 +- .../src/Controller/EtherFaxActions.php | 171 ++- .../src/Controller/RCFaxClient.php | 1231 +++++++++++++++++ .../src/Controller/TwilioSMSClient.php | 19 +- .../oe-module-faxsms/version.php | 2 +- src/Common/Utils/FileUtils.php | 120 +- 20 files changed, 3404 insertions(+), 307 deletions(-) create mode 100644 interface/modules/custom_modules/oe-module-faxsms/setup_email.php create mode 100644 interface/modules/custom_modules/oe-module-faxsms/setup_rc.php create mode 100644 interface/modules/custom_modules/oe-module-faxsms/src/Controller/RCFaxClient.php diff --git a/composer.json b/composer.json index b7bd46aecdd..fca84368cfc 100644 --- a/composer.json +++ b/composer.json @@ -77,7 +77,8 @@ "vlucas/phpdotenv": "5.5.0", "waryway/php-traits-library": "1.0.4", "yubico/u2flib-server": "1.0.2", - "twilio/sdk": "7.12.0" + "twilio/sdk": "7.16.2", + "ringcentral/ringcentral-php": "3.0.1" }, "config": { "platform": { diff --git a/composer.lock b/composer.lock index 4b703f0b3da..3ef863cc061 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1f7af00752aeae4abc29c2437615cae9", + "content-hash": "0df2348dc099b9fefe4bb43ba76d9d32", "packages": [ { "name": "academe/authorizenet-objects", @@ -702,6 +702,53 @@ }, "time": "2023-02-07T12:51:48+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, { "name": "ezyang/htmlpurifier", "version": "v4.16.0", @@ -7120,6 +7167,63 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "pubnub/pubnub", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/pubnub/php.git", + "reference": "26de0a59dda29f1246378995f96c4f95f6d64980" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pubnub/php/zipball/26de0a59dda29f1246378995f96c4f95f6d64980", + "reference": "26de0a59dda29f1246378995f96c4f95f6d64980", + "shasum": "" + }, + "require": { + "php": "^7.4|>=8.0", + "psr/log": "^1.1|^2.0|^3.0", + "rmccue/requests": "^2.0" + }, + "require-dev": { + "monolog/monolog": "^2.9 || ^3.0", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "PubNub\\": "src/PubNub" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "proprietary" + ], + "authors": [ + { + "name": "PubNub", + "email": "support@pubnub.com" + } + ], + "description": "This is the official PubNub PHP SDK repository.", + "homepage": "http://www.pubnub.com/", + "keywords": [ + "ajax", + "api", + "push", + "real time", + "real-time", + "realtime" + ], + "support": { + "issues": "https://github.com/pubnub/php/issues", + "source": "https://github.com/pubnub/php/tree/v6.3.0" + }, + "time": "2024-06-19T07:31:01+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -7345,6 +7449,738 @@ ], "time": "2023-04-15T23:01:58+00:00" }, + { + "name": "ratchet/pawl", + "version": "v0.4.1", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/Pawl.git", + "reference": "af70198bab77a582b31169d3cc3982bed25c161f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/Pawl/zipball/af70198bab77a582b31169d3cc3982bed25c161f", + "reference": "af70198bab77a582b31169d3cc3982bed25c161f", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0", + "guzzlehttp/psr7": "^2.0 || ^1.7", + "php": ">=5.4", + "ratchet/rfc6455": "^0.3.1", + "react/socket": "^1.9" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8" + }, + "suggest": { + "reactivex/rxphp": "~2.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Ratchet\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Asynchronous WebSocket client", + "keywords": [ + "Ratchet", + "async", + "client", + "websocket", + "websocket client" + ], + "support": { + "issues": "https://github.com/ratchetphp/Pawl/issues", + "source": "https://github.com/ratchetphp/Pawl/tree/v0.4.1" + }, + "time": "2021-12-10T14:32:34+00:00" + }, + { + "name": "ratchet/rfc6455", + "version": "v0.3.1", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2 || ^1.7", + "php": ">=5.4.2" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3.1" + }, + "time": "2021-12-09T23:20:49+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.15.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038", + "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.11", + "react/event-loop": "^1.2", + "react/promise": "^3 || ^2.6 || ^1.2.1", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.15.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-12-15T11:02:10+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "ringcentral/ringcentral-php", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/ringcentral/ringcentral-php.git", + "reference": "ebda301de452e4357eac982e092d2aec02153ad8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ringcentral/ringcentral-php/zipball/ebda301de452e4357eac982e092d2aec02153ad8", + "reference": "ebda301de452e4357eac982e092d2aec02153ad8", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^6.3.3|^7.4.1", + "guzzlehttp/psr7": "^2.1.0", + "php": ">=7.2", + "pubnub/pubnub": "^4.7.0|^6.0", + "ratchet/pawl": "^0.4.1", + "symfony/event-dispatcher": "^2|^3|^4|^5|^6" + }, + "require-dev": { + "macfja/phar-builder": "^0.2.8", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.5.12|^8.0|^9.0", + "react/async": "^3.0" + }, + "suggest": { + "ext-openssl": "to decrypt PubNub messages" + }, + "type": "library", + "extra": { + "phar-builder": { + "compression": "gzip", + "name": "ringcentral.phar", + "output-dir": "./dist", + "entry-point": "./vendor/autoload.php", + "skip-shebang": true, + "include": [], + "excluded": [ + "vendor/bin" + ], + "include-dev": false + } + }, + "autoload": { + "psr-4": { + "RingCentral\\SDK\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kirill Konshin", + "email": "kirill.konshin@ringcentral.com" + }, + { + "name": "Byrne Reese", + "email": "byrne.reese@ringcentral.com" + } + ], + "description": "RingCentral Platform PHP SDK", + "homepage": "http://developers.ringcentral.com", + "keywords": [ + "api", + "connect", + "php", + "platform", + "ringcentral", + "sdk" + ], + "support": { + "issues": "https://github.com/ringcentral/ringcentral-php/issues", + "source": "https://github.com/ringcentral/ringcentral-php/tree/3.0.1" + }, + "time": "2024-03-12T02:58:29+00:00" + }, + { + "name": "rmccue/requests", + "version": "v2.0.12", + "source": { + "type": "git", + "url": "https://github.com/WordPress/Requests.git", + "reference": "fb67e3d392ff6b89a90e96f19745662f4ecd62b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/Requests/zipball/fb67e3d392ff6b89a90e96f19745662f4ecd62b1", + "reference": "fb67e3d392ff6b89a90e96f19745662f4ecd62b1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "php-parallel-lint/php-console-highlighter": "^0.5.0", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0", + "requests/test-server": "dev-main", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.6", + "wp-coding-standards/wpcs": "^2.0", + "yoast/phpunit-polyfills": "^1.0.0" + }, + "suggest": { + "art4/requests-psr18-adapter": "For using Requests as a PSR-18 HTTP Client", + "ext-curl": "For improved performance", + "ext-openssl": "For secure transport support", + "ext-zlib": "For improved performance when decompressing encoded streams" + }, + "type": "library", + "autoload": { + "files": [ + "library/Deprecated.php" + ], + "psr-4": { + "WpOrg\\Requests\\": "src/" + }, + "classmap": [ + "library/Requests.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Ryan McCue", + "homepage": "https://rmccue.io/" + }, + { + "name": "Alain Schlesser", + "homepage": "https://github.com/schlessera" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl" + }, + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/Requests/graphs/contributors" + } + ], + "description": "A HTTP library written in PHP, for human beings.", + "homepage": "https://requests.ryanmccue.info/", + "keywords": [ + "curl", + "fsockopen", + "http", + "idna", + "ipv6", + "iri", + "sockets" + ], + "support": { + "docs": "https://requests.ryanmccue.info/", + "issues": "https://github.com/WordPress/Requests/issues", + "source": "https://github.com/WordPress/Requests" + }, + "time": "2024-07-08T08:10:42+00:00" + }, { "name": "robthree/twofactorauth", "version": "v2.0.0", @@ -9811,16 +10647,16 @@ }, { "name": "twilio/sdk", - "version": "7.12.0", + "version": "7.16.2", "source": { "type": "git", - "url": "git@github.com:twilio/twilio-php.git", - "reference": "f281d7b793f02377ab0f38e7166e16820acc3eae" + "url": "https://github.com/twilio/twilio-php.git", + "reference": "02ad214b0cc9fc513bd67df251d54aed8901284f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twilio/twilio-php/zipball/f281d7b793f02377ab0f38e7166e16820acc3eae", - "reference": "f281d7b793f02377ab0f38e7166e16820acc3eae", + "url": "https://api.github.com/repos/twilio/twilio-php/zipball/02ad214b0cc9fc513bd67df251d54aed8901284f", + "reference": "02ad214b0cc9fc513bd67df251d54aed8901284f", "shasum": "" }, "require": { @@ -9856,7 +10692,11 @@ "sms", "twilio" ], - "time": "2023-10-19T11:57:30+00:00" + "support": { + "issues": "https://github.com/twilio/twilio-php/issues", + "source": "https://github.com/twilio/twilio-php/tree/7.16.2" + }, + "time": "2024-04-01T10:34:42+00:00" }, { "name": "vlucas/phpdotenv", @@ -12483,7 +13323,8 @@ }, "platform-dev": [], "platform-overrides": { - "php": "8.1" + "php": "8.1", + "ext-intl": "8.1" }, "plugin-api-version": "2.6.0" } diff --git a/interface/modules/custom_modules/oe-module-faxsms/composer.json b/interface/modules/custom_modules/oe-module-faxsms/composer.json index 1cc9e2b08d5..1549c470ca3 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/composer.json +++ b/interface/modules/custom_modules/oe-module-faxsms/composer.json @@ -18,8 +18,5 @@ "psr-4": {"OpenEMR\\Modules\\FaxSMS\\": "src/"} }, "require": { - }, - "conflict": { - "openemr/openemr": "<7.0.1" } } diff --git a/interface/modules/custom_modules/oe-module-faxsms/contact.php b/interface/modules/custom_modules/oe-module-faxsms/contact.php index e6a82db6009..a14a89b3579 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/contact.php +++ b/interface/modules/custom_modules/oe-module-faxsms/contact.php @@ -24,6 +24,9 @@ die("<h3>" . xlt("Not Authorised!") . "</h3>"); } $logged_in = $clientApp->authenticate(); +if ($logged_in != 1) { + die("<h3>" . text($logged_in) . "</h3>"); +} $isSMS = $clientApp->getRequest('isSMS', false); $isEmail = $clientApp->getRequest('isEmail', false); $isForward = $isFax = 0; @@ -215,12 +218,16 @@ function (data) { const getContactBook = function (e, rtnpid) { e.preventDefault(); let btnClose = <?php echo xlj("Cancel"); ?>; - dlgopen('', '', 'modal-lg', 500, '', '', { + dlgopen('', '', 'modal-lg', '', '', '', { buttons: [ {text: btnClose, close: true, style: 'primary btn-sm'} ], url: top.webroot_url + '/interface/usergroup/addrbook_list.php?popup=2&type=' + encodeURIComponent(<?php echo js_escape($serviceType); ?>), - dialogId: 'fax' + dialogId: 'fax', + resolvePromiseOn: 'close', + sizeHeight: 'full' + }).then(function (contact) { + top.restoreSession(); }); }; </script> @@ -248,20 +255,19 @@ function (data) { <div class="row"> <div class="col-md-12"> <div class="form-group forwardExclude smsExclude faxExclude"> - <label for="form_pid"><?php echo xlt('MRN') ?></label> + <label for="form_pid"><?php echo xlt('MRN or PID') ?></label> <input id="form_pid" type="text" name="form_pid" class="form-control" - placeholder="<?php echo xla('If Applicable for charting.') ?>" + title="<?php echo xla('If Applicable for charting.') ?>" value="<?php echo attr($interface_pid ?? 0) ?>" /> </div> <div class="form-group show-detail"> <label for="form_name"><?php echo xlt('Name') ?></label> <input id="form_name" type="text" name="name" class="form-control" - placeholder="<?php echo xla('Not Required') ?>" + placeholder="<?php echo xla('First name. Optionally if included then sent.') ?>" value="<?php echo attr($details['fname'] ?? '') ?>" /> - <div class="form-group show-detail smsExclude faxExclude"> - <!--<label for="form_lastname"><?php /*echo xlt('Lastname') */ ?></label>--> + <div class="form-group mt-1 show-detail smsExclude faxExclude"> <input id="form_lastname" type="text" name="surname" class="form-control" - placeholder="<?php echo xla('Not Required') ?>" + placeholder="<?php echo xla('Last name. Optionally if included then sent.') ?>" value="<?php echo attr($details['lname'] ?? '') ?>" /> </div> <div class="form-group faxExclude smsExclude show-detail"> @@ -274,7 +280,7 @@ function (data) { echo($isSMTP ? xla('Forward to email address if address is included.') : xla('Unavailable! Setup SMTP in Config Notifications.')); } ?>" - title="<?php echo xla('Forward to an email address.') ?>" /> + title="<?php echo xla('Attach and send to an email Address.') ?>" /> </div> <div class="form-group"> <label for="form_phone"><?php echo xlt('Recipient Phone') ?></label> @@ -287,7 +293,7 @@ function (data) { <div class="form-group"> <label for="form_message"><?php echo xlt('Message') ?></label> <textarea id="form_message" name="comments" class="form-control" placeholder=" - <?php echo xla('Add a note to recipient.'); ?>" rows="6"><?php echo $default_message; ?></textarea> + <?php echo "\n" . xla('Add a note to recipient or cover sheet. If enabled, double click for Text Templates.'); ?>" rows="6"><?php echo $default_message; ?></textarea> </div> <?php } ?> <div> diff --git a/interface/modules/custom_modules/oe-module-faxsms/library/rc_sms_notification.php b/interface/modules/custom_modules/oe-module-faxsms/library/rc_sms_notification.php index 4f0658cd1ae..1e44eeedef3 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/library/rc_sms_notification.php +++ b/interface/modules/custom_modules/oe-module-faxsms/library/rc_sms_notification.php @@ -23,12 +23,12 @@ $_SERVER['SERVER_NAME'] = 'localhost'; $backpic = ""; $clientApp = null; +$emailApp = null; // for cron $error = ''; $runtime = []; // Check for other arguments and perform your script logic - if (($argc ?? null) > 1) { foreach ($argv as $k => $v) { if ($k == 0) { @@ -68,16 +68,29 @@ $TYPE = ''; if (!empty($runtime['type'])) { $TYPE = strtoupper($runtime['type']); +} elseif ($_GET['type'] ?? '' === 'email') { + $TYPE = $runtime['type'] = "EMAIL"; } else { $TYPE = $runtime['type'] = "SMS"; // default } + $CRON_TIME = 150; // use service if needed if ($TYPE === "SMS") { - $_SESSION['authUser'] = $runtime['user'] ?? ''; + $_SESSION['authUser'] = $runtime['user'] ?? $_SESSION['authUser']; $clientApp = AppDispatch::getApiService('sms'); $cred = $clientApp->getCredentials(); - if (!$clientApp->verifyAcl('admin', 'docs', $runtime['user'] ?? '')) { + + if (!$clientApp->verifyAcl('Clinicians', 'docs', $runtime['user'] ?? '')) { + die("<h3>" . xlt("Not Authorised!") . "</h3>"); + } +} +if ($TYPE === "EMAIL") { + $_SESSION['authUser'] = $runtime['user'] ?? $_SESSION['authUser']; + $emailApp = AppDispatch::getApiService('email'); + $cred = $emailApp->getEmailSetup(); + + if (!$emailApp->verifyAcl('Clinicians', 'docs', $runtime['user'] ?? '')) { die("<h3>" . xlt("Not Authorised!") . "</h3>"); } } @@ -108,16 +121,17 @@ <title><?php echo xlt("SMS Notification") ?></title> </head> <style> - html { - font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; - font-size: 14px; - } + html { + font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; + font-size: 14px; + } </style> <body> <body> <div> <div> - <p class="text-center"><h2><?php echo xlt("Working and may take a few minutes to finish.") ?></h2></p> + <p class="text-center"> + <h2><?php echo xlt("Working and may take a few minutes to finish.") ?></h2></p> </div> <?php if ($bTestRun) { @@ -129,7 +143,7 @@ flush(); // for every event found $plast = ''; - echo "<h3>========================" . text($TYPE) . " | " . text(date("Y-m-d H:i:s")) . " =========================</h3>"; + echo "<h3>======================== " . text($TYPE) . " | " . text(date("Y-m-d H:i:s")) . " =========================</h3>"; for ($p = 0; $p < count($db_patient); $p++) { ob_flush(); flush(); @@ -156,37 +170,80 @@ if ($remain_hour >= -($CRON_TIME) && $remain_hour <= $CRON_TIME) { //set message $db_sms_msg['message'] = cron_SetMessage($prow, $db_sms_msg); - $isValid = isValidPhone($prow['phone_cell']); // send sms to patient - if not in test mode - if ($bTestRun == 0 && $isValid) { + if ($bTestRun == 0) { cron_InsertNotificationLogEntry($TYPE, $prow, $db_sms_msg); - $clientApp->sendSMS( - $prow['phone_cell'], - $db_sms_msg['email_subject'], - $db_sms_msg['message'], - $db_sms_msg['email_sender'] - ); - } - if (!$isValid) { - $strMsg .= "<strong style='color:red'>\n* " . xlt("INVALID Mobile Phone#") . text('$prow["phone_cell"]') . " " . xlt("SMS NOT SENT Patient") . ":</strong>" . text($prow['fname']) . " " . text($prow['lname']) . "</b>"; - $db_sms_msg['message'] = xlt("Error: INVALID Mobile Phone") . '# ' . text($prow['phone_cell']) . xlt("SMS NOT SENT For") . ": " . text($prow['fname']) . " " . text($prow['lname']); - if ($bTestRun == 0) { - cron_InsertNotificationLogEntry($TYPE, $prow, $db_sms_msg); + if ($TYPE == 'SMS' && $clientApp != null) { + $isValid = isValidPhone($prow['phone_cell']); + if ($isValid) { + $error = $clientApp->sendSMS( + $prow['phone_cell'] ?? '', + $db_sms_msg['email_subject'] ?? '', + $db_sms_msg['message'] ?? '', + $db_sms_msg['email_sender'] ?? '' + ); + if (stripos($error, 'error') !== false) { + $strMsg .= " | " . xlt("Error:") . "<strong> " . text($error) . "</strong>"; + echo(nl2br($strMsg)); + exit; + } + } + if (!$isValid) { + $strMsg .= "<strong style='color:red'>\n* " . xlt("INVALID Mobile Phone#") . text('$prow["phone_cell"]') . " " . xlt("SMS NOT SENT Patient") . ":</strong>" . text($prow['fname']) . " " . text($prow['lname']) . "</b>"; + $db_sms_msg['message'] = xlt("Error: INVALID Mobile Phone") . '# ' . text($prow['phone_cell']) . xlt("SMS NOT SENT For") . ": " . text($prow['fname']) . " " . text($prow['lname']); + if ($bTestRun == 0) { + cron_InsertNotificationLogEntry($TYPE, $prow, $db_sms_msg); + } + } else { + $strMsg .= " | " . xlt("SMS SENT SUCCESSFULLY TO") . "<strong> " . text($prow['phone_cell']) . "</strong>"; + cron_UpdateEntry($TYPE, $prow['pid'], $prow['pc_eid'], $prow['pc_recurrtype']); + } + if ((int)$prow['pc_recurrtype'] > 0) { + $row = fetchRecurrences($prow['pid']); + $strMsg .= "\n<strong>" . xlt("A Recurring") . " " . text($row[0]['pc_catname']) . " " . xlt("Event Occuring") . " " . text($row[0]['pc_recurrspec']) . "</strong>"; + } + $strMsg .= "\n" . text($db_sms_msg['message']) . "\n"; + echo(nl2br($strMsg)); + } + if ($TYPE == 'EMAIL' && $emailApp != null) { + $isValid = $emailApp->validEmail($prow['email']); + if ($isValid) { + try { + $error = $emailApp->emailReminder( + $prow['email'] ?? '', + $db_sms_msg['message'], + ); + } catch (\PHPMailer\PHPMailer\Exception $e) { + $error = 'Error' . ' ' . $e->getMessage(); + } + if (stripos($error, 'error') !== false) { + $strMsg .= " | " . xlt("Error:") . "<strong> " . text($error) . "</strong>"; + echo(nl2br($strMsg)); + exit; + } + } + if (!$isValid) { + $strMsg .= "<strong style='color:red'>\n* " . xlt("INVALID Email") . text('$prow["email"]') . " " . xlt("EMAIL NOT SENT Patient") . ":</strong>" . text($prow['fname']) . " " . text($prow['lname']) . "</b>"; + $db_sms_msg['message'] = xlt("Error: INVALID EMAIL") . '# ' . text($prow['email']) . xlt("EMAIL NOT SENT For") . ": " . text($prow['fname']) . " " . text($prow['lname']); + if ($bTestRun == 0) { + cron_InsertNotificationLogEntry($TYPE, $prow, $db_sms_msg); + } + } else { + $strMsg .= " | " . xlt("EMAILED SUCCESSFULLY TO") . "<strong> " . text($prow['email']) . "</strong>"; + cron_UpdateEntry($TYPE, $prow['pid'], $prow['pc_eid'], $prow['pc_recurrtype']); + } + if ((int)$prow['pc_recurrtype'] > 0) { + $row = fetchRecurrences($prow['pid']); + $strMsg .= "\n<strong>" . xlt("A Recurring") . " " . text($row[0]['pc_catname']) . " " . xlt("Event Occuring") . " " . text($row[0]['pc_recurrspec']) . "</strong>"; + } + $strMsg .= "\n" . text($db_sms_msg['message']) . "\n"; + echo(nl2br($strMsg)); } - } else { - $strMsg .= " | " . xlt("SENT SUCCESSFULLY TO") . "<strong> " . text($prow['phone_cell']) . "</strong>"; - cron_UpdateEntry($TYPE, $prow['pid'], $prow['pc_eid'], $prow['pc_recurrtype']); - } - if ((int)$prow['pc_recurrtype'] > 0) { - $row = fetchRecurrences($prow['pid']); - $strMsg .= "\n<strong>" . xlt("A Recurring") . " " . text($row[0]['pc_catname']) . " " . xlt("Event Occuring") . " " . text($row[0]['pc_recurrspec']) . "</strong>"; } - $strMsg .= "\n" . text($db_sms_msg['message']) . "\n"; - echo (nl2br($strMsg)); } } - unset($clientApp); + unset($emailApp); echo "<br /><h2>" . xlt("Done!") . "</h2>"; ?> </div> @@ -343,7 +400,7 @@ function displayHelp(): void { //echo text($helpt); $help = - <<<HELP + <<<HELP Usage: php rc_sms_notification.php [options] Example: php rc_sms_notification.php site=default user=admin type=sms testrun=1 diff --git a/interface/modules/custom_modules/oe-module-faxsms/library/setup_services.php b/interface/modules/custom_modules/oe-module-faxsms/library/setup_services.php index 5de3658db7e..b5973852d75 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/library/setup_services.php +++ b/interface/modules/custom_modules/oe-module-faxsms/library/setup_services.php @@ -32,18 +32,32 @@ <!DOCTYPE HTML> <html lang="eng"> <head> - <title>><?php echo xlt("Enable Vendors") ?></title> + <title><?php echo xlt("Enable Vendors") ?></title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <?php if (count($vendors ?? []) === 0) { $boot->createVendorGlobals(); $vendors = $boot->getVendorGlobals(); } - + $isRCSMS = $vendors['oefax_enable_sms'] == 1 ? '1' : '0'; + $isRCFax = $vendors['oefax_enable_fax'] == 1 ? '1' : '0'; + $isEMAIL = $vendors['oe_enable_email'] == 4 ? '1' : '0'; + $setupUrl = './../setup.php'; + if ($isRCFax) { + $setupUrl = './../setup_rc.php'; + } Header::setupHeader(); ?> <script> + let ServiceFax = <?php echo js_escape($isRCFax) ?>; + let ServiceSMS = <?php echo js_escape($isRCSMS); ?>; + let ServiceEmail = <?php echo js_escape($isEMAIL); ?>; + function toggleSetup(id, type = 'single') { + let url = './../setup.php'; + if (ServiceFax === '1') { + url = './../setup_rc.php'; + } let dialog = $("#dialog").is(':checked'); if (!dialog || id === 'set-service') { $(".frame").addClass("d-none"); @@ -51,13 +65,17 @@ function toggleSetup(id, type = 'single') { return false; } if (id === 'set-fax') { + let url = './../setup.php'; + if (ServiceSMS === '1') { + url = './../setup_rc.php'; + } let title = 'Fax Module Credentials'; let params = { buttons: [{text: 'Cancel', close: true, style: 'default btn-sm'}], sizeHeight: 'full', allowDrag: false, type: 'iframe', - url: './../setup.php?type=fax&module_config=-1' + url: url + '?type=fax&module_config=-1' } return dlgopen('', '', 'modal-mlg', '', '', title, params); } @@ -68,7 +86,18 @@ function toggleSetup(id, type = 'single') { sizeHeight: 'full', allowDrag: false, type: 'iframe', - url: './../setup.php?type=sms&module_config=-1' + url: url + '?type=sms&module_config=-1' + } + return dlgopen('', '', 'modal-lg', '', '', title, params); + } + if (id === 'set-email') { + let title = 'Email Module Credentials'; + let params = { + buttons: [{text: 'Cancel', close: true, style: 'default btn-sm'}], + sizeHeight: 'full', + allowDrag: false, + type: 'iframe', + url: './../setup_email.php?type=email&module_config=-1' } return dlgopen('', '', 'modal-lg', '', '', title, params); } @@ -85,13 +114,16 @@ function toggleSetup(id, type = 'single') { </script> </head> <body> - <div class="w-100"> + <div class="w-100 container-xl"> <div class="form-group m-2 p-2 bg-dark"> <button class="btn btn-outline-light" onclick="toggleSetup('set-service')"><?php echo xlt("Enable Accounts"); ?><i class="fa fa-caret"></i></button> <?php if (!empty($vendors['oefax_enable_sms'])) { ?> - <button class="btn btn-outline-light" onclick="toggleSetup('set-sms')"><?php echo xlt("Setup SMS Account"); ?><span class="caret"></span></button> - <?php } if (!empty($vendors['oefax_enable_fax'])) { ?> - <button class="btn btn-outline-light" onclick="toggleSetup('set-fax')"><?php echo xlt("Setup Fax Account"); ?><span class="caret"></span></button> + <button class="btn btn-outline-light" onclick="toggleSetup('set-sms')"><?php echo xlt("Setup SMS Account"); ?><span class="caret"></span></button> + <?php } + if (!empty($vendors['oefax_enable_fax'])) { ?> + <button class="btn btn-outline-light" onclick="toggleSetup('set-fax')"><?php echo xlt("Setup Fax Account"); ?><span class="caret"></span></button> + <?php } if (!empty($vendors['oe_enable_email'])) { ?> + <button class="btn btn-outline-light" onclick="toggleSetup('set-email')"><?php echo xlt("Setup Email Account"); ?><span class="caret"></span></button> <?php } ?> <span class="checkbox text-light br-dark" title="Use Dialog or Panels"> <label for="dialog"><?php echo xlt("Render in dialog."); ?></label> @@ -112,7 +144,7 @@ function toggleSetup(id, type = 'single') { <div class="col-sm-6" title="Enable SMS Support. Remember to setup credentials."> <select class="form-control persist" name="sms_vendor" id="sms_vendor"> <option value="0" <?php echo $vendors['oefax_enable_sms'] == '0' ? 'selected' : ''; ?>><?php echo xlt("Disabled"); ?></option> - <!-- Placeholder for RC or another service --> + <option value="1" <?php echo $vendors['oefax_enable_sms'] == '1' ? 'selected' : ''; ?>><?php echo xlt("RingCentral SMS"); ?></option> <option value="2" <?php echo $vendors['oefax_enable_sms'] == '2' ? 'selected' : ''; ?>><?php echo xlt("Twilio SMS"); ?></option> </select> </div> @@ -122,7 +154,7 @@ function toggleSetup(id, type = 'single') { <div class="col-sm-6" title="Enable Fax Support. Remember to setup credentials."> <select class="form-control persist" name="fax_vendor" id="fax_vendor"> <option value="0" <?php echo $vendors['oefax_enable_fax'] == '0' ? 'selected' : ''; ?>><?php echo xlt("Disabled"); ?></option> - <!-- Placeholder for RC or another service --> + <option value="1" <?php echo $vendors['oefax_enable_fax'] == '1' ? 'selected' : ''; ?>><?php echo xlt("RingCentral Fax"); ?></option> <option value="3" <?php echo $vendors['oefax_enable_fax'] == '3' ? 'selected' : ''; ?>><?php echo xlt("etherFAX"); ?></option> </select> </div> @@ -132,7 +164,7 @@ function toggleSetup(id, type = 'single') { <div class="col-sm-6" title="Enable Email Client Support."> <select class="form-control persist" name="email_vendor" id="email_vendor"> <option value="0" <?php echo $vendors['oe_enable_email'] == '0' ? 'selected' : ''; ?>><?php echo xlt("Disabled"); ?></option> - <option value="1" <?php echo $vendors['oe_enable_email'] == '1' ? 'selected' : ''; ?>><?php echo xlt("Enabled"); ?></option> + <option value="4" <?php echo $vendors['oe_enable_email'] == '4' ? 'selected' : ''; ?>><?php echo xlt("Enabled"); ?></option> </select> </div> </div> @@ -156,15 +188,32 @@ function toggleSetup(id, type = 'single') { </div> <!-- iframes to hold setup account scripts. Dialogs replace these if requested in UI --> <?php if (!empty($vendors['oefax_enable_fax'])) { ?> - <div id="set-fax" class="frame d-none"> - <h3 class="text-center"><?php echo xlt("Setup Fax Account"); ?></h3> - <iframe src="./../setup.php?type=fax&module_config=1&mode=flat" style="border:none;height:100vh;width:100%;"></iframe> - </div> - <?php } if (!empty($vendors['oefax_enable_sms'])) { ?> - <div id="set-sms" class="frame d-none"> - <h3 class="text-center"><?php echo xlt("Setup SMS Account"); ?></h3> - <iframe src="./../setup.php?type=sms&module_config=1&mode=flat" style="border:none;height:100vh;width:100%;"></iframe> - </div> + <div id="set-fax" class="frame d-none"> + <h3 class="text-center"><?php echo xlt("Setup Fax Account"); ?></h3> + <iframe src="<?php + $setupUrl = './../setup.php'; + if ($isRCFax) { + $setupUrl = './../setup_rc.php'; + } + echo attr($setupUrl . '?type=fax&module_config=1&mode=flat'); ?>" style="border:none;height:100vh;width:100%;"></iframe> + </div> + <?php } + if (!empty($vendors['oefax_enable_sms'])) { ?> + <div id="set-sms" class="frame d-none"> + <h3 class="text-center"><?php echo xlt("Setup SMS Account"); ?></h3> + <iframe src="<?php + $setupUrl = './../setup.php'; + if ($isRCSMS) { + $setupUrl = './../setup_rc.php'; + } + echo attr($setupUrl . '?type=sms&module_config=1&mode=flat'); ?>" style="border:none;height:100vh;width:100%;"></iframe> + </div> + <?php } ?> + <?php if (!empty($vendors['oe_enable_email'])) { ?> + <div id="set-email" class="frame d-none"> + <h3 class="text-center"><?php echo xlt("Setup Email Account"); ?></h3> + <iframe src="<?php echo attr('./../setup_email.php?type=email&module_config=1&mode=flat'); ?>" style="border:none;height:100vh;width:100%;"></iframe> + </div> <?php } ?> </div> </body> diff --git a/interface/modules/custom_modules/oe-module-faxsms/messageUI.php b/interface/modules/custom_modules/oe-module-faxsms/messageUI.php index c26ea773b27..ba0fd5b6c9c 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/messageUI.php +++ b/interface/modules/custom_modules/oe-module-faxsms/messageUI.php @@ -19,7 +19,8 @@ $serviceType = $_REQUEST['type'] ?? ''; $clientApp = AppDispatch::getApiService($serviceType); $service = $clientApp::getServiceType(); -$title = $service == "2" ? xlt('Twilio SMS') : ''; +$title = $service == "1" ? xlt('RingCentral') : ''; +$title = $service == "2" ? xlt('Twilio SMS') : $title; $title = $service == "3" ? xlt('etherFAX') : $title; $tabTitle = $serviceType == "sms" ? xlt('SMS') : xlt('FAX'); ?> @@ -38,6 +39,7 @@ ";let currentService=" . js_escape($service) . ";let serviceType=" . js_escape($serviceType) . "</script>"; ?> <script type="text/javascript" src="<?php echo $GLOBALS['assets_static_relative']; ?>/dropzone/dist/dropzone.js"></script> + <script> $(function () { $('.datepicker').datetimepicker({ @@ -53,13 +55,22 @@ $("#todate").val(new Date().toJSON().slice(0, 10)); $(".other").hide(); - if (currentService == '2') { + if (currentService == '1' && serviceType == 'fax') { + $(".rc-hide").hide(); + $(".rc-fax-hide").hide(); + } else if (currentService == '1' && serviceType == 'sms') { + $(".rc-hide").hide(); + } else if (currentService == '2') { $(".etherfax").hide(); } else if (currentService == '3') { $(".twilio").hide(); $(".etherfax-hide").hide(); $(".etherfax").show(); } + if (serviceType == 'sms') { + $(".sms-hide").hide(); + } + retrieveMsgs(); $('#received').tab('show'); }); @@ -73,7 +84,11 @@ dlgopen(url, '', 'modal-sm', 700, '', title, { // dialog restores session buttons: [ {text: btnClose, close: true, style: 'secondary btn-sm'} - ] + ], + resolvePromiseOn: 'close', + sizeHeight: 'full' + }).then(function (contact) { + top.restoreSession(); }); }; @@ -111,12 +126,16 @@ console.log('Session restore failed!'); } e.preventDefault(); + let url = top.webroot_url + '/interface/modules/custom_modules/oe-module-faxsms/setup.php'; + if (currentService === '1') { + url = top.webroot_url + '/interface/modules/custom_modules/oe-module-faxsms/setup_rc.php'; + } let msg = <?php echo xlj('Credentials and Notifications') ?>; dlgopen('', 'setup', 'modal-md', 700, '', msg, { buttons: [ {text: 'Cancel', close: true, style: 'secondary btn-sm'} ], - url: "./setup.php?type=" + encodeURIComponent(serviceType) + url: url + "?type=" + encodeURIComponent(serviceType) }); }; @@ -168,7 +187,7 @@ function showPrint(base64, _contentType = 'image/tiff') { } // Function to get or dispose of document. - async function getDocument(e, docuri, docid, downFlag, deleteFlag = '') { + async function getDocument(e, docuri, docid, downFlag, deleteFlag = '', massDelete = false) { try { top.restoreSession(); } catch (error) { @@ -189,7 +208,7 @@ function showPrint(base64, _contentType = 'image/tiff') { return false; } } - if (deleteFlag == 'true') { + if (deleteFlag == 'true' && !massDelete) { let yn = confirm( xl("Are you sure you want to continue with delete?") ); @@ -216,7 +235,15 @@ function showPrint(base64, _contentType = 'image/tiff') { } catch { data = json; } + if (data.error) { + alertMsg(data.error); + return false; + } + if (deleteFlag == 'true') { + if (massDelete) { + return false; + } setTimeout(retrieveMsgs, 1000); return false; } @@ -238,17 +265,14 @@ function showPrint(base64, _contentType = 'image/tiff') { file_path: data.path, content: base64 }) - }) - .then(response => response.json()) - .then(result => { + }).then(response => response.json()).then(result => { // Download the file. result.url is temp file path of tiff to pdf by image conversion in JS or imagick. if (result.success) { location.href = "disposeDocument?type=fax&action=download&file_path=" + encodeURIComponent(result.url); } else { console.error('Failed to prepare the file for download:', jsText(result.message)); } - }) - .catch(error => { + }).catch(error => { console.error('Error:', error); }); @@ -284,7 +308,6 @@ function showPrint(base64, _contentType = 'image/tiff') { resolve(imageData); })); } - return Promise.all(promises); } catch (error) { console.error('Failed to convert TIFF to images:', error); @@ -329,7 +352,7 @@ function showDocument(_base64, _contentType = 'image/tiff') { view[i] = binary.charCodeAt(i); } - const blob = new Blob([view], { type: _contentType }); + const blob = new Blob([view], {type: _contentType}); const dataUrl = URL.createObjectURL(blob); displayInNewWindow(dataUrl); } catch (e) { @@ -365,7 +388,13 @@ function retrieveMsgs(e = '', req = '') { e.stopPropagation(); } - const actionUrl = (serviceType === 'fax') ? 'fax/getPending?type=fax' : 'fetchSMSList?type=sms'; + let actionUrl = (serviceType === 'fax') ? 'getPending?type=fax' : ''; + if (serviceType === 'sms' && currentService == '1') { //RC + actionUrl = 'getPending?type=sms'; + } else if (serviceType === 'sms') { //Twilio + actionUrl = 'fetchSMSList?type=sms'; + } + const datefrom = $('#fromdate').val(); const dateto = $('#todate').val(); @@ -393,7 +422,6 @@ function retrieveMsgs(e = '', req = '') { alertMsg(data.error); return false; } - // Populate our cards rcvDetailsBody.append(data[0]); sentDetailsBody.append(data[1]); @@ -533,7 +561,6 @@ function createPatient(e, faxId, recordId, data) { ); return false; } - // drop bucket const queueMsg = '' + <?php echo xlj('Fax Queue. Drop files or Click here for Fax Contact form.') ?>; Dropzone.autoDiscover = false; @@ -576,6 +603,32 @@ function createPatient(e, faxId, recordId, data) { } }); }); + + document.addEventListener('DOMContentLoaded', function () { + const deleteSelectedFaxesButton = document.querySelectorAll('.delete-selected-items'); + deleteSelectedFaxesButton.forEach(button => { + button.addEventListener('click', function () { + const selectedFaxes = document.querySelectorAll('.delete-fax-checkbox:checked'); + const faxIds = Array.from(selectedFaxes).map(checkbox => checkbox.value); + + if (faxIds.length === 0) { + alert('No faxes selected for deletion.'); + return; + } + + if (!confirm(`Are you sure you want to delete ${faxIds.length} faxes?`)) { + return; + } + + faxIds.forEach(id => { + getDocument('', null, id, 'false', 'true', true); + }); + + setTimeout(retrieveMsgs, 1000); + return false; + }); + }); + }); </script> </head> <body class="body_top"> @@ -626,35 +679,134 @@ function createPatient(e, faxId, recordId, data) { <!-- Nav tabs --> <ul id="tab-menu" class="nav nav-pills" role="tablist"> <li class="nav-item" role="tab"> - <a class="nav-link active" href="#received" aria-controls="received" role="tab" data-toggle="tab"><?php echo xlt("Received") ?><span class="fa fa-redo ml-1" onclick="retrieveMsgs('', this)" - title="<?php echo xla('Click to refresh using current date range. Refreshing just this tab.') ?>"></span></a> + <a class="nav-link active" href="#received" aria-controls="received" role="tab" data-toggle="tab"><?php echo xlt("Received") ?> + <span class="fa fa-redo ml-1" onclick="retrieveMsgs('', this)" + title="<?php echo xla('Click to refresh using current date range. Refreshing just this tab.') ?>"> + </span> + </a> </li> - <li class="nav-item etherfax-hide" role="tab"><a class="nav-link" href="#sent" aria-controls="sent" role="tab" data-toggle="tab"><?php echo xlt("Sent") ?></a></li> - <li class="nav-item etherfax-hide" role="tab"><a class="nav-link" href="#messages" aria-controls="messages" role="tab" data-toggle="tab"><?php echo xlt("SMS Log") ?></a></li> + <li class="nav-item" role="tab"><a class="nav-link" href="#sent" aria-controls="sent" role="tab" data-toggle="tab"><?php echo xlt("Sent") ?></a></li> + <li class="nav-item rc-fax-hide etherfax-hide" role="tab"><a class="nav-link" href="#messages" aria-controls="messages" role="tab" data-toggle="tab"><?php echo xlt("SMS Log") ?></a></li> <li class="nav-item" role="tab"><a class="nav-link" href="#logs" aria-controls="logs" role="tab" data-toggle="tab"><?php echo xlt("Call Log") ?></a></li> - <li class="nav-item etherfax-hide" role="tab"> + <li class="nav-item etherfax-hide rc-fax-hide" role="tab"> <a class="nav-link" href="#alertlogs" aria-controls="alertlogs" role="tab" data-toggle="tab"><?php echo xlt("Reminder Notifications Log") ?><span class="fa fa-redo ml-1" onclick="getNotificationLog(event, this)" title="<?php echo xla('Click to refresh using current date range. Refreshing just this tab.') ?>"></span></a> </li> - <li class="nav-item etherfax" role="tab"><a class="nav-link" href="#upLoad" aria-controls="logs" role="tab" data-toggle="tab"><?php echo xlt("Fax Drop Box") ?></a></li> + <li class="nav-item sms-hide etherfax" role="tab"><a class="nav-link" href="#upLoad" aria-controls="logs" role="tab" data-toggle="tab"><?php echo xlt("Fax Drop Box") ?></a></li> </ul> <!-- Tab panes --> - <div class="tab-content"> - <div role="tabpanel" class="container-fluid tab-pane fade" id="received"> - <?php if ($service == '3') { ?> + <?php if ($service != '1') { ?> + <div class="tab-content"> + <div role="tabpanel" class="container-fluid tab-pane fade" id="received"> + <?php if ($service == '3') { ?> + <div class="table-responsive"> + <table class="table table-sm" id="rcvdetails"> + <thead> + <tr> + <th><?php echo xlt("Time") ?></th> + <th><?php echo xlt("Caller #") ?></th> + <th><?php echo xlt("Caller Id") ?></th> + <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Pages") ?></th> + <th><?php echo xlt("Length") ?></th> + <th><?php echo xlt("Status") ?></th> + <th><a role='button' href='javascript:void(0)' class='btn btn-link fa fa-eye' onclick="toggleDetail('collapse')"></a><?php echo xlt("Extracted") ?></th> + <th><?php echo xlt("MRN Match") ?></th> + <th><?php echo xlt("Actions") ?></th> + <th><i role="button" id="delete-selected-received" title="<?php echo xla("Delete selected fax documents") ?>" class="delete-selected-items text-danger fa fa-trash"></i></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items Try Refresh") ?></td> + </tr> + </tbody> + </table> + </div> + <?php } else { // all others ?> + <div class="table-responsive"> + <table class="table table-sm" id="rcvdetails"> + <thead> + <tr> + <th><?php echo xlt("Time") ?></th> + <th><?php echo xlt("Type") ?></th> + <th class=""><?php echo xlt("Message") ?></th> + <th><?php echo xlt("From") ?></th> + <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Result") ?></th> + <th><?php echo xlt("Reply") ?></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items Try Refresh") ?></td> + </tr> + </tbody> + </table> + </div> + <?php } ?> + </div> + <div role="tabpanel" class="container-fluid tab-pane fade" id="sent"> + <?php if ($service == '3') { ?> + <div class="table-responsive"> + <table class="table table-sm" id="sent-details"> + <thead> + <tr> + <th><?php echo xlt("Time") ?></th> + <th><?php echo xlt("Caller #") ?></th> + <th><?php echo xlt("Caller Id") ?></th> + <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Pages") ?></th> + <th><?php echo xlt("Length") ?></th> + <th><?php echo xlt("Status") ?></th> + <th><?php echo xlt("Actions") ?></th> + <th><i role="button" id="delete-selected-sent" title="<?php echo xlt("Delete selected fax documents") ?>" class="delete-selected-items text-danger fa fa-trash"></i></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items Try Refresh") ?></td> + </tr> + </tbody> + </table> + </div> + <?php } else { ?> + <div class="table-responsive"> + <table class="table table-sm table-striped" id="sent-details"> + <thead> + <tr> + <th><?php echo xlt("Start Time") ?></th> + <th class="twilio"><?php echo xlt("Price") ?></th> + <th class="etherfax"><?php echo xlt("Type") ?></th> + <th><?php echo xlt("Message") ?></th> + <th><?php echo xlt("From") ?></th> + <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Result") ?></th> + <th class="etherfax"><?php echo xlt("Download") ?></th> + <th class="twilio"><?php echo xlt("Reply") ?></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items Try Refresh") ?></td> + </tr> + </tbody> + </table> + </div> + <?php } ?> + </div> + <div role="tabpanel" class="container-fluid tab-pane fade" id="messages"> <div class="table-responsive"> - <table class="table table-sm" id="rcvdetails"> + <table class="table table-sm table-striped" id="msgdetails"> <thead> <tr> - <th><?php echo xlt("Time") ?></th> - <th><?php echo xlt("Caller #") ?></th> - <th><?php echo xlt("Caller Id") ?></th> + <th><?php echo xlt("Date") ?></th> + <th><?php echo xlt("Type") ?></th> + <th><?php echo xlt("From") ?></th> <th><?php echo xlt("To") ?></th> - <th><?php echo xlt("Pages") ?></th> - <th><?php echo xlt("Length") ?></th> - <th><a role='button' href='javascript:void(0)' class='btn btn-link fa fa-eye' onclick="toggleDetail('collapse')"></a><?php echo xlt("Extracted") ?></th> - <th><?php echo xlt("MRN Match") ?></th> - <th><?php echo xlt("Actions") ?></th> + <th><?php echo xlt("Result") ?></th> + <th class="other"><?php echo xlt("Download") ?></th> + <th><?php echo xlt("View") ?></th> </tr> </thead> <tbody> @@ -664,18 +816,19 @@ function createPatient(e, faxId, recordId, data) { </tbody> </table> </div> - <?php } else { ?> + </div> + <div role="tabpanel" class="container-fluid tab-pane fade" id="logs"> <div class="table-responsive"> - <table class="table table-sm" id="rcvdetails"> + <table class="table table-sm table-striped" id="logdetails"> <thead> <tr> - <th><?php echo xlt("Time") ?></th> + <th><?php echo xlt("Date") ?></th> <th><?php echo xlt("Type") ?></th> - <th class=""><?php echo xlt("Message") ?></th> <th><?php echo xlt("From") ?></th> <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Action") ?></th> <th><?php echo xlt("Result") ?></th> - <th><?php echo xlt("Reply") ?></th> + <th><?php echo xlt("Id") ?></th> </tr> </thead> <tbody> @@ -685,106 +838,162 @@ function createPatient(e, faxId, recordId, data) { </tbody> </table> </div> - <?php } ?> - </div> - <div role="tabpanel" class="container-fluid tab-pane fade" id="sent"> - <div class="table-responsive"> - <table class="table table-sm table-striped" id="sent-details"> - <thead> - <tr> - <th><?php echo xlt("Start Time") ?></th> - <th class="twilio"><?php echo xlt("Price") ?></th> - <th class="etherfax"><?php echo xlt("Type") ?></th> - <th><?php echo xlt("Message") ?></th> - <th><?php echo xlt("From") ?></th> - <th><?php echo xlt("To") ?></th> - <th><?php echo xlt("Result") ?></th> - <th class="etherfax"><?php echo xlt("Download") ?></th> - <th class="twilio"><?php echo xlt("Reply") ?></th> - </tr> - </thead> - <tbody> - <tr> - <td><?php echo xlt("No Items Try Refresh") ?></td> - </tr> - </tbody> - </table> </div> - </div> - <div role="tabpanel" class="container-fluid tab-pane fade" id="messages"> - <div class="table-responsive"> - <table class="table table-sm table-striped" id="msgdetails"> - <thead> - <tr> - <th><?php echo xlt("Date") ?></th> - <th><?php echo xlt("Type") ?></th> - <th><?php echo xlt("From") ?></th> - <th><?php echo xlt("To") ?></th> - <th><?php echo xlt("Result") ?></th> - <th class="other"><?php echo xlt("Download") ?></th> - <th><?php echo xlt("View") ?></th> - </tr> - </thead> - <tbody> - <tr> - <td><?php echo xlt("No Items Try Refresh") ?></td> - </tr> - </tbody> - </table> + <div role="tabpanel" class="container-fluid tab-pane fade etherfax-hide" id="alertlogs"> + <div class="table-responsive"> + <table class="table table-sm table-striped" id="alertdetails"> + <thead> + <tr> + <th><?php echo xlt("Id") ?></th> + <th><?php echo xlt("Date Sent") ?></th> + <th><?php echo xlt("Appt Date Time") ?></th> + <th><?php echo xlt("Patient") ?></th> + <th><?php echo xlt("Message") ?></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items") ?></td> + </tr> + </tbody> + </table> + </div> </div> - </div> - <div role="tabpanel" class="container-fluid tab-pane fade" id="logs"> - <div class="table-responsive"> - <table class="table table-sm table-striped" id="logdetails"> - <thead> - <tr> - <th><?php echo xlt("Date") ?></th> - <th><?php echo xlt("Type") ?></th> - <th><?php echo xlt("From") ?></th> - <th><?php echo xlt("To") ?></th> - <th><?php echo xlt("Action") ?></th> - <th><?php echo xlt("Result") ?></th> - <th><?php echo xlt("Id") ?></th> - </tr> - </thead> - <tbody> - <tr> - <td><?php echo xlt("No Items Try Refresh") ?></td> - </tr> - </tbody> - </table> + <div role="tabpanel" class="container-fluid tab-pane fade" id="upLoad"> + <div class="panel container-fluid"> + <div id="fax-queue-container"> + <div id="fax-queue"> + <form id="faxQueue" method="post" enctype="multipart/form-data" class="dropzone"></form> + </div> + </div> + </div> </div> </div> - <div role="tabpanel" class="container-fluid tab-pane fade" id="alertlogs"> - <div class="table-responsive"> - <table class="table table-sm table-striped" id="alertdetails"> - <thead> - <tr> - <th><?php echo xlt("Id") ?></th> - <th><?php echo xlt("Date Sent") ?></th> - <th><?php echo xlt("Appt Date Time") ?></th> - <th><?php echo xlt("Patient") ?></th> - <th><?php echo xlt("Message") ?></th> - </tr> - </thead> - <tbody> - <tr> - <td><?php echo xlt("No Items") ?></td> - </tr> - </tbody> - </table> + <?php } + if ($service == '1') { ?> + <div class="tab-content"> + <div role="tabpanel" class="container-fluid tab-pane fade" id="received"> + <div class="table-responsive"> + <table class="table table-sm table-striped" id="rcvdetails"> + <thead> + <tr> + <th><?php echo xlt("Start Time") ?></th> + <th><?php echo xlt("End Time") ?></th> + <th><?php echo xlt("Pages") ?></th> + <th><?php echo xlt("From") ?></th> + <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Status") ?></th> + <th><?php echo xlt("Actions") ?></th> + <th><i role="button" id="delete-selected-received" title="<?php echo xla("Delete selected fax documents") ?>" class="delete-selected-items text-danger fa fa-trash"></i></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items Try Refresh") ?></td> + </tr> + </tbody> + </table> + </div> </div> - </div> - <div role="tabpanel" class="container-fluid tab-pane fade" id="upLoad"> - <div class="panel container-fluid"> - <div id="fax-queue-container"> - <div id="fax-queue"> - <form id="faxQueue" method="post" enctype="multipart/form-data" class="dropzone"></form> + <div role="tabpanel" class="container-fluid tab-pane fade" id="sent"> + <div class="table-responsive"> + <table class="table table-sm table-striped" id="sent-details"> + <thead> + <tr> + <th><?php echo xlt("Start Time") ?></th> + <th><?php echo xlt("End Time") ?></th> + <th><?php echo xlt("Pages") ?></th> + <th><?php echo xlt("From") ?></th> + <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Status") ?></th> + <th><?php echo xlt("Actions") ?></th> + <th><i role="button" id="delete-selected-received" title="<?php echo xla("Delete selected fax documents") ?>" class="delete-selected-items text-danger fa fa-trash"></i></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items Try Refresh") ?></td> + </tr> + </tbody> + </table> + </div> + </div> + <div role="tabpanel" class="container-fluid tab-pane fade d-none" id="messages"> + <div class="table-responsive"> + <table class="table table-sm table-striped" id="msgdetails"> + <thead> + <tr> + <th><?php echo xlt("Date") ?></th> + <th><?php echo xlt("Type") ?></th> + <th><?php echo xlt("From") ?></th> + <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Result") ?></th> + <th class="twilio"><?php echo xlt("Download") ?></th> + <th class="ringcentral"><?php echo xlt("Message") ?></th> + <th><?php echo xlt("View") ?></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items Try Refresh") ?></td> + </tr> + </tbody> + </table> + </div> + </div> + <div role="tabpanel" class="container-fluid tab-pane fade" id="logs"> + <div class="table-responsive"> + <table class="table table-sm table-striped" id="logdetails"> + <thead> + <tr> + <th><?php echo xlt("Date") ?></th> + <th><?php echo xlt("Type") ?></th> + <th><?php echo xlt("From") ?></th> + <th><?php echo xlt("To") ?></th> + <th><?php echo xlt("Action") ?></th> + <th><?php echo xlt("Result") ?></th> + <th><?php echo xlt("Id") ?></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items Try Refresh") ?></td> + </tr> + </tbody> + </table> + </div> + </div> + <div role="tabpanel" class="container-fluid tab-pane fade rc-fax-hide" id="alertlogs"> + <div class="table-responsive"> + <table class="table table-sm table-striped" id="alertdetails"> + <thead> + <tr> + <th><?php echo xlt("Id") ?></th> + <th><?php echo xlt("Date Sent") ?></th> + <th><?php echo xlt("Appt Date Time") ?></th> + <th><?php echo xlt("Patient") ?></th> + <th><?php echo xlt("Message") ?></th> + </tr> + </thead> + <tbody> + <tr> + <td><?php echo xlt("No Items") ?></td> + </tr> + </tbody> + </table> + </div> + </div> + <div role="tabpanel" class="container-fluid tab-pane sms-hide fade in active" id="upLoad"> + <div class="panel container-fluid"> + <div id="fax-queue-container"> + <div id="fax-queue"> + <form id="faxQueue" method="post" enctype="multipart/form-data" class="dropzone"></form> + </div> </div> </div> </div> </div> - </div> + <?php } ?> </div> </div> </div> diff --git a/interface/modules/custom_modules/oe-module-faxsms/moduleConfig.php b/interface/modules/custom_modules/oe-module-faxsms/moduleConfig.php index d802842eb3a..4bfb0eed2eb 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/moduleConfig.php +++ b/interface/modules/custom_modules/oe-module-faxsms/moduleConfig.php @@ -16,6 +16,5 @@ ?> <div id="set-services"> - <h3 class="text-center"><?php echo xlt("Select Services"); ?></h3> <iframe src="library/setup_services.php?module_config=1" style="border:none;height:100vh;width:100%;"></iframe> </div> diff --git a/interface/modules/custom_modules/oe-module-faxsms/openemr.bootstrap.php b/interface/modules/custom_modules/oe-module-faxsms/openemr.bootstrap.php index bb0b2632ace..3be44f6c164 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/openemr.bootstrap.php +++ b/interface/modules/custom_modules/oe-module-faxsms/openemr.bootstrap.php @@ -65,7 +65,7 @@ function oe_module_faxsms_add_menu_item(MenuEvent $event): MenuEvent $menuItem->requirement = 0; $menuItem->target = 'sms'; $menuItem->menu_id = 'mod0'; - $menuItem->label = $allowSMS == '2' ? xlt("Manage Twilio Messaging") : xlt("Manage Messaging"); + $menuItem->label = $allowSMS == '2' ? xlt("Manage Twilio Messaging") : xlt("RingCentral SMS"); $menuItem->url = "/interface/modules/custom_modules/oe-module-faxsms/messageUI.php?type=sms"; $menuItem->children = []; $menuItem->acl_req = ["patients", "docs"]; @@ -75,15 +75,29 @@ function oe_module_faxsms_add_menu_item(MenuEvent $event): MenuEvent $menuItem2->requirement = 0; $menuItem2->target = 'fax'; $menuItem2->menu_id = 'mod1'; - $menuItem2->label = $allowFax == '3' ? xlt("Manage etherFAX") : xlt("Manage FAX"); + $menuItem2->label = $allowFax == '3' ? xlt("Manage etherFAX") : xlt("RingCentral FAX"); $menuItem2->url = "/interface/modules/custom_modules/oe-module-faxsms/messageUI.php?type=fax"; $menuItem2->children = []; $menuItem2->acl_req = ["patients", "docs"]; $menuItem2->global_req = ["oefax_enable_fax"]; + + $menuItemSetup = new stdClass(); + $menuItemSetup->requirement = 0; + $menuItemSetup->target = 'setup'; + $menuItemSetup->menu_id = 'mod2'; + $menuItemSetup->label = xlt("Setup Module Services"); + $menuItemSetup->url = "/interface/modules/custom_modules/oe-module-faxsms/library/setup_services.php?module_config=1"; + $menuItemSetup->children = []; + $menuItemSetup->acl_req = ["patients", "docs"]; + $menuItemSetup->global_req = ["oefax_enable_fax"]; + // Child of Manage Modules top menu. foreach ($menu as $item) { if ($item->menu_id == 'modimg') { // ensure service is on in globals. + if (!empty($allowSMS) || !empty($allowFax)) { + $item->children[] = $menuItemSetup; + } if (!empty($allowFax)) { $item->children[] = $menuItem2; } @@ -155,10 +169,15 @@ function doFax(e, filePath, mime='') { e.preventDefault(); let btnClose = <?php echo xlj("Cancel"); ?>; let title = <?php echo xlj("Send To Contact"); ?>; - let url = top.webroot_url + - '/interface/modules/custom_modules/oe-module-faxsms/contact.php?isDocuments=1&type=fax&file=' + + let url = top.webroot_url + '/interface/modules/custom_modules/oe-module-faxsms/contact.php?isDocuments=1&type=fax&file=' + encodeURIComponent(filePath) + '&mime=' + encodeURIComponent(mime) + '&docid=' + encodeURIComponent(docid); - dlgopen(url, 'faxto', 'modal-md', 700, '', title, {buttons: [{text: btnClose, close: true, style: 'primary'}]}); + dlgopen(url, 'faxto', 'modal-md', 'full', '', title, { + buttons: [], + sizeHeight: 'full', + resolvePromiseOn: 'close' + }).then(function () { + top.restoreSession(); + }); return false; } <?php } diff --git a/interface/modules/custom_modules/oe-module-faxsms/setup.php b/interface/modules/custom_modules/oe-module-faxsms/setup.php index 316b397c81f..6dd9d86bef9 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/setup.php +++ b/interface/modules/custom_modules/oe-module-faxsms/setup.php @@ -172,6 +172,53 @@ <textarea id="form_message" type="text" rows="3" name="smsmessage" class="form-control" value='<?php echo attr($c['smsMessage']) ?>'><?php echo text($c['smsMessage']) ?></textarea> </div> + <?php } elseif ($service == '1') { + ?> <!-- RC --> + <div class="checkbox"> + <label> + <input id="form_production" type="checkbox" name="production" <?php echo attr($c['production']) ? ' checked' : '' ?>> + <?php echo xlt("Production Check") ?> + </label> + </div> + <div class="form-group"> + <label for="form_username"><?php echo xlt("Twilio Account Sid") ?> *</label> + <input id="form_username" type="text" name="username" class="form-control" + required="required" value='<?php echo attr($c['username']) ?>' /> + </div> + <div class="form-group"> + <label for="form_password"><?php echo xlt("Twilio Auth Token") ?> *</label> + <input id="form_password" type="password" name="password" class="form-control" + required="required" value='<?php echo attr($c['password']) ?>' /> + </div> + <div class="form-group"> + <label for="form_smsnumber"><?php echo xlt("SMS Number") ?></label> + <input id="form_smsnumber" type="text" name="smsnumber" class="form-control" + value='<?php echo attr($c['smsNumber']) ?>' required /> + </div> + <div class="form-group"> + <label for="form_key"><?php echo xlt("Twilio Api Sid") ?> *</label> + <input id="form_key" type="text" name="key" class="form-control" + required="required" value='<?php echo attr($c['appKey']) ?>' /> + </div> + <div class="form-group"> + <label for="form_secret"><?php echo xlt("Twilio Api Secret") ?> *</label> + <input id="form_secret" type="password" name="secret" class="form-control" + required="required" value='<?php echo attr($c['appSecret']) ?>' /> + </div> + <div class=" form-group"> + <label for="form_nhours"><?php echo xlt("Appointments Advance Notify (Hours)") ?> *</label> + <input id="form_nhours" type="text" name="smshours" class="form-control" + placeholder="<?php echo xla('Please enter number of hours before appointment') ?> *" + required="required" value='<?php echo attr($c['smsHours']) ?>' /> + </div> + <div class="form-group"> + <label for="form_message"><?php echo xlt("Message Template") ?> *</label> + <span style="font-size:12px;font-style: italic"> + <?php echo xlt("Replace Tags") ?>: <?php echo text("***NAME***, ***PROVIDER***, ***DATE***, ***STARTTIME***, ***ENDTIME***, ***ORG***"); ?> + </span> + <textarea id="form_message" type="text" rows="3" name="smsmessage" class="form-control" + value='<?php echo attr($c['smsMessage']) ?>'><?php echo text($c['smsMessage']) ?></textarea> + </div> <?php } ?> <div> <span class="text-muted"><strong>*</strong> <?php echo xlt("These fields are required.") ?> </span> diff --git a/interface/modules/custom_modules/oe-module-faxsms/setup_email.php b/interface/modules/custom_modules/oe-module-faxsms/setup_email.php new file mode 100644 index 00000000000..bf509df3983 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-faxsms/setup_email.php @@ -0,0 +1,163 @@ +<?php + +/** + * Patient Reminder Setup + * + * @package OpenEMR + * @link http://www.open-emr.org + * @author Jerry Padgett <sjpadgett@gmail.com> + * @copyright Copyright (c) 2024 Jerry Padgett <sjpadgett@gmail.com> + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +require_once(__DIR__ . "/../../../globals.php"); + +use OpenEMR\Core\Header; +use OpenEMR\Modules\FaxSMS\Controller\EmailClient; + +$emailSetup = new EmailClient(); +$credentials = $emailSetup->getEmailSetup(); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $credentials = array( + 'sender_name' => $_POST['sender_name'], + 'sender_email' => $_POST['sender_email'], + 'notification_email' => $_POST['notification_email'], + 'email_transport' => $_POST['email_transport'], + 'smtp_host' => $_POST['smtp_host'], + 'smtp_port' => $_POST['smtp_port'], + 'smtp_user' => $_POST['smtp_user'], + 'smtp_password' => $_POST['smtp_password'], + 'smtp_security' => $_POST['smtp_security'], + 'notification_hours' => $_POST['notification_hours'], + 'smsMessage' => $_POST['smsmessage'] + ); + $emailSetup->saveEmailSetup($credentials); +} + +?> +<!DOCTYPE html> +<html> +<head> + <title><?php echo xlt("Patient Reminder Setup") ?></title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <?php Header::setupHeader(); ?> + <script> + const popNotify = function (e, ppath) { + try { + top.restoreSession(); + } catch (error) { + console.log('Session restore failed!'); + } + let msg = <?php echo xlj('Are you sure you wish to send all scheduled reminders now?') ?>; + if (e === 'live') { + let yn = confirm(msg); + if (!yn) { + return false; + } + } + let msg1 = <?php echo xlj('Appointment Reminder Alerts!') ?>; + dlgopen(ppath, '_blank', 1240, 900, true, msg1); + }; + </script> +</head> +<body> + <div class="container"> + <div class="alert alert-info"> + <?php echo xlt("Use Config Notifications to setup SMTP. Setup here is work in progress for a secondary client. Sending Email reminders is still available from Reminder Actions dropdown.") ?> + </div> + <div class="d-flex justify-content-between align-items-center"> + <h4><?php echo xlt("Setup Patient Reminder Credentials") ?></h4> + <div class="dropdown mr-1"> + <button class="btn btn-outline-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <?php echo xlt("Reminder Actions") ?> + </button> + <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> + <a class="dropdown-item" href="#" onclick="popNotify('', './library/rc_sms_notification.php?dryrun=1&type=email&site=<?php echo attr_url($_SESSION['site_id']) ?>')"><?php echo xlt('Test Email Reminders'); ?></a> + <a class="dropdown-item" href="#" onclick="popNotify('live', './library/rc_sms_notification.php?type=email&site=<?php echo attr_url($_SESSION['site_id']) ?>')"><?php echo xlt('Send Email Reminders'); ?></a> + </div> + </div> + </div> + <form class="form" id="setup-form" method="POST" role="form"> + <div class="messages"></div> + <div class="row"> + <div class="col-md-6"> + <div class="form-group"> + <label for="sender_name"><?php echo xlt("Patient Reminder Sender Name") ?></label> + <input id="sender_name" type="text" name="sender_name" class="form-control" value='<?php echo attr($credentials['sender_name']) ?>' required> + </div> + </div> + <div class="col-md-6"> + <div class="form-group"> + <label for="sender_email"><?php echo xlt("Patient Reminder Sender Email") ?></label> + <input id="sender_email" type="email" name="sender_email" class="form-control" value='<?php echo attr($credentials['sender_email']) ?>' required> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-6"> + <div class="form-group"> + <label for="notification_email"><?php echo xlt("Notification Email Address") ?></label> + <input id="notification_email" type="email" name="notification_email" class="form-control" value='<?php echo attr($credentials['notification_email']) ?>' required> + </div> + </div> + <div class="col-md-6"> + <div class="form-group"> + <label for="email_transport"><?php echo xlt("Email Transport Method") ?></label> + <input id="email_transport" type="text" name="email_transport" class="form-control" value='<?php echo attr($credentials['email_transport']) ?>' required> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-6"> + <div class="form-group"> + <label for="smtp_host"><?php echo xlt("SMTP Server Hostname") ?></label> + <input id="smtp_host" type="text" name="smtp_host" class="form-control" value='<?php echo attr($credentials['smtp_host']) ?>' required> + </div> + </div> + <div class="col-md-6"> + <div class="form-group"> + <label for="smtp_port"><?php echo xlt("SMTP Server Port Number") ?></label> + <input id="smtp_port" type="number" name="smtp_port" class="form-control" value='<?php echo attr($credentials['smtp_port']) ?>' required> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-6"> + <div class="form-group"> + <label for="smtp_user"><?php echo xlt("SMTP User for Authentication") ?></label> + <input id="smtp_user" type="text" name="smtp_user" class="form-control" value='<?php echo attr($credentials['smtp_user']) ?>' required> + </div> + </div> + <div class="col-md-6"> + <div class="form-group"> + <label for="smtp_password"><?php echo xlt("SMTP Password for Authentication") ?></label> + <input id="smtp_password" type="password" name="smtp_password" class="form-control" value='<?php echo attr($credentials['smtp_password']) ?>' required> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-6"> + <div class="form-group"> + <label for="smtp_security"><?php echo xlt("SMTP Security Protocol") ?></label> + <input id="smtp_security" type="text" name="smtp_security" class="form-control" value='<?php echo attr($credentials['smtp_security']) ?>' required> + </div> + </div> + <div class="col-md-6"> + <div class="form-group"> + <label for="notification_hours"><?php echo xlt("Email Notification Hours") ?></label> + <input id="notification_hours" type="number" name="notification_hours" class="form-control" value='<?php echo attr($credentials['notification_hours']) ?>' required> + </div> + </div> + </div> + <div class="form-group"> + <label for="form_message"><?php echo xlt("Message Template") ?></label> + <span style="font-size:12px;font-style: italic;"> + <?php echo xlt("Tags") ?>: ***NAME***, ***PROVIDER***, ***DATE***, ***STARTTIME***, ***ENDTIME***, ***ORG***</span> + <textarea id="form_message" rows="3" name="smsmessage" class="form-control" required><?php echo text($credentials['smsMessage']) ?></textarea> + </div> + <button type="submit" class="btn btn-success float-right m-2"><?php echo xlt("Save Settings") ?></button> + </form> + </div> +</body> +</html> diff --git a/interface/modules/custom_modules/oe-module-faxsms/setup_rc.php b/interface/modules/custom_modules/oe-module-faxsms/setup_rc.php new file mode 100644 index 00000000000..dddf4991110 --- /dev/null +++ b/interface/modules/custom_modules/oe-module-faxsms/setup_rc.php @@ -0,0 +1,198 @@ +<?php + +/** + * Fax SMS Module Member + * + * @package OpenEMR + * @link http://www.open-emr.org + * @author Jerry Padgett <sjpadgett@gmail.com> + * @copyright Copyright (c) 2018-2024 Jerry Padgett <sjpadgett@gmail.com> + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +namespace OpenEMR\Modules\FaxSMS\Controllers; + +require_once(__DIR__ . "/../../../globals.php"); + +use OpenEMR\Core\Header; +use OpenEMR\Modules\FaxSMS\Controller\AppDispatch; + +// kick off app endpoints controller +$serviceType = $_REQUEST['type'] ?? $_SESSION["oefax_current_module_type"] ?? ''; +// kick off app endpoints controller +$clientApp = AppDispatch::getApiService($serviceType); +$service = $clientApp::getServiceType(); +if (!$clientApp->verifyAcl()) { + die("<h3>" . xlt("Not Authorised!") . "</h3>"); +} +$c = $clientApp->getCredentials(); +echo "<script>var pid=" . js_escape($pid) . "</script>"; +?> +<!DOCTYPE html> +<html> +<head> + <title>><?php echo xlt("Setup") ?></title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <?php Header::setupHeader(); + echo "<script>let Service=" . js_escape($service) . "</script>"; + ?> + <script> + $(function () { + $('#setup-form').on('submit', function (e) { + if (!e.isDefaultPrevented()) { + $(window).scrollTop(0); + let wait = '<i class="fa fa-cog fa-spin fa-4x"></i>'; + let url = 'saveSetup?type=' + encodeURIComponent(<?php echo js_escape($serviceType) ?>); + $.ajax({ + type: "POST", + url: url, + data: $(this).serialize(), + success: function (data) { + var err = (data.search(/Exception/) !== -1 ? 1 : 0); + if (!err) { + err = (data.search(/Error:/) !== -1 ? 1 : 0); + } + var messageAlert = 'alert-' + (err !== 0 ? 'danger' : 'success'); + var messageText = data; + var alertBox = '<div class="alert ' + messageAlert + ' alert-dismissable"><button type="button" ' + + 'class="close" data-dismiss="alert" aria-hidden="true">×</button>' + messageText + '</div>'; + if (messageAlert && messageText) { + // inject the alert to .messages div in our form + $('#setup-form').find('.messages').html(alertBox); + if (!err) { + // empty the form + $('#setup-form')[0].reset(); + setTimeout(function () { + $('#setup-form').find('.messages').remove(); + <?php if (!$module_config) { ?> + dlgclose(); + <?php } else { ?> + location.reload(); + <?php } ?> + }, 2000); + } + } + } + }); + return false; + } + }); + if (Service === '2') { + $(".ringcentral").hide(); + } else { + $(".twilio").hide(); + } + }); + + function openJwtWindow() { + const clientId = document.getElementById('form_key').value; + if (clientId) { + const url = 'https://developers.ringcentral.com/console/my-credentials/create?client_id=' + encodeURIComponent(clientId); + window.open(url, '_blank', 'width=1200,height=800'); + } else { + let message = xl("Please enter Client ID first."); + (async (message, time) => { + await asyncAlertMsg(message, time, 'warning', 'lg'); + })(message,1500).then(res => { + console.log(res); + }); + } + } + + function togglePasswordVisibility(id) { + var input = document.getElementById(id); + var icon = document.querySelector(`#${id} + .toggle-password i`); + if (input.type === "password") { + input.type = "text"; + icon.classList.remove("fa-eye"); + icon.classList.add("fa-eye-slash"); + } else { + input.type = "password"; + icon.classList.remove("fa-eye-slash"); + icon.classList.add("fa-eye"); + } + } + </script> +</head> +<body> + <div class="container-fluid"> + <?php if ($module_config) { ?> + <h4><?php echo xlt("Setup Credentials") ?></h4> + <?php } ?> + <form class="form" id="setup-form" role="form"> + <div class="messages"></div> + <div class="row"> + <div class="col-md-12"> + <div class="checkbox"> + <button type="submit" class="btn btn-success float-right m-2" value=""><?php echo xlt("Save Settings") ?></button> + <label> + <input id="form_production" type="checkbox" name="production" <?php echo attr($c['production']) ? ' checked' : '' ?>> + <?php echo xlt("Production Check") ?> + </label> + </div> + <div class="form-group"> + <label for="form_extension"><?php echo xlt("Extension") ?></label> + <input id="form_extension" type="text" name="extension" class="form-control" required="required" value='<?php echo attr($c['extension']) ?>'> + </div> + <div class="form-group"> + <label for="form_phone"><?php echo xlt("FAX Phone Number") ?></label> + <input type="tel" class="form-control" id="form_phone" name="phone" value='<?php echo attr($c['phone']) ?>'> + </div> + <div class="form-group"> + <label for="form_smsnumber"><?php echo xlt("SMS Phone Number") ?></label> + <input id="form_smsnumber" type="tel" name="smsnumber" class="form-control" + value='<?php echo attr($c['smsNumber']) ?>'> + </div> + </div> + <div class="col-md"> + <div class="form-group"> + <label for="form_key"><?php echo xlt("Client ID") ?> *</label> + <div class="input-group"> + <input id="form_key" type="password" name="key" class="form-control" + required="required" value='<?php echo attr($c['appKey']) ?>'> + <div class="input-group-append toggle-password" onclick="togglePasswordVisibility('form_key')"> + <span class="input-group-text"><i class="fa fa-eye"></i></span> + </div> + </div> + </div> + <div class="form-group"> + <label for="form_secret"><?php echo xlt("Client Secret") ?> *</label> + <div class="input-group"> + <input id="form_secret" type="password" name="secret" class="form-control" + required="required" value='<?php echo attr($c['appSecret']) ?>'> + <div class="input-group-append toggle-password" onclick="togglePasswordVisibility('form_secret')"> + <span class="input-group-text"><i class="fa fa-eye"></i></span> + </div> + </div> + </div> + <div class="form-group"> + <label for="form_jwt"><?php echo xlt("Copy and Paste JWT") ?> *</label> + <span class="form-group"> + <button type="button" class="btn btn-primary btn-download btn-sm mb-1 p-0 px-1 float-right" onclick="openJwtWindow()"><?php echo xlt("Create JWT") ?></button> + </span> + <textarea id="form_jwt" type="text" rows="3" name="jwt" class="form-control small" required="required"><?php echo attr($c['jwt']) ?></textarea> + </div> + <div class=" form-group"> + <label for="form_nhours"><?php echo xlt("Appointment Advance Notification (Hours)") ?> *</label> + <input id="form_nhours" type="text" name="smshours" class="form-control" + placeholder="<?php echo xlt('Please enter number of hours before appointment') ?> *" + required="required" value='<?php echo attr($c['smsHours']) ?>'> + </div> + </div> + <div class="col-md-12"> + <div class="form-group"> + <label for="form_message"><?php echo xlt("Message Template") ?> *</label> + <span style="font-size:12px;font-style: italic;"> +<?php echo xlt("Tags") ?>: ***NAME***, ***PROVIDER***, ***DATE***, ***STARTTIME***, ***ENDTIME***, ***ORG***</span> + <textarea id="form_message" type="text" rows="3" name="smsmessage" class="form-control small" + required="required" value='<?php echo attr($c['smsMessage']) ?>'><?php echo attr($c['smsMessage']) ?></textarea> + </div> + </div> + <p class="text-muted"><strong>*</strong> <?php echo xlt("These fields are required.") ?> </p> + </div> + </div> + <button type="submit" class="btn btn-success float-right m-2" value=""><?php echo xlt("Save Settings") ?></button> + </form> + </div> +</body> +</html> diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/BootstrapService.php b/interface/modules/custom_modules/oe-module-faxsms/src/BootstrapService.php index 0321a208e0d..43498f6b27f 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/src/BootstrapService.php +++ b/interface/modules/custom_modules/oe-module-faxsms/src/BootstrapService.php @@ -114,9 +114,9 @@ public function saveModuleListenerGlobals($items): void /** * @param $settings - * @return mixed + * @return array|false|null */ - public function persistSetupSettings($settings): mixed + public function persistSetupSettings($settings): array|null|false { // vendor for backup of setup globals. $vendor = '_persisted'; diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/AppDispatch.php b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/AppDispatch.php index bb4568bdc17..f7963a8507a 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/AppDispatch.php +++ b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/AppDispatch.php @@ -6,7 +6,7 @@ * @package OpenEMR * @link http://www.open-emr.org * @author Jerry Padgett <sjpadgett@gmail.com> - * @copyright Copyright (c) 2018-2023 Jerry Padgett <sjpadgett@gmail.com> + * @copyright Copyright (c) 2018-2024 Jerry Padgett <sjpadgett@gmail.com> * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General public License 3 */ @@ -15,7 +15,6 @@ use MyMailer; use OpenEMR\Common\Acl\AclMain; use OpenEMR\Common\Session\SessionUtil; -use OpenEMR\Events\Messaging\SendNotificationEvent; /** * Class AppDispatch @@ -26,7 +25,7 @@ abstract class AppDispatch { const ACTION_DEFAULT = 'index'; static $_apiService; - static $_apiModule; + static mixed $_apiModule; public string $authErrorDefault; public static $timeZone; protected $crypto; @@ -99,7 +98,7 @@ private function dispatchActions(): void public function getSession($param = null, $default = null): mixed { if ($param) { - return $_SESSION[$param] ?? $default; + return $_SESSION[$param] ?: $default; } return $this->_session; @@ -113,7 +112,7 @@ public function getSession($param = null, $default = null): mixed public function getQuery($param = null, $default = null): mixed { if ($param) { - return $this->_query[$param] ?? $default; + return $this->_query[$param] ?: $default; } return $this->_query; @@ -223,7 +222,7 @@ static function getServiceInstance($type) case 0: break; case 1: - // for new service in future + return new RCFaxClient(); break; case 2: return new TwilioSMSClient(); @@ -233,7 +232,7 @@ static function getServiceInstance($type) case 0: break; case 1: - // for new service in future + return new RCFaxClient(); break; case 3: return new EtherFaxActions(); @@ -242,7 +241,7 @@ static function getServiceInstance($type) switch ($s) { case 0: break; - case 1: + case 4: return new EmailClient(); } } @@ -284,6 +283,8 @@ static function getModuleType(): mixed return self::$_apiModule; } + //abstract function faxProcessUploads(); + /** * @return string|bool */ @@ -312,7 +313,7 @@ abstract function fetchReminderCount(): string|bool; public function getPost($param = null, $default = null): mixed { if ($param) { - return $this->_post[$param] ?? $default; + return $this->_post[$param] ?: $default; } return $this->_post; @@ -362,6 +363,7 @@ protected function saveSetup(array $setup = []): string $smsNumber = $this->getRequest('smsnumber'); $smsMessage = $this->getRequest('smsmessage'); $smsHours = $this->getRequest('smshours'); + $jwt = $this->getRequest('jwt'); $setup = array( 'username' => "$username", 'extension' => "$ext", @@ -370,13 +372,14 @@ protected function saveSetup(array $setup = []): string 'password' => "$password", 'appKey' => "$appkey", 'appSecret' => "$appsecret", - 'server' => "", - 'portal' => "", + 'server' => !$production ? 'https://platform.devtest.ringcentral.com' : "https://platform.ringcentral.com", + 'portal' => !$production ? "https://service.devtest.ringcentral.com/" : "https://service.ringcentral.com/", 'smsNumber' => "$smsNumber", 'production' => $production, - 'redirect_url' => "", + 'redirect_url' => $this->getRequest('redirect_url'), 'smsHours' => $smsHours, - 'smsMessage' => $smsMessage + 'smsMessage' => $smsMessage, + 'jwt' => $jwt ?? '', ); } @@ -405,7 +408,7 @@ protected function saveSetup(array $setup = []): string public function getRequest($param = null, $default = null): mixed { if ($param) { - return $this->_request[$param] ?? $default; + return $this->_request[$param] ?: $default; } return $this->_request; @@ -418,7 +421,7 @@ static function getModuleVendor(): mixed { switch ((string)self::getServiceType()) { case '1': - return '_default_email'; + return '_ringcentral'; case '2': return '_twilio'; case '3': @@ -427,6 +430,56 @@ static function getModuleVendor(): mixed return null; } + public function getEmailSetup(): mixed + { + $vendor = '_email'; + $this->authUser = (int)$this->getSession('authUserID'); + if (!($GLOBALS['oerestrict_users'] ?? null)) { + $this->authUser = 0; + } + $credentials = sqlQuery("SELECT * FROM `module_faxsms_credentials` WHERE `auth_user` = ? AND `vendor` = ?", array($this->authUser, $vendor)); + + if (empty($credentials['smtp_user']) || empty($credentials['smtp_host']) || empty($credentials['smtp_password'])) { + $credentials = array( + 'sender_name' => $GLOBALS['patient_reminder_sender_name'], + 'sender_email' => $GLOBALS['patient_reminder_sender_email'], + 'notification_email' => $GLOBALS['practice_return_email_path'], + 'email_transport' => $GLOBALS['EMAIL_METHOD'], + 'smtp_host' => $GLOBALS['SMTP_HOST'], + 'smtp_port' => $GLOBALS['SMTP_PORT'], + 'smtp_user' => $GLOBALS['SMTP_USER'], + 'smtp_password' => $GLOBALS['SMTP_PASS'], + 'smtp_security' => $GLOBALS['SMTP_SECURE'], + 'notification_hours' => $GLOBALS['EMAIL_NOTIFICATION_HOUR'] + ); + if (empty($credentials['smsMessage'] ?? '')) { + $credentials['smsMessage'] = "A courtesy reminder for ***NAME*** \r\nFor the appointment scheduled on: ***DATE*** At: ***STARTTIME*** Until: ***ENDTIME*** \r\nWith: ***PROVIDER*** Of: ***ORG***\r\nPlease call if unable to attend."; + } + return $credentials; + } else { + $credentials = $credentials['credentials']; + if (empty($credentials['smsMessage'] ?? '')) { + $credentials['smsMessage'] = "A courtesy reminder for ***NAME*** \r\nFor the appointment scheduled on: ***DATE*** At: ***STARTTIME*** Until: ***ENDTIME*** \r\nWith: ***PROVIDER*** Of: ***ORG***\r\nPlease call if unable to attend."; + } + } + + $decrypt = $this->crypto->decryptStandard($credentials); + $decode = json_decode($decrypt, true); + return $decode; + } + + public function saveEmailSetup($credentials): void + { + $vendor = '_email'; + $this->authUser = (int)$this->getSession('authUserID'); + if (!($GLOBALS['oerestrict_users'] ?? null)) { + $this->authUser = 0; + } + $encoded = json_encode($credentials); + $encrypted = $this->crypto->encryptStandard($encoded); + sqlStatement("INSERT INTO `module_faxsms_credentials` (auth_user, vendor, credentials, updated) VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE credentials = VALUES(credentials), updated = VALUES(updated)", array($this->authUser, $vendor, $encrypted)); + } + /** * Common credentials storage between services * the service class will set specific credential. @@ -458,13 +511,15 @@ protected function getSetup(): mixed 'redirect_url' => '', 'smsHours' => "50", 'smsMessage' => "A courtesy reminder for ***NAME*** \r\nFor the appointment scheduled on: ***DATE*** At: ***STARTTIME*** Until: ***ENDTIME*** \r\nWith: ***PROVIDER*** Of: ***ORG***\r\nPlease call if unable to attend.", + 'jwt' => '', ); return $credentials; } else { $credentials = $credentials['credentials']; } - $decode = json_decode($this->crypto->decryptStandard($credentials), true); + $decrypt = $this->crypto->decryptStandard($credentials); + $decode = json_decode($decrypt, true); if (empty($decode['smsMessage'])) { $decode['smsMessage'] = "A courtesy reminder for ***NAME*** \r\nFor the appointment scheduled on: ***DATE*** At: ***STARTTIME*** Until: ***ENDTIME*** \r\nWith: ***PROVIDER*** Of: ***ORG***\r\nPlease call if unable to attend."; } @@ -500,7 +555,7 @@ public function getPatientDetails(): bool|string * @param $email * @return bool */ - protected function validEmail($email): bool + public function validEmail($email): bool { if (preg_match("/^[_a-z0-9-]+(\.[_a-z0-9-\+]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/i", $email)) { return true; @@ -559,16 +614,8 @@ public function mailEmail($email, $from_name, $body, string $subject = '', strin * @param $u * @return bool */ - public function verifyAcl($sect = 'admin', $v = 'docs', $u = ''): bool + public function verifyAcl($sect = 'Clinicians', $v = 'docs', $u = ''): bool { return AclMain::aclCheckCore($sect, $v, $u); } - - /** - * @return null - */ - private function indexAction() - { - return null; - } } diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/EmailClient.php b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/EmailClient.php index 085ed9139d9..b7433031dcf 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/EmailClient.php +++ b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/EmailClient.php @@ -14,6 +14,7 @@ use MyMailer; use OpenEMR\Common\Crypto\CryptoGen; +use PHPMailer\PHPMailer\Exception; use Symfony\Component\HttpClient\HttpClient; class EmailClient extends AppDispatch @@ -22,10 +23,11 @@ class EmailClient extends AppDispatch public $baseDir; public $uriDir; public $serverUrl; - public $credentials; + public mixed $credentials; public string $portalUrl; protected $crypto; private EmailClient $client; + private bool $smtpEnabled; public function __construct() { @@ -35,6 +37,7 @@ public function __construct() $this->crypto = new CryptoGen(); $this->baseDir = $GLOBALS['temporary_files_dir']; $this->uriDir = $GLOBALS['OE_SITE_WEBROOT']; + $this->smtpEnabled = !empty($GLOBALS['SMTP_PASS'] ?? null) && !empty($GLOBALS["SMTP_USER"] ?? null); parent::__construct(); } @@ -55,10 +58,6 @@ public function getCredentials(): mixed return $credentials; } - /** - * @return bool|string - */ - /** * @return string */ @@ -81,7 +80,7 @@ public function sendFax(): string|bool * @param $acl * @return int */ - public function authenticate($acl = ['admin', 'doc']): int + public function authenticate($acl = ['patient', 'doc']): int { list($s, $v) = $acl; return $this->verifyAcl($s, $v); @@ -107,6 +106,50 @@ public function sendEmail(): string return js_escape($statusMsg); } + /** + * @throws Exception + */ + public function emailDocument($email, $body, $file, array $user = []): string + { + $from_name = ($user['fname'] ?? '') . ' ' . ($user['lname'] ?? ''); + $desc = xlt("Comment") . ":\n" . text($body) . "\n" . xlt("This email has an attached fax document."); + $mail = new MyMailer(); + $from_name = text($from_name); + $from = $GLOBALS["practice_return_email_path"]; + $mail->AddReplyTo($from, $from_name); + $mail->SetFrom($from, $from); + $mail->AddAddress($email, $email); + $mail->Subject = xlt("Forwarded Fax Document"); + $mail->Body = $desc; + $mail->AddAttachment($file); + + return $mail->Send() ? xlt("Email successfully sent.") : xlt("Error: Email failed") . text($mail->ErrorInfo); + } + + /** + * @throws Exception + */ + public function emailReminder($email, $body): false|string + { + $hasEmail = $this->validEmail($email); + if (!$hasEmail) { + return js_escape(xlt("Error: Missing valid email address. Try again.")); + } + if (!$this->smtpEnabled) { + return text(js_escape('SMTP not setup.')); + } + $from_name = text($GLOBALS["Patient Reminder Sender Name"] ?? 'UNK'); + $desc = text($body); + $mail = new MyMailer(); + $from = text($GLOBALS["practice_return_email_path"]); + $mail->AddReplyTo($from, $from_name); + $mail->SetFrom($from, $from); + $mail->AddAddress($email, $email); + $mail->Subject = xlt("A Reminder for You"); + $mail->Body = $desc; + + return $mail->Send(); + } /** * @return false|string */ diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/EtherFaxActions.php b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/EtherFaxActions.php index 29b73491a93..f015d6a7208 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/EtherFaxActions.php +++ b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/EtherFaxActions.php @@ -169,6 +169,8 @@ public function sendSMS(): string } /** + * TODO: decrypt document + * * @return string * @throws \PHPMailer\PHPMailer\Exception */ @@ -345,6 +347,8 @@ public function forwardFax(): string } /** + * Rest Endpoint + * * @return string|void */ public function getPending() @@ -352,65 +356,140 @@ public function getPending() if (!$this->authenticate()) { return $this->authErrorDefault; } - // Let us fetch any new pending faxes to queue. - $newFaxCount = $this->pollAndInsertAllPendingFax(); - $pull = $this->fetchReminderCount(); - $dateFrom = date("Y-m-d H:i:s", strtotime(($this->getRequest('datefrom') . 'T00:00:01'))); - $dateTo = date("Y-m-d H:i:s", strtotime(($this->getRequest('dateto') . 'T23:59:59'))); + + $this->pollAndInsertAllPendingFax(); + $dateFrom = date("Y-m-d H:i:s", strtotime($this->getRequest('datefrom') . 'T00:00:01')); + $dateTo = date("Y-m-d H:i:s", strtotime($this->getRequest('dateto') . 'T23:59:59')); $faxStore = $this->fetchFaxQueue($dateFrom, $dateTo, false); - $responseMsgs = [0 => '', 2 => xlt('Not Implemented')]; + + $responseMsg = [0 => '', 2 => xlt('Not Implemented')]; foreach ($faxStore as $faxDetails) { $id = $faxDetails->JobId; $record_id = $faxDetails->RecordId; $faxDate = strtotime($faxDetails->ReceivedOn . ' UTC'); - $to = $faxDetails->CalledNumber; - $from = $faxDetails->CallingNumber; - $params = $faxDetails->DocumentParams; - $showFlag = 0; - $recogized = $faxDetails->AnalyzeFormResult->AnalyzeResult->DocumentResults ?? []; - - $form = ''; - foreach ($recogized as $r) { - $details = null; - $form = "<tr id='" . text($id) . "' class='d-none collapse-all'><td colspan='12'><div class='container table-responsive'><table class='table table-sm table-bordered table-dark'>"; - $form .= "<thead><tr><th>" . xlt("Parameter") . "</th><th>" . xlt("Value") . "</th><th>" . xlt("Confidence") . " : " . text($r->DocTypeConfidence * 100) . "</th></tr></thead><tbody>"; - $parse = $this->parseValidators($r->Fields) ?? []; - $pid_assumed = sqlQuery( - "Select pid From patient_data Where fname = ? And lname = ? And DOB = ?", - [$parse['fname'] ?? '', $parse['lname'] ?? '', date("Y-m-d", strtotime(($parse['DOB'] ?? '')))] - )['pid'] ?? 'No'; - - foreach ($r->Fields as $field) { - if ($field->Text == 'unselected' || empty($field->Text)) { - continue; - } - $showFlag++; - $form .= "<tr><td>" . text(str_replace(" - ", "-", $field->Name)) . "</td><td>" . text($field->Text) . "</td><td>" . text($field->Confidence * 100) . "</td></tr>"; - } - $form .= "</tbody></table></div></td></tr>"; + $formattedDate = date('M j, Y g:i:sa T', $faxDate); + $docLen = round($faxDetails->DocumentParams->Length / 1000, 2) . "KB"; + $transactionType = $this->getTransactionTypeWord($faxDetails->TransactionType); + + $recognizeResult = $faxDetails->AnalyzeFormResult->AnalyzeResult->DocumentResults ?? []; + $form = $this->generateFaxForm($id, $recognizeResult); + + $pid_assumed = $this->getAssumedPatientId($recognizeResult); + + $actionLinks = $this->generateActionLinks($id, $record_id, $pid_assumed); + $detailLink = $this->generateDetailLink($id, $recognizeResult); + + if ($faxDetails->TransactionType == '0') { + $faxRow = "<tr> + <td>" . text($formattedDate) . "</td> + <td>" . text($faxDetails->CallingNumber) . "</td> + <td>" . text($faxDetails->RemoteId) . "</td> + <td>" . text($faxDetails->CalledNumber) . "</td> + <td>" . text($faxDetails->PagesReceived) . "</td> + <td>" . text($docLen) . "</td> + <td>" . text($transactionType) . "</td> + <td class='text-left'>" . $detailLink . "</td> + <td class='text-center'>" . text($pid_assumed) . "</td> + <td class='text-left'>" . $actionLinks . "</td> + <td class='text-center'><input type='checkbox' class='delete-fax-checkbox' value='" . attr($id) . "'></td></tr>"; + } else { + $faxRow = "<tr> + <td>" . text($formattedDate) . "</td> + <td>" . text($faxDetails->CallingNumber) . "</td> + <td>" . text($faxDetails->RemoteId) . "</td> + <td>" . text($faxDetails->CalledNumber) . "</td> + <td>" . text($faxDetails->PagesReceived) . "</td> + <td>" . text($docLen) . "</td> + <td>" . text($transactionType) . "</td> + <td class='text-left'>" . $actionLinks . "</td> + <td class='text-center'><input type='checkbox' class='delete-fax-checkbox' value='" . attr($id) . "'></td></tr>"; } + $responseMsg[$faxDetails->TransactionType == '0' ? 0 : 1] .= $faxRow . $form; + } + + if (empty($responseMsg[0])) { + $responseMsg[0] = xlt("Currently inbox is empty."); + } - $patientLink = "<a role='button' href='javascript:void(0)' onclick=\"createPatient(event, " . attr_js($id) . ", " . attr_js($record_id) . ", " . attr_js(json_encode($parse ?? [])) . ")\"> <i class='fa fa-chart-simple mr-2' title='" . xla("Chart fax or Create patient and chart fax to documents.") . "'></i></a>"; - $messageLink = "<a role='button' href='javascript:void(0)' onclick=\"notifyUser(event, " . attr_js($id) . ", " . attr_js($record_id) . ", " . attr_js(($pid_assumed ?? 0)) . ")\"> <i class='fa fa-paper-plane mr-2' title='" . xla("Notify a user and attach this fax to message.") . "'></i></a>"; - $downloadLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'true')\"> <i class='fa fa-file-download mr-2' title='" . xla("Download and delete fax") . "'></i></a>"; - $viewLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'false')\"> <i class='fa fa-file-pdf mr-2' title='" . xla("View fax document") . "'></i></a>"; - $deleteLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'false', 'true')\"> <i class='text-danger fa fa-trash mr-2' title='" . xla("Delete this fax document") . "'></i></a>"; - $forwardLink = "<a role='button' href='javascript:void(0)' onclick=\"forwardFax(event, " . attr_js($id) . ")\"> <i class='fa fa-forward mr-2' title='" . xla("Forward fax to new fax recipient or email attachment.") . "'></i></a>"; - $detailLink = $showFlag ? "<a role='button' href='javascript:void(0)' class='btn btn-link fa fa-eye' onclick='toggleDetail(\"#" . text($id) . "\")'></a>" . text($showFlag) . ' ' . xlt("Items") : ''; + echo json_encode($responseMsg); + exit(); + } + + private function getTransactionTypeWord($transactionType) + { + $transactionTypes = [ + '0' => xlt('Received'), + '1' => xlt('Sent'), + '2' => xlt('Forwarded'), + '3' => xlt('Failed') + // Add more as needed + ]; + + return $transactionTypes[$transactionType] ?? xlt('Unknown'); + } - $faxFormattedDate = date('M j, Y g:i:sa T', $faxDate); - $docLen = text(round($params->Length / 1000, 2)) . "KB"; - $responseMsgs[0] .= "<tr><td>" . text($faxFormattedDate) . "</td><td>" . text($from) . "</td><td>" . text($faxDetails->RemoteId) . "</td><td>" . text($to) . "</td><td>" . text($faxDetails->PagesReceived) . "</td><td>" . text($docLen) . "</td><td class='text-left'>" . $detailLink . "</td><td class='text-center'>" . text($pid_assumed ?? '') . "</td><td class='text-left'>" . $patientLink . $messageLink . $forwardLink . $viewLink . $downloadLink . $deleteLink . "</td></tr>"; - $responseMsgs[0] .= $form; + private function getAssumedPatientId($recognized) + { + foreach ($recognized as $r) { + $parse = $this->parseValidators($r->Fields) ?? []; + return sqlQuery( + "SELECT pid FROM patient_data WHERE fname = ? AND lname = ? AND DOB = ?", + [$parse['fname'] ?? '', $parse['lname'] ?? '', date("Y-m-d", strtotime($parse['DOB'] ?? ''))] + )['pid'] ?? 'No'; + } + return 'No'; + } + + private function generateFaxForm($id, $recognized) + { + if (empty($recognized)) { + return ''; } - if (empty($responseMsgs[0])) { - $responseMsgs[0] = xlt("Currently inbox is empty."); + $form = "<tr id='" . attr($id) . "' class='d-none collapse-all'><td colspan='12'><div class='container table-responsive'><table class='table table-sm table-bordered table-dark'>"; + $form .= "<thead><tr><th>" . xlt("Parameter") . "</th><th>" . xlt("Value") . "</th><th>" . xlt("Confidence") . "</th></tr></thead><tbody>"; + + foreach ($recognized as $r) { + foreach ($r->Fields as $field) { + if ($field->Text == 'unselected' || empty($field->Text)) { + continue; + } + + $form .= "<tr><td>" . text(str_replace(" - ", "-", $field->Name)) . "</td><td>" . text($field->Text) . "</td><td>" . text($field->Confidence * 100) . "</td></tr>"; + } } - echo json_encode($responseMsgs); - exit(); + $form .= "</tbody></table></div></td></tr>"; + return $form; + } + + private function generateActionLinks($id, $record_id, $pid_assumed) + { + return "<a role='button' href='javascript:void(0)' onclick=\"createPatient(event, " . attr_js($id) . ", " . attr_js($record_id) . ", " . attr_js(json_encode([])) . ")\"> + <i class='fa fa-chart-simple mr-2' title='" . xla("Chart fax or Create patient and chart fax to documents.") . "'></i> + </a> + <a role='button' href='javascript:void(0)' onclick=\"notifyUser(event, " . attr_js($id) . ", " . attr_js($record_id) . ", " . attr_js($pid_assumed) . ")\"> + <i class='fa fa-paper-plane mr-2' title='" . xla("Notify a user and attach this fax to message.") . "'></i> + </a> + <a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'true')\"> + <i class='fa fa-file-download mr-2' title='" . xla("Download and delete fax") . "'></i> + </a> + <a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'false')\"> + <i class='fa fa-file-pdf mr-2' title='" . xla("View fax document") . "'></i> + </a> + <a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'false', 'true')\"> + <i class='text-danger fa fa-trash mr-2' title='" . xla("Delete this fax document") . "'></i> + </a> + <a role='button' href='javascript:void(0)' onclick=\"forwardFax(event, " . attr_js($id) . ")\"> + <i class='fa fa-forward mr-2' title='" . xla("Forward fax to new fax recipient or email attachment.") . "'></i> + </a>"; + } + + private function generateDetailLink($id, $recognized) + { + $showFlag = count($recognized); + return $showFlag ? "<a role='button' href='javascript:void(0)' class='btn btn-link fa fa-eye' onclick='toggleDetail(\"#" . text($id) . "\")'></a>" . text($showFlag) . ' ' . xlt("Items") : ''; } /** diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/RCFaxClient.php b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/RCFaxClient.php new file mode 100644 index 00000000000..d0498de301b --- /dev/null +++ b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/RCFaxClient.php @@ -0,0 +1,1231 @@ +<?php + +/** + * Fax SMS Module Member + * + * @package OpenEMR + * @link http://www.open-emr.org + * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 + */ + +namespace OpenEMR\Modules\FaxSMS\Controller; + +use Document; +use Exception; +use MyMailer; +use OpenEMR\Common\Crypto\CryptoGen; +use OpenEMR\Common\Utils\FileUtils; +use OpenEMR\Services\ImageUtilities\HandleImageService; +use RingCentral\SDK\Http\ApiException; +use RingCentral\SDK\SDK; + +class RCFaxClient extends AppDispatch +{ + public $baseDir; + public $uriDir; + public $serverUrl; + public $redirectUrl; + public $portalUrl; + public $credentials; + public $cacheDir; + public $apiBase; + public $apiService; + protected $platform; + protected $rcsdk; + protected $crypto; + + public function __construct() + { + $this->crypto = new CryptoGen(); + $this->baseDir = $GLOBALS['temporary_files_dir']; + $this->uriDir = $GLOBALS['OE_SITE_WEBROOT']; + $this->cacheDir = $GLOBALS['OE_SITE_DIR'] . '/documents/logs_and_misc/_cache'; + $this->credentials = $this->getCredentials(); + $this->portalUrl = $this->credentials['production'] ? "https://service.ringcentral.com/" : "https://service.devtest.ringcentral.com/"; + $this->serverUrl = $this->credentials['production'] ? "https://platform.ringcentral.com" : "https://platform.devtest.ringcentral.com"; + $this->redirectUrl = $this->credentials['redirect_url']; + $this->initializeSDK(); + parent::__construct(); + } + + /** + * @return int|string + */ + public function authenticateRingCentral(): int|string + { + try { + $authback = $this->cacheDir . DIRECTORY_SEPARATOR . 'platform.json'; + $cachedAuth = $this->getCachedAuth($authback); + if (!empty($cachedAuth['refresh_token'])) { + $this->platform->auth()->setData($cachedAuth); + } + + if ($this->platform->loggedIn()) { + return $this->refreshToken(); + } else { + return $this->loginWithJWT(); + } + } catch (Exception $e) { + return text($e->getMessage()); + } + } + + /** + * @param string $authback + * @return array + */ + private function getCachedAuth(string $authback): array + { + if (file_exists($authback)) { + $cachedAuth = file_get_contents($authback); + $cachedAuth = json_decode($this->crypto->decryptStandard($cachedAuth), true); + unlink($authback); +// Remove cached file after reading + return $cachedAuth; + } + return []; + } + + /** + * @return int|string + */ + private function refreshToken(): int|string + { + try { + $this->platform->refresh(); + } catch (Exception $e) { + return $this->loginWithJWT(); + } + $this->setSession('sessionAccessToken', $this->platform->auth()->data()); + $this->cacheAuthData($this->platform); + return 1; + } + + /** + * @return int|string + */ + private function loginWithJWT(): int|string + { + $jwt = trim($this->credentials['jwt'] ?? ''); + try { + $this->platform->login(['jwt' => $jwt]); + if ($this->platform->loggedIn()) { + $this->setSession('sessionAccessToken', $this->platform->auth()->data()); + $this->cacheAuthData($this->platform); + return 1; + } + } catch (ApiException $e) { + return "API Error: " . text($e->getMessage()) . " - " . text($e->getCode()); + } catch (Exception $e) { + return "Error: " . text($e->getMessage()); + } + return "Login with JWT failed."; + } + + /** + * @param $platform + * @return void + */ + private function cacheAuthData($platform): void + { + $data = $platform->auth()->data(); + $encryptedData = $this->crypto->encryptStandard(json_encode($data)); + file_put_contents($this->cacheDir . DIRECTORY_SEPARATOR . 'platform.json', $encryptedData); + } + + /** + * @return void + * @throws Exception + */ + private function initializeSDK(): void + { + if (isset($this->credentials['appKey'], $this->credentials['appSecret'])) { + $this->rcsdk = new SDK($this->credentials['appKey'], $this->credentials['appSecret'], $this->serverUrl, 'OpenEMR', '1.0.0'); + $this->platform = $this->rcsdk->platform(); + } else { + throw new Exception("App Key and App Secret are required to initialize SDK."); + } + } + + /** + * @return int|string + */ + public function authenticate(): int|string + { + if (empty($this->credentials['appKey'])) { + $this->credentials = $this->getCredentials(); + if (empty($this->credentials['appKey'])) { + return 'Missing or Invalid RingCentral Credentials. Please contact your administrator.'; + // No credentials set + } + } + $error = $this->authenticateRingCentral(); + if (is_numeric($error)) { + return $error; + } + return $error; + } + + /** + * Used by fax file drag and drop + * + * @return string + */ + public function faxProcessUploads(): string + { + if (empty($_FILES['fax']) || $_FILES['fax']['error'] !== UPLOAD_ERR_OK) { + error_log('Error: No file uploaded or upload error.'); + return ''; + } + + $name = basename($_FILES['fax']['name']); + $tmpName = $_FILES['fax']['tmp_name']; + $targetDir = $this->baseDir . '/send'; + if (!file_exists($targetDir) && !mkdir($targetDir, 0777, true)) { + error_log('Error: Failed to create directory.'); + return ''; + } + + $filepath = $targetDir . "/" . $name; + if (!move_uploaded_file($tmpName, $filepath)) { + error_log('Error: Failed to move uploaded file.'); + return ''; + } + + return $filepath; + } + + /** + * @param string $toPhone + * @param string $subject + * @param string $message + * @param string $from + * @return string|bool + */ + public function sendSMS(string $toPhone = '', string $subject = '', string $message = '', string $from = ''): string|bool + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); + // goes to alert + } + + $smsNumber = $this->credentials['smsNumber']; + if ($smsNumber) { + try { + $this->platform->post('/account/~/extension/~/sms', [ + 'from' => ['phoneNumber' => $smsNumber], + 'to' => [['phoneNumber' => $toPhone]], + 'text' => $message, + ]); + sleep(1); + // RC may only allow 1/second. + return true; + } catch (ApiException $e) { + return text("API Error: " . $e->getMessage() . " - " . $e->getCode()); + } + } + + return true; + } + + /** + * @return array + */ + public function getCredentials(): array + { + if (!file_exists($this->cacheDir)) { + mkdir($this->cacheDir, 0777, true); + } + return AppDispatch::getSetup(); + } + + /** + * API Endpoint for sending + * @return string + */ + public function forwardFax(): string + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); // goes to alert + } + + $jobId = $this->getRequest('docid'); + $email = $this->getRequest('email'); + $faxNumber = $this->formatPhone($this->getRequest('phone')); + $hasEmail = $this->validEmail($email); + $smtpEnabled = !empty($GLOBALS['SMTP_PASS'] ?? null) && !empty($GLOBALS["SMTP_USER"] ?? null); + $user = $this::getLoggedInUser(); + $facility = substr($user['facility'], 0, 20); + $csid = $this->formatPhone($this->credentials['phone']); + $tag = xlt("Forwarded"); + $statusMsg = xlt("Forwarding Requests") . "<br />"; + + if (!$hasEmail && empty($faxNumber)) { + return js_escape(xlt("Error: Nothing to forward. Try again.")); + } + + try { + // Fetch the fax message details + $messageDetailsResponse = $this->platform->get("/account/~/extension/~/message-store/{$jobId}"); + $messageDetails = $messageDetailsResponse->json(); + + // Fetch the fax content + $contentUri = $messageDetails->attachments[0]->uri; + $apiResponse = $this->platform->get($contentUri); + $contentType = $apiResponse->response()->getHeader('Content-Type')[0]; + $rawData = (string)$apiResponse->raw(); + + $ext = $this->getExtensionFromContentType($contentType); + $type = $this->getTypeFromContentType($contentType); + $filePath = $this->baseDir . "/send/" . ($jobId . $ext); + + if (!file_exists($this->baseDir . '/send')) { + mkdir($this->baseDir . '/send', 0777, true); + } + file_put_contents($filePath, $rawData); + + if ($hasEmail && $smtpEnabled) { + $statusMsg .= self::emailDocument($email, $this->getRequest('comments'), $filePath, $user) . "<br />"; + } + if ($faxNumber) { + try { + $this->sendFax( + $faxNumber, + $filePath, + $user['username'], + $jobId, + $contentType + ); + $statusMsg .= xlt("Successfully forwarded fax to") . ' ' . text($faxNumber) . "<br />"; + } catch (Exception $e) { + return js_escape('Error: ' . text($e->getMessage())); + } + } + unlink($filePath); + return js_escape($statusMsg); + } catch (ApiException | Exception $e) { + return js_escape('Error: ' . text($e->getMessage())); + } + } + + /** + * @param $phone + * @param $file + * @param $name + * @param $comments + * @param $fileName + * @return bool|string + */ + public function sendFax($phone = '', $file = '', $name = '', $comments = '', $fileName = null): bool|string + { + // Authenticate and refresh token if needed + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); // goes to alert + } + + // Ensure some needed args if not past in or from API abstracted endpoint sendFax(). + $isContent = $this->getRequest('isContent'); // remember this flag is set in patient report and not just it has content. + $file = $this->getRequest('file', $file); // could be content or file path. + $isFilePath = is_file($file); + $isDocuments = (int)$this->getRequest('isDocuments', 0); //from patient documents + $docId = $this->getRequest('docid'); + $phone = $this->formatPhone($this->getRequest('phone', $phone)); + $comments = trim($this->getRequest('comments', $comments)); + $email = $this->getRequest('email'); + $hasEmail = $this->validEmail($email); + $smtpEnabled = !empty($GLOBALS['SMTP_PASS'] ?? null) && !empty($GLOBALS["SMTP_USER"] ?? null); + $user = $this::getLoggedInUser(); + $name = $this->getRequest('name', $name) . ' ' . $this->getRequest('surname', ''); + $fileName = $fileName ?? pathinfo($file, PATHINFO_BASENAME); + // validate/format file path + if (is_file($file)) { + if (str_starts_with($file, 'file://')) { + $file = substr($file, 7); + } + $realPath = realpath($file); + if ($realPath !== false) { + $file = str_replace("\\", "/", $realPath); + } else { + return xlt('Error: No content'); + } + } + // Check if the content is from patient report + if ($isContent) { + $content = $file; + $file = 'report-' . attr($GLOBALS['pid']) . '.pdf'; + } else { + // Is it from patient documents + if ($isDocuments) { + $content = (new Document($docId))->get_data(); + } else { + // Get the content of the file or the file path + $content = (is_file($file) && empty($content)) ? file_get_contents($file) : $file; + } + if (empty($content)) { + return xlt('Error: No content to send.'); + } + } + + // Decrypt content if needed + if ($this->crypto->cryptCheckStandard($content)) { + $content = $this->crypto->decryptStandard($content, null, 'database'); + } + + // Email the document if email is provided and SMTP is enabled. + // TODO: need check to ensure not from forward fax + $error = false; + if ($hasEmail && $smtpEnabled) { + try { + self::emailDocument($email, $comments, $file, $user); + $error = false; + } catch (\PHPMailer\PHPMailer\Exception $e) { + $error = true; + } + } + // Request to send the fax + try { + $this->sendFaxRequest($phone, $content, $fileName, $comments, $name); + // debug error log + error_log($phone . ' ' . $fileName . ' ' . $comments . ' ' . $name); + return xlt('Fax Successfully Sent') . ($error === true ? ("<br />" . xlt("Email Failed")) : ''); + } catch (Exception $e) { + return 'Error: ' . text(js_escape($e->getMessage())); + } + } + + /** + * @param $phone + * @param $content + * @param $fileName + * @param $comments + * @param $name + * @return void + * @throws Exception + */ + private function sendFaxRequest($phone, $content, $fileName = '', $comments = 'No Comment', $name = ''): void + { + // Almost always $content is file content but lets check in case it is a file path + if (is_file($content)) { + $content = file_get_contents($content); + } + try { + $mime = FileUtils::fileGetMimeType($fileName, $content); + $type = $mime['type']; + $fileName = $mime['filePath']; + if (empty($type)) { + $type = mime_content_type($content); + } + //error_log($phone . ' ' . $fileName . ' ' . $type . ' ' . $name); + $request = $this->rcsdk->createMultipartBuilder() + ->setBody([ + 'to' => [['phoneNumber' => $phone, 'name' => $name]], + 'faxResolution' => 'High', + 'coverPageText' => text($comments) + ]) + ->add($content, $fileName, ['Content-Type' => (string)$type]) + ->request('/account/~/extension/~/fax'); + $this->platform->sendRequest($request); + } catch (ApiException $e) { + throw new Exception($this->handleApiException($e)); + } + } + + /** + * @param ApiException $e + * @return string + */ + private function handleApiException(ApiException $e): string + { + $error = $e->apiResponse ? $e->apiResponse->text() : $e->getMessage(); + + if (stripos($error, 'invalid_grant') !== false) { + try { + $this->platform->login(['jwt' => $this->credentials['jwt']]); + if ($this->platform->loggedIn()) { + $this->cacheAuthData($this->platform); + return 'Fax Successfully Sent'; + } + } catch (Exception $ex) { + return "Re-authentication Error: " . text($ex->getMessage()); + } + } + return "API Error: " . text($e->getMessage()) . " - " . text($e->getCode()) . "\n" . text(json_encode($e->apiResponse ? $e->apiResponse->json() : [], JSON_PRETTY_PRINT)); + } + + /** + * @return string + */ + public function getStoredDoc(): string + { + $docuri = $this->getRequest('docuri'); + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); + // goes to alert + } + + try { + $apiResponse = $this->platform->get($docuri); + } catch (ApiException $e) { + return "Error: Retrieving Fax: " . text($e->getMessage() . $e->apiResponse()->request()->getUri()->__toString()); + } + + $contentType = $apiResponse->response()->getHeader('Content-Type')[0]; + $rawData = (string)$apiResponse->raw(); + if ($contentType == 'application/pdf') { + return 'data:application/pdf;base64,' . rawurlencode(base64_encode($rawData)); + } elseif ($contentType == 'image/tiff') { + return 'data:image/tiff;base64,' . rawurlencode(base64_encode($rawData)); + } else { + return $rawData; + } + } + + /** + * @param string $contentType + * @return string + */ + public function getExtensionFromContentType(string $contentType): string + { + switch ($contentType) { + case 'application/pdf': + return 'pdf'; + case 'text/plain': + return 'txt'; + case 'image/tiff': + return 'tiff'; + case 'image/jpeg': + return 'jpeg'; + case 'image/jpg': + return 'jpg'; + case 'image/gif': + return 'gif'; + case 'image/png': + return 'png'; + case 'application/xml': + return 'xml'; + case 'audio/wav': + case 'audio/x-wav': + return 'wav'; + default: + return 'application/pdf'; + } + } + + /** + * @param string $contentType + * @return string + */ + private function getTypeFromContentType(string $contentType): string + { + switch ($contentType) { + case 'application/pdf': + case 'image/tiff': + return 'Fax'; + case 'audio/wav': + case 'audio/x-wav': + return 'Audio'; + default: + return 'Text'; + } + } + + /** + * @param string $content + * @return void + */ + public function disposeDoc($content = ''): void + { + $where = $this->getSession('where'); + if (file_exists($where)) { + ob_clean(); + header("Cache-Control: public"); + header("Content-Description: File Transfer"); + header("Content-Disposition: attachment; filename=" . basename($where)); + header("Content-Type: application/download"); + header("Content-Transfer-Encoding: binary"); + header('Content-Length: ' . filesize($where)); + readfile($where); + unlink($where); + exit; + } + die(xlt('Problem with download. Use browser back button')); + } + + /** + * @return string + */ + public function viewFax(): string + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); // goes to alert + } + + $jobId = $this->getRequest('docid'); + $isDownload = $this->getRequest('download') == 'true'; + $isDelete = $this->getRequest('delete') == 'true'; + + $messageStoreDir = $this->baseDir; + if (!file_exists($messageStoreDir)) { + mkdir($messageStoreDir, 0777, true); + } + + try { + // Fetch the message details + $messageDetailsResponse = $this->platform->get("/account/~/extension/~/message-store/{$jobId}"); + if ($messageDetailsResponse->response()->getStatusCode() !== 200) { + return json_encode(['error' => "Error: Retrieving Fax: " . $messageDetailsResponse->response()->getReasonPhrase()]); + } + $messageDetails = $messageDetailsResponse->json(); + + if ($isDelete) { + // Delete the message + $this->platform->delete("/account/~/extension/~/message-store/{$jobId}"); + return json_encode('success'); + } + + $contentUri = $messageDetails->attachments[0]->uri; + $apiResponse = $this->platform->get($contentUri); + $contentType = $apiResponse->response()->getHeader('Content-Type')[0]; + $rawData = (string)$apiResponse->raw(); + + if ($isDownload) { + $filePath = $this->saveFaxToFile($rawData, $jobId, $contentType); + $this->setSession('where', $filePath); + return text(json_encode(['base64' => base64_encode($rawData), 'mime' => $contentType, 'path' => $filePath])); + } + return text(json_encode(['base64' => base64_encode($rawData), 'mime' => $contentType])); + } catch (ApiException $e) { + return text(json_encode(['error' => "Error: Retrieving Fax: " . $e->getMessage()])); + } + } + + /** + * @param string $jobId + * @return mixed + */ + public function fetchFaxFromQueue(string $jobId): mixed + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); // goes to alert + } + + try { + $apiResponse = $this->platform->get("/account/~/extension/~/message-store/{$jobId}/content"); + $contentType = $apiResponse->response()->getHeader('Content-Type')[0]; + $rawData = (string)$apiResponse->raw(); + + return [ + 'contentType' => $contentType, + 'data' => base64_encode($rawData) + ]; + } catch (ApiException $e) { + return text(json_encode(['error' => "API Error: " . $e->getMessage()])); + } catch (Exception $e) { + return text(json_encode(['error' => "Error: " . $e->getMessage()])); + } + } + + /** + * @param string $data + * @param string $contentType + * @return string + */ + private function formatFaxDataUrl(string $data, string $contentType): string + { + switch ($contentType) { + case 'application/pdf': + return 'data:application/pdf;base64,' . base64_encode($data); + case 'image/tiff': + case 'image/tif': + return 'data:image/tiff;base64,' . base64_encode($data); + default: + return 'data:text/plain;base64,' . base64_encode($data); + } + } + + /** + * @param string $data + * @param string $jobId + * @param string $contentType + * @return string + */ + private function saveFaxToFile(string $data, string $jobId, string $contentType): string + { + $fileExtension = $this->getFileExtension($contentType); + $fileName = "Fax_{$jobId}." . $fileExtension; + $filePath = $this->baseDir . DIRECTORY_SEPARATOR . $fileName; + + file_put_contents($filePath, $data); + + return $filePath; + } + + /** + * @param string $contentType + * @return string + */ + private function getFileExtension(string $contentType): string + { + switch ($contentType) { + case 'application/pdf': + return 'pdf'; + case 'image/tiff': + case 'image/tif': + return 'tiff'; + default: + return 'txt'; + } + } + + /** + * @param $encodedFax + * @return string + * @throws Exception + */ + public function formatFax($encodedFax): string + { + $control = new HandleImageService(); + $formatted_document = $control->convertImageToPdf($encodedFax, ''); + + return $formatted_document ? base64_encode($formatted_document) : false; + } + + /** + * @return string + */ + public function disposeDocument(): string + { + $response = ['success' => false, 'message' => '', 'url' => '']; + $where = $this->getRequest('file_path') ?? $this->getSession('where'); + + if (empty($where)) { + die(xlt('Problem with download. Use browser back button')); + } + + $content = $this->getRequest('content', ''); + $action = $this->getRequest('action'); + + if ($action == 'download') { + $this->sendFile($where); + sleep(2); + unlink($where); + exit; + } + + if (!empty($content) && $action == 'setup') { + $decodedContent = base64_decode($content); + if (file_put_contents($where, $decodedContent) !== false) { + $response['success'] = true; + $response['url'] = $where; + } else { + $response['message'] = 'Failed to write file'; + } + } elseif ($action == 'setup') { + $response['success'] = true; + $response['url'] = $where; + } + + return json_encode($response); + } + + /** + * @param string $filePath + * @return void + */ + private function sendFile(string $filePath): void + { + ob_end_clean(); + header("Cache-Control: public"); + header("Content-Description: File Transfer"); + header("Content-Disposition: attachment; filename=" . basename($filePath)); + header("Content-Type: application/pdf"); + header("Content-Transfer-Encoding: binary"); + header('Content-Length: ' . filesize($filePath)); + + readfile($filePath); + exit; + } + + /** + * @param string $messageId + * @return string + */ + public function downloadFax(string $messageId): string + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); // goes to alert + } + + try { + $response = $this->platform->get("/account/~/extension/~/message-store/{$messageId}/content"); + $contentType = $response->response()->getHeader('Content-Type')[0]; + $fileExtension = $this->getFileExtension($contentType); + $fileName = "fax_{$messageId}." . $fileExtension; + + // Save the file locally + $filePath = $this->cacheDir . DIRECTORY_SEPARATOR . $fileName; + file_put_contents($filePath, $response->raw()); + + // Prepare the file for download + header('Content-Description: File Transfer'); + header('Content-Type: ' . $contentType); + header('Content-Disposition: attachment; filename="' . basename($filePath) . '"'); + header('Content-Transfer-Encoding: binary'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + header('Content-Length: ' . filesize($filePath)); + readfile($filePath); + + // Optionally, you can delete the file after download + unlink($filePath); + + exit; // Stop further script execution + } catch (ApiException $e) { + return text(json_encode(['error' => "API Error: " . $e->getMessage()])); + } catch (Exception $e) { + return text(json_encode(['error' => "Error: " . $e->getMessage()])); + } + } + + /** + * @return string + */ + public function getUser(): string + { + $id = $this->getRequest('uid'); + $query = "SELECT * FROM users WHERE id = ?"; + $result = sqlStatement($query, [$id]); + $u = sqlFetchArray($result); + return json_encode([$u['fname'], $u['lname'], $u['fax']]); + } + + /** + * @return string + */ + public function getNotificationLog(): string + { + $type = $this->getRequest('type'); + $fromDate = $this->getRequest('datefrom'); + $toDate = $this->getRequest('dateto'); + try { + $query = "SELECT notification_log.* FROM notification_log WHERE notification_log.dSentDateTime > ? AND notification_log.dSentDateTime < ?"; + $res = sqlStatement($query, [$fromDate, $toDate]); + $responseMsg = ''; + while ($nrow = sqlFetchArray($res)) { + $adate = ($nrow['pc_eventDate'] . '::' . $nrow['pc_startTime']); + $pinfo = str_replace("|||", " ", $nrow['patient_info']); + $msg = text($nrow["message"]); + $responseMsg .= "<tr><td>" . text($nrow["pc_eid"]) . "</td><td>" . text($nrow["dSentDateTime"]) . "</td><td>" . text($adate) . "</td><td>" . text($pinfo) . "</td><td>" . text($msg) . "</td></tr>"; + } + } catch (\Exception $e) { + return 'Error: ' . text($e->getMessage()) . PHP_EOL; + } + + return $responseMsg; + } + + /** + * @return string + */ + public function getCallLogs(): string + { + $fromDate = $this->getRequest('datefrom'); + $toDate = $this->getRequest('dateto'); + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); + // goes to alert + } + + try { + $pageCount = 1; + $recordCountPerPage = 100; + $timePerCallLogRequest = 10; + $flag = true; + $timeFrom = '00:00:00.000Z'; + $timeTo = '23:59:59.000Z'; + $responseMsg = ""; + while ($flag) { + $start = microtime(true); + $dateFrom = $fromDate . 'T' . $timeFrom; + $dateTo = $toDate . 'T' . $timeTo; + $apiResponse = $this->platform->get('/account/~/extension/~/call-log', [ + 'dateFrom' => $dateFrom, + 'dateTo' => $dateTo, + 'perPage' => 500, + 'page' => $pageCount + ]); + foreach ($apiResponse->json()->records as $value) { + $responseMsg .= "<tr><td>" . text(str_replace(["T", "Z"], " ", $value->startTime)) . "</td><td>" . text($value->type) . "</td><td>" . text($value->from->name) . "</td><td>" . text($value->to->name) . "</td><td>" . text($value->action) . "</td><td>" . text($value->result) . "</td><td>" . text($value->message->id) . "</td></tr>"; + } + + $end = microtime(true); + $time = ($end - $start); + if (isset($apiResponse->json()->navigation->nextPage)) { + if ($time < $timePerCallLogRequest) { + sleep($timePerCallLogRequest - $time); + sleep(5); + $pageCount++; + } + } else { + $flag = false; + } + } + } catch (ApiException $e) { + return xlt('HTTP Error') . ': ' . text($e->getMessage()) . PHP_EOL; + } + + return $responseMsg; + } + + /** + * @return false|string + */ + public function getPending(): false|string + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return json_encode(['error' => js_escape($authErrorMsg)]); + } + + $dateFrom = $this->getRequest('datefrom'); + $dateTo = $this->getRequest('dateto'); + $serviceType = $this->getRequest('type', ''); + + try { + $messageStoreDir = $this->baseDir; + + if (!file_exists($messageStoreDir) && !mkdir($messageStoreDir, 0777, true) && !is_dir($messageStoreDir)) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $messageStoreDir)); + } + + $dateFrom .= 'T00:00:01.000Z'; + $dateTo .= 'T23:59:59.000Z'; + + $messageStoreList = $this->platform->get('/account/~/extension/~/message-store', [ + 'dateFrom' => $dateFrom, + 'dateTo' => $dateTo, + ])->json()->records; + + $responseMsg = $this->processMessageStoreList($messageStoreList, $serviceType); + } catch (ApiException $e) { + $responseMsg = "<tr><td>" . text($e->getMessage()) . " : " . xlt('Ensure account credentials are correct.') . "</td></tr>"; + return json_encode(['error' => $responseMsg]); + } + + return json_encode($responseMsg ?: [xlt("Nothing to report"), xlt("Nothing to report"), xlt("Nothing to report")]); + } + + private function processMessageStoreList($messageStoreList, $serviceType): array + { + $responseMsg = []; + foreach ($messageStoreList as $messageStore) { + if (property_exists($messageStore, 'attachments')) { + foreach ($messageStore->attachments as $attachment) { + $id = attr($attachment->id); + $uri = $attachment->uri; + $to = $messageStore->to[0]->name . " " . $messageStore->to[0]->phoneNumber; + $from = $messageStore->from->name . " " . $messageStore->from->phoneNumber; + $status = $messageStore->messageStatus . $messageStore->from->faxErrorCode; + $faxFormattedDate = date('M j, Y g:i:sa T', strtotime($messageStore->creationTime)); + $updateDate = date('M j Y g:i:sa T', strtotime($messageStore->lastModifiedTime)); + + $links = $this->generateActionLinks($id, $uri); + $checkbox = "<input type='checkbox' class='delete-fax-checkbox' value='" . attr($id) . "'>"; + $type = strtolower($messageStore->type); + $direction = strtolower($messageStore->direction); + $readStatus = $messageStore->readStatus; + if ($type === "sms") { + $messageText = $this->getMessageContent($uri); + $responseMsg[2] .= "<tr><td>" . text($faxFormattedDate) . "</td><td>" . text($messageStore->type) . "</td><td>" . text($from) . "</td><td>" . text($to) . "</td><td>" . text($status) . "</td><td><span class='$id'>" . text(substr($messageText, 0, 30)) . "</span><div class='d-none $id'>" . text($messageText) . "</div></td><td class='btn-group'>" . attr($links['sms']) . "</td><td class='text-center'>" . $checkbox . "</td></tr>"; + } elseif ($direction === "inbound" && $type === $serviceType) { + $status = $messageStore->to[0]->faxErrorCode ?: $messageStore->messageStatus; + $responseMsg[0] .= "<tr><td>" . text($faxFormattedDate) . "</td><td>" . text($updateDate) . "</td><td>" . text($messageStore->faxPageCount) . "</td><td>" . text($from) . "</td><td>" . text($messageStore->subject) . "</td><td>" . text($status) . "</td><td class='text-left'>" . $links['inbound'] . "</td><td class='text-center'>" . $checkbox . "</td></tr>"; + } elseif ($direction === "outbound" && $type === $serviceType) { + $status = $messageStore->to[0]->faxErrorCode ?: $messageStore->messageStatus; + $responseMsg[1] .= "<tr><td>" . text($faxFormattedDate) . "</td><td>" . text($updateDate) . "</td><td>" . text($messageStore->faxPageCount) . + "</td><td>" . text($from) . "</td><td>" . text($to) . "</td><td>" . text($status) . "</td><td>" . $links['outbound'] . "</td><td class='text-center'>" . $checkbox . "</td></tr>"; + } + } + } + } + + return $responseMsg; + } + + private function generateActionLinks($id, $uri): array + { + $patientLink = "<a role='button' href='javascript:void(0)' onclick=\"createPatient(event, " . attr_js($id) . ", " . attr_js($id) . ", " . attr_js(json_encode([])) . ")\"> <i class='fa fa-chart-simple mr-2' title='" . xla("Chart fax or Create patient and chart fax to documents.") . "'></i></a>"; + $messageLink = "<a role='button' href='javascript:void(0)' onclick=\"notifyUser(event, " . attr_js($id) . ", " . attr_js($id) . ", " . attr_js(0) . ")\"> <i class='fa fa-paper-plane mr-2' title='" . xla("Notify a user and attach this fax to message.") . "'></i></a>"; + $downloadLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'true')\"> <i class='fa fa-file-download mr-2' title='" . xla("Download and delete fax") . "'></i></a>"; + $viewLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'false')\"> <i class='fa fa-file-pdf mr-2' title='" . xla("View fax document") . "'></i></a>"; + $deleteLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'false', 'true')\"> <i class='text-danger fa fa-trash mr-2' title='" . xla("Delete this fax document") . "'></i></a>"; + $forwardLink = "<a role='button' href='javascript:void(0)' onclick=\"forwardFax(event, " . attr_js($id) . ")\"> <i class='fa fa-forward mr-2' title='" . xla("Forward fax to new fax recipient or email attachment.") . "'></i></a>"; + + $vtoggle = "<a href='javascript:' onclick=messageShow(" . attr_js($id) . ")><span class='mx-1 fa fa-eye-slash fa-1x'></span></a>"; + $vreply = "<a href='javascript:' onclick=messageReply(" . attr_js($id) . ")><span class='mx-1 fa fa-reply'></span></a>"; + + return [ + 'sms' => $vtoggle . $vreply, + 'inbound' => $patientLink . $messageLink . $forwardLink . $viewLink . $downloadLink . $deleteLink, + 'outbound' => $viewLink . $downloadLink . $deleteLink + ]; + } + + /** + * @param array $responseMsg + * @param $messageStore + * @param $attachment + * @return void + */ + private function formatMessageStore(array &$responseMsg, $messageStore, $attachment): void + { + $id = $attachment->id; + $uri = $attachment->uri; + $to = $messageStore->to[0]->name . " " . $messageStore->to[0]->phoneNumber; + $from = $messageStore->from->name . " " . $messageStore->from->phoneNumber; + $errors = $messageStore->to[0]->faxErrorCode ? "why: " . $messageStore->to[0]->faxErrorCode : $messageStore->from->faxErrorCode; + $status = $messageStore->messageStatus . " " . $errors; + $patientLink = "<a role='button' href='javascript:void(0)' onclick=\"createPatient(event, " . attr_js($id) . ", " . attr_js($id) . ", " . attr_js(json_encode($parse ?? [])) . ")\"> <i class='fa fa-chart-simple mr-2' title='" . xla("Chart fax or Create patient and chart fax to documents.") . "'></i></a>"; + $messageLink = "<a role='button' href='javascript:void(0)' onclick=\"notifyUser(event, " . attr_js($id) . ", " . attr_js($id) . ", " . attr_js(($pid_assumed ?? 0)) . ")\"> <i class='fa fa-paper-plane mr-2' title='" . xla("Notify a user and attach this fax to message.") . "'></i></a>"; + $downloadLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'true')\"> <i class='fa fa-file-download mr-2' title='" . xla("Download and delete fax") . "'></i></a>"; + $viewLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'false')\"> <i class='fa fa-file-pdf mr-2' title='" . xla("View fax document") . "'></i></a>"; + $deleteLink = "<a role='button' href='javascript:void(0)' onclick=\"getDocument(event, null, " . attr_js($id) . ", 'false', 'true')\"> <i class='text-danger fa fa-trash mr-2' title='" . xla("Delete this fax document") . "'></i></a>"; + $forwardLink = "<a role='button' href='javascript:void(0)' onclick=\"forwardFax(event, " . attr_js($id) . ")\"> <i class='fa fa-forward mr-2' title='" . xla("Forward fax to new fax recipient or email attachment.") . "'></i></a>"; + + $faxFormattedDate = date('M j, Y g:i:sa T', strtotime($messageStore->lastModifiedTime)); + $docLen = text(round(1024 / 1024, 2)) . "KB"; // todo add length + $responseMsg[0] .= "<tr><td>" . text($faxFormattedDate) . "</td><td>" . text($from) . "</td><td>" . text('todo: add caller id') . "</td><td>" . text($to) . "</td><td>" . text($messageStore->PagesReceived) . "</td><td>" . text($docLen) . "</td><td class='text-left'>" . /*$detailLink .*/ + "</td><td class='text-center'>" . text($pid_assumed ?? '') . "</td><td class='text-left'>" . $patientLink . $messageLink . $forwardLink . $viewLink . $downloadLink . $deleteLink . "</td></tr>"; + //$responseMsg[0] .= $form; + $aUrl = "<a href='#' onclick=getDocument(event," . attr_js($uri) . "," . attr_js($id) . ",'true')>" . text($id) . " <span class='fa fa-download'></span></a></br>"; + $vUrl = "<a href='#' onclick=getDocument(event," . attr_js($uri) . "," . attr_js($id) . ",'false')> <span class='fa fa-file-pdf-o'></span></a></br>"; + if ($status != 'failed' && $this->formatPhone($this->credentials['smsNumber']) != $messageStore->from) { + $vreply = "<a href='javaScript:' onclick=messageReply(" . attr_js($messageStore->from) . ")><span class='mx-1 fa fa-reply'></span></a>"; + } else { + $vreply = "<a href='#' title='SMS failure'> <span class='fa fa-file-pdf text-danger'></span></a></br>"; + } + $row = "<tr> + <td>" . text(str_replace(["T", "Z"], " ", $messageStore->lastModifiedTime)) . "</td> + <td>" . text($messageStore->type) . "</td> + <td>" . text($from) . "</td> + <td>" . text($to) . "</td> + <td>" . text($status) . "</td> + <td>" . ($aUrl) . "</td> + <td>" . ($vUrl) . "</td>"; + if (strtolower($messageStore->type) === "sms") { + $row .= "<td>" . ($vreply) . "</td>"; + } + + $row .= "</tr>"; + if (strtolower($messageStore->type) === "sms") { + $responseMsg[2] .= $row; + // sms + } elseif (strtolower($messageStore->direction) === "inbound") { + $responseMsg[0] .= $row; +// in fax + } else { + $responseMsg[1] .= $row; +// out fax + } + } + + /** + * @param $number + * @return string + */ + public function formatPhone($number): string + { + // this is u.s only. need E-164 + $n = preg_replace('/[^0-9]/', '', $number); + if (stripos($n, '1') === 0) { + $n = '+' . $n; + } else { + $n = '+1' . $n; + } + return $n; + } + + /** + * @return string|void + */ + public function getMessage() + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); + // goes to alert + } + + try { + $messageStoreDir = $this->baseDir; + if (!file_exists($messageStoreDir)) { + mkdir($messageStoreDir, 0777, true); + } + + $messageStoreList = $this->platform->get('/account/~/extension/~/message-store', [ + 'messageType' => "", + 'dateFrom' => '2018-05-01' + ])->json()->records; + $timePerMessageStore = 6; + $responseMsgs = ""; + foreach ($messageStoreList as $messageStore) { + if (property_exists($messageStore, 'attachments')) { + foreach ($messageStore->attachments as $attachment) { + $id = $attachment->id; + $uri = $attachment->uri; + try { + $apiResponse = $this->platform->get($uri); + } catch (ApiException $e) { + $responseMsgs .= "<tr><td>Errors: " . text($e->getMessage()) . $e->apiResponse()->request()->getUri()->__toString() . "</td></tr>"; + continue; + } + + $ext = $this->getExtensionFromContentType($apiResponse->response()->getHeader('Content-Type')[0]); + $type = $this->getTypeFromContentType($apiResponse->response()->getHeader('Content-Type')[0]); + $start = microtime(true); + file_put_contents("{$messageStoreDir}/{$type}_{$id}.{$ext}", $apiResponse->raw()); + $responseMsgs .= "<tr><td>" . $messageStore->creationTime . "</td><td>" . $messageStore->type . "</td><td>" . $messageStore->from->name . "</td><td>" . $messageStore->to->name . "</td><td>" . $messageStore->availability . "</td><td>" . $messageStore->messageStatus . "</td><td>" . $messageStore->message->id . "</td></tr>"; + $end = microtime(true); + $time = ($end - $start); + if ($time < $timePerMessageStore) { + sleep($timePerMessageStore - $time); + } + } + } else { + echo xlt("Does not have messages") . PHP_EOL; + } + } + } catch (ApiException $e) { + echo "<tr><td>Error: " . text($e->getMessage() . $e->apiResponse()->request()->getUri()->__toString()) . "</td></tr>"; + } + + exit; + } + + /** + * @return string|null + */ + protected function index(): ?string + { + if (!$this->getSession('pid', '')) { + $pid = $this->getRequest('patient_id'); + $this->setSession('pid', $pid); + } + + return null; + } + + /** + * @return mixed + */ + public function sendEmail(): mixed + { + return null; + } + + /** + * @return string|bool + */ + public function fetchReminderCount(): string|bool + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); // goes to alert + } + + if (self::$_apiModule == 'sms') { + return '0'; + } + try { + $platform = $this->rcsdk->platform(); + $response = $platform->get('/restapi/v1.0/account/~/extension/~/message-store', [ + 'messageType' => 'Fax', + 'direction' => 'Inbound', + 'availability' => 'Alive' + ]); + $json = $response->json(); + return (string) text(count($json->records)); + } catch (Exception $e) { + error_log('Error fetching incoming faxes: ' . text($e->getMessage())); + return false; + } + } + + /** + * @param $pid + * @param $jobId + * @param $fileName + * @return string + */ + public function chartFaxDocument($pid, $jobId, $fileName = null): string + { + $authErrorMsg = $this->authenticate(); + if ($authErrorMsg !== 1) { + return text(js_escape($authErrorMsg)); // goes to alert + } + + // Determine the category ID + $catid = sqlQuery("SELECT id FROM `categories` WHERE `name` = 'FAX'")['id'] ?? sqlQuery("SELECT id FROM `categories` WHERE `name` = 'Medical Record'")['id']; + + try { + // Fetch the fax message details + $messageDetailsResponse = $this->platform->get("/account/~/extension/~/message-store/{$jobId}"); + $messageDetails = $messageDetailsResponse->json(); + + // Fetch the fax content + $contentUri = $messageDetails->attachments[0]->uri; + $apiResponse = $this->platform->get($contentUri); + $contentType = $apiResponse->response()->getHeader('Content-Type')[0]; + $rawData = (string)$apiResponse->raw(); + + // Determine file extension and file name + $ext = $this->getExtensionFromContentType($contentType); + $fileName = $fileName ?? xlt("fax") . '_' . text($jobId) . $ext; + $content = $rawData; + + // Create a new document and save it + $document = new Document(); + $result = $document->createDocument($pid, $catid, $fileName, $contentType, $content); + + return $result ? xlt("Error: Failed to save document. Category Fax") : xlt("Chart Success"); + } catch (ApiException $e) { + return json_encode(['error' => "Error: Retrieving Fax: " . text($e->getMessage())]); + } catch (Exception $e) { + return json_encode(['error' => "Error: " . text($e->getMessage())]); + } + } + + /** + * @param $email + * @param $body + * @param $file + * @param array $user + * @return string + * @throws \PHPMailer\PHPMailer\Exception + */ + public static function emailDocument($email, $body, $file, array $user = []): string + { + $from_name = ($user['fname'] ?? '') . ' ' . ($user['lname'] ?? ''); + $desc = xlt("Comment") . ":\n" . text($body) . "\n" . xlt("This email has an attached fax document."); + $mail = new MyMailer(); + $from_name = text($from_name); + $from = $GLOBALS["practice_return_email_path"]; + $mail->AddReplyTo($from, $from_name); + $mail->SetFrom($from, $from); + $mail->AddAddress($email, $email); + $mail->Subject = xlt("Forwarded Fax Document"); + $mail->Body = $desc; + $mail->AddAttachment($file); + + return $mail->Send() ? xlt("Email successfully sent.") : xlt("Error: Email failed") . text($mail->ErrorInfo); + } +} diff --git a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/TwilioSMSClient.php b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/TwilioSMSClient.php index 276b9fdce9a..a8b6db5b0db 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/src/Controller/TwilioSMSClient.php +++ b/interface/modules/custom_modules/oe-module-faxsms/src/Controller/TwilioSMSClient.php @@ -54,7 +54,7 @@ public function fetchSMSFilteredList($dateFrom, $dateTo) * @param $uiDateRangeFlag * @return false|string|null */ - public function fetchSMSList($uiDateRangeFlag = true) + public function fetchSMSList($uiDateRangeFlag = true): false|string|null { return $this->_getPending($uiDateRangeFlag); } @@ -62,7 +62,7 @@ public function fetchSMSList($uiDateRangeFlag = true) /** * @return array|mixed */ - public function getCredentials() + public function getCredentials(): mixed { $credentials = appDispatch::getSetup(); $this->accountSID = $credentials['username'] ?? ''; @@ -77,16 +77,9 @@ public function getCredentials() return $credentials; } - /** - * @param $tophone - * @param $subject - * @param $message - * @param $from - * @return mixed - */ - public function sendSMS($tophone = '', $subject = '', $message = '', $from = ''): mixed + public function sendSMS($toPhone = '', $subject = '', $message = '', $from = ''): mixed { - $tophone = $tophone ?: $this->getRequest('phone'); + $toPhone = $toPhone ?: $this->getRequest('phone'); $from = $from ?: $this->getRequest('from'); $message = $message ?: $this->getRequest('comments'); @@ -95,11 +88,11 @@ public function sendSMS($tophone = '', $subject = '', $message = '', $from = '') } else { $from = $this->formatPhone($from); } - $tophone = $this->formatPhone($tophone); + $toPhone = $this->formatPhone($toPhone); try { $twilio = new Client($this->appKey, $this->appSecret, $this->sid); $message = $twilio->messages->create( - $tophone, + $toPhone, array( "body" => text($message), "from" => attr($from) diff --git a/interface/modules/custom_modules/oe-module-faxsms/version.php b/interface/modules/custom_modules/oe-module-faxsms/version.php index 949c3471ca3..b6b0cc44cfb 100644 --- a/interface/modules/custom_modules/oe-module-faxsms/version.php +++ b/interface/modules/custom_modules/oe-module-faxsms/version.php @@ -7,5 +7,5 @@ * */ $v_major = '4'; -$v_minor = '0'; +$v_minor = '1'; $v_patch = '0'; diff --git a/src/Common/Utils/FileUtils.php b/src/Common/Utils/FileUtils.php index 87a172c15ef..2dd9041eebc 100644 --- a/src/Common/Utils/FileUtils.php +++ b/src/Common/Utils/FileUtils.php @@ -7,6 +7,7 @@ * @link https://www.open-emr.org * @author Kevin McCormick Longview, Texas * @author Stephen Nielson <snielson@discoverandchange.com> + * @author Jerry Padgett <sjpadgett@gmail.com> * @copyright Copyright (c) Kevin McCormick Longview, Texas * @copyright Copyright (c) 2023 Discover and Change, Inc. <snielson@discoverandchange.com> * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 @@ -16,6 +17,52 @@ class FileUtils { + /** + * Map of file extensions to MIME types. + */ + private static array $mimeTypes = [ + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'doc' => 'application/msword', + 'rtf' => 'application/rtf', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + ]; + /** * adapted from http://scratch99.com/web-development/javascript/convert-bytes-to-mb-kb/ * @@ -24,7 +71,7 @@ class FileUtils * * @return string */ - public static function getHumanReadableFileSize($bytes) + public static function getHumanReadableFileSize($bytes): string { $sizes = array('Bytes', 'KB', 'MB', 'GB', 'TB'); if ($bytes == 0) { @@ -39,4 +86,75 @@ public static function getHumanReadableFileSize($bytes) return round($bytes / pow(1024, $i), 1) . ' ' . $sizes[$i]; } } + + /** + * Determines the MIME type of file or content. + * + * @param string $filePath + * @param string $content + * @return array + */ + public static function fileGetMimeType($filePath, &$content): false|array + { + $f_info = finfo_open(FILEINFO_MIME_TYPE); + if (!empty($content)) { + $mimeType = finfo_buffer($f_info, $content); + finfo_close($f_info); + // Check if filePath has an extension, if not add it based on MIME type + $filePath = self::ensureExtension($filePath, $mimeType); + return ['type' => $mimeType, 'filePath' => $filePath]; + } + + if (!empty($filePath) && !file_exists($filePath)) { + finfo_close($f_info); + return false; + } + + $mimeType = finfo_file($f_info, $filePath); + finfo_close($f_info); + // Check if filePath has an extension, if not add it based on MIME type + $filePath = self::ensureExtension($filePath, $mimeType); + + return ['type' => $mimeType, 'filePath' => $filePath]; + } + + /** + * Retrieves the MIME type based on the file extension. + * + * @param string $extension + * @return string + */ + public static function getMimeTypeFromExtension($extension, $default = 'text/plain'): string + { + return self::$mimeTypes[strtolower($extension)] ?? $default; + } + + /** + * Retrieves the file extension based on the MIME type. + * + * @param string $mimeType + * @return string + */ + public static function getExtensionFromMimeType($mimeType): string + { + $extension = array_search(strtolower($mimeType), self::$mimeTypes); + return $extension !== false ? $extension : ''; + } + + /** + * Ensures the file path has an appropriate extension based on the MIME type. + * + * @param string $filePath + * @param string $mimeType + * @return string + */ + public static function ensureExtension($filePath, $mimeType): string + { + $pathInfo = pathinfo($filePath); + if (empty($pathInfo['extension'])) { + $extension = self::getExtensionFromMimeType($mimeType); + $filePath .= '.' . $extension; + } + return $filePath; + } }