src/Custom/ABFController.php line 132

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 $loggerRequestStack $requestStackParameterBagInterface $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'$key0$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 (=== 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'$key0$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 $date1string $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(0000);
  288.         $date->setTime(0000);
  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 $accountstring $subaccountstring $clientTokenstring $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 $subaccountsstring $clientTokenstring $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 $emailstring $adminTokenstring $clientIp '0.0.0.0'string $phone nullbool $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$i1), $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 = [0123456789];
  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. }