Completed
Push — master ( 1f59c8...e3081b )
by Nikola
04:10 queued 14s
created

getFormCsrfToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3.243

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 20
ccs 7
cts 10
cp 0.7
rs 9.4286
cc 3
eloc 9
nc 3
nop 0
crap 3.243
1
<?php
2
/*
3
 * This file is part of the Exchange Rate package, an RunOpenCode project.
4
 *
5
 * (c) 2016 RunOpenCode
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace RunOpenCode\ExchangeRate\Source;
11
12
use GuzzleHttp\Client as GuzzleClient;
13
use GuzzleHttp\Client;
14
use GuzzleHttp\Cookie\CookieJar;
15
use RunOpenCode\ExchangeRate\Contract\RateInterface;
16
use RunOpenCode\ExchangeRate\Contract\SourceInterface;
17
use RunOpenCode\ExchangeRate\Exception\ConfigurationException;
18
use RunOpenCode\ExchangeRate\Exception\SourceNotAvailableException;
19
use RunOpenCode\ExchangeRate\Exception\UnknownRateTypeException;
20
use RunOpenCode\ExchangeRate\Log\LoggerAwareTrait;
21
use RunOpenCode\ExchangeRate\Source\Api\NationalBankOfSerbiaXmlSaxParser;
22
use RunOpenCode\ExchangeRate\Utils\CurrencyCodeUtil;
23
use Symfony\Component\DomCrawler\Crawler;
24
25
/**
26
 * Class NationalBankOfSerbiaDomCrawlerSource
27
 *
28
 * This crawler crawls from National bank of Serbia public form for rates.
29
 *
30
 * @package RunOpenCode\ExchangeRate\Source
31
 */
32
class NationalBankOfSerbiaDomCrawlerSource implements SourceInterface
33
{
34
    /**
35
     * API - Source of rates from National bank of Serbia.
36
     */
37
    const SOURCE = 'http://www.nbs.rs/kursnaListaModul/naZeljeniDan.faces';
38
39
    /**
40
     * API - Name of source.
41
     */
42
    const NAME = 'national_bank_of_serbia';
43
44
    use LoggerAwareTrait;
45
46
    /**
47
     * List of supported exchange rate types and currencies.
48
     *
49
     * @var array
50
     */
51
    private static $supports = array(
52
        'default' => array('EUR', 'AUD', 'CAD', 'CNY', 'HRK', 'CZK', 'DKK', 'HUF', 'JPY', 'KWD', 'NOK', 'RUB', 'SEK', 'CHF',
53
                           'GBP', 'USD', 'BAM', 'PLN', 'ATS', 'BEF', 'FIM', 'FRF', 'DEM', 'GRD', 'IEP', 'ITL', 'LUF', 'PTE',
54
                           'ESP'),
55
        'foreign_cache_buying' => array('EUR', 'CHF', 'USD'),
56
        'foreign_cache_selling' => array('EUR', 'CHF', 'USD'),
57
        'foreign_exchange_buying' => array('EUR', 'AUD', 'CAD', 'CNY', 'DKK', 'JPY', 'NOK', 'RUB', 'SEK', 'CHF', 'GBP', 'USD'),
58
        'foreign_exchange_selling' => array('EUR', 'AUD', 'CAD', 'CNY', 'DKK', 'JPY', 'NOK', 'RUB', 'SEK', 'CHF', 'GBP', 'USD')
59
    );
60
61
    /**
62
     * @var array
63
     */
64
    protected $cache;
65
66
    /**
67
     * @var Client
68
     */
69
    protected $guzzleClient;
70
71
    /**
72
     * @var CookieJar
73
     */
74
    protected $guzzleCookieJar;
75
76 6
    public function __construct()
77
    {
78 6
        $this->cache = array();
79 6
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84 2
    public function getName()
85
    {
86 2
        return self::NAME;
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92 6
    public function fetch($currencyCode, $rateType = 'default', \DateTime $date = null)
93
    {
94 6
        $currencyCode = CurrencyCodeUtil::clean($currencyCode);
95
96 4
        $this->validate($currencyCode, $rateType);
97
98 4
        if ($date === null) {
99 2
            $date = new \DateTime('now');
100 2
        }
101
102 4
        if (!array_key_exists($rateType, $this->cache)) {
103
104
            try {
105
106 4
                $this->load($date, $rateType);
107
108 4
            } catch (\Exception $e) {
109 2
                $message = sprintf('Unable to load data from "%s" for "%s" of rate type "%s".', $this->getName(), $currencyCode, $rateType);
110
111 2
                $this->getLogger()->emergency($message);;
112 2
                throw new SourceNotAvailableException($message, 0, $e);
113
            }
114 2
        }
115
116 2
        if (array_key_exists($currencyCode, $this->cache[$rateType])) {
117 2
            return $this->cache[$rateType][$currencyCode];
118
        }
119
120
        $message = sprintf('API Changed: source "%s" does not provide currency code "%s" for rate type "%s".', $this->getName(), $currencyCode, $rateType);
121
        $this->getLogger()->critical($message);
122
        throw new \RuntimeException($message);
123
    }
124
125
    /**
126
     * Check if currency code and rate type are supported by this source.
127
     *
128
     * @param string $currencyCode Currency code.
129
     * @param string $rateType Rate type.
130
     * @throws ConfigurationException If currency code is not supported by source and rate type.
131
     * @throws UnknownRateTypeException If rate type is unknown.
132
     */
133 4
    protected function validate($currencyCode, $rateType)
134
    {
135 4
        if (!array_key_exists($rateType, self::$supports)) {
136
            throw new UnknownRateTypeException(sprintf('Unknown rate type "%s" for source "%s".', $rateType, $this->getName()));
137
        }
138
139 4
        if (!in_array($currencyCode, self::$supports[$rateType], true)) {
140
            throw new ConfigurationException(sprintf('Unsupported currency code "%s" for source "%s" and rate type "%s".', $currencyCode, $this->getName(), $rateType));
141
        }
142 4
    }
143
144
    /**
145
     * @param \DateTime $date
146
     * @return RateInterface[]
147
     * @throws SourceNotAvailableException
148
     */
149 4
    protected function load(\DateTime $date, $rateType)
150
    {
151 4
        $xml = $this->executeHttpRequest(self::SOURCE, 'POST', array(), array(
152 4
            'index:brKursneListe:' => '',
153 4
            'index:year' => $date->format('Y'),
154 4
            'index:inputCalendar1' => $date->format('d/m/Y'),
155 4
            'index:vrsta' => call_user_func(function($rateType) {
156
                switch ($rateType) {
157 4
                    case 'foreign_cache_buying':        // FALL TROUGH
158 4
                    case 'foreign_cache_selling':
159
                        return 1;
160
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
161 4
                    case 'foreign_exchange_buying':     // FALL TROUGH
162 4
                    case 'foreign_exchange_selling':
163
                        return 2;
164
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
165 4
                    default:
166 4
                        return 3;
167
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
168 4
                }
169 4
            }, $rateType),
170 4
            'index:prikaz' => 3, // XML
171 4
            'index:buttonShow' => 'Show',
172 4
            'index' => 'index',
173 4
            'com.sun.faces.VIEW' => $this->getFormCsrfToken()
174 2
        ));
175
176 2
        $rates = NationalBankOfSerbiaXmlSaxParser::parseXml($xml);
177
178
        /**
179
         * @var RateInterface $rate
180
         */
181 2
        foreach ($rates as $rate) {
182
183 2
            if (!array_key_exists($rate->getRateType(), $this->cache)) {
184 2
                $this->cache[$rate->getRateType()] = array();
185 2
            }
186
187 2
            $this->cache[$rate->getRateType()][$rate->getCurrencyCode()] = $rate;
188 2
        }
189 2
    }
190
191
    /**
192
     * Get NBS's form CSRF token.
193
     *
194
     * @return string CSRF token.
195
     */
196 4
    protected function getFormCsrfToken()
197
    {
198 4
        $crawler = new Crawler($this->executeHttpRequest(self::SOURCE, 'GET'));
199
200 2
        $hiddens = $crawler->filter('input[type="hidden"]');
201
202
        /**
203
         * @var \DOMElement $hidden
204
         */
205 2
        foreach ($hiddens as $hidden) {
206
207 2
            if ($hidden->getAttribute('id') === 'com.sun.faces.VIEW') {
208 2
                return $hidden->getAttribute('value');
209
            }
210 2
        }
211
212
        $message = 'FATAL ERROR: National Bank of Serbia changed it\'s API, unable to extract token.';
213
        $this->getLogger()->emergency($message);
214
        throw new \RuntimeException($message);
215
    }
216
217
    /**
218
     * Execute HTTP request and get raw body response.
219
     *
220
     * @param string $url URL to fetch.
221
     * @param string $method HTTP Method.
222
     * @param array $params Params to send with request.
223
     * @return string
224
     */
225 4
    protected function executeHttpRequest($url, $method, array $query = array(), array $params = array())
226
    {
227 4
        $client = $this->getGuzzleClient();
228
229 4
        $response = $client->request($method, $url, array(
230 4
            'cookies' => $this->getGuzzleCookieJar(),
231 4
            'form_params' => $params,
232
            'query' => $query
233 4
        ));
234
235 2
        return $response->getBody()->getContents();
236
    }
237
238
    /**
239
     * Get Guzzle Client.
240
     *
241
     * @return Client
242
     */
243 4
    protected function getGuzzleClient()
244
    {
245 4
        if ($this->guzzleClient === null) {
246 4
            $this->guzzleClient = new Client(array('cookies' => true));
247 4
        }
248
249 4
        return $this->guzzleClient;
250
    }
251
252
    /**
253
     * Get Guzzle CookieJar.
254
     *
255
     * @return CookieJar
256
     */
257 4
    protected function getGuzzleCookieJar()
258
    {
259 4
        if ($this->guzzleCookieJar === null) {
260 4
            $this->guzzleCookieJar = new CookieJar();
261 4
        }
262
263 4
        return $this->guzzleCookieJar;
264
    }
265
}
266