Completed
Pull Request — v3 (#473)
by
unknown
08:48
created

GameQ::removeFilter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 9.7998
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * This file is part of GameQ.
4
 *
5
 * GameQ is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU Lesser General Public License as published by
7
 * the Free Software Foundation; either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * GameQ is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Lesser General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Lesser General Public License
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18
namespace GameQ;
19
20
use GameQ\Exception\Protocol as ProtocolException;
21
use GameQ\Exception\Query as QueryException;
22
23
/**
24
 * Base GameQ Class
25
 *
26
 * This class should be the only one that is included when you use GameQ to query
27
 * any games servers.
28
 *
29
 * Requirements: See wiki or README for more information on the requirements
30
 *  - PHP 5.4.14+
31
 *    * Bzip2 - http://www.php.net/manual/en/book.bzip2.php
32
 *
33
 * @author Austin Bischoff <[email protected]>
34
 *
35
 * @property bool   $debug
36
 * @property string $capture_packets_file
37
 * @property int    $stream_timeout
38
 * @property int    $timeout
39
 * @property int    $write_wait
40
 */
41
class GameQ
42
{
43
    /*
44
     * Constants
45
     */
46
47
    /* Static Section */
48
49
    /**
50
     * Holds the instance of itself
51
     *
52
     * @type self
53
     */
54
    protected static $instance = null;
55
56
    /**
57
     * Create a new instance of this class
58
     *
59
     * @return \GameQ\GameQ
60
     */
61 1
    public static function factory()
62
    {
63
64
        // Create a new instance
65 1
        self::$instance = new self();
66
67
        // Return this new instance
68 1
        return self::$instance;
69
    }
70
71
    /* Dynamic Section */
72
73
    /**
74
     * Default options
75
     *
76
     * @type array
77
     */
78
    protected $options = [
79
        'debug'                => false,
80
        'timeout'              => 3, // Seconds
81
        'filters'              => [
82
            // Default normalize
83
            'normalize_d751713988987e9331980363e24189ce' => [
84
                'filter'  => 'normalize',
85
                'options' => [],
86
            ],
87
        ],
88
        // Advanced settings
89
        'stream_timeout'       => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info
90
        'write_wait'           => 500,
91
        // How long (in micro-seconds) to pause between writing to server sockets, helps cpu usage
92
93
        // Used for generating protocol test data
94
        'capture_packets_file' => null,
95
    ];
96
97
    /**
98
     * Array of servers being queried
99
     *
100
     * @type array
101
     */
102
    protected $servers = [];
103
104
    /**
105
     * The query library to use.  Default is Native
106
     *
107
     * @type string
108
     */
109
    protected $queryLibrary = 'GameQ\\Query\\Native';
110
111
    /**
112
     * Holds the instance of the queryLibrary
113
     *
114
     * @type \GameQ\Query\Core|null
115
     */
116
    protected $query = null;
117
118
    /**
119
     * GameQ constructor.
120
     *
121
     * Do some checks as needed so this will operate
122
     */
123 223
    public function __construct()
124
    {
125
        // Check for missing utf8_encode function
126 223
        if (!function_exists('utf8_encode')) {
127
            throw new \Exception("PHP's utf8_encode() function is required - "
128
                . "http://php.net/manual/en/function.utf8-encode.php.  Check your php installation.");
129
        }
130 223
    }
131
132
    /**
133
     * Get an option's value
134
     *
135
     * @param mixed $option
136
     *
137
     * @return mixed|null
138
     */
139 216
    public function __get($option)
140
    {
141
142 216
        return isset($this->options[$option]) ? $this->options[$option] : null;
143
    }
144
145
    /**
146
     * Set an option's value
147
     *
148
     * @param mixed $option
149
     * @param mixed $value
150
     *
151
     * @return bool
152
     */
153 218
    public function __set($option, $value)
154
    {
155
156 218
        $this->options[$option] = $value;
157
158 218
        return true;
159
    }
160
161
    /**
162
     * Chainable call to __set, uses set as the actual setter
163
     *
164
     * @param mixed $var
165
     * @param mixed $value
166
     *
167
     * @return $this
168
     */
169 218
    public function setOption($var, $value)
170
    {
171
172
        // Use magic
173 218
        $this->{$var} = $value;
174
175 218
        return $this; // Make chainable
176
    }
177
178
    /**
179
     * Add a single server
180
     *
181
     * @param array $server_info
182
     *
183
     * @return $this
184
     */
185 2
    public function addServer(array $server_info = [])
186
    {
187
188
        // Add and validate the server
189 2
        $this->servers[uniqid()] = new Server($server_info);
190
191 2
        return $this; // Make calls chainable
192
    }
193
194
    /**
195
     * Add multiple servers in a single call
196
     *
197
     * @param array $servers
198
     *
199
     * @return $this
200
     */
201 2
    public function addServers(array $servers = [])
202
    {
203
204
        // Loop through all the servers and add them
205 2
        foreach ($servers as $server_info) {
206 2
            $this->addServer($server_info);
207 2
        }
208
209 2
        return $this; // Make calls chainable
210
    }
211
212
    /**
213
     * Add a set of servers from a file or an array of files.
214
     * Supported formats:
215
     * JSON
216
     *
217
     * @param array $files
218
     *
219
     * @return $this
220
     * @throws \Exception
221
     */
222 1
    public function addServersFromFiles($files = [])
223
    {
224
225
        // Since we expect an array let us turn a string (i.e. single file) into an array
226 1
        if (!is_array($files)) {
227 1
            $files = [$files];
228 1
        }
229
230
        // Iterate over the file(s) and add them
231 1
        foreach ($files as $file) {
232
            // Check to make sure the file exists and we can read it
233 1
            if (!file_exists($file) || !is_readable($file)) {
234 1
                continue;
235
            }
236
237
            // See if this file is JSON
238 1
            if (($servers = json_decode(file_get_contents($file), true)) === null
239 1
                && json_last_error() !== JSON_ERROR_NONE
240 1
            ) {
241
                // Type not supported
242 1
                continue;
243
            }
244
245
            // Add this list of servers
246 1
            $this->addServers($servers);
247 1
        }
248
249 1
        return $this;
250
    }
251
252
    /**
253
     * Clear all of the defined servers
254
     *
255
     * @return $this
256
     */
257 2
    public function clearServers()
258
    {
259
260
        // Reset all the servers
261 2
        $this->servers = [];
262
263 2
        return $this; // Make Chainable
264
    }
265
266
    /**
267
     * Add a filter to the processing list
268
     *
269
     * @param string $filterName
270
     * @param array  $options
271
     *
272
     * @return $this
273
     */
274 3
    public function addFilter($filterName, $options = [])
275
    {
276
        // Create the filter hash so we can run multiple versions of the same filter
277 3
        $filterHash = sprintf('%s_%s', strtolower($filterName), md5(json_encode($options)));
278
279
        // Add the filter
280 3
        $this->options['filters'][$filterHash] = [
281 3
            'filter'  => strtolower($filterName),
282 3
            'options' => $options,
283
        ];
284
285 3
        unset($filterHash);
286
287 3
        return $this;
288
    }
289
290
    /**
291
     * Remove an added filter
292
     *
293
     * @param string $filterHash
294
     *
295
     * @return $this
296
     */
297 216
    public function removeFilter($filterHash)
298
    {
299
        // Make lower case
300 216
        $filterHash = strtolower($filterHash);
301
302
        // Remove this filter if it has been defined
303 216
        if (array_key_exists($filterHash, $this->options['filters'])) {
304 3
            unset($this->options['filters'][$filterHash]);
305 3
        }
306
307 216
        unset($filterHash);
308
309 216
        return $this;
310
    }
311
312
    /**
313
     * Return the list of applied filters
314
     *
315
     * @return array
316
     */
317
    public function listFilters()
318
    {
319
        return $this->options['filters'];
320
    }
321
322
    /**
323
     * Main method used to actually process all of the added servers and return the information
324
     *
325
     * @return array
326
     * @throws \Exception
327
     */
328
    public function process()
329
    {
330
331
        // Initialize the query library we are using
332
        $class = new \ReflectionClass($this->queryLibrary);
333
334
        // Set the query pointer to the new instance of the library
335
        $this->query = $class->newInstance();
336
337
        unset($class);
338
339
        // Define the return
340
        $results = [];
341
342
        // @todo: Add break up into loop to split large arrays into smaller chunks
343
344
        // Do server challenge(s) first, if any
345
        $this->doChallenges();
346
347
        // Do packets for server(s) and get query responses
348
        $this->doQueries();
349
350
        // Now we should have some information to process for each server
351
        foreach ($this->servers as $server) {
352
            /* @var $server \GameQ\Server */
353
354
            // Parse the responses for this server
355
            $result = $this->doParseResponse($server);
356
357
            // Apply the filters
358
            $result = array_merge($result, $this->doApplyFilters($result, $server));
359
360
            // Sort the keys so they are alphabetical and nicer to look at
361
            ksort($result);
362
363
            // Add the result to the results array
364
            $results[$server->id()] = $result;
365
        }
366
367
        return $results;
368
    }
369
370
    /**
371
     * Do server challenges, where required
372
     */
373
    protected function doChallenges()
374
    {
375
376
        // Initialize the sockets for reading
377
        $sockets = [];
378
379
        // By default we don't have any challenges to process
380
        $server_challenge = false;
381
382
        // Do challenge packets
383
        foreach ($this->servers as $server_id => $server) {
384
            /* @var $server \GameQ\Server */
385
386
            // This protocol has a challenge packet that needs to be sent
387
            if ($server->protocol()->hasChallenge()) {
388
                // We have a challenge, set the flag
389
                $server_challenge = true;
390
391
                // Let's make a clone of the query class
392
                $socket = clone $this->query;
393
394
                // Set the information for this query socket
395
                $socket->set(
396
                    $server->protocol()->transport(),
397
                    $server->ip,
398
                    $server->port_query,
399
                    $this->timeout
400
                );
401
402
                try {
403
                    // Now write the challenge packet to the socket.
404
                    $socket->write($server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
405
406
                    // Add the socket information so we can reference it easily
407
                    $sockets[(int)$socket->get()] = [
408
                        'server_id' => $server_id,
409
                        'socket'    => $socket,
410
                    ];
411
                } catch (QueryException $e) {
412
                    // Check to see if we are in debug, if so bubble up the exception
413
                    if ($this->debug) {
414
                        throw new \Exception($e->getMessage(), $e->getCode(), $e);
415
                    }
416
                }
417
418
                unset($socket);
419
420
                // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu
421
                usleep($this->write_wait);
422
            }
423
        }
424
425
        // We have at least one server with a challenge, we need to listen for responses
426
        if ($server_challenge) {
427
            // Now we need to listen for and grab challenge response(s)
428
            $responses = call_user_func_array(
429
                [$this->query, 'getResponses'],
430
                [$sockets, $this->timeout, $this->stream_timeout]
431
            );
432
433
            // Iterate over the challenge responses
434
            foreach ($responses as $socket_id => $response) {
435
                // Back out the server_id we need to update the challenge response for
436
                $server_id = $sockets[$socket_id]['server_id'];
437
438
                // Make this into a buffer so it is easier to manipulate
439
                $challenge = new Buffer(implode('', $response));
440
441
                // Grab the server instance
442
                /* @var $server \GameQ\Server */
443
                $server = $this->servers[$server_id];
444
445
                // Apply the challenge
446
                $server->protocol()->challengeParseAndApply($challenge);
447
448
                // Add this socket to be reused, has to be reused in GameSpy3 for example
449
                $server->socketAdd($sockets[$socket_id]['socket']);
450
451
                // Clear
452
                unset($server);
453
            }
454
        }
455
    }
456
457
    /**
458
     * Run the actual queries and get the response(s)
459
     */
460
    protected function doQueries()
461
    {
462
463
        // Initialize the array of sockets
464
        $sockets = [];
465
466
        // Iterate over the server list
467
        foreach ($this->servers as $server_id => $server) {
468
            /* @var $server \GameQ\Server */
469
470
            // Invoke the beforeSend method
471
            $server->protocol()->beforeSend($server);
472
473
            // Get all the non-challenge packets we need to send
474
            $packets = $server->protocol()->getPacket('!' . Protocol::PACKET_CHALLENGE);
475
476
            if (count($packets) == 0) {
477
                // Skip nothing else to do for some reason.
478
                continue;
479
            }
480
481
            // Try to use an existing socket
482
            if (($socket = $server->socketGet()) === null) {
483
                // Let's make a clone of the query class
484
                $socket = clone $this->query;
485
486
                // Set the information for this query socket
487
                $socket->set(
488
                    $server->protocol()->transport(),
489
                    $server->ip,
490
                    $server->port_query,
491
                    $this->timeout
492
                );
493
            }
494
495
            try {
496
                // Iterate over all the packets we need to send
497
                foreach ($packets as $packet_data) {
498
                    // Now write the packet to the socket.
499
                    $socket->write($packet_data);
500
501
                    // Let's sleep shortly so we are not hammering out calls rapid fire style
502
                    usleep($this->write_wait);
503
                }
504
505
                unset($packets);
506
507
                // Add the socket information so we can reference it easily
508
                $sockets[(int)$socket->get()] = [
509
                    'server_id' => $server_id,
510
                    'socket'    => $socket,
511
                ];
512
            } catch (QueryException $e) {
513
                // Check to see if we are in debug, if so bubble up the exception
514
                if ($this->debug) {
515
                    throw new \Exception($e->getMessage(), $e->getCode(), $e);
516
                }
517
518
                break;
519
            }
520
521
            // Clean up the sockets, if any left over
522
            $server->socketCleanse();
523
        }
524
525
        // Now we need to listen for and grab response(s)
526
        $responses = call_user_func_array(
527
            [$this->query, 'getResponses'],
528
            [$sockets, $this->timeout, $this->stream_timeout]
529
        );
530
531
        // Iterate over the responses
532
        foreach ($responses as $socket_id => $response) {
533
            // Back out the server_id
534
            $server_id = $sockets[$socket_id]['server_id'];
535
536
            // Grab the server instance
537
            /* @var $server \GameQ\Server */
538
            $server = $this->servers[$server_id];
539
540
            // Save the response from this packet
541
            $server->protocol()->packetResponse($response);
542
543
            unset($server);
544
        }
545
546
        // Now we need to close all of the sockets
547
        foreach ($sockets as $socketInfo) {
548
            /* @var $socket \GameQ\Query\Core */
549
            $socket = $socketInfo['socket'];
550
551
            // Close the socket
552
            $socket->close();
553
554
            unset($socket);
555
        }
556
557
        unset($sockets);
558
    }
559
560
    /**
561
     * Parse the response for a specific server
562
     *
563
     * @param \GameQ\Server $server
564
     *
565
     * @return array
566
     * @throws \Exception
567
     */
568 215
    protected function doParseResponse(Server $server)
0 ignored issues
show
Complexity introduced by
This operation has 300 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
569
    {
570
571
        try {
572
            // @codeCoverageIgnoreStart
573
            // We want to save this server's response to a file (useful for unit testing)
574
            if (!is_null($this->capture_packets_file)) {
575
                file_put_contents(
576
                    $this->capture_packets_file,
577
                    implode(PHP_EOL . '||' . PHP_EOL, $server->protocol()->packetResponse())
578
                );
579
            }
580
            // @codeCoverageIgnoreEnd
581
582
            // Get the server response
583 215
            $results = $server->protocol()->processResponse();
584
585
            // Check for online before we do anything else
586 182
            $results['gq_online'] = (count($results) > 0);
587 215
        } catch (ProtocolException $e) {
588
            // Check to see if we are in debug, if so bubble up the exception
589 33
            if ($this->debug) {
590 19
                throw new \Exception($e->getMessage(), $e->getCode(), $e);
591
            }
592
593
            // We ignore this server
594
            $results = [
595 14
                'gq_online' => false,
596 14
            ];
597
        }
598
599
        // Now add some default stuff
600 196
        $results['gq_address'] = (isset($results['gq_address'])) ? $results['gq_address'] : $server->ip();
601 196
        $results['gq_port_client'] = $server->portClient();
602 196
        $results['gq_port_query'] = (isset($results['gq_port_query'])) ? $results['gq_port_query'] : $server->portQuery();
603 196
        $results['gq_protocol'] = $server->protocol()->getProtocol();
604 196
        $results['gq_type'] = (string)$server->protocol();
605 196
        $results['gq_name'] = $server->protocol()->nameLong();
606 196
        $results['gq_transport'] = $server->protocol()->transport();
607
608
        // Process the join link
609 196
        if (!isset($results['gq_joinlink']) || empty($results['gq_joinlink'])) {
610 196
            $results['gq_joinlink'] = $server->getJoinLink();
611 196
        }
612
613 196
        return $results;
614
    }
615
616
    /**
617
     * Apply any filters to the results
618
     *
619
     * @param array         $results
620
     * @param \GameQ\Server $server
621
     *
622
     * @return array
623
     */
624 2
    protected function doApplyFilters(array $results, Server $server)
625
    {
626
627
        // Loop over the filters
628 2
        foreach ($this->options['filters'] as $filterOptions) {
629
            // Try to do this filter
630
            try {
631
                // Make a new reflection class
632 2
                $class = new \ReflectionClass(sprintf('GameQ\\Filters\\%s', ucfirst($filterOptions['filter'])));
633
634
                // Create a new instance of the filter class specified
635 1
                $filter = $class->newInstanceArgs([$filterOptions['options']]);
636
637
                // Apply the filter to the data
638 1
                $results = $filter->apply($results, $server);
639 2
            } catch (\ReflectionException $e) {
640
                // Invalid, skip it
641 1
                continue;
642
            }
643 2
        }
644
645 2
        return $results;
646
    }
647
}
648