Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like SafeBrowsingClient often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use SafeBrowsingClient, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
31 | class SafeBrowsingClient extends AbstractServiceClient |
||
32 | { |
||
33 | /** |
||
34 | * @var string |
||
35 | */ |
||
36 | protected $serviceDomain = 'sba.yandex.net'; |
||
37 | |||
38 | /** |
||
39 | * @var |
||
40 | */ |
||
41 | protected $apiKey; |
||
42 | |||
43 | /** |
||
44 | * @var string |
||
45 | */ |
||
46 | protected $appVer = '2.3'; |
||
47 | |||
48 | /** |
||
49 | * @var string |
||
50 | */ |
||
51 | protected $pVer = '2.3'; |
||
52 | |||
53 | /** |
||
54 | * @var array |
||
55 | */ |
||
56 | protected $malwareShavars = [ |
||
57 | 'ydx-malware-shavar', |
||
58 | 'ydx-phish-shavar', |
||
59 | 'goog-malware-shavar', |
||
60 | 'goog-phish-shavar' |
||
61 | ]; |
||
62 | |||
63 | /** |
||
64 | * @param string $apiKey |
||
65 | */ |
||
66 | 30 | public function __construct($apiKey = '') |
|
67 | { |
||
68 | 30 | $this->setApiKey($apiKey); |
|
69 | 30 | } |
|
70 | |||
71 | /** |
||
72 | * @param string $apiKey |
||
73 | */ |
||
74 | 30 | public function setApiKey($apiKey) |
|
75 | { |
||
76 | 30 | $this->apiKey = $apiKey; |
|
77 | 30 | } |
|
78 | |||
79 | /** |
||
80 | * @return string |
||
81 | */ |
||
82 | 3 | public function getApiKey() |
|
83 | { |
||
84 | 3 | return $this->apiKey; |
|
85 | } |
||
86 | |||
87 | /** |
||
88 | * @param array $malwareShavars |
||
89 | */ |
||
90 | 2 | public function setMalwareShavars($malwareShavars) |
|
91 | { |
||
92 | 2 | $this->malwareShavars = $malwareShavars; |
|
93 | 2 | } |
|
94 | |||
95 | /** |
||
96 | * @return array |
||
97 | */ |
||
98 | 1 | public function getMalwareShavars() |
|
99 | { |
||
100 | 1 | return $this->malwareShavars; |
|
101 | } |
||
102 | |||
103 | /** |
||
104 | * Get url to service resource with parameters |
||
105 | * |
||
106 | * @param string $resource |
||
107 | * @return string |
||
108 | */ |
||
109 | 14 | public function getServiceUrl($resource = '') |
|
110 | { |
||
111 | 14 | return $this->serviceScheme . '://' . $this->serviceDomain . '/' |
|
112 | 14 | . $resource . '?client=api&apikey=' . $this->apiKey . '&appver=' . $this->appVer . '&pver=' . $this->pVer; |
|
113 | } |
||
114 | |||
115 | /** |
||
116 | * Get url to service Lookup resource with parameters |
||
117 | * |
||
118 | * @param string $url |
||
119 | * @return string |
||
120 | */ |
||
121 | 2 | public function getLookupUrl($url = '') |
|
122 | { |
||
123 | 2 | $pVer = '3.5'; //Specific version |
|
124 | 2 | return $this->serviceScheme . '://' . $this->serviceDomain . '/' |
|
125 | 2 | . 'lookup?client=api&apikey=' . $this->apiKey . '&pver=' . $pVer . '&url=' . $url; |
|
126 | } |
||
127 | |||
128 | /** |
||
129 | * Get url to service Check Adult resource with parameters |
||
130 | * |
||
131 | * @param string $url |
||
132 | * @return string |
||
133 | */ |
||
134 | 6 | public function getCheckAdultUrl($url = '') |
|
135 | { |
||
136 | 6 | $pVer = '4.0'; //Specific version |
|
137 | 6 | return $this->serviceScheme . '://' . $this->serviceDomain . '/' |
|
138 | 6 | . 'cp?client=api&pver=' . $pVer . '&url=' . $url; |
|
139 | } |
||
140 | |||
141 | /** |
||
142 | * Sends a request |
||
143 | * |
||
144 | * @param string $method HTTP method |
||
145 | * @param string|UriInterface $uri URI object or string. |
||
146 | * @param array $options Request options to apply. |
||
147 | * |
||
148 | * @return Response |
||
149 | * |
||
150 | * @throws ForbiddenException |
||
151 | * @throws UnauthorizedException |
||
152 | * @throws SafeBrowsingException |
||
153 | * @throws NotFoundException |
||
154 | */ |
||
155 | 23 | protected function sendRequest($method, $uri, array $options = []) |
|
156 | { |
||
157 | try { |
||
158 | 23 | $response = $this->getClient()->request($method, $uri, $options); |
|
|
|||
159 | 23 | } catch (ClientException $ex) { |
|
160 | 4 | $result = $ex->getResponse(); |
|
161 | 4 | $code = $result->getStatusCode(); |
|
162 | 4 | $message = $result->getReasonPhrase(); |
|
163 | |||
164 | 4 | if ($code === 403) { |
|
165 | 1 | throw new ForbiddenException($message); |
|
166 | } |
||
167 | |||
168 | 3 | if ($code === 401) { |
|
169 | 1 | throw new UnauthorizedException($message); |
|
170 | } |
||
171 | |||
172 | 2 | if ($code === 404) { |
|
173 | 1 | throw new NotFoundException($message); |
|
174 | } |
||
175 | |||
176 | 1 | throw new SafeBrowsingException( |
|
177 | 1 | 'Service responded with error code: "' . $code . '" and message: "' . $message . '"', |
|
178 | $code |
||
179 | 1 | ); |
|
180 | } |
||
181 | |||
182 | 19 | return $response; |
|
183 | } |
||
184 | |||
185 | /** |
||
186 | * @param string $bodyString |
||
187 | * @see https://developers.google.com/safe-browsing/developers_guide_v2#HTTPRequestForHashes |
||
188 | * @return array |
||
189 | */ |
||
190 | 3 | View Code Duplication | public function checkHash($bodyString = '') |
191 | { |
||
192 | 3 | $resource = 'gethash'; |
|
193 | |||
194 | 3 | $response = $this->sendRequest( |
|
195 | 3 | 'POST', |
|
196 | 3 | $this->getServiceUrl($resource), |
|
197 | [ |
||
198 | 'body' => $bodyString |
||
199 | 3 | ] |
|
200 | 3 | ); |
|
201 | |||
202 | return [ |
||
203 | 3 | 'code' => $response->getStatusCode(), |
|
204 | 3 | 'data' => $response->getBody() |
|
205 | 3 | ]; |
|
206 | } |
||
207 | |||
208 | /** |
||
209 | * @param string $bodyString |
||
210 | * @see https://developers.google.com/safe-browsing/developers_guide_v2#HTTPRequestForData |
||
211 | * @return array |
||
212 | */ |
||
213 | 10 | View Code Duplication | public function getChunks($bodyString = '') |
214 | { |
||
215 | 10 | $resource = 'downloads'; |
|
216 | |||
217 | 10 | $response = $this->sendRequest( |
|
218 | 10 | 'POST', |
|
219 | 10 | $this->getServiceUrl($resource), |
|
220 | [ |
||
221 | 'body' => $bodyString |
||
222 | 10 | ] |
|
223 | 10 | ); |
|
224 | |||
225 | return [ |
||
226 | 10 | 'code' => $response->getStatusCode(), |
|
227 | 10 | 'data' => $response->getBody() |
|
228 | 10 | ]; |
|
229 | } |
||
230 | |||
231 | /** |
||
232 | * @see https://developers.google.com/safe-browsing/developers_guide_v2#HTTPRequestForList |
||
233 | * @return array |
||
234 | */ |
||
235 | 1 | public function getShavarsList() |
|
241 | |||
242 | /** |
||
243 | * @param string $url |
||
244 | * @return string|false |
||
245 | */ |
||
246 | 2 | public function lookup($url) |
|
247 | { |
||
248 | 2 | $response = $this->sendRequest('GET', $this->getLookupUrl($url)); |
|
249 | 2 | if ($response->getStatusCode() === 200) { |
|
250 | 1 | return $response->getBody()->getContents(); |
|
251 | } |
||
252 | 1 | return false; |
|
253 | } |
||
254 | |||
255 | /** |
||
256 | * @param string $url |
||
257 | * @return bool |
||
258 | */ |
||
259 | 6 | public function checkAdult($url) |
|
267 | |||
268 | /** |
||
269 | * @param string $url |
||
270 | * @return string |
||
271 | */ |
||
272 | 1 | public function getChunkByUrl($url) |
|
273 | { |
||
274 | 1 | $client = $this->getClient(); |
|
275 | |||
276 | 1 | $host = parse_url($url, PHP_URL_HOST); |
|
277 | 1 | $headers = $client->getConfig('headers'); |
|
278 | 1 | if ($host) { |
|
279 | 1 | $headers['Host'] = $host; |
|
280 | 1 | } |
|
281 | |||
282 | 1 | $response = $this->sendRequest( |
|
283 | 1 | 'GET', |
|
284 | 1 | $url, |
|
285 | [ |
||
286 | 'headers' => $headers |
||
287 | 1 | ] |
|
288 | 1 | ); |
|
289 | 1 | return $response->getBody()->getContents(); |
|
290 | } |
||
291 | |||
292 | /** |
||
293 | * @param string $url |
||
294 | * @return bool|array |
||
295 | * @throws \Exception |
||
296 | */ |
||
297 | 3 | public function searchUrl($url) |
|
298 | { |
||
299 | 3 | $hashes = $this->getHashesByUrl($url); |
|
300 | |||
301 | 3 | foreach ($hashes as $hash) { |
|
302 | 3 | $prefixPack = pack("H*", $hash['prefix']); |
|
303 | 3 | $prefixSize = strlen($hash['prefix']) / 2; |
|
304 | 3 | $length = count($prefixPack) * $prefixSize; |
|
305 | 3 | $bodyString = "$prefixSize:$length\n" . $prefixPack; |
|
306 | 3 | $result = $this->checkHash($bodyString); |
|
307 | |||
308 | 3 | if ($result['code'] == 200 && !empty($result['data'])) { |
|
309 | 1 | $malwareShavars = $this->getFullHashes($result['data']); |
|
310 | 1 | foreach ($malwareShavars as $fullHashes) { |
|
311 | 1 | foreach ($fullHashes as $fullHash) { |
|
312 | 1 | if ($fullHash === $hash['full']) { |
|
313 | 1 | return $hash; |
|
314 | } |
||
315 | 1 | } |
|
316 | 1 | } |
|
317 | 3 | } elseif ($result['code'] == 204 && strlen($result['data']) == 0) { |
|
318 | //204 Means no match |
||
319 | 1 | } else { |
|
320 | 1 | throw new SafeBrowsingException( |
|
321 | 1 | "ERROR: Invalid response returned from Safe Browsing ({$result['code']})" |
|
322 | 1 | ); |
|
323 | } |
||
324 | 2 | } |
|
325 | 1 | return false; |
|
326 | } |
||
327 | |||
328 | /** |
||
329 | * @param string $responseData |
||
330 | * @return array |
||
331 | */ |
||
332 | 1 | public function getFullHashes($responseData) |
|
333 | { |
||
334 | 1 | $hashesData = []; |
|
335 | 1 | while (strlen($responseData) > 0) { |
|
336 | 1 | $splithead = explode("\n", $responseData, 2); |
|
337 | |||
338 | 1 | list($listname, $malwareId, $length) = explode(':', $splithead[0]); |
|
339 | 1 | $data = bin2hex(substr($splithead[1], 0, $length)); |
|
340 | 1 | while (strlen($data) > 0) { |
|
341 | 1 | $hashesData[$listname][$malwareId] = substr($data, 0, 64); |
|
342 | 1 | $data = substr($data, 64); |
|
343 | 1 | } |
|
344 | 1 | $responseData = substr($splithead[1], $length); |
|
345 | 1 | } |
|
346 | 1 | return $hashesData; |
|
347 | } |
||
348 | |||
349 | /** |
||
350 | * @param string $url |
||
351 | * @return array |
||
352 | */ |
||
353 | 5 | public function getHashesByUrl($url) |
|
388 | |||
389 | /** |
||
390 | * @param array $hosts |
||
391 | * @return array |
||
392 | */ |
||
393 | 5 | private function getHashesByHosts($hosts) |
|
394 | { |
||
395 | 5 | $hashes = []; |
|
396 | 5 | foreach ($hosts as $host) { |
|
397 | 5 | $hashes[] = $this->getHashByHost($host); |
|
398 | 5 | } |
|
399 | 5 | return $hashes; |
|
400 | } |
||
401 | |||
402 | /** |
||
403 | * @param string $host |
||
404 | * @return array |
||
405 | */ |
||
406 | 5 | private function getHashByHost($host) |
|
412 | |||
413 | /** |
||
414 | * @param array $savedChunks |
||
415 | * @return string |
||
416 | * @throws SafeBrowsingException |
||
417 | */ |
||
418 | 11 | private function prepareDownloadsRequest($savedChunks = []) |
|
419 | { |
||
420 | 11 | $body = ''; |
|
421 | 11 | if (count($this->malwareShavars) < 1) { |
|
422 | 1 | throw new SafeBrowsingException( |
|
423 | 'ERROR: Empty malware shavars' |
||
424 | 1 | ); |
|
425 | } |
||
426 | |||
427 | 10 | foreach ($this->malwareShavars as $malwareShavar) { |
|
428 | 10 | if ($savedChunks && isset($savedChunks[$malwareShavar])) { |
|
429 | //ydx-malware-shavar;s:18888-19061:a:21355-21687 |
||
430 | |||
431 | 1 | $range = ''; |
|
432 | 1 | if (isset($savedChunks[$malwareShavar]['removed']) |
|
433 | 1 | && isset($savedChunks[$malwareShavar]['removed']['min']) |
|
434 | 1 | && isset($savedChunks[$malwareShavar]['removed']['max']) |
|
435 | 1 | && $savedChunks[$malwareShavar]['removed']['min'] > 0 |
|
436 | 1 | && $savedChunks[$malwareShavar]['removed']['max'] > 0 |
|
437 | 1 | ) { |
|
438 | 1 | $range .= 's:' . $savedChunks[$malwareShavar]['removed']['min'] |
|
439 | 1 | . '-' . $savedChunks[$malwareShavar]['removed']['max']; |
|
440 | 1 | } |
|
441 | |||
442 | 1 | if (isset($savedChunks[$malwareShavar]['added']) |
|
443 | 1 | && isset($savedChunks[$malwareShavar]['added']['min']) |
|
444 | 1 | && isset($savedChunks[$malwareShavar]['added']['max']) |
|
445 | 1 | && $savedChunks[$malwareShavar]['added']['min'] > 0 |
|
446 | 1 | && $savedChunks[$malwareShavar]['added']['max'] > 0 |
|
447 | 1 | ) { |
|
448 | 1 | if ($range) { |
|
449 | 1 | $range .= ':'; |
|
450 | 1 | } |
|
451 | 1 | $range .= 'a:' . $savedChunks[$malwareShavar]['added']['min'] |
|
452 | 1 | . '-' . $savedChunks[$malwareShavar]['added']['max']; |
|
453 | |||
454 | 1 | $body .= $malwareShavar . ';' . $range . "\n"; |
|
455 | 1 | } |
|
456 | 1 | } else { |
|
457 | 10 | $body .= $malwareShavar . ";\n"; |
|
458 | } |
||
459 | 10 | } |
|
460 | 10 | return $body; |
|
461 | } |
||
462 | |||
463 | /** |
||
464 | * Get malwares prefixes data |
||
465 | * |
||
466 | * @param array $savedChunks |
||
467 | * @return array |
||
468 | * @throws SafeBrowsingException |
||
469 | */ |
||
470 | 11 | public function getMalwaresData($savedChunks = []) |
|
559 | |||
560 | /** |
||
561 | * Parsing chunk |
||
562 | * |
||
563 | * @param string $data |
||
564 | * @return array |
||
565 | * @throws SafeBrowsingException |
||
566 | */ |
||
567 | 6 | private function parseChunk($data) |
|
631 | } |
||
632 |
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.