| [ Index ] |
PHP Cross Reference of YOURLS |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Aura SQL wrapper for YOURLS that creates the almighty 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 (e.g. 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 Aura\Sql\ExtendedPdo; 17 use PDO; 18 use PDOStatement; 19 20 class YDB extends ExtendedPdo { 21 22 /** 23 * Debug mode, default false 24 * @var bool 25 */ 26 protected bool $debug = false; 27 28 /** 29 * Page context (ie "infos", "bookmark", "plugins"...) 30 * @var string 31 */ 32 protected string $context = ''; 33 34 /** 35 * Information related to a short URL keyword (e.g. timestamp, long URL, ...) 36 * 37 * @var array 38 * 39 */ 40 protected array $infos = []; 41 42 /** 43 * Is YOURLS installed and ready to run? 44 * @var bool 45 */ 46 protected bool $installed = false; 47 48 /** 49 * Options 50 * @var array 51 */ 52 protected array $option = []; 53 54 /** 55 * Plugin admin pages information 56 * @var array 57 */ 58 protected array $plugin_pages = []; 59 60 /** 61 * Plugin information 62 * @var array 63 */ 64 protected array $plugins = []; 65 66 /** 67 * Are we emulating prepare statements ? 68 * @var bool 69 */ 70 protected bool $is_emulate_prepare; 71 72 /** 73 * Bypass shunt filter? See fetch_wrapper() 74 * @var bool 75 */ 76 private bool $bypass_shunt_filter = false; 77 78 /** 79 * @since 1.7.3 80 * @param string $dsn The data source name 81 * @param string $user The username 82 * @param string $pass The password 83 * @param array $options Driver-specific options 84 */ 85 public function __construct($dsn, $user, $pass, $options) { 86 parent::__construct($dsn, $user, $pass, $options); 87 } 88 89 /** 90 * Init everything needed 91 * 92 * Everything we need to set up is done here in init(), not in the constructor, so even 93 * when the connection fails (e.g. config error or DB dead), the constructor has worked, 94 * and we have a $ydb object properly instantiated (and for instance yourls_die() can 95 * correctly die, even if using $ydb methods) 96 * 97 * @since 1.7.3 98 * @return void 99 */ 100 public function init() { 101 $this->connect_to_DB(); 102 103 $this->set_emulate_state(); 104 105 $this->start_profiler(); 106 } 107 108 /** 109 * Check if we emulate prepare statements, and set bool flag accordingly 110 * 111 * Check if current driver can PDO::getAttribute(PDO::ATTR_EMULATE_PREPARES) 112 * Some combinations of PHP/MySQL don't support this function. See 113 * https://travis-ci.org/YOURLS/YOURLS/jobs/271423782#L481 114 * 115 * @since 1.7.3 116 * @return void 117 */ 118 public function set_emulate_state() { 119 try { 120 $this->is_emulate_prepare = $this->getAttribute(PDO::ATTR_EMULATE_PREPARES); 121 } catch (\PDOException $e) { 122 $this->is_emulate_prepare = false; 123 } 124 } 125 126 /** 127 * Get emulate status 128 * 129 * @since 1.7.3 130 * @return bool 131 */ 132 public function get_emulate_state() { 133 return $this->is_emulate_prepare; 134 } 135 136 /** 137 * Initiate real connection to DB server 138 * 139 * This is to check that the server is running and/or the config is OK 140 * 141 * @since 1.7.3 142 * @return void 143 * @throws \PDOException 144 */ 145 public function connect_to_DB() { 146 try { 147 list($dsn, $_user, $_pwd, $_opt, $_queries) = $this->args; 148 $this->connect($dsn); 149 } catch ( \Exception $e ) { 150 $this->dead_or_error($e); 151 } 152 } 153 154 /** 155 * Die with an error message 156 * 157 * @since 1.7.3 158 * 159 * @param \Exception $exception 160 * 161 * @return void 162 */ 163 public function dead_or_error(\Exception $exception) { 164 // Use any /user/db_error.php file 165 $file = YOURLS_USERDIR . '/db_error.php'; 166 if(file_exists($file)) { 167 if(yourls_include_file_sandbox( $file ) === true) { 168 die(); 169 } 170 } 171 172 $message = yourls__( 'Incorrect DB config, or could not connect to DB' ); 173 $message .= '<br/>' . get_class($exception) .': ' . $exception->getMessage(); 174 yourls_die( yourls__( $message ), yourls__( 'Fatal error' ), 503 ); 175 die(); 176 177 } 178 179 /** 180 * Start a Message Logger 181 * 182 * @since 1.7.3 183 * @see includes/Database/Logger.php 184 * @see includes/Database/Profiler.php 185 * @return void 186 */ 187 public function start_profiler() { 188 // Instantiate a custom logger and make it the profiler 189 $yourls_logger = new Logger(); 190 $profiler = new Profiler($yourls_logger); 191 $this->setProfiler($profiler); 192 193 /* By default, make "query" the log level. This way, each internal logging triggered 194 * by Aura SQL will be a "query", and logging triggered by yourls_debug_log() will be 195 * a "debug". See includes/functions-debug.php:yourls_debug_log() 196 */ 197 $profiler->setLoglevel('query'); 198 } 199 200 /** 201 * @param string $context 202 * @return void 203 */ 204 public function set_html_context($context) { 205 $this->context = $context; 206 } 207 208 /** 209 * @return string 210 */ 211 public function get_html_context() { 212 return $this->context; 213 } 214 215 // Options low level functions, see \YOURLS\Database\Options 216 217 /** 218 * @param string $name 219 * @param mixed $value 220 * @return void 221 */ 222 public function set_option($name, $value) { 223 $this->option[$name] = $value; 224 } 225 226 /** 227 * @param string $name 228 * @return bool 229 */ 230 public function has_option($name) { 231 return array_key_exists($name, $this->option); 232 } 233 234 /** 235 * @param string $name 236 * @return string 237 */ 238 public function get_option($name) { 239 return $this->option[$name]; 240 } 241 242 /** 243 * @param string $name 244 * @return void 245 */ 246 public function delete_option($name) { 247 unset($this->option[$name]); 248 } 249 250 251 // Infos (related to keyword) low level functions 252 253 /** 254 * @param string $keyword 255 * @param mixed $infos 256 * @return void 257 */ 258 public function set_infos($keyword, $infos) { 259 $this->infos[$keyword] = $infos; 260 } 261 262 /** 263 * @param string $keyword 264 * @return bool 265 */ 266 public function has_infos($keyword) { 267 return array_key_exists($keyword, $this->infos); 268 } 269 270 /** 271 * @param string $keyword 272 * @return array 273 */ 274 public function get_infos($keyword) { 275 return $this->infos[$keyword]; 276 } 277 278 /** 279 * @param string $keyword 280 * @return void 281 */ 282 public function delete_infos($keyword) { 283 if (isset($this->infos[$keyword])) { 284 unset($this->infos[$keyword]); 285 } 286 } 287 288 /** 289 * @param string $keyword 290 * @param mixed $infos 291 * @return void 292 */ 293 public function update_infos_if_exists($keyword, $infos) { 294 if ($this->has_infos($keyword) && $this->infos[$keyword]) { 295 $this->infos[$keyword] = array_merge($this->infos[$keyword], $infos); 296 } 297 } 298 299 /** 300 * @todo: infos & options are working the same way here. Abstract this. 301 */ 302 303 304 // Plugin low level functions, see functions-plugins.php 305 306 /** 307 * @return array 308 */ 309 public function get_plugins() { 310 return $this->plugins; 311 } 312 313 /** 314 * @param array $plugins 315 * @return void 316 */ 317 public function set_plugins(array $plugins) { 318 $this->plugins = $plugins; 319 } 320 321 /** 322 * @param string $plugin plugin filename 323 * @return void 324 */ 325 public function add_plugin($plugin) { 326 $this->plugins[] = $plugin; 327 } 328 329 /** 330 * @param string $plugin plugin filename 331 * @return void 332 */ 333 public function remove_plugin($plugin) { 334 unset($this->plugins[$plugin]); 335 } 336 337 338 // Plugin Pages low level functions, see functions-plugins.php 339 340 /** 341 * @return array 342 */ 343 public function get_plugin_pages() { 344 return is_array( $this->plugin_pages ) ? $this->plugin_pages : []; 345 } 346 347 /** 348 * @param array $pages 349 * @return void 350 */ 351 public function set_plugin_pages(array $pages) { 352 $this->plugin_pages = $pages; 353 } 354 355 /** 356 * @param string $slug 357 * @param string $title 358 * @param callable $function 359 * @return void 360 */ 361 public function add_plugin_page( $slug, $title, $function ) { 362 $this->plugin_pages[ $slug ] = [ 363 'slug' => $slug, 364 'title' => $title, 365 'function' => $function, 366 ]; 367 } 368 369 /** 370 * @param string $slug 371 * @return void 372 */ 373 public function remove_plugin_page( $slug ) { 374 unset( $this->plugin_pages[ $slug ] ); 375 } 376 377 /** 378 * Return count of SQL queries performed 379 * 380 * @since 1.7.3 381 * @return int 382 */ 383 public function get_num_queries() { 384 return count( (array) $this->get_queries() ); 385 } 386 387 /** 388 * Return SQL queries performed 389 * 390 * @since 1.7.3 391 * @return array 392 */ 393 public function get_queries() { 394 $queries = $this->getProfiler()->getLogger()->getMessages(); 395 396 // Only keep messages that start with "SQL " 397 $queries = array_filter($queries, function($query) {return substr( $query, 0, 4 ) === "SQL ";}); 398 399 return $queries; 400 } 401 402 /** 403 * Set YOURLS installed state 404 * 405 * @since 1.7.3 406 * @param bool $bool 407 * @return void 408 */ 409 public function set_installed($bool) { 410 $this->installed = $bool; 411 } 412 413 /** 414 * Get YOURLS installed state 415 * 416 * @since 1.7.3 417 * @return bool 418 */ 419 public function is_installed() { 420 return $this->installed; 421 } 422 423 /** 424 * Return MySQL version 425 * 426 * @since 1.7.3 427 * @return string 428 */ 429 public function mysql_version() { 430 return $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION); 431 } 432 433 /** 434 * Fetch the number of affected rows 435 * 436 * @since 1.10.4 437 * @param string $statement SQL statement to execute 438 * @param array $values Optional. Values to bind to the statement. Default empty array. 439 * @return int Number of affected rows 440 */ 441 public function fetchAffected(string $statement, array $values = []): int { 442 return $this->fetch_wrapper('fetchAffected', $statement, $values); 443 } 444 445 /** 446 * Fetch all rows 447 * 448 * @since 1.10.4 449 * @param string $statement SQL statement to execute 450 * @param array $values Optional. Values to bind to the statement. Default empty array. 451 * @return array All rows returned by the query 452 */ 453 public function fetchAll(string $statement, array $values = []): array { 454 return $this->fetch_wrapper('fetchAll', $statement, $values); 455 } 456 457 /** 458 * Fetch all rows as associative arrays 459 * 460 * @since 1.10.4 461 * @param string $statement SQL statement to execute 462 * @param array $values Optional. Values to bind to the statement. Default empty array. 463 * @return array All rows as associative arrays 464 */ 465 public function fetchAssoc(string $statement, array $values = []): array { 466 return $this->fetch_wrapper('fetchAssoc', $statement, $values); 467 } 468 469 /** 470 * Fetch a single column from all rows 471 * 472 * @since 1.10.4 473 * @param string $statement SQL statement to execute 474 * @param array $values Optional. Values to bind to the statement. Default empty array. 475 * @return array First column values from all rows 476 */ 477 public function fetchCol(string $statement, array $values = []): array { 478 return $this->fetch_wrapper('fetchCol', $statement, $values); 479 } 480 481 /** 482 * Fetch rows grouped by the first column 483 * 484 * @since 1.10.4 485 * @param string $statement SQL statement to execute 486 * @param array $values Optional. Values to bind to the statement. Default empty array. 487 * @param int $style Optional. PDO fetch style constant. Default PDO::FETCH_COLUMN. 488 * @return array Rows grouped by the first column value 489 */ 490 public function fetchGroup(string $statement, array $values = [], int $style = PDO::FETCH_COLUMN): array { 491 return $this->fetch_wrapper('fetchGroup', $statement, $values, $style); 492 } 493 494 /** 495 * Fetch a single row as an object 496 * 497 * @since 1.10.4 498 * @param string $statement SQL statement to execute 499 * @param array $values Optional. Values to bind to the statement. Default empty array. 500 * @param string $class Optional. Class name for the returned object. Default 'stdClass'. 501 * @param array $args Optional. Constructor arguments for the class. Default empty array. 502 * @return object|false Object representing the row, or false if no rows found 503 */ 504 public function fetchObject(string $statement, array $values = [], string $class = 'stdClass', array $args = []): object|false { 505 return $this->fetch_wrapper('fetchObject', $statement, $values, $class, $args); 506 } 507 508 /** 509 * Fetch all rows as objects 510 * 511 * @since 1.10.4 512 * @param string $statement SQL statement to execute 513 * @param array $values Optional. Values to bind to the statement. Default empty array. 514 * @param string $class Optional. Class name for the returned objects. Default 'stdClass'. 515 * @param array $args Optional. Constructor arguments for the class. Default empty array. 516 * @return array All rows as objects 517 */ 518 public function fetchObjects(string $statement, array $values = [], string $class = 'stdClass', array $args = []): array { 519 return $this->fetch_wrapper('fetchObjects', $statement, $values, $class, $args); 520 } 521 522 /** 523 * Fetch a single row as an array 524 * 525 * @since 1.10.4 526 * @param string $statement SQL statement to execute 527 * @param array $values Optional. Values to bind to the statement. Default empty array. 528 * @return array|false Associative array representing the row, or false if no rows found 529 */ 530 public function fetchOne(string $statement, array $values = []): array|false { 531 return $this->fetch_wrapper('fetchOne', $statement, $values); 532 } 533 534 /** 535 * Fetch key-value pairs 536 * 537 * @since 1.10.4 538 * @param string $statement SQL statement to execute 539 * @param array $values Optional. Values to bind to the statement. Default empty array. 540 * @return array Associative array of key-value pairs 541 */ 542 public function fetchPairs(string $statement, array $values = []): array { 543 return $this->fetch_wrapper('fetchPairs', $statement, $values); 544 } 545 546 /** 547 * Fetch a single value 548 * 549 * @since 1.10.4 550 * @param string $statement SQL statement to execute 551 * @param array $values Optional. Values to bind to the statement. Default empty array. 552 * @return mixed Single value from the query result 553 */ 554 public function fetchValue(string $statement, array $values = []): mixed { 555 return $this->fetch_wrapper('fetchValue', $statement, $values); 556 } 557 558 /** 559 * Performs a query with bound values and returns the resulting PDOStatement 560 * You most likely should not use this method directly. Use the fetch_* methods instead. 561 * 562 * @since 1.10.4 563 * @param string $statement The SQL statement to perform. 564 * @param array $values Values to bind to the query 565 * @return PDOStatement 566 */ 567 public function perform(string $statement, array $values = []): PDOStatement { 568 return $this->fetch_wrapper('perform', $statement, $values); 569 } 570 571 /** 572 * Wrapper for all fetch methods, allowing plugins to intercept and modify query results. 573 * 574 * @since 1.10.4 575 * @param string $method The parent fetch method name to call (e.g., 'fetchAll', 'fetchValue') 576 * @param mixed ...$args Variable number of arguments to pass to the parent method 577 * @return mixed The cached result if available, otherwise the fresh query result 578 */ 579 public function fetch_wrapper(string $method, ...$args): mixed { 580 // Allow plugins to short-circuit the whole function if we're not in bypass mode 581 if (!$this->bypass_shunt_filter) { 582 $pre = yourls_apply_filter('shunt_fetch_wrapper', yourls_shunt_default(), $method, ...$args); 583 if (yourls_shunt_default() !== $pre) { 584 return $pre; 585 } 586 } 587 588 // Filter the query statement 589 $args[0] = yourls_apply_filter('fetch_wrapper_statement', $args[0], $method, $args); 590 591 return parent::$method( ...$args); 592 } 593 594 /** 595 * Execute a callback with filters temporarily disabled 596 * 597 * This method allows bypassing the plugin filter system for the duration of the callback execution. Useful to 598 * prevent infinite loops when a filter needs to call the original method without re-triggering itself. 599 * 600 * Example usage: 601 * $ydb = yourls_get_db('write-get_from_cache'); 602 * $result = $ydb->withoutFilters(function($db) use ($method, $args) { 603 * return $db->fetch_wrapper($method, ...$args); 604 * }); 605 * 606 * @since 1.10.4 607 * @param callable $callback 608 * @return mixed 609 */ 610 public function without_filters(callable $callback): mixed { 611 $this->bypass_shunt_filter = true; 612 try { 613 return $callback($this); 614 } finally { 615 $this->bypass_shunt_filter = false; 616 } 617 } 618 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Wed Apr 8 05:10:41 2026 | Cross-referenced by PHPXref 0.7.1 |