Completed
Pull Request — master (#2345)
by ྅༻ Ǭɀħ
01:36
created

includes/Database/YDB.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * Aura SQL wrapper for YOURLS that creates the allmighty YDB object.
5
 *
6
 * A fine example of a "class that knows too much" (see https://en.wikipedia.org/wiki/God_object)
7
 *
8
 * Note to plugin authors: you most likely SHOULD NOT use directly methods and properties of this class. Use instead
9
 * function wrappers (eg don't use $ydb->option, or $ydb->set_option(), use yourls_*_options() functions instead).
10
 *
11
 * @since 1.7.3
12
 */
13
14
namespace YOURLS\Database;
15
16
use YOURLS\Admin\Logger;
17
use Aura\Sql\ExtendedPdo;
18
use PDO;
19
20
class YDB extends ExtendedPdo {
21
22
    /**
23
     * Debug mode, default false
24
     * @var bool
25
     */
26
    protected $debug = false;
27
28
    /**
29
     * Page context (ie "infos", "bookmark", "plugins"...)
30
     * @var string
31
     */
32
    protected $context = '';
33
34
    /**
35
     * Information related to a short URL keyword (eg timestamp, long URL, ...)
36
     *
37
     * @var array
38
     *
39
     */
40
    protected $infos = array();
41
42
    /**
43
     * Is YOURLS installed and ready to run?
44
     * @var bool
45
     */
46
    protected $installed = false;
47
48
    /**
49
     * Options
50
     * @var array
51
     */
52
    protected $option = array();
53
54
    /**
55
     * Plugin admin pages informations
56
     * @var array
57
     */
58
    protected $plugin_pages = array();
59
60
    /**
61
     * Plugin informations
62
     * @var array
63
     */
64
    protected $plugins = array();
65
66
    /**
67
     * Are we emulating prepare statements ?
68
     * @var bool
69
     */
70
    protected $is_emulate_prepare;
71
72
    /**
73
     * @since 1.7.3
74
     * @param string $dsn         The data source name
75
     * @param string $user        The username
76
     * @param string $pass        The password
77
     * @param array  $options     Driver-specific options
78
     * @param array  $attributes  Attributes to set after a connection
79
     */
80
    public function __construct($dsn, $user, $pass, $options, $attributes) {
81
        parent::__construct($dsn, $user, $pass, $options, $attributes);
82
    }
83
84
    /**
85
     * Init everything needed
86
     *
87
     * Everything we need to set up is done here in init(), not in the constructor, so even
88
     * when the connection fails (eg config error or DB dead), the constructor has worked
89
     * and we have a $ydb object properly instantiated (and for instance yourls_die() can
90
     * correctly die, even if using $ydb methods)
91
     *
92
     * @since  1.7.3
93
     * @return void
94
     */
95
    public function init() {
96
        $this->connect_to_DB();
97
98
        $this->set_emulate_state();
99
100
        $this->start_profiler();
101
    }
102
103
    /**
104
     * Check if we emulate prepare statements, and set bool flag accordingly
105
     *
106
     * Check if current driver can PDO::getAttribute(PDO::ATTR_EMULATE_PREPARES)
107
     * Some combinations of PHP/MySQL don't support this function. See
108
     * https://travis-ci.org/YOURLS/YOURLS/jobs/271423782#L481
109
     *
110
     * @since  1.7.3
111
     * @return void
112
     */
113
    public function set_emulate_state() {
114
        try {
115
            $this->is_emulate_prepare = $this->getAttribute(PDO::ATTR_EMULATE_PREPARES);
116
        } catch (\PDOException $e) {
117
            $this->is_emulate_prepare = false;
118
        }
119
    }
120
121
    /**
122
     * Get emulate status
123
     *
124
     * @since  1.7.3
125
     * @return bool
126
     */
127
    public function get_emulate_state() {
128
        return $this->is_emulate_prepare;
129
    }
130
131
    /**
132
     * Initiate real connection to DB server
133
     *
134
     * This is to check that the server is running and/or the config is OK
135
     *
136
     * @since  1.7.3
137
     * @return void
138
     * @throws \PDOException
139
     */
140
    public function connect_to_DB() {
141
        try {
142
            $this->connect();
143
        } catch ( \Exception $e ) {
144
            $this->dead_or_error($e);
145
        }
146
    }
147
148
    /**
149
     * Die with an error message
150
     *
151
     * @since  1.7.3
152
     *
153
     * @param \Exception $exception
154
     *
155
     * @return void
156
     */
157
    public function dead_or_error(\Exception $exception) {
158
        // Use any /user/db_error.php file
159
        if( file_exists( YOURLS_USERDIR . '/db_error.php' ) ) {
160
            include_once( YOURLS_USERDIR . '/db_error.php' );
161
            die();
162
        }
163
164
        $message  = yourls__( 'Incorrect DB config, or could not connect to DB' );
165
        $message .= '<br/>' . get_class($exception) .': ' . $exception->getMessage();
166
167
        yourls_die( yourls__( $message ), yourls__( 'Fatal error' ), 503 );
168
        die();
169
    }
170
171
    /**
172
     * Start a Message Logger
173
     *
174
     * @since  1.7.3
175
     * @see    \YOURLS\Admin\Logger
176
     * @see    \Aura\Sql\Profiler
177
     * @return void
178
     */
179
    public function start_profiler() {
180
        $this->profiler = new Logger($this);
181
    }
182
183
    /**
184
     * @param string $context
185
     */
186
    public function set_html_context($context) {
187
        $this->context = $context;
188
    }
189
190
    /**
191
     * @return string
192
     */
193
    public function get_html_context() {
194
        return $this->context;
195
    }
196
197
    // Options low level functions, see \YOURLS\Database\Options
198
199
    /**
200
     * @param string $name
201
     * @param mixed  $value
202
     */
203
    public function set_option($name, $value) {
204
        $this->option[$name] = $value;
205
    }
206
207
    /**
208
     * @param  string $name
209
     * @return bool
210
     */
211
    public function has_option($name) {
212
        return array_key_exists($name, $this->option);
213
    }
214
215
    /**
216
     * @param  string $name
217
     * @return string
218
     */
219
    public function get_option($name) {
220
        return $this->option[$name];
221
    }
222
223
    /**
224
     * @param string $name
225
     */
226
    public function delete_option($name) {
227
        unset($this->option[$name]);
228
    }
229
230
231
    // Infos (related to keyword) low level functions
232
233
    /**
234
     * @param string $keyword
235
     * @param mixed  $infos
236
     */
237
    public function set_infos($keyword, $infos) {
238
        $this->infos[$keyword] = $infos;
239
    }
240
241
    /**
242
     * @param  string $keyword
243
     * @return bool
244
     */
245
    public function has_infos($keyword) {
246
        return array_key_exists($keyword, $this->infos);
247
    }
248
249
    /**
250
     * @param  string $keyword
251
     * @return array
252
     */
253
    public function get_infos($keyword) {
254
        return $this->infos[$keyword];
255
    }
256
257
    /**
258
     * @param string $keyword
259
     */
260
    public function delete_infos($keyword) {
261
        unset($this->infos[$keyword]);
262
    }
263
264
    /**
265
     * @todo: infos & options are working the same way here. Abstract this.
266
     */
267
268
269
    // Plugin low level functions, see functions-plugins.php
270
271
    /**
272
     * @return array
273
     */
274
    public function get_plugins() {
275
        return $this->plugins;
276
    }
277
278
    /**
279
     * @param array $plugins
280
     */
281
    public function set_plugins(array $plugins) {
282
        $this->plugins = $plugins;
283
    }
284
285
    /**
286
     * @param string $plugin  plugin filename
287
     */
288
    public function add_plugin($plugin) {
289
        $this->plugins[] = $plugin;
290
    }
291
292
    /**
293
     * @param string $plugin  plugin filename
294
     */
295
    public function remove_plugin($plugin) {
296
        unset($this->plugins[$plugin]);
297
    }
298
299
300
    // Plugin Pages low level functions, see functions-plugins.php
301
302
    /**
303
     * @return array
304
     */
305
    public function get_plugin_pages() {
306
        return $this->plugin_pages;
307
    }
308
309
    /**
310
     * @param array $pages
311
     */
312
    public function set_plugin_pages(array $pages) {
313
        $this->plugin_pages = $pages;
314
    }
315
316
    /**
317
     * @param string   $slug
318
     * @param string   $title
319
     * @param callable $function
320
     */
321
    public function add_plugin_page($slug, $title, $function) {
322
        $this->plugin_pages[$slug] = array(
323
            'slug'     => $slug,
324
            'title'    => $title,
325
            'function' => $function,
326
        );
327
    }
328
329
    /**
330
     * @param string $slug
331
     */
332
    public function remove_plugin_page($slug) {
333
        unset($this->plugin_pages[$slug]);
334
    }
335
336
337
    /**
338
     * Return count of SQL queries performed
339
     *
340
     * @since  1.7.3
341
     * @return int
342
     */
343
    public function get_num_queries() {
344
        return count( (array) $this->get_queries() );
345
    }
346
347
    /**
348
     * Return SQL queries performed
349
     *
350
     * Aura\Sql\Profiler logs every PDO command issued. But depending on PDO::ATTR_EMULATE_PREPARES, some are
351
     * actually sent to the mysql server or not :
352
     *  - if PDO::ATTR_EMULATE_PREPARES is true, prepare() statements are not sent to the server and are performed
353
     *    internally, so they are removed from the logger
354
     *  - if PDO::ATTR_EMULATE_PREPARES is false, prepare() statements are actually performed by the mysql server,
355
     *    and count as an actual query
356
     *
357
     * Resulting array is something like:
358
     *   array (
359
     *      0 => array (
360
     *           'duration' => 1.0010569095611572265625,
361
     *           'function' => 'connect',
362
     *           'statement' => NULL,
363
     *           'bind_values' => array (),
364
     *           'trace' => ...back trace...,
365
     *       ),
366
     *       // key index might not be sequential if 'prepare' function are filtered out
367
     *       2 => array (
368
     *           'duration' => 0.000999927520751953125,
369
     *           'function' => 'perform',
370
     *           'statement' => 'SELECT option_value FROM yourls_options WHERE option_name = :option_name LIMIT 1',
371
     *           'bind_values' => array ( 'option_name' => 'test_option' ),
372
     *           'trace' => ...back trace...,
373
     *       ),
374
     *   );
375
     *
376
     * @since  1.7.3
377
     * @return array
378
     */
379
    public function get_queries() {
380
        $queries = $this->getProfiler()->getProfiles();
381
382
        if ($this->get_emulate_state()) {
383
            // keep queries if $query['function'] != 'prepare'
384
            $queries = array_filter($queries, function($query) {return $query['function'] !== 'prepare';});
385
        }
386
387
        return $queries;
388
    }
389
390
    /**
391
     * Set YOURLS installed state
392
     *
393
     * @since  1.7.3
394
     * @param  bool $bool
395
     * @return void
396
     */
397
    public function set_installed($bool) {
398
        $this->installed = $bool;
399
    }
400
401
    /**
402
     * Get YOURLS installed state
403
     *
404
     * @since  1.7.3
405
     * @return bool
406
     */
407
    public function is_installed() {
408
        return $this->installed;
409
    }
410
411
    /**
412
     * Return standardized DB version
413
     *
414
     * The regex removes everything that's not a number at the start of the string, or remove anything that's not a number and what
415
     * follows after that.
416
     *   'omgmysql-5.5-ubuntu-4.20' => '5.5'
417
     *   'mysql5.5-ubuntu-4.20'     => '5.5'
418
     *   '5.5-ubuntu-4.20'          => '5.5'
419
     *   '5.5-beta2'                => '5.5'
420
     *   '5.5'                      => '5.5'
421
     *
422
     * @since  1.7.3
423
     * @return string
424
     */
425
    public function mysql_version() {
426
        $version = $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
427
        return $version;
428
    }
429
430
    /**
431
     * Deprecated properties since 1.7.3, unused in 3rd party plugins as far as I know
432
     *
433
     * $ydb->DB_driver
434
     * $ydb->captured_errors
435
     * $ydb->dbh
436
     * $ydb->result
437
     * $ydb->rows_affected
438
     * $ydb->show_errors
439
     */
440
441
    /**
442
     * Deprecated functions since 1.7.3
443
     */
444
445
    // @codeCoverageIgnoreStart
446
447
    public function escape($string) {
448
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
449
        // This will escape using PDO->quote(), but then remove the enclosing quotes
450
        return substr($this->quote($string), 1, -1);
451
    }
452
453
    public function get_col($query) {
454
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
455
        yourls_debug_log('LEGACY SQL: '.$query);
456
        return $this->fetchCol($query);
457
    }
458
459
    public function get_results($query) {
460
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
461
        yourls_debug_log('LEGACY SQL: '.$query);
462
        $stm = parent::query($query);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (query() instead of get_results()). Are you sure this is correct? If so, you might want to change this to $this->query().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
463
        return($stm->fetchAll(PDO::FETCH_OBJ));
464
    }
465
466
    public function get_row($query) {
467
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
468
        yourls_debug_log('LEGACY SQL: '.$query);
469
        $row = $this->fetchObjects($query);
470
        return isset($row[0]) ? $row[0] : null;
471
    }
472
473
    public function get_var($query) {
474
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
475
        yourls_debug_log('LEGACY SQL: '.$query);
476
        return $this->fetchValue($query);
477
    }
478
479
    public function query($query) {
480
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
481
        yourls_debug_log('LEGACY SQL: '.$query);
482
        return $this->fetchAffected($query);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->fetchAffected($query); (integer) is incompatible with the return type declared by the interface Aura\Sql\PdoInterface::query of type PDOStatement.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
483
    }
484
    // @codeCoverageIgnoreEnd
485
}
486