src/Custom/ABFController.php line 150

Open in your IDE?
  1. <?php
  2. /**
  3.  * @file
  4.  *   Contriller for use in the app that adds some constantly used helper methods.
  5.  *
  6.  * @ToDo Add a server side password validation function to test for all the password requirements.
  7.  */
  8. namespace App\Custom;
  9. use App\Custom\ABFWebService;
  10. use Psr\Log\LoggerInterface;
  11. use stdClass;
  12. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  13. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  14. use Symfony\Component\HttpFoundation\RequestStack;
  15. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  16. /**
  17.  * Controller for the app pages
  18.  */
  19. class ABFController extends AbstractController
  20. {
  21.     /**
  22.      * Logging.
  23.      *
  24.      * @var LoggerInterface
  25.      */
  26.     protected $logger;
  27.     /**
  28.      * Session.
  29.      *
  30.      * @var SessionInterface
  31.      */
  32.     protected $session;
  33.     /**
  34.      * @var RequestStack
  35.      */
  36.     protected $requestStack;
  37.     /**
  38.      * Parameters that we want to use immediately.
  39.      */
  40.     protected $params;
  41.     /**
  42.      * Session timeout in seconds
  43.      *
  44.      * @var int
  45.      */
  46.     protected $sessionTimeout;
  47.     /**
  48.      * Link to API interface.
  49.      *
  50.      * @var ABFWebService
  51.      */
  52.     protected $api;
  53.     /**
  54.      * Constructor
  55.      *
  56.      * @param LoggerInterface       $logger       Access to the logging system.
  57.      * @param RequestStack          $requestStack Interface to the PHP request.
  58.      * @param ParameterBagInterface $params       Interface to the parameters of the app.
  59.      */
  60.     public function __construct(LoggerInterface $logger, RequestStack $requestStack, ParameterBagInterface $params)
  61.     {
  62.         $this->logger = $logger;
  63.         $this->requestStack = $requestStack;
  64.         $this->params = $params;
  65.         $this->session = $this->requestStack->getSession();
  66.         $this->sessionTimeout = $this->params->get('session.timeout');
  67.         $this->api = $this->getWebservice();
  68.     }
  69.     /**
  70.      * Load an instance of the ABFWebService for communication with the API
  71.      *
  72.      * @return ABFWebService
  73.      */
  74.     protected function getWebservice(): ABFWebService
  75.     {
  76.         return new ABFWebService(
  77.             $this->params->get('api.host'),
  78.             $this->params->get('api.port'),
  79.             $this->params->get('api.user'),
  80.             $this->params->get('api.pass'),
  81.             $this->params->get('api.queue')
  82.         );
  83.     }
  84.     /**
  85.      * Handler for ABFErrorExceptions.
  86.      *
  87.      * 412 Errors are sent when the API returns OK = 0.
  88.      * 418 Errors are sent when an exception happened in the packet and queue process.
  89.      *
  90.      * @param ABFErrorException $e The thrown exception.
  91.      *
  92.      * @return string The message to print.
  93.      */
  94.     protected function handleABFError(ABFErrorException $e): string
  95.     {
  96.         $message = '';
  97.         // Log the error locally.
  98.         $this->logger->error(sprintf('ABF Error: %d - %s', $e->getCode(), $e->getMessage()));
  99.         switch ($e->getCode()) {
  100.             case 412:
  101.                 $message = sprintf("The system could not handle your request at this time. (%s)", $e->getMessage());
  102.                 break;
  103.             case 418:
  104.                 $message = "The connection to the system was interrupted. Please try your request again";
  105.                 break;
  106.             default:
  107.                 $message = "An error has occurred in the background. Please try your request again.";
  108.         }
  109.         return $message;
  110.     }
  111.     /**
  112.      * Sets the session timer.
  113.      *
  114.      */
  115.     protected function setSessionTimer(): void
  116.     {
  117.         $this->session->set('t', time());
  118.     }
  119.     /**
  120.      * Check to see if session is still valid for the time peroid.
  121.      * Session needs to be cleared at 20min.
  122.      *
  123.      * @return bool
  124.      */
  125.     protected function isSessionValid(): bool
  126.     {
  127.         // Test if session exists.
  128.         if (!$this->session) {
  129.             $this->session->start();
  130.             return false;
  131.         }
  132.         $startTime = (int) $this->session->get('t', 0);
  133.         // If the app session timeout is 5min or less just use that.
  134.         // Otherwise use 80% of the session timeout value.
  135.         // The Goal is to have the app timer run out before the api timer for the session.
  136.         $period = (600 >= $this->sessionTimeout ? $this->sessionTimeout : (int) $this->sessionTimeout * 0.8);
  137.         $now = time();
  138.         $this->logger->info(sprintf('Starttime: %d, Period: %d, Now: %d', $startTime, $period, $now));
  139.         $this->logger->info(sprintf("Start + period is after now? %s", (($startTime + $period) > $now)));
  140.         return ($startTime + $period) > $now;
  141.     }
  142.     /**
  143.      * Create a stable one way encryption of a password.
  144.      *
  145.      * @param string $password
  146.      *   The password string to encrypt
  147.      *
  148.      * @return string
  149.      *   The encrypted password.
  150.      */
  151.     protected function encryptPassword(string $password): string
  152.     {
  153.         // Use the app secret as the salt.
  154.         $salt = $this->params->get('app.secret');
  155.         $this->logger->info("Salt: " . $salt);
  156.         return crypt(trim($password), $salt);
  157.     }
  158.     /**
  159.      * Encrypt the provided string.
  160.      *
  161.      * @param string $term
  162.      *   The string to be encrypted.
  163.      *
  164.      * @return string
  165.      *   The encrypted string.
  166.      */
  167.     protected function encrypt(string $term): string
  168.     {
  169.         // Use the app secret as the key.
  170.         $key = $this->params->get('app.secret');
  171.         // Generate the initialization vector.
  172.         $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
  173.         // Encrypt the provided string.
  174.         $encryptedString = openssl_encrypt($term, 'aes-256-cbc', $key, 0, $iv);
  175.         // Attach the initialization vector to the encrypted_string
  176.         // and base64 endcode the lot.
  177.         // phpcs:ignore
  178.         return base64_encode($encryptedString . '||' . $iv);
  179.     }
  180.     /**
  181.      * Decrypt the provided string using the provided key.
  182.      *
  183.      * @param string|null $encryptedString
  184.      *   The string to decrypt.
  185.      *
  186.      * @return string
  187.      *   The original plaintext string.
  188.      */
  189.     protected function decrypt(string $encryptedString = null): string
  190.     {
  191.         if (0 === strlen($encryptedString)) {
  192.             // need to have something to return
  193.             return '';
  194.         }
  195.         // Use the app secret as the key.
  196.         $key = $this->params->get('app.secret');
  197.         // Decode and split apart the encrypted string to get what we want to
  198.         // decrypt and the initialization vector.
  199.         list($encryptedText, $iv) = explode('||', \base64_decode($encryptedString), 2);
  200.         // Perform the final decryption and return th plaintext string.
  201.         return openssl_decrypt($encryptedText, 'aes-256-cbc', $key, 0, $iv);
  202.     }
  203.     /**
  204.      * Returns an array of the values for the given key from each of the objects submitted.
  205.      *
  206.      * @param array $objectArray The array of objects to search.
  207.      * @param array $keys        The array of keys to use in the search.
  208.      *
  209.      * @return array Array of dictionaries to return.
  210.      */
  211.     protected function getItemsFromObject(array $objectArray, array $keys): array
  212.     {
  213.         $out = [];
  214.         //$this->logger->info('the original object');
  215.         //$this->logger->info(print_r($objectArray, true));
  216.         foreach ($objectArray as $obj) {
  217.             $keysArePresent = true;
  218.             foreach ($keys as $key) {
  219.                 if (!array_key_exists($key, $obj)) {
  220.                     $keysArePresent = false;
  221.                 }
  222.             }
  223.             if ($keysArePresent) {
  224.                 $thisItem = [];
  225.                 foreach ($keys as $key) {
  226.                     $thisItem[$key] = $obj[$key];
  227.                 }
  228.                 $out[] = $thisItem;
  229.             }
  230.         }
  231.         //$this->logger->info('the resulting object.');
  232.         //$this->logger->info(print_r($out, true));
  233.         return $out;
  234.     }
  235.     /**
  236.      * Returns the first and last days of the current month
  237.      *
  238.      * @param DateTime|null $date Optional date in the month.
  239.      *
  240.      * @return array Array of dates [firstOfMonth, lastOfMonth].
  241.      */
  242.     protected function getMonthStartAndEnd(\DateTime $date = null): array
  243.     {
  244.         if (!$date) {
  245.             $date = new \DateTime();
  246.         }
  247.         return [
  248.             // phpcs:ignore -- Concatenation operator spacing.
  249.             $date->format('Y-m') . '-01',
  250.             $date->format('Y-m-t'),
  251.         ];
  252.     }
  253.     /**
  254.      * Takes two dates and returns them oldest first.
  255.      *
  256.      * @param string $date1 First date.
  257.      * @param string $date2 Second date.
  258.      *
  259.      * @return array Sorted dates.
  260.      */
  261.     protected function putDatesInCorrectOrder(string $date1, string $date2): array
  262.     {
  263.         try {
  264.             // Cast the date strings to DateTime.
  265.             $d1 = new \DateTime($date1);
  266.             $d2 = new \DateTime($date2);
  267.             if ($d1 > $d2) {
  268.                 return [$date2, $date1];
  269.             }
  270.         } catch (\Exception $e) {
  271.             $this->logger->warning(sprintf('Error putting dates in correct order: %s', $e->getMessage()));
  272.         }
  273.         return [$date1, $date2];
  274.     }
  275.     /**
  276.      * Make sure a given date is not in the past.
  277.      * Test a date to make sure it is at least today. If not return today.
  278.      *
  279.      * @param DateTime $date The date to test.
  280.      *
  281.      * @return DateTime
  282.      */
  283.     protected function dateFloor(\DateTime $date): \DateTime
  284.     {
  285.         $today = new \DateTime();
  286.         // Set the time of the date to the same thing for both.
  287.         $today->setTime(0, 0, 0, 0);
  288.         $date->setTime(0, 0, 0, 0);
  289.         if ($today <= $date) {
  290.             return $date;
  291.         }
  292.         return $today;
  293.     }
  294.     /**
  295.      * Get 18 months ago. Used to limit how far back to get transactions.
  296.      *
  297.      * @param bool $dateAsString Return the date as a string. Default false.
  298.      *
  299.      * @return DateTime The first day of the month 18 months prior.
  300.      */
  301.     protected function getTwentyFourMonthsAgo(bool $dateAsString = false): \DateTime
  302.     {
  303.         // Now
  304.         $d = new \DateTime();
  305.         // Set date to first of the month.
  306.         $d->setDate($d->format('Y'), $d->format('m'), 1);
  307.         // Subtract 18 months
  308.         $d->sub(new \DateInterval('P24M'));
  309.         return $dateAsString ? $d->format('Y-m-d') : $d;
  310.     }
  311.     /**
  312.      * Return the subaccount nickname and account as a string.
  313.      *
  314.      * @param string $account     The main account string.
  315.      * @param string $subaccount  The subaccount to get the nickname for.
  316.      * @param string $clientToken The logged in users's token.
  317.      * @param string $clientIp    The client IP address.
  318.      *                            Default 0.0.0.0
  319.      *
  320.      * @return string The formatted nickname string.
  321.      */
  322.     protected function getAccountNameString(string $account, string $subaccount, string $clientToken, string $clientIp = '0.0.0.0.'): string
  323.     {
  324.         try {
  325.             // Update the session timer
  326.             $this->setSessionTimer();
  327.             $subaccounts = $this->api->abfGetSubaccounts(
  328.                 $account,
  329.                 $clientToken,
  330.                 $clientIp
  331.             );
  332.             foreach ($subaccounts as $sub) {
  333.                 if ($subaccount === $sub['account'] && strlen($sub['nickname'])) {
  334.                     return sprintf('%s (%s)', $sub['nickname'], $sub['account']);
  335.                 }
  336.             }
  337.         } catch (ABFTimeoutException $e) {
  338.             $this->logger->warning(sprintf('getAccountNameString: %s', $e->getMessage()));
  339.         } catch (ABFErrorException $e) {
  340.             $this->logger->warning(sprintf('getAccountNameString: %s', $e->getMessage()));
  341.         } catch (\Exception $e) {
  342.             $this->logger->warning(sprintf('getAccountNameString: %s', $e->getMessage()));
  343.         }
  344.         return $subaccount;
  345.     }
  346.     /**
  347.      * Return an array of subaccounts and their balances in cents and dollars.
  348.      *
  349.      * @param string $account     The main account string.
  350.      * @param array  $subaccounts An array of sub account names to return balances for.
  351.      * @param string $clientToken The logged in user's token.
  352.      * @param string $clientIp    The client's IP address.
  353.      *                            Default = 0.0.0.0
  354.      *
  355.      * @return array An array with the subaccount names as keys with values array of the balance in dollars and cents.
  356.      */
  357.     protected function getSubaccountBalances(string $account, array $subaccounts, string $clientToken, string $clientIp = '0.0.0.0'): array
  358.     {
  359.         $out = [];
  360.         // Preload return array with a reasonable amount.
  361.         foreach ($subaccounts as $sa) {
  362.             $out[$sa] = [
  363.                 'dollars' => 10000.00,
  364.                 'cents' => 1000000,
  365.             ];
  366.         }
  367.         try {
  368.             // Update the session timer
  369.             $this->setSessionTimer();
  370.             $result = $this->api->abfGetSubaccounts(
  371.                 $account,
  372.                 $clientToken,
  373.                 $clientIp
  374.             );
  375.             foreach ($result as $sa) {
  376.                 $thisSubAccount = $sa['account'];
  377.                 if (array_key_exists($thisSubAccount, $out)) {
  378.                     $out[$thisSubAccount]['cents'] = $sa['balance'];
  379.                     $out[$thisSubAccount]['dollars'] = $sa['balance'] / 100;
  380.                 }
  381.             }
  382.         } catch (ABFTimeoutException $e) {
  383.             $this->logger->warning(sprintf('getSubaccountBalances Timeout Exception: %s', $e->getMessage()));
  384.         } catch (ABFErrorException $e) {
  385.             $this->logger->warning(sprintf('getSubaccountBalances Timeout Exception: %s', $this->handleABFError($e)));
  386.         } catch (\Exception $e) {
  387.             $this->logger->warning(sprintf('getSubaccountBalances Timeout Exception: %s', $e->getMessage()));
  388.         }
  389.         return $out;
  390.     }
  391.     /**
  392.      * Ping system to send the verification code.
  393.      *
  394.      * @param string      $email      The email address of the account manager.
  395.      * @param string      $adminToken The API admin token.
  396.      * @param string      $clientIp   The client's IP address.
  397.      *                                Default = 0.0.0.0
  398.      * @param string|null $phone      The phone number of the account manager. Default to null.
  399.      * @param bool        $resend     If this is a resend of the code. Default false.
  400.      *
  401.      * @return array the type and code of the flash message to present.
  402.      */
  403.     protected function sendVerificationCode(string $email, string $adminToken, string $clientIp = '0.0.0.0', string $phone = null, bool $resend = false): array
  404.     {
  405.         $type = '';
  406.         $message = '';
  407.         try {
  408.             // Update the session timer
  409.             $this->setSessionTimer();
  410.             if ($phone) {
  411.                 // Send code.
  412.                 $sentLink = $this->api->sendSmsVerification(
  413.                     $phone,
  414.                     $email,
  415.                     $adminToken,
  416.                     $clientIp
  417.                 );
  418.                 if (!$sentLink) {
  419.                     throw new \Exception('Something went wrong sending the verification code to your device. Please try again later.');
  420.                 }
  421.                 // Add success message.
  422.                 $message = $resend
  423.                     ? "The validation code has been re-sent to your device. Please enter this code in the form below. It is valid for one hour."
  424.                     : "A validation code has been sent to your device. Please enter this code in the form below. It is valid for one hour.";
  425.                 $type = 'success';
  426.             } else {
  427.                 // Send code.
  428.                 $sentLink = $this->api->sendEmailVerification(
  429.                     $email,
  430.                     $adminToken,
  431.                     $clientIp
  432.                 );
  433.                 if (!$sentLink) {
  434.                     throw new \Exception('Something went wrong sending the verification code to your email. Please try again later.');
  435.                 }
  436.                 // Add success message.
  437.                 $message = $resend
  438.                     ? "The validation code has been re-sent to your registered email address. Please enter this code in the form below. It is valid for one hour."
  439.                     : "A validation code has been sent to your registered email address. Please enter this code in the form below. It is valid for one hour.";
  440.                 $type = 'success';
  441.             }
  442.         } catch (ABFTimeoutException $e) {
  443.             $type = 'danger';
  444.             $message = $e->getMessage();
  445.         } catch (ABFErrorException $e) {
  446.             $type = 'danger';
  447.             $message = $this->handleABFError($e);
  448.         } catch (\Exception $e) {
  449.             $type = 'danger';
  450.             $message = $e->getMessage();
  451.         }
  452.         return [$type, $message];
  453.     }
  454.     /**
  455.      * Returns html describing the password requirements
  456.      *
  457.      * @return string
  458.      */
  459.     protected function getPasswordRequirementText(): string
  460.     {
  461.         $constraints = $this->getPasswordConstraints();
  462.         $specialCharacterList = implode(', ', $this->getCharacters('special'));
  463.         $html = '
  464.         <div class="password-text">
  465.             <p>Valid passwords meet the following criteria:</p>
  466.             <ul>
  467.         ';
  468.         $html .= sprintf('<li>Are at least %d characters long.</li>', $constraints->minLength);
  469.         $html .= $constraints->requireLowercase ? '<li>Contain at least one lowercase letter.</li>' : '';
  470.         $html .= $constraints->requireUppercase ? '<li>Contain at least one uppercase (capital) letter.</li>' : '';
  471.         $html .= $constraints->requireNumber ? '<li>Contain at least one number (0 - 9).</li>' : '';
  472.         $html .= $constraints->requireSpecialChar ? sprintf('<li>Contain at least one special character (%s).</li>', $specialCharacterList) : '';
  473.         $html .= '
  474.             </ul>
  475.         </div>';
  476.         return $html;
  477.     }
  478.     /**
  479.      * Tests to see if a given password meets the system password standards.
  480.      *
  481.      * @param string $password The password string to validate.
  482.      *
  483.      * @return array Array containing [bool, string]: Valid state, Error string.
  484.      */
  485.     protected function passwordIsValid(string $password): array
  486.     {
  487.         $constraints = $this->getPasswordConstraints();
  488.         try {
  489.             // Verify length of password.
  490.             if ($constraints->minLength > strlen($password)) {
  491.                 throw new \Exception(sprintf('The password must be at least %d characters long.', $constraints->minLength));
  492.             }
  493.             // Verify Lowercase char is present
  494.             if ($constraints->requireLowercase) {
  495.                 if (!$this->stringContains($password, $this->getCharacters('lowercase'))) {
  496.                     throw new \Exception('The password must contain at least one lowercase letter.');
  497.                 }
  498.             }
  499.             // Verify Uppercase char is present
  500.             if ($constraints->requireUppercase) {
  501.                 if (!$this->stringContains($password, $this->getCharacters('uppercase'))) {
  502.                     throw new \Exception('The password must contain at least one uppercase letter.');
  503.                 }
  504.             }
  505.             // Verify number is present
  506.             if ($constraints->requireNumber) {
  507.                 if (!$this->stringContains($password, $this->getCharacters('numbers'))) {
  508.                     throw new \Exception('The password must contain at least one number');
  509.                 }
  510.             }
  511.             // Verify Lowercase char is present
  512.             if ($constraints->requireSpecialChar) {
  513.                 if (!$this->stringContains($password, $this->getCharacters('special'))) {
  514.                     throw new \Exception('The password must contain at least one special character.');
  515.                 }
  516.             }
  517.             return [true, ''];
  518.         } catch (\Exception $e) {
  519.             return [false, $e->getMessage()];
  520.         }
  521.     }
  522.     /**
  523.      * Return an object with all the password constraints
  524.      *
  525.      * @return stdClass
  526.      */
  527.     protected function getPasswordConstraints(): stdClass
  528.     {
  529.         $obj = new stdClass();
  530.         $obj->minLength = (int) $this->params->get('password.min_length');
  531.         $obj->requireLowercase = (bool) $this->params->get('password.require_lowercase');
  532.         $obj->requireUppercase = (bool) $this->params->get('password.require_uppercase');
  533.         $obj->requireNumber = (bool) $this->params->get('password.require_number');
  534.         $obj->requireSpecialChar = (bool) $this->params->get('password.require_special_character');
  535.         return $obj;
  536.     }
  537.     /**
  538.      * Testing function to see if at least one character in the string is contained in the provided array.
  539.      *
  540.      * @param string $string         The string to test.
  541.      * @param array  $characterArray The array of characters to test against.
  542.      *
  543.      * @return bool If the string contains at least one item from the array.
  544.      */
  545.     private function stringContains(string $string, array $characterArray): bool
  546.     {
  547.         for ($i = 0; $i < strlen($string); $i++) {
  548.             if (in_array(substr($string, $i, 1), $characterArray)) {
  549.                 return true;
  550.             }
  551.         }
  552.         return false;
  553.     }
  554.     /**
  555.      * Returns array of characters used in password tests.
  556.      *
  557.      * @param string $set The array set to return.
  558.      *
  559.      * @return array
  560.      */
  561.     private function getCharacters(string $set): array
  562.     {
  563.         $out = [];
  564.         switch ($set) {
  565.             case 'uppercase':
  566.                 $out = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
  567.                 break;
  568.             case 'numbers':
  569.                 $out = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
  570.                 break;
  571.             case 'special':
  572.                 $out = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '.', ',', '-', '_', '+', '=', ':', ';'];
  573.                 break;
  574.             default:
  575.                 $out = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
  576.         }
  577.         return $out;
  578.     }
  579. }