Completed
Push — master ( 55fcd6...852ed1 )
by Armando
04:20 queued 01:59
created

BotManager::validateSecret()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 12
rs 8.8571
cc 6
eloc 6
nc 4
nop 1
1
<?php
2
/**
3
 * This file is part of the TelegramBotManager package.
4
 *
5
 * (c) Armando Lüscher <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace NPM\TelegramBotManager;
12
13
use Longman\TelegramBot\Telegram;
14
use Longman\TelegramBot\TelegramLog;
15
use Longman\TelegramBot\Entities;
16
17
/**
18
 * Class BotManager.php
19
 *
20
 * Leave all member variables public to allow easy modification.
21
 *
22
 * @package NPM\TelegramBotManager
23
 */
24
class BotManager
25
{
26
    /**
27
     * @var Telegram
28
     */
29
    public $telegram;
30
31
    /**
32
     * @var string The output for testing, instead of echoing
33
     */
34
    public $test_output;
35
36
    /**
37
     * @var string Telegram Bot API key
38
     */
39
    protected $api_key = '';
40
41
    /**
42
     * @var string Telegram Bot name
43
     */
44
    public $botname;
45
46
    /**
47
     * @var string Secret string to validate calls
48
     */
49
    public $secret;
50
51
    /**
52
     * @var string Action to be executed
53
     */
54
    public $action = 'handle';
55
56
    /**
57
     * @var string URI of the webhook
58
     */
59
    public $webhook;
60
61
    /**
62
     * @var string Path to the self-signed certificate
63
     */
64
    public $selfcrt;
65
66
    /**
67
     * @var array Array of logger files to set
68
     */
69
    public $logging;
70
71
    /**
72
     * @var array List of admins to enable
73
     */
74
    public $admins;
75
76
    /**
77
     * @var array MySQL credentials to use
78
     */
79
    public $mysql;
80
81
    /**
82
     * @var string Custom download path to set
83
     */
84
    public $download_path;
85
86
    /**
87
     * @var string Custom upload path to set
88
     */
89
    public $upload_path;
90
91
    /**
92
     * @var array Custom commands paths to set
93
     */
94
    public $commands_paths;
95
96
    /**
97
     * @var array List of custom command configs
98
     */
99
    public $command_configs;
100
101
    /**
102
     * @var string Botan token to enable botan.io support
103
     */
104
    public $botan_token;
105
106
    /**
107
     * @var string Custom raw JSON string to use as input
108
     */
109
    public $custom_input;
110
111
    /**
112
     * @var array List of valid actions that can be called
113
     */
114
    private static $valid_actions = [
115
        'set',
116
        'unset',
117
        'reset',
118
        'handle'
119
    ];
120
121
    /**
122
     * @var array List of valid extra parameters that can be passed
123
     */
124
    private static $valid_params = [
125
        'api_key',
126
        'botname',
127
        'secret',
128
        'webhook',
129
        'selfcrt',
130
        'logging',
131
        'admins',
132
        'mysql',
133
        'download_path',
134
        'upload_path',
135
        'commands_paths',
136
        'command_configs',
137
        'botan_token',
138
        'custom_input'
139
    ];
140
141
142
    /**
143
     * BotManager constructor that assigns all necessary member variables.
144
     *
145
     * @param array $vars
146
     *
147
     * @throws \Exception
148
     */
149
    public function __construct(array $vars)
150
    {
151
        if (!isset($vars['api_key'], $vars['botname'], $vars['secret'])) {
152
            throw new \Exception('Some vital info is missing (api_key, botname or secret)');
153
        }
154
155
        // Set all vital and extra parameters.
156
        foreach ($vars as $var => $value) {
157
            in_array($var, self::$valid_params, true) && $this->$var = $value;
158
        }
159
    }
160
161
    /**
162
     * Run this thing in all its glory!
163
     *
164
     * @throws \Exception
165
     */
166
    public function run()
167
    {
168
        // If this script is called via CLI, make it work just the same.
169
        $this->makeCliFriendly();
170
171
        // Initialise logging.
172
        $this->initLogging();
173
174
        // Make sure this is a valid call.
175
        $this->validateSecret();
176
177
        // Check for a valid action and set member variable.
178
        $this->validateAndSetAction();
179
180
        // Set up a new Telegram instance.
181
        $this->telegram = new Telegram($this->api_key, $this->botname);
182
183
        if ($this->isAction(['set', 'unset', 'reset'])) {
184
            $this->validateAndSetWebhook();
185
        } elseif ($this->isAction('handle')) {
186
            // Set any extras.
187
            $this->setBotExtras();
188
            $this->handleRequest();
189
        }
190
191
        return $this;
192
    }
193
194
    /**
195
     * Check if this script is being called from CLI.
196
     *
197
     * @return bool
198
     */
199
    public function isCli()
200
    {
201
        return PHP_SAPI === 'cli';
202
    }
203
204
    /**
205
     * Allow this script to be called via CLI.
206
     *
207
     * $ php entry.php s=<secret> a=<action> l=<loop>
208
     */
209
    public function makeCliFriendly()
0 ignored issues
show
Coding Style introduced by
makeCliFriendly uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
makeCliFriendly uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
210
    {
211
        // If we're running from CLI, properly set $_GET.
212
        if ($this->isCli()) {
213
            // We don't need the first arg (the file name).
214
            $args = array_slice($_SERVER['argv'], 1);
215
216
            foreach ($args as $arg) {
217
                @list($key, $val) = explode('=', $arg);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
218
                isset($key, $val) && $_GET[$key] = $val;
219
            }
220
        }
221
222
        return $this;
223
    }
224
225
    /**
226
     * Initialise all loggers.
227
     */
228
    public function initLogging()
229
    {
230
        if (is_array($this->logging)) {
231
            foreach ($this->logging as $logger => $logfile) {
232
                ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
233
                ('error' === $logger) && TelegramLog::initErrorLog($logfile);
234
                ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
235
            }
236
        }
237
238
        return $this;
239
    }
240
241
    /**
242
     * Make sure the passed secret is valid.
243
     *
244
     * @param bool $force Force validation, even on CLI.
245
     *
246
     * @return $this
247
     * @throws \Exception
248
     */
249
    public function validateSecret($force = false)
0 ignored issues
show
Coding Style introduced by
validateSecret uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
250
    {
251
        // If we're running from CLI, secret isn't necessary.
252
        if ($force || !$this->isCli()) {
253
            $secretGet = isset($_GET['s']) ? (string)$_GET['s'] : '';
254
            if (empty($this->secret) || $secretGet !== $this->secret) {
255
                throw new \Exception('Invalid access');
256
            }
257
        }
258
259
        return $this;
260
    }
261
262
    /**
263
     * Make sure the action is valid and set the member variable.
264
     *
265
     * @throws \Exception
266
     */
267
    public function validateAndSetAction()
0 ignored issues
show
Coding Style introduced by
validateAndSetAction uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
268
    {
269
        // Only set the action if it has been passed, else use the default.
270
        isset($_GET['a']) && $this->action = (string)$_GET['a'];
271
272
        if (!$this->isAction(self::$valid_actions)) {
273
            throw new \Exception('Invalid action');
274
        }
275
276
        return $this;
277
    }
278
279
    /**
280
     * Make sure the webhook is valid and perform the requested webhook operation.
281
     *
282
     * @throws \Exception
283
     */
284
    public function validateAndSetWebhook()
285
    {
286
        if (empty($this->webhook) && $this->isAction(['set', 'reset'])) {
287
            throw new \Exception('Invalid webhook');
288
        }
289
290
        if ($this->isAction(['unset', 'reset'])) {
291
            $this->test_output = $this->telegram->unsetWebHook()->getDescription();
292
        }
293
        if ($this->isAction(['set', 'reset'])) {
294
            $this->test_output = $this->telegram->setWebHook(
295
                $this->webhook . '?a=handle&s=' . $this->secret,
296
                $this->selfcrt
297
            )->getDescription();
298
        }
299
300
        (@constant('PHPUNIT_TEST') !== true) && print($this->test_output . PHP_EOL);
301
302
        return $this;
303
    }
304
305
    /**
306
     * Check if the current action is one of the passed ones.
307
     *
308
     * @param string|array $actions
309
     *
310
     * @return bool
311
     */
312
    public function isAction($actions)
313
    {
314
        // Make sure we're playing with an array without empty values.
315
        $actions = array_filter((array)$actions);
316
317
        return in_array($this->action, $actions, true);
318
    }
319
320
    /**
321
     * Get the param of how long (in seconds) the script should loop.
322
     *
323
     * @return int
324
     */
325
    public function getLoopTime()
0 ignored issues
show
Coding Style introduced by
getLoopTime uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
326
    {
327
        $loop_time = 0;
328
329
        if (isset($_GET['l'])) {
330
            $loop_time = (int)$_GET['l'];
331
            if ($loop_time <= 0) {
332
                $loop_time = 604800; // 7 days.
333
            }
334
        }
335
336
        return $loop_time;
337
    }
338
339
    /**
340
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
341
     *
342
     * @throws \Exception
343
     */
344
    public function handleRequest()
345
    {
346
        if (empty($this->webhook)) {
347
            if ($loop_time = $this->getLoopTime()) {
348
                $this->handleGetUpdatesLoop($loop_time);
349
            } else {
350
                $this->handleGetUpdates();
351
            }
352
        } else {
353
            $this->handleWebhook();
354
        }
355
356
        return $this;
357
    }
358
359
    /**
360
     * Loop the getUpdates method for the passed amount of seconds.
361
     *
362
     * @param int $loop_time_in_seconds
363
     *
364
     * @return $this
365
     */
366
    public function handleGetUpdatesLoop($loop_time_in_seconds)
367
    {
368
        // Remember the time we started this loop.
369
        $now = time();
370
371
        echo 'Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL;
372
373
        while ($now > time() - $loop_time_in_seconds) {
374
            $this->handleGetUpdates();
375
376
            // Chill a bit.
377
            sleep(2);
378
        }
379
380
        return $this;
381
    }
382
383
    /**
384
     * Handle the updates using the getUpdates method.
385
     */
386
    public function handleGetUpdates()
387
    {
388
        echo date('Y-m-d H:i:s', time()) . ' - ';
389
390
        $response = $this->telegram->handleGetUpdates();
391
        if ($response->isOk()) {
392
            $results = array_filter((array)$response->getResult());
393
394
            printf('Updates processed: %d' . PHP_EOL, count($results));
395
396
            /** @var Entities\Update $result */
397
            foreach ($results as $result) {
398
                $chat_id = 0;
399
                $text    = 'Nothing';
400
401
                $update_content = $result->getUpdateContent();
402
                if ($update_content instanceof Entities\Message) {
403
                    $chat_id = $update_content->getFrom()->getId();
404
                    $text    = $update_content->getText();
405
                } elseif ($update_content instanceof Entities\InlineQuery || $update_content instanceof Entities\ChosenInlineResult) {
406
                    $chat_id = $update_content->getFrom()->getId();
407
                    $text    = $update_content->getQuery();
408
                }
409
410
                printf(
411
                    '%d: %s' . PHP_EOL,
412
                    $chat_id,
413
                    preg_replace('/\s+/', ' ', trim($text))
414
                );
415
            }
416
        } else {
417
            printf('Failed to fetch updates: %s' . PHP_EOL, $response->printError());
418
        }
419
420
        return $this;
421
    }
422
423
    /**
424
     * Handle the updates using the Webhook method.
425
     *
426
     * @throws \Exception
427
     */
428
    public function handleWebhook()
429
    {
430
        $this->telegram->handle();
431
432
        return $this;
433
    }
434
435
    /**
436
     * Set any extra bot features that have been assigned on construction.
437
     */
438
    public function setBotExtras()
439
    {
440
        $this->admins         && $this->telegram->enableAdmins($this->admins);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->admins of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
441
        $this->mysql          && $this->telegram->enableMySql($this->mysql);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->mysql of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
442
        $this->botan_token    && $this->telegram->enableBotan($this->botan_token);
443
        $this->commands_paths && $this->telegram->addCommandsPaths($this->commands_paths);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->commands_paths of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
444
        $this->custom_input   && $this->telegram->setCustomInput($this->custom_input);
445
        $this->download_path  && $this->telegram->setDownloadPath($this->download_path);
446
        $this->upload_path    && $this->telegram->setUploadPath($this->upload_path);
447
448
        if (is_array($this->command_configs)) {
449
            foreach ($this->command_configs as $command => $config) {
450
                $this->telegram->setCommandConfig($command, $config);
451
            }
452
        }
453
454
        return $this;
455
    }
456
}
457