diff --git a/config/autoload/local.php.dist b/config/autoload/local.php.dist index c35482a..b66dfb6 100644 --- a/config/autoload/local.php.dist +++ b/config/autoload/local.php.dist @@ -12,5 +12,7 @@ return [ 'koin.pass' => '', 'szep.card' => '', - 'szep.key' => '', + 'szep.birthDate' => '', + 'szep.email' => '', + 'szep.pass' => '', ]; diff --git a/data/soap-xmls/SZEP_queryCard.xml b/data/soap-xmls/SZEP_queryCard.xml deleted file mode 100644 index b30a0aa..0000000 --- a/data/soap-xmls/SZEP_queryCard.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - BANKKARTYASZAMLAEGYENLEGLEKERDEZES - - BANKKARTYASUGYFEL - %s - %s - %s - %s - - \ No newline at end of file diff --git a/data/soap-xmls/SZEP_startWorkflowSynch.xml b/data/soap-xmls/SZEP_startWorkflowSynch.xml deleted file mode 100644 index ea15ddd..0000000 --- a/data/soap-xmls/SZEP_startWorkflowSynch.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - SZEPKARTYASZAMLATORTENET - - - - diff --git a/data/soap-xmls/pollMain_ws12.xml b/data/soap-xmls/pollMain_ws12.xml new file mode 100644 index 0000000..c92bcc2 --- /dev/null +++ b/data/soap-xmls/pollMain_ws12.xml @@ -0,0 +1,8 @@ + + + + + %s + + + diff --git a/data/soap-xmls/soapMainSync_ws12.xml b/data/soap-xmls/soapMainSync_ws12.xml new file mode 100644 index 0000000..fcb43f7 --- /dev/null +++ b/data/soap-xmls/soapMainSync_ws12.xml @@ -0,0 +1,9 @@ + + + + +%s + + + + diff --git a/data/soap-xmls/soapMainWithMobilToken_ws12.xml b/data/soap-xmls/soapMainWithMobilToken_ws12.xml new file mode 100644 index 0000000..0f597be --- /dev/null +++ b/data/soap-xmls/soapMainWithMobilToken_ws12.xml @@ -0,0 +1,9 @@ + + + + + %s + + + + diff --git a/data/soap-xmls/soapPayload.xml b/data/soap-xmls/soapPayload.xml new file mode 100644 index 0000000..ee30741 --- /dev/null +++ b/data/soap-xmls/soapPayload.xml @@ -0,0 +1,5 @@ + + + %s + %s + \ No newline at end of file diff --git a/src/App/Command/PeriodicSZEPCommand.php b/src/App/Command/PeriodicSZEPCommand.php index 43d0077..8bd6180 100644 --- a/src/App/Command/PeriodicSZEPCommand.php +++ b/src/App/Command/PeriodicSZEPCommand.php @@ -8,6 +8,7 @@ use App\Service\SZEPManagerService; use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\ORMException; use Doctrine\ORM\TransactionRequiredException; +use Exception; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -36,12 +37,10 @@ class PeriodicSZEPCommand extends Command * @param InputInterface $input * @param OutputInterface $output * @return int|null|void - * @throws ORMException - * @throws OptimisticLockException - * @throws TransactionRequiredException + * @throws Exception */ protected function execute(InputInterface $input, OutputInterface $output) { - $this->szepManager->pollRecent(); + $this->szepManager->pollTransactions(); } } diff --git a/src/App/Service/SZEPManagerService.php b/src/App/Service/SZEPManagerService.php index da098ca..360d218 100644 --- a/src/App/Service/SZEPManagerService.php +++ b/src/App/Service/SZEPManagerService.php @@ -8,9 +8,6 @@ use App\Entity\SZEPCardEntry; use DateInterval; use DateTimeImmutable; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\OptimisticLockException; -use Doctrine\ORM\ORMException; -use Doctrine\ORM\TransactionRequiredException; use DOMDocument; use DOMElement; use DOMXPath; @@ -19,18 +16,23 @@ use GuzzleHttp\Client; class SZEPManagerService { - const TEMPLATE_WORKFLOW = "data/soap-xmls/SZEP_startWorkflowSynch.xml"; - const TEMPLATE_QUERY_CARD = "data/soap-xmls/SZEP_queryCard.xml"; + const TEMPLATE_SOAP_SYNC_WORKFLOW = "data/soap-xmls/soapMainSync_ws12.xml"; + const TEMPLATE_SOAP_MOBILE_WORKFLOW = "data/soap-xmls/soapMainWithMobilToken_ws12.xml"; + const TEMPLATE_SOAP_PAYLOAD = "data/soap-xmls/soapPayload.xml"; + const TEMPLATE_POLL_TRANSACTION = "data/soap-xmls/pollMain_ws12.xml"; - const CERTIFICATE_WEB_PATH = "https://www.otpbankdirekt.hu/homebank/mobilalkalmazas/certificate"; - const CERTIFICATE_CACHED_PATH = "data/cache/SZEP_cert.pem"; - - const SOAP_ENDPOINT = "https://www.otpbankdirekt.hu/mwaccesspublic1984/mwaccess"; + const SOAP_ENDPOINT = "https://www.otpbankdirekt.hu/mwaccesspublic12/mwaccess"; const POCKET_FOOD = 'Vendéglátás'; const POCKET_SPORT = 'Szabadidő'; const POCKET_LODGING = 'Szálláshely'; + const POCKET_MAP = [ + 7 => self::POCKET_FOOD, + 8 => self::POCKET_SPORT, + 9 => self::POCKET_LODGING, + ]; + const TAG_POCKET_FOOD = 'SZÉP kártya - étel'; const TAG_POCKET_SPORT = 'SZÉP kártya - szabadidő'; const TAG_POCKET_LODGING = 'SZÉP kártya - szállás'; @@ -76,99 +78,201 @@ class SZEPManagerService */ public function pollBalance(): ?array { - if (null !== ($pollResult = $this->getRecentXml())) { - return $this->parseBalance($pollResult); - } - return null; + return $this->getBalance(); } /** - * @throws ORMException - * @throws OptimisticLockException - * @throws TransactionRequiredException * @throws Exception */ - public function pollRecent() + public function pollTransactions() { - if (null !== ($pollResult = $this->getRecentXml())) { - $this->parseResult($pollResult); + $this->getTransactions(); + } + + private function buildPayload(string $action, array $parameters): string + { + $template = file_get_contents(self::TEMPLATE_SOAP_PAYLOAD); + $values = ""; + foreach ($parameters as $key => $value) { + $values .= sprintf('<%1$s>%2$s', $key, $value); } + return sprintf($template, $action, $values); + } + + /** + * Poll a transaction and return its + * @param string $transactionId + * @param string $selector + * @return DOMElement + */ + private function pollTransaction(string $transactionId, string $selector): DOMElement + { + $soapResponse = $this->sendSoapRequest(sprintf(file_get_contents(self::TEMPLATE_POLL_TRANSACTION), $transactionId)); + while ('true' != $this->parseSoapResponse($soapResponse, '//return/m:Completed', false)) { + sleep(1); + $soapResponse = $this->sendSoapRequest(sprintf(file_get_contents(self::TEMPLATE_POLL_TRANSACTION), $transactionId)); + } + $pollResultXml = $this->parseSoapResponse($soapResponse, '//return/m:Result', true); + $domDocument = new DOMDocument(); + $domDocument->loadXML($pollResultXml); + $documentXpath = new DOMXPath($domDocument); + /** @var DOMElement $returnElement */ + $returnElement = $documentXpath->query($selector)->item(0); + return $returnElement; + } + + private function fetchMobileApplicationToken(string $cardNo, string $birthDate): string + { + $action = 'SZEPAPP_REGISZTRACIO'; + $parameters = [ + 'isClientCode' => 'SZEPAPP', + 'isOperation' => 'CHECK', + 'isCardNumber' => $cardNo, + 'isBirthDate' => $birthDate, + ]; + $soapXml = sprintf( + file_get_contents(self::TEMPLATE_SOAP_SYNC_WORKFLOW), + $action, + $this->encryptPayload($this->buildPayload($action, $parameters)) + ); + + $soapResponseXml = $this->getParsedSoapResponse($soapXml, '//return/m:Result', true); + $domDocument = new DOMDocument(); + $domDocument->loadXML($soapResponseXml); + $documentXpath = new DOMXPath($domDocument); + $documentXpath->registerNamespace('m',"java:hu.iqsoft.otp.mw.access"); + /** @var DOMElement $returnElement */ + $returnElement = $documentXpath->query('//answer/mobilalkalmazasToken')->item(0); + return $returnElement->textContent; + } + + private function getMobileApplicationToken(): string + { + return $this->fetchMobileApplicationToken($this->config['szep.card'], + $this->config['szep.birthDate']); + } + + private function fetchAuthToken(string $appToken, string $email, string $pass): ?string + { + $action = 'SZEPAPP_JELSZO'; + $hashedPass = $this->hashString($pass); + $parameters = [ + 'isClientCode' => 'SZEPAPP', + 'isOperation' => 'CHECK', + 'isMobilalkalmazasToken' => $appToken, + 'isMobilalkalmazasAzonosito' => $email, + 'isEmail' => $this->hashString($email), + 'isPassword' => $hashedPass, + 'isOldPassword' => $hashedPass, + ]; + $soapXml = sprintf( + file_get_contents(self::TEMPLATE_SOAP_MOBILE_WORKFLOW), + $action, + $this->encryptPayload($this->buildPayload($action, $parameters)) + ); + $transactionId = $this->getParsedSoapResponse($soapXml, '//return', false); + + return $this->pollTransaction($transactionId, '//answer/authToken')->textContent; + } + + private function getAuthToken(string $appToken): ?string + { + return $this->fetchAuthToken($appToken, + $this->config['szep.email'], + $this->config['szep.pass']); + } + + private function getBalance(): ?array + { + $appToken = $this->getMobileApplicationToken(); + $authToken = $this->getAuthToken($appToken); + $action = 'SZEPAPP_LEKERDEZES_KARTYA'; + $parameters = [ + 'isClientCode' => 'SZEPAPP', + 'isCardType' => 'S', + 'isMobilalkalmazasToken' => $appToken, + 'isMobilalkalmazasAzonosito' => $this->config['szep.email'], + 'isAuthToken' => $authToken, + ]; + $soapXml = sprintf( + file_get_contents(self::TEMPLATE_SOAP_MOBILE_WORKFLOW), + $action, + $this->encryptPayload($this->buildPayload($action, $parameters)) + ); + $transactionId = $this->getParsedSoapResponse($soapXml, '//return', false); + $resultXml = $this->pollTransaction($transactionId, '//answer/resultset'); + $documentXpath = new DOMXPath($resultXml->ownerDocument); + $activeRecord = $documentXpath->query('//record/status[text()="A"]/parent::record', $resultXml)->item(0); + $pocketSelector = '//pocketCode[text()="%d"]/parent::balanceRec/balance'; + return [ + 'vendeglatas' => intval($documentXpath->query(sprintf($pocketSelector, 7), $activeRecord)->item(0)->textContent), + 'szabadido' => intval($documentXpath->query(sprintf($pocketSelector, 8), $activeRecord)->item(0)->textContent), + 'szallashely' => intval($documentXpath->query(sprintf($pocketSelector, 9), $activeRecord)->item(0)->textContent), + ]; } /** * @return string * @throws Exception */ - private function getRecentXml(): ?string + private function getTransactions(): ?string { - $certificate = $this->getCertificate(); - openssl_public_encrypt($this->config['szep.card'], $cryptCardId, $certificate, OPENSSL_PKCS1_PADDING); - openssl_public_encrypt($this->config['szep.key'], $cryptCardKey, $certificate, OPENSSL_PKCS1_PADDING); + $appToken = $this->getMobileApplicationToken(); + $authToken = $this->getAuthToken($appToken); $endDate = new DateTimeImmutable(); - $startDate = $endDate->sub(new DateInterval('P7D')); - - $gueryTemplate = file_get_contents(self::TEMPLATE_QUERY_CARD); - $query = sprintf($gueryTemplate, - "X" . base64_encode($cryptCardId), - base64_encode($cryptCardKey), - $startDate->format("Y.m.d"), - $endDate->format("Y.m.d") + $startDate = $endDate->sub(new DateInterval('P14D')); + $action = 'SZEPAPP_LEKERDEZES_TRANZAKCIO'; + $parameters = [ + 'isClientCode' => 'SZEPAPP', + 'isCardNumber' => $this->config['szep.card'], + 'isMobilalkalmazasToken' => $appToken, + 'isMobilalkalmazasAzonosito' => $this->config['szep.email'], + 'isAuthToken' => $authToken, + 'isTransactionCount' => 100, + 'isStartingDate' => $startDate->format("Y.m.d"), + 'isClosingDate' => $endDate->format("Y.m.d"), + 'isAmountFrom' => 0, + 'isAmountTo' => 999999, + ]; + $soapXml = sprintf( + file_get_contents(self::TEMPLATE_SOAP_MOBILE_WORKFLOW), + $action, + $this->encryptPayload($this->buildPayload($action, $parameters)) ); - $soapXml = sprintf(file_get_contents(self::TEMPLATE_WORKFLOW), $this->encryptPayload($query)); - try{ - $soapResponseXml = $this->doSoapRequest($soapXml); - } catch (Exception $e) { - return null; - } - - $domDocument = new DOMDocument(); - $domDocument->loadXML($soapResponseXml); - - $documentXpath = new DOMXPath($domDocument); - /** @var DOMElement $returnElement */ - $returnElement = $documentXpath->query('//return/result')->item(0); - return base64_decode($returnElement->textContent); - } - - private function parseBalance(string $resultXml): array - { - $domDocument = new DOMDocument(); - $domDocument->loadXML($resultXml); - - $documentXpath = new DOMXPath($domDocument); - return [ - 'vendeglatas' => intval($documentXpath->query('/answer/szamla_osszeg7')->item(0)->textContent), - 'szabadido' => intval($documentXpath->query('/answer/szamla_osszeg8')->item(0)->textContent), - 'szallashely' => intval($documentXpath->query('/answer/szamla_osszeg9')->item(0)->textContent), - ]; - } - - /** - * Parse the decoded payload - * @param string $resultXml - * @throws ORMException - * @throws OptimisticLockException - * @throws TransactionRequiredException - * @throws Exception - */ - private function parseResult(string $resultXml) - { - $domDocument = new DOMDocument(); - $domDocument->loadXML($resultXml); - - $documentXpath = new DOMXPath($domDocument); + $transactionId = $this->getParsedSoapResponse($soapXml, '//return', false); + $resultSetElement = $this->pollTransaction($transactionId, '//answer/resultset'); + $documentXpath = new DOMXPath($resultSetElement->ownerDocument); /** @var DOMElement[] $recordElements */ - $recordElements = $documentXpath->query('/answer/resultset/record'); + $recordElements = $documentXpath->query('//record', $resultSetElement); $newRecords = []; - foreach ($recordElements as $element) { - $date = trim($documentXpath->query('./datum', $element)->item(0)->textContent); - $amount = trim($documentXpath->query('./osszeg', $element)->item(0)->textContent); - $merchant = trim($documentXpath->query('./ellenoldali_nev', $element)->item(0)->textContent); - $pocket = trim($documentXpath->query('./alszamla', $element)->item(0)->textContent); + $transactionWay = trim($documentXpath + ->query('./transactionWay', $element) + ->item(0) + ->textContent); + $directionMultiplier = $transactionWay == 'J' ? 1 : -1; + $date = trim($documentXpath + ->query('./date', $element) + ->item(0) + ->textContent); + /** + * TODO: some dumbass called this parameter "ammount" + */ + $amount = $directionMultiplier * intval($documentXpath + ->query('./ammount', $element) + ->item(0) + ->textContent); + $merchant = trim($documentXpath + ->query('./beneficiaryName', $element) + ->item(0) + ->textContent); + $pocket = self::POCKET_MAP[intval($documentXpath + ->query('./pocketCode', $element) + ->item(0) + ->textContent)]; $hash = md5("$date#$amount#$merchant#$pocket"); if (null != $this->em->find(SZEPCardEntry::class, $hash)) { @@ -215,9 +319,15 @@ class SZEPManagerService { $tags = []; - if (false !== strpos($SZEPCardEntry->getMerchant(), "Sigma Technology")) { + if (false !== strpos($SZEPCardEntry->getMerchant(), "SIGMA TECHNOLOGY MAGYARORSZÁG INFOR")) { $tags[] = 'Sigma'; $tags[] = 'Cafeteria'; + } elseif (false !== strpos($SZEPCardEntry->getMerchant(), "Hong He Kinai Büfé")) { + $tags[] = 'Junk food'; + $tags[] = 'Kínai'; + } elseif (false !== strpos($SZEPCardEntry->getMerchant(), "VIN.VIN étterem")) { + $tags[] = 'Junk food'; + $tags[] = 'Vietnámi'; } elseif (false !== strpos($SZEPCardEntry->getMerchant(), "Ericsson Ház Étterem")) { $tags[] = 'Ericsson Ház'; $tags[] = 'Menza'; @@ -253,6 +363,16 @@ class SZEPManagerService return $tags; } + /** + * Return sha512 hash of the md5sum of the input string + * @param string $input + * @return string + */ + private function hashString(string $input): string + { + return strtoupper(openssl_digest(strtoupper(md5($input)), 'sha512')); + } + /** * Returns AES encrypted base64 encoded $payload * @param string $payload @@ -271,7 +391,7 @@ class SZEPManagerService * @param string $soapXml * @return string */ - private function doSoapRequest(string $soapXml): string + private function sendSoapRequest(string $soapXml): string { $response = $this->httpClient->post(self::SOAP_ENDPOINT, [ 'body' => $soapXml, @@ -279,18 +399,20 @@ class SZEPManagerService return $response->getBody()->getContents(); } - /** - * Returns the cached public certificate - * @return string - */ - private function getCertificate(): string + private function parseSoapResponse(string $soapResponseXml, string $selector, bool $decode): string { - if (file_exists(self::CERTIFICATE_CACHED_PATH)) { - return file_get_contents(self::CERTIFICATE_CACHED_PATH); - } + $domDocument = new DOMDocument(); + $domDocument->loadXML($soapResponseXml); + $documentXpath = new DOMXPath($domDocument); + $documentXpath->registerNamespace('m',"java:hu.iqsoft.otp.mw.access"); + /** @var DOMElement $returnElement */ + $returnElement = $documentXpath->query($selector)->item(0); + return $decode ? base64_decode($returnElement->textContent) : $returnElement->textContent; + } - $cert = file_get_contents(self::CERTIFICATE_WEB_PATH); - file_put_contents(self::CERTIFICATE_CACHED_PATH, $cert); - return $cert; + private function getParsedSoapResponse(string $soapXml, string $selector, bool $decode): string + { + $soapResponseXml = $this->sendSoapRequest($soapXml); + return $this->parseSoapResponse($soapResponseXml, $selector, $decode); } }