Passed
Push — master ( fa238f...bca209 )
by Kris
02:21
created

ApiManager::cleanMessage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 16
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
/**
4
 *     _    _                    ___ ____  ____  ____
5
 *    / \  | |__  _   _ ___  ___|_ _|  _ \|  _ \| __ )
6
 *   / _ \ | '_ \| | | / __|/ _ \| || |_) | | | |  _ \
7
 *  / ___ \| |_) | |_| \__ \  __/| ||  __/| |_| | |_) |
8
 * /_/   \_\_.__/ \__,_|___/\___|___|_|   |____/|____/
9
 *
10
 * This file is part of Kristuff\AbsuseIPDB.
11
 *
12
 * (c) Kristuff <[email protected]>
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 *
17
 * @version    0.2.0
18
 * @copyright  2020 Kristuff
19
 */
20
21
namespace Kristuff\AbuseIPDB;
22
23
/**
24
 * Class ApiManager
25
 * 
26
 * The main class to work with the AbuseIPDB API v2 
27
 */
28
class ApiManager extends ApiDefintion
29
{
30
    /**
31
     * AbuseIPDB API key
32
     *  
33
     * @access protected
34
     * @var string $aipdbApiKey  
35
     */
36
    protected $aipdbApiKey = null; 
37
38
    /**
39
     * AbuseIPDB user id 
40
     * 
41
     * @access protected
42
     * @var string $aipdbUserId  
43
     */
44
    protected $aipdbUserId = null; 
45
46
    /**
47
     * The ips to remove from message
48
     * Generally you will add to this list yours ipv4 and ipv6, and the hostname
49
     * 
50
     * @access protected
51
     * @var array $selfIps  
52
     */
53
    protected $selfIps = []; 
54
55
    /**
56
     * Constructor
57
     * 
58
     * @access public
59
     * @param string  $apiKey     The AbuseIPDB api key
60
     * @param string  $userId     The AbuseIPDB user's id
61
     * @param array   $myIps      The Ips/domain name you dont want to display in report messages
62
     * 
63
     */
64
    public function __construct(string $apiKey, string $userId, array $myIps = [])
65
    {
66
        $this->aipdbApiKey = $apiKey;
67
        $this->aipdbUserId = $userId;
68
        $this->selfIps = $myIps;
69
    }
70
71
    /**
72
     * Get the current configuration in a indexed array
73
     * 
74
     * @access public 
75
     * @return array
76
     */
77
    public function getConfig()
78
    {
79
        return array(
80
            'userId'  => $this->aipdbUserId,
81
            'apiKey'  => $this->aipdbApiKey,
82
            'selfIps' => $this->selfIps,
83
            
84
            // TODO  default report cat 
85
        );
86
    }
87
88
    /**
89
     * Get a new instance of ApiManager with config stored in a Json file
90
     * 
91
     * @access public 
92
     * @static
93
     * @param string    $configPath     The configuration file path
94
     * 
95
     * @return \Kristuff\AbuseIPDB\ApiManager
96
     * @throws \InvalidArgumentException                        If the given file does not exist
97
     * @throws \Kristuff\AbuseIPDB\InvalidPermissionException   If the given file is not readable 
98
     */
99
    public static function fromConfigFile(string $configPath)
100
    {
101
102
        // check file exists
103
        if (!file_exists($configPath) || !is_file($configPath)){
104
            throw new \InvalidArgumentException('The file [' . $configPath . '] does not exist.');
105
        }
106
107
        // check file is readable
108
        if (!is_readable($configPath)){
109
            throw new InvalidPermissionException('The file [' . $configPath . '] is not readable.');
110
        }
111
112
        // todo check file exist
113
        $keyConfig = self::loadJsonFile($configPath);
114
        $selfIps = [];
115
        
116
        // Look for other optional config files in the same directory 
117
        $selfIpsConfigPath = pathinfo($configPath, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR . 'self_ips.json';
118
        if (file_exists($selfIpsConfigPath)){
119
            $selfIps = self::loadJsonFile($selfIpsConfigPath)->self_ips;
120
        }
121
122
        $app = new ApiManager($keyConfig->api_key, $keyConfig->user_id, $selfIps);
123
        
124
        return $app;
125
    }
126
127
    /**
128
     * Get the list of report categories
129
     * 
130
     * @access public 
131
     * @return array
132
     */
133
    public function getCategories()
134
    {
135
        return $this->aipdbApiCategories;
136
    }
137
138
    /**
139
     * Performs a 'report' api request
140
     * 
141
     * Result, in json format will be something like this:
142
     *  {
143
     *       "data": {
144
     *         "ipAddress": "127.0.0.1",
145
     *         "abuseConfidenceScore": 52
146
     *       }
147
     *  }
148
     * 
149
     * @access public
150
     * @param string    $ip             The ip to report
151
     * @param string    $categories     The report categories
152
     * @param string    $message        The report message
153
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
154
     *
155
     * @return object|array
156
     * @throws \InvalidArgumentException
157
     */
158
    public function report(string $ip = '', string $categories = '', string $message = '', bool $returnArray = false)
159
    {
160
         // ip must be set
161
        if (empty($ip)){
162
            throw new \InvalidArgumentException('Ip was empty');
163
        }
164
165
        // categories must be set
166
        if (empty($categories)){
167
            throw new \InvalidArgumentException('categories list was empty');
168
        }
169
170
        // message must be set
171
          if (empty($message)){
172
            throw new \InvalidArgumentException('report message was empty');
173
        }
174
175
        // validates categories, clean message 
176
        $cats = $this->validateCategories($categories);
177
        $msg = $this->cleanMessage($message);
178
179
        // report AbuseIPDB request
180
        return $this->apiRequest('report', [
181
            'ip' => $ip,
182
            'categories' => $cats,
183
            'comment' => $msg
184
            ],
185
            'POST', $returnArray
186
        );
187
    }
188
189
    /**
190
     * Check if the category(ies) given is/are valid
191
     * Check for shortname or id, and categories that can't be used alone 
192
     * 
193
     * @access protected
194
     * @param array $categories       The report categories list
195
     *
196
     * @return string               Formatted string id list ('18,2,3...')
197
     * @throws \InvalidArgumentException
198
     */
199
    protected function validateCategories(string $categories)
200
    {
201
        // the return categories string
202
        $catsString = ''; 
203
204
        // used when cat that can't be used alone
205
        $needAnother = null;
206
207
        // parse given categories
208
        $cats = explode(',', $categories);
209
210
        foreach ($cats as $cat) {
211
212
            // get index on our array of categories
213
            $catIndex    = is_numeric($cat) ? $this->getCategoryIndex($cat, 1) : $this->getCategoryIndex($cat, 0);
214
215
            // check if found
216
            if ($catIndex === false ){
217
                throw new \InvalidArgumentException('Invalid report category was given : ['. $cat .  ']');
218
            }
219
220
            // get Id
221
            $catId = $this->aipdbApiCategories[$catIndex][1];
222
223
            // need another ?
224
            if ($needAnother !== false){
225
226
                // is a standalone cat ?
227
                if ($this->aipdbApiCategories[$catIndex][3] === false) {
228
                    $needAnother = true;
229
230
                } else {
231
                    // ok, continue with other at least one given
232
                    // no need to reperform this check
233
                    $needAnother = false;
234
                }
235
            }
236
237
            // set or add to cats list 
238
            $catsString = ($catsString === '') ? $catId : $catsString .','.$catId;
239
        }
240
241
        if ($needAnother !== false){
0 ignored issues
show
introduced by
The condition $needAnother !== false is always true.
Loading history...
242
            throw new \InvalidArgumentException('Invalid report category paremeter given: some categories can\'t be used alone');
243
        }
244
245
        // if here that ok
246
        return $catsString;
247
    }
248
249
    /**
250
     * Perform a 'check' api request
251
     * 
252
     * @access public
253
     * @param string    $ip             The ip to check
254
     * @param string    $maxAge         Max age in days
255
     * @param bool      $verbose        True to get the full response. Default is false
256
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
257
     * 
258
     * @return object|array
259
     * @throws \InvalidArgumentException    When maxAge is not a numeric value, when maxAge is less than 1 or 
260
     *                                      greater than 365, or when ip value was not set. 
261
     */
262
    public function check(string $ip = null, string $maxAge = '30', bool $verbose = false, bool $returnArray = false)
263
    {
264
        
265
        if (!is_numeric($maxAge)){
266
            throw new \InvalidArgumentException('maxAge must be a numeric value (' . $maxAge . ' was given)');
267
        }
268
        $maxAge = intval($maxAge);
269
270
        // max age must less or equal to 365
271
        if ($maxAge > 365 || $maxAge < 1){
272
            throw new \InvalidArgumentException('maxAge must be at least 1 and less than 365 (' . $maxAge . ' was given)');
273
        }
274
275
        //ip must be set
276
        if (empty($ip)){
277
            throw new \InvalidArgumentException('ip argument must be set (null given)');
278
        }
279
280
        // minimal data
281
        $data = [
282
            'ipAddress'     => $ip, 
283
            'maxAgeInDays'  => $maxAge,  
284
        ];
285
286
        // option
287
        if ($verbose){
288
           $data['verbose'] = true;
289
        }
290
291
        // check AbuseIPDB request
292
        return $this->apiRequest('check', $data, 'GET', $returnArray) ;
293
    }
294
295
    /**
296
     * Perform a cURL request       
297
     * 
298
     * @access protected
299
     * @param string    $path           The api end path 
300
     * @param array     $data           The request data 
301
     * @param string    $method         The request method. Default is 'GET' 
302
     * @param bool      $returnArray    True to return an indexed array instead of an object. Default is false. 
303
     * 
304
     * @return object|array
305
     */
306
    protected function apiRequest(string $path, array $data, string $method = 'GET', bool $returnArray = false) 
307
    {
308
309
310
        // set api url
311
        $url = $this->aipdbApiEndpoint . $path; 
312
313
        // open curl connection
314
        $ch = curl_init(); 
315
  
316
        // set the method and data to send
317
        if ($method == 'POST') {
318
            curl_setopt($ch, CURLOPT_POST, true);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

318
            curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_POST, true);
Loading history...
319
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
320
        } else {
321
            $url .= '?' . http_build_query($data);
322
        }
323
         
324
        // set the url to call
325
        curl_setopt($ch, CURLOPT_URL, $url);
326
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
327
      
328
        // set the AbuseIPDB API Key as a header
329
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
330
            'Accept: application/json;',
331
            'Key: ' . $this->aipdbApiKey,
332
        ]);
333
  
334
      // execute curl call
335
      $result = curl_exec($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

335
      $result = curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
336
  
337
      // close connection
338
      curl_close($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

338
      curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
339
  
340
      // return response as object / array
341
      return json_decode($result, $returnArray);
342
    }
343
344
    /** 
345
     * Clean message in case it comes from fail2ban <matches>
346
     * https://wiki.shaunc.com/wikka.php?wakka=ReportingToAbuseIPDBWithFail2Ban
347
     * 
348
     * @access public
349
     * @param string      $message           The original message 
350
     *  
351
	 * @return string
352
     */
353
    protected function cleanMessage(string $message)
354
    {
355
        // Remove backslashes and sensitive information from the report
356
        $message = str_replace('\\', '', $message);
357
358
        // Remove self ips
359
        foreach ($this->myIps as $ip){
0 ignored issues
show
Bug Best Practice introduced by
The property myIps does not exist on Kristuff\AbuseIPDB\ApiManager. Did you maybe forget to declare it?
Loading history...
360
            $message = str_replace($ip, '[MUNGED]', $message);
361
        } 
362
363
        // If we're reporting spam, further munge any email addresses in the report
364
        $emailPattern = "/[^@\s]*@[^@\s]*\.[^@\s]*/";
365
        $emailRemplacement = "[MUNGED]";
366
        preg_replace($emailPattern, $emailRemplacement, $message);
367
        
368
        return $message;
369
    }
370
371
    /** 
372
     * Load and returns decoded Json from given file  
373
     *
374
     * @access public
375
     * @static
376
	 * @param string    $filePath       The file's full path
377
	 * @param bool      $trowError      Throw error on true or silent process. Default is true
378
     *  
379
	 * @return object|null 
380
     * @throws \Exception
381
     * @throws \LogicException
382
     */
383
    protected static function loadJsonFile(string $filePath, bool $throwError = true)
384
    {
385
        // check file exists
386
        if (!file_exists($filePath) || !is_file($filePath)){
387
           if ($throwError) {
388
                throw new \Exception('Config file not found');
389
           }
390
           return null;  
391
        }
392
393
        // get and parse content
394
        $content = file_get_contents($filePath);
395
        $json    = json_decode(utf8_encode($content));
396
397
        // check for errors
398
        if ($json == null && json_last_error() != JSON_ERROR_NONE){
399
            if ($throwError) {
400
                throw new \LogicException(sprintf("Failed to parse config file Error: '%s'", json_last_error_msg()));
401
            }
402
        }
403
404
        return $json;        
405
    }
406
}