Completed
Branch develop (818027)
by Armando
03:16
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 List of valid actions that can be called
68
     */
69
    private static $valid_actions = [
70
        'set',
71
        'unset',
72
        'reset',
73
        'handle'
74
    ];
75
76
    /**
77
     * @var array List of valid extra parameters that can be passed
78
     */
79
    private static $valid_params = [
80
        'api_key',
81
        'botname',
82
        'secret',
83
        'webhook',
84
        'selfcrt',
85
        'logging',
86
        'admins',
87
        'mysql',
88
        'download_path',
89
        'upload_path',
90
        'commands_paths',
91
        'command_configs',
92
        'botan_token',
93
        'custom_input'
94
    ];
95
96
97
    /**
98
     * BotManager constructor that assigns all necessary member variables.
99
     *
100
     * @param array $vars
101
     *
102
     * @throws \Exception
103
     */
104
    public function __construct(array $vars)
105
    {
106
        if (!isset($vars['api_key'], $vars['botname'], $vars['secret'])) {
107
            throw new \Exception('Some vital info is missing (api_key, botname or secret)');
108
        }
109
110
        // Set all vital and extra parameters.
111
        foreach ($vars as $var => $value) {
112
            in_array($var, self::$valid_params, true) && $this->$var = $value;
113
        }
114
    }
115
116
    /**
117
     * Run this thing in all its glory!
118
     *
119
     * @throws \Exception
120
     */
121
    public function run()
122
    {
123
        // If this script is called via CLI, make it work just the same.
124
        $this->makeCliFriendly();
125
126
        // Initialise logging.
127
        $this->initLogging();
128
129
        // Make sure this is a valid call.
130
        $this->validateSecret();
131
132
        // Check for a valid action and set member variable.
133
        $this->validateAndSetAction();
134
135
        // Set up a new Telegram instance.
136
        $this->telegram = new Telegram($this->api_key, $this->botname);
137
138
        if ($this->isAction(['set', 'unset', 'reset'])) {
139
            $this->validateAndSetWebhook();
140
        } elseif ($this->isAction('handle')) {
141
            // Set any extras.
142
            $this->setBotExtras();
143
            $this->handleRequest();
144
        }
145
146
        return $this;
147
    }
148
149
    /**
150
     * Check if this script is being called from CLI.
151
     *
152
     * @return bool
153
     */
154
    public function isCli()
155
    {
156
        return PHP_SAPI === 'cli';
157
    }
158
159
    /**
160
     * Allow this script to be called via CLI.
161
     *
162
     * $ php entry.php s=<secret> a=<action> l=<loop>
163
     */
164
    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...
165
    {
166
        // If we're running from CLI, properly set $_GET.
167
        if ($this->isCli()) {
168
            // We don't need the first arg (the file name).
169
            $args = array_slice($_SERVER['argv'], 1);
170
171
            foreach ($args as $arg) {
172
                @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...
173
                isset($key, $val) && $_GET[$key] = $val;
174
            }
175
        }
176
177
        return $this;
178
    }
179
180
    /**
181
     * Initialise all loggers.
182
     */
183
    public function initLogging()
184
    {
185
        if (isset($this->logging) && is_array($this->logging)) {
186
            foreach ($this->logging as $logger => $logfile) {
0 ignored issues
show
Bug introduced by
The property logging does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
187
                ('debug' === $logger) && TelegramLog::initDebugLog($logfile);
188
                ('error' === $logger) && TelegramLog::initErrorLog($logfile);
189
                ('update' === $logger) && TelegramLog::initUpdateLog($logfile);
190
            }
191
        }
192
193
        return $this;
194
    }
195
196
    /**
197
     * Make sure the passed secret is valid.
198
     *
199
     * @param bool $force Force validation, even on CLI.
200
     *
201
     * @return $this
202
     * @throws \Exception
203
     */
204
    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...
205
    {
206
        // If we're running from CLI, secret isn't necessary.
207
        if ($force || !$this->isCli()) {
208
            $secretGet = isset($_GET['s']) ? (string)$_GET['s'] : '';
209
            if (empty($this->secret) || $secretGet !== $this->secret) {
210
                throw new \Exception('Invalid access');
211
            }
212
        }
213
214
        return $this;
215
    }
216
217
    /**
218
     * Make sure the action is valid and set the member variable.
219
     *
220
     * @throws \Exception
221
     */
222
    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...
223
    {
224
        // Only set the action if it has been passed, else use the default.
225
        isset($_GET['a']) && $this->action = (string)$_GET['a'];
226
227
        if (!$this->isAction(self::$valid_actions)) {
228
            throw new \Exception('Invalid action');
229
        }
230
231
        return $this;
232
    }
233
234
    /**
235
     * Make sure the webhook is valid and perform the requested webhook operation.
236
     *
237
     * @throws \Exception
238
     */
239
    public function validateAndSetWebhook()
240
    {
241
        if (empty($this->webhook) && $this->isAction(['set', 'reset'])) {
242
            throw new \Exception('Invalid webhook');
243
        }
244
245
        if ($this->isAction(['unset', 'reset'])) {
246
            $this->test_output = $this->telegram->unsetWebHook()->getDescription();
247
        }
248
        if ($this->isAction(['set', 'reset'])) {
249
            $this->test_output = $this->telegram->setWebHook(
250
                $this->webhook . '?a=handle&s=' . $this->secret,
251
                $this->selfcrt
252
            )->getDescription();
253
        }
254
255
        (@constant('PHPUNIT_TEST') !== true) && print($this->test_output . PHP_EOL);
256
257
        return $this;
258
    }
259
260
    /**
261
     * Check if the current action is one of the passed ones.
262
     *
263
     * @param $actions
264
     *
265
     * @return bool
266
     */
267
    public function isAction($actions)
268
    {
269
        // Make sure we're playing with an array without empty values.
270
        $actions = array_filter((array)$actions);
271
272
        return in_array($this->action, $actions, true);
273
    }
274
275
    /**
276
     * Get the param of how long (in seconds) the script should loop.
277
     *
278
     * @return int
279
     */
280
    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...
281
    {
282
        $loop_time = 0;
283
284
        if (isset($_GET['l'])) {
285
            $loop_time = (int)$_GET['l'];
286
            if ($loop_time <= 0) {
287
                $loop_time = 604800; // 7 days.
288
            }
289
        }
290
291
        return $loop_time;
292
    }
293
294
    /**
295
     * Handle the request, which calls either the Webhook or getUpdates method respectively.
296
     *
297
     * @throws \Exception
298
     */
299
    public function handleRequest()
300
    {
301
        if (empty($this->webhook)) {
302
            if ($loop_time = $this->getLoopTime()) {
303
                $this->handleGetUpdatesLoop($loop_time);
304
            } else {
305
                $this->handleGetUpdates();
306
            }
307
        } else {
308
            $this->handleWebhook();
309
        }
310
311
        return $this;
312
    }
313
314
    /**
315
     * Loop the getUpdates method for the passed amount of seconds.
316
     *
317
     * @param $loop_time_in_seconds int
318
     *
319
     * @return $this
320
     */
321
    public function handleGetUpdatesLoop($loop_time_in_seconds)
322
    {
323
        // Remember the time we started this loop.
324
        $now = time();
325
326
        echo 'Looping getUpdates until ' . date('Y-m-d H:i:s', $now + $loop_time_in_seconds) . PHP_EOL;
327
328
        while ($now > time() - $loop_time_in_seconds) {
329
            $this->handleGetUpdates();
330
331
            // Chill a bit.
332
            sleep(2);
333
        }
334
335
        return $this;
336
    }
337
338
    /**
339
     * Handle the updates using the getUpdates method.
340
     */
341
    public function handleGetUpdates()
342
    {
343
        echo date('Y-m-d H:i:s', time()) . ' - ';
344
345
        $response = $this->telegram->handleGetUpdates();
346
        if ($response->isOk()) {
347
            $results = array_filter((array)$response->getResult());
348
349
            printf('Updates processed: %d' . PHP_EOL, count($results));
350
351
            /** @var Entities\Update $result */
352
            foreach ($results as $result) {
353
                $chat_id = 0;
354
                $text    = 'Nothing';
355
356
                $update_content = $result->getUpdateContent();
357
                if ($update_content instanceof Entities\Message) {
358
                    $chat_id = $update_content->getFrom()->getId();
359
                    $text    = $update_content->getText();
360
                } elseif ($update_content instanceof Entities\InlineQuery || $update_content instanceof Entities\ChosenInlineResult) {
361
                    $chat_id = $update_content->getFrom()->getId();
362
                    $text    = $update_content->getQuery();
363
                }
364
365
                printf(
366
                    '%d: %s' . PHP_EOL,
367
                    $chat_id,
368
                    preg_replace('/\s+/', ' ', trim($text))
369
                );
370
            }
371
        } else {
372
            printf('Failed to fetch updates: %s' . PHP_EOL, $response->printError());
373
        }
374
375
        return $this;
376
    }
377
378
    /**
379
     * Handle the updates using the Webhook method.
380
     *
381
     * @throws \Exception
382
     */
383
    public function handleWebhook()
384
    {
385
        $this->telegram->handle();
386
387
        return $this;
388
    }
389
390
    /**
391
     * Set any extra bot features that have been assigned on construction.
392
     */
393
    public function setBotExtras()
394
    {
395
        isset($this->admins)         && $this->telegram->enableAdmins((array)$this->admins);
396
        isset($this->mysql)          && $this->telegram->enableMySql($this->mysql);
0 ignored issues
show
Bug introduced by
The property mysql does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
397
        isset($this->botan_token)    && $this->telegram->enableBotan($this->botan_token);
0 ignored issues
show
Bug introduced by
The property botan_token does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
398
        isset($this->commands_paths) && $this->telegram->addCommandsPaths((array)$this->commands_paths);
399
        isset($this->custom_input)   && $this->telegram->setCustomInput($this->custom_input);
0 ignored issues
show
Bug introduced by
The property custom_input does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
400
        isset($this->download_path)  && $this->telegram->setDownloadPath($this->download_path);
0 ignored issues
show
Bug introduced by
The property download_path does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
401
        isset($this->upload_path)    && $this->telegram->setUploadPath($this->upload_path);
0 ignored issues
show
Bug introduced by
The property upload_path does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
402
403
        if (isset($this->command_configs) && is_array($this->command_configs)) {
404
            foreach ($this->command_configs as $command => $config) {
0 ignored issues
show
Bug introduced by
The property command_configs does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
405
                $this->telegram->setCommandConfig($command, $config);
406
            }
407
        }
408
409
        return $this;
410
    }
411
}
412