diff --git a/composer.lock b/composer.lock index 988a797..d8919ca 100644 --- a/composer.lock +++ b/composer.lock @@ -66,16 +66,16 @@ }, { "name": "chillerlan/php-qrcode", - "version": "5.0.5", + "version": "4.4.2", "source": { "type": "git", "url": "https://github.com/chillerlan/php-qrcode.git", - "reference": "7b66282572fc14075c0507d74d9837dab25b38d6" + "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/7b66282572fc14075c0507d74d9837dab25b38d6", - "reference": "7b66282572fc14075c0507d74d9837dab25b38d6", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4", + "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4", "shasum": "" }, "require": { @@ -84,15 +84,11 @@ "php": "^7.4 || ^8.0" }, "require-dev": { - "chillerlan/php-authenticator": "^4.3.1 || ^5.2.1", - "ext-fileinfo": "*", - "phan/phan": "^5.5.2", - "phpcompatibility/php-compatibility": "10.x-dev", + "phan/phan": "^5.4.5", "phpmd/phpmd": "^2.15", "phpunit/phpunit": "^9.6", "setasign/fpdf": "^1.8.2", - "slevomat/coding-standard": "^8.23.0", - "squizlabs/php_codesniffer": "^4.0.0" + "squizlabs/php_codesniffer": "^3.11" }, "suggest": { "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", @@ -102,26 +98,17 @@ "type": "library", "autoload": { "psr-4": { - "chillerlan\\QRCode\\": "src" + "chillerlan\\QRCode\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT", - "Apache-2.0" + "MIT" ], "authors": [ { "name": "Kazuhiko Arase", - "homepage": "https://github.com/kazuhikoarase/qrcode-generator" - }, - { - "name": "ZXing Authors", - "homepage": "https://github.com/zxing/zxing" - }, - { - "name": "Ashot Khanamiryan", - "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder" + "homepage": "https://github.com/kazuhikoarase" }, { "name": "Smiley", @@ -133,42 +120,39 @@ "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" } ], - "description": "A QR Code generator and reader with a user-friendly API. PHP 7.4+", + "description": "A QR code generator with a user friendly API. PHP 7.4+", "homepage": "https://github.com/chillerlan/php-qrcode", "keywords": [ "phpqrcode", "qr", "qr code", - "qr-reader", "qrcode", - "qrcode-generator", - "qrcode-reader" + "qrcode-generator" ], "support": { - "docs": "https://php-qrcode.readthedocs.io", "issues": "https://github.com/chillerlan/php-qrcode/issues", - "source": "https://github.com/chillerlan/php-qrcode" + "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.2" }, "funding": [ { "url": "https://ko-fi.com/codemasher", - "type": "Ko-Fi" + "type": "ko_fi" } ], - "time": "2025-11-23T23:51:44+00:00" + "time": "2024-11-15T15:36:24+00:00" }, { "name": "chillerlan/php-settings-container", - "version": "3.2.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/chillerlan/php-settings-container.git", - "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" + "reference": "a0a487cbf5344f721eb504bf0f59bada40c381b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", - "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/a0a487cbf5344f721eb504bf0f59bada40c381b7", + "reference": "a0a487cbf5344f721eb504bf0f59bada40c381b7", "shasum": "" }, "require": { @@ -176,11 +160,13 @@ "php": "^8.1" }, "require-dev": { + "phan/phan": "^5.5.2", "phpmd/phpmd": "^2.15", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-deprecation-rules": "^1.2", + "phpstan/phpstan": "^2.1.31", + "phpstan/phpstan-deprecation-rules": "^2.0.3", "phpunit/phpunit": "^10.5", - "squizlabs/php_codesniffer": "^3.10" + "slevomat/coding-standard": "^8.22", + "squizlabs/php_codesniffer": "^4.0" }, "type": "library", "autoload": { @@ -205,7 +191,8 @@ "Settings", "configuration", "container", - "helper" + "helper", + "property hook" ], "support": { "issues": "https://github.com/chillerlan/php-settings-container/issues", @@ -221,20 +208,20 @@ "type": "ko_fi" } ], - "time": "2024-07-16T11:13:48+00:00" + "time": "2026-03-20T21:10:52+00:00" }, { "name": "composer/ca-bundle", - "version": "1.5.10", + "version": "1.5.12", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63" + "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/961a5e4056dd2e4a2eedcac7576075947c28bf63", - "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", + "reference": "00a2f4201641d5c53f7fc0195e6c8d9fcc321a78", "shasum": "" }, "require": { @@ -281,7 +268,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.10" + "source": "https://github.com/composer/ca-bundle/tree/1.5.12" }, "funding": [ { @@ -293,7 +280,7 @@ "type": "github" } ], - "time": "2025-12-08T15:06:51+00:00" + "time": "2026-05-19T11:26:22+00:00" }, { "name": "geoip2/geoip2", @@ -507,21 +494,21 @@ }, { "name": "onelogin/php-saml", - "version": "4.3.1", + "version": "4.3.2", "source": { "type": "git", "url": "https://github.com/SAML-Toolkits/php-saml.git", - "reference": "b009f160e4ac11f49366a45e0d45706b48429353" + "reference": "26b3a47349415e5b7aa300ba4ab7fc316c65f19e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SAML-Toolkits/php-saml/zipball/b009f160e4ac11f49366a45e0d45706b48429353", - "reference": "b009f160e4ac11f49366a45e0d45706b48429353", + "url": "https://api.github.com/repos/SAML-Toolkits/php-saml/zipball/26b3a47349415e5b7aa300ba4ab7fc316c65f19e", + "reference": "26b3a47349415e5b7aa300ba4ab7fc316c65f19e", "shasum": "" }, "require": { "php": ">=7.3", - "robrichards/xmlseclibs": ">=3.1.4" + "robrichards/xmlseclibs": "^3.1.5" }, "require-dev": { "pdepend/pdepend": "^2.8.0", @@ -567,7 +554,7 @@ "type": "github" } ], - "time": "2025-12-09T10:50:49+00:00" + "time": "2026-05-07T22:38:04+00:00" }, { "name": "ozh/bookmarkletgen", @@ -822,16 +809,16 @@ }, { "name": "rmccue/requests", - "version": "v2.0.17", + "version": "v2.0.18", "source": { "type": "git", "url": "https://github.com/WordPress/Requests.git", - "reference": "74d1648cc34e16a42ea25d548fc73ec107a90421" + "reference": "2e5b8434e0dd54b35bcf1e9a5b52ba2ad84cf773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/Requests/zipball/74d1648cc34e16a42ea25d548fc73ec107a90421", - "reference": "74d1648cc34e16a42ea25d548fc73ec107a90421", + "url": "https://api.github.com/repos/WordPress/Requests/zipball/2e5b8434e0dd54b35bcf1e9a5b52ba2ad84cf773", + "reference": "2e5b8434e0dd54b35bcf1e9a5b52ba2ad84cf773", "shasum": "" }, "require": { @@ -904,20 +891,20 @@ "issues": "https://github.com/WordPress/Requests/issues", "source": "https://github.com/WordPress/Requests" }, - "time": "2025-12-12T17:47:19+00:00" + "time": "2026-04-30T00:41:23+00:00" }, { "name": "robrichards/xmlseclibs", - "version": "3.1.4", + "version": "3.1.5", "source": { "type": "git", "url": "https://github.com/robrichards/xmlseclibs.git", - "reference": "bc87389224c6de95802b505e5265b0ec2c5bcdbd" + "reference": "03062be78178cbb5e8f605cd255dc32a14981f92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/bc87389224c6de95802b505e5265b0ec2c5bcdbd", - "reference": "bc87389224c6de95802b505e5265b0ec2c5bcdbd", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/03062be78178cbb5e8f605cd255dc32a14981f92", + "reference": "03062be78178cbb5e8f605cd255dc32a14981f92", "shasum": "" }, "require": { @@ -944,9 +931,9 @@ ], "support": { "issues": "https://github.com/robrichards/xmlseclibs/issues", - "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.4" + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.5" }, - "time": "2025-12-08T11:57:53+00:00" + "time": "2026-03-13T10:31:56+00:00" }, { "name": "spatie/array-to-xml", @@ -1018,16 +1005,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + "reference": "dc21118016c039a66235cf93d96b435ffb282412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", "shasum": "" }, "require": { @@ -1081,7 +1068,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" }, "funding": [ { @@ -1101,20 +1088,20 @@ "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2026-05-25T15:22:23+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -1166,7 +1153,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -1186,20 +1173,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -1251,7 +1238,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -1271,7 +1258,7 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "yourls/seans-qrcode", @@ -1279,10 +1266,10 @@ "dist": { "type": "path", "url": "user/plugins/seans-qrcode", - "reference": "d162de53f08a19e0b27335362a74788712f1d20e" + "reference": "3e788bc8c3c0079257114a51b0c9f3b4d44dd7cf" }, "require": { - "chillerlan/php-qrcode": "^4.4.0 || ^5.0.3", + "chillerlan/php-qrcode": "^4.4.0", "ext-gd": "*", "ext-mbstring": "*", "php": "^8.0" diff --git a/css/style.css b/css/style.css index 1f10841..d475bca 100644 --- a/css/style.css +++ b/css/style.css @@ -32,7 +32,7 @@ a, a:link, a:active, a:visited { a:hover { text-decoration: underline; } -h1 {height:50px;margin:0;float:right;max-width:500px;} +h1 {margin:0;float:right;max-width:500px;} h1 a {text-align:right;font-size:20px;float:right;} h1 a, h1 a:link, h1 a:active, h1 a:visited {color:#2a85b3} h1 a:hover{text-decoration:none;} @@ -287,6 +287,7 @@ a.bookmarklet:hover { margin-left:15%; padding-left:10px; margin-bottom:5px; + clear: both; } .jquery-notify-bar { diff --git a/includes/Config/Config.php b/includes/Config/Config.php index cef031b..2f726d2 100644 --- a/includes/Config/Config.php +++ b/includes/Config/Config.php @@ -13,48 +13,48 @@ class Config { /** * @var string */ - protected $root; + protected string $root; /** * @var string */ - protected $config; + protected string $config; /** * @since 1.7.3 - * @param string $config Optional user defined config path + * @param string $config Optional user defined config path */ - public function __construct($config = '') { - $this->set_root( $this->fix_win32_path( dirname( dirname( __DIR__ ) ) ) ); + public function __construct(string $config = '') { + $this->set_root( $this->fix_win32_path(dirname(__DIR__, 2)) ); $this->set_config($config); } /** - * Convert antislashes to slashes + * Convert backslashes to slashes * * @since 1.7.3 - * @param string $path + * @param string $path * @return string path with \ converted to / */ - public function fix_win32_path($path) { + public function fix_win32_path(string $path): string { return str_replace('\\', '/', $path); } /** * @since 1.7.3 - * @param string $config path to config file + * @param string $config path to config file * @return void */ - public function set_config($config) { + public function set_config(string $config): void { $this->config = $config; } /** * @since 1.7.3 - * @param string $root path to YOURLS root directory + * @param string $root path to YOURLS root directory * @return void */ - public function set_root($root) { + public function set_root(string $root): void { $this->root = $root; } @@ -65,7 +65,7 @@ public function set_root($root) { * @return string path to found config file * @throws ConfigException */ - public function find_config() { + public function find_config(): string { $config = $this->fix_win32_path($this->config); @@ -99,7 +99,7 @@ public function find_config() { * @return void * @throws ConfigException */ - public function define_core_constants() { + public function define_core_constants(): void { // Check minimal config job has been properly done $must_haves = array('YOURLS_DB_USER', 'YOURLS_DB_PASS', 'YOURLS_DB_NAME', 'YOURLS_DB_HOST', 'YOURLS_DB_PREFIX', 'YOURLS_SITE'); foreach($must_haves as $must_have) { @@ -201,16 +201,11 @@ public function define_core_constants() { if (!defined( 'YOURLS_ADMIN_SSL' )) define( 'YOURLS_ADMIN_SSL', false ); - // if set to true, verbose debug infos. Will break things. Don't enable. - if (!defined( 'YOURLS_DEBUG' )) - define( 'YOURLS_DEBUG', false ); - - // Error reporting - if (defined( 'YOURLS_DEBUG' ) && YOURLS_DEBUG == true ) { - error_reporting( -1 ); - } else { - error_reporting( E_ERROR | E_PARSE ); + // if set to true, verbose debug infos + if (!defined( 'YOURLS_DEBUG' )) { + define('YOURLS_DEBUG', false); } + } } diff --git a/includes/Database/Logger.php b/includes/Database/Logger.php index 3c256db..56bc087 100644 --- a/includes/Database/Logger.php +++ b/includes/Database/Logger.php @@ -52,6 +52,11 @@ class Logger extends AbstractLogger { public function log($level, string|\Stringable $message, array $context = []): void { // if it's an internal SQL query, format the message, otherwise store a string if($level === 'query') { + // Get the real function name that called the query (not just "perform") + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6); + // We should have at index 5 "fetchSomething" (eg fetchAll, fetchOne, etc), otherwise make it "perform" + $function = $backtrace[5]['function'] ?? 'perform'; + $context['function'] = str_starts_with($function, 'fetch') ? $function : 'perform'; $this->messages[] = sprintf( 'SQL %s: %s (%s s)', $context['function'], diff --git a/includes/Database/YDB.php b/includes/Database/YDB.php index 803f345..6be405e 100644 --- a/includes/Database/YDB.php +++ b/includes/Database/YDB.php @@ -13,10 +13,9 @@ namespace YOURLS\Database; -use \Aura\Sql\ExtendedPdo; -use \YOURLS\Database\Profiler; -use \YOURLS\Database\Logger; +use Aura\Sql\ExtendedPdo; use PDO; +use PDOStatement; class YDB extends ExtendedPdo { @@ -24,13 +23,13 @@ class YDB extends ExtendedPdo { * Debug mode, default false * @var bool */ - protected $debug = false; + protected bool $debug = false; /** * Page context (ie "infos", "bookmark", "plugins"...) * @var string */ - protected $context = ''; + protected string $context = ''; /** * Information related to a short URL keyword (e.g. timestamp, long URL, ...) @@ -38,45 +37,50 @@ class YDB extends ExtendedPdo { * @var array * */ - protected $infos = []; + protected array $infos = []; /** * Is YOURLS installed and ready to run? * @var bool */ - protected $installed = false; + protected bool $installed = false; /** * Options * @var array */ - protected $option = []; + protected array $option = []; /** * Plugin admin pages information * @var array */ - protected $plugin_pages = []; + protected array $plugin_pages = []; /** * Plugin information * @var array */ - protected $plugins = []; + protected array $plugins = []; /** * Are we emulating prepare statements ? * @var bool */ - protected $is_emulate_prepare; + protected bool $is_emulate_prepare; + + /** + * Bypass shunt filter? See fetch_wrapper() + * @var bool + */ + private bool $bypass_shunt_filter = false; /** * @since 1.7.3 - * @param string $dsn The data source name - * @param string $user The username - * @param string $pass The password - * @param array $options Driver-specific options - * @param array $attributes Attributes to set after a connection + * @param string $dsn The data source name + * @param string $user The username + * @param string $pass The password + * @param array $options Driver-specific options */ public function __construct($dsn, $user, $pass, $options) { parent::__construct($dsn, $user, $pass, $options); @@ -176,8 +180,8 @@ public function dead_or_error(\Exception $exception) { * Start a Message Logger * * @since 1.7.3 - * @see \Aura\Sql\Profiler\Profiler - * @see \Aura\Sql\Profiler\MemoryLogger + * @see includes/Database/Logger.php + * @see includes/Database/Profiler.php * @return void */ public function start_profiler() { @@ -187,8 +191,8 @@ public function start_profiler() { $this->setProfiler($profiler); /* By default, make "query" the log level. This way, each internal logging triggered - * by Aura SQL will be a "query", and logging triggered by yourls_debug() will be - * a "message". See includes/functions-debug.php:yourls_debug() + * by Aura SQL will be a "query", and logging triggered by yourls_debug_log() will be + * a "debug". See includes/functions-debug.php:yourls_debug_log() */ $profiler->setLoglevel('query'); } @@ -370,7 +374,6 @@ public function remove_plugin_page( $slug ) { unset( $this->plugin_pages[ $slug ] ); } - /** * Return count of SQL queries performed * @@ -427,4 +430,189 @@ public function mysql_version() { return $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION); } + /** + * Fetch the number of affected rows + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @return int Number of affected rows + */ + public function fetchAffected(string $statement, array $values = []): int { + return $this->fetch_wrapper('fetchAffected', $statement, $values); + } + + /** + * Fetch all rows + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @return array All rows returned by the query + */ + public function fetchAll(string $statement, array $values = []): array { + return $this->fetch_wrapper('fetchAll', $statement, $values); + } + + /** + * Fetch all rows as associative arrays + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @return array All rows as associative arrays + */ + public function fetchAssoc(string $statement, array $values = []): array { + return $this->fetch_wrapper('fetchAssoc', $statement, $values); + } + + /** + * Fetch a single column from all rows + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @return array First column values from all rows + */ + public function fetchCol(string $statement, array $values = []): array { + return $this->fetch_wrapper('fetchCol', $statement, $values); + } + + /** + * Fetch rows grouped by the first column + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @param int $style Optional. PDO fetch style constant. Default PDO::FETCH_COLUMN. + * @return array Rows grouped by the first column value + */ + public function fetchGroup(string $statement, array $values = [], int $style = PDO::FETCH_COLUMN): array { + return $this->fetch_wrapper('fetchGroup', $statement, $values, $style); + } + + /** + * Fetch a single row as an object + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @param string $class Optional. Class name for the returned object. Default 'stdClass'. + * @param array $args Optional. Constructor arguments for the class. Default empty array. + * @return object|false Object representing the row, or false if no rows found + */ + public function fetchObject(string $statement, array $values = [], string $class = 'stdClass', array $args = []): object|false { + return $this->fetch_wrapper('fetchObject', $statement, $values, $class, $args); + } + + /** + * Fetch all rows as objects + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @param string $class Optional. Class name for the returned objects. Default 'stdClass'. + * @param array $args Optional. Constructor arguments for the class. Default empty array. + * @return array All rows as objects + */ + public function fetchObjects(string $statement, array $values = [], string $class = 'stdClass', array $args = []): array { + return $this->fetch_wrapper('fetchObjects', $statement, $values, $class, $args); + } + + /** + * Fetch a single row as an array + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @return array|false Associative array representing the row, or false if no rows found + */ + public function fetchOne(string $statement, array $values = []): array|false { + return $this->fetch_wrapper('fetchOne', $statement, $values); + } + + /** + * Fetch key-value pairs + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @return array Associative array of key-value pairs + */ + public function fetchPairs(string $statement, array $values = []): array { + return $this->fetch_wrapper('fetchPairs', $statement, $values); + } + + /** + * Fetch a single value + * + * @since 1.10.4 + * @param string $statement SQL statement to execute + * @param array $values Optional. Values to bind to the statement. Default empty array. + * @return mixed Single value from the query result + */ + public function fetchValue(string $statement, array $values = []): mixed { + return $this->fetch_wrapper('fetchValue', $statement, $values); + } + + /** + * Performs a query with bound values and returns the resulting PDOStatement + * You most likely should not use this method directly. Use the fetch_* methods instead. + * + * @since 1.10.4 + * @param string $statement The SQL statement to perform. + * @param array $values Values to bind to the query + * @return PDOStatement + */ + public function perform(string $statement, array $values = []): PDOStatement { + return $this->fetch_wrapper('perform', $statement, $values); + } + + /** + * Wrapper for all fetch methods, allowing plugins to intercept and modify query results. + * + * @since 1.10.4 + * @param string $method The parent fetch method name to call (e.g., 'fetchAll', 'fetchValue') + * @param mixed ...$args Variable number of arguments to pass to the parent method + * @return mixed The cached result if available, otherwise the fresh query result + */ + public function fetch_wrapper(string $method, ...$args): mixed { + // Allow plugins to short-circuit the whole function if we're not in bypass mode + if (!$this->bypass_shunt_filter) { + $pre = yourls_apply_filter('shunt_fetch_wrapper', yourls_shunt_default(), $method, ...$args); + if (yourls_shunt_default() !== $pre) { + return $pre; + } + } + + // Filter the query statement + $args[0] = yourls_apply_filter('fetch_wrapper_statement', $args[0], $method, $args); + + return parent::$method( ...$args); + } + + /** + * Execute a callback with filters temporarily disabled + * + * This method allows bypassing the plugin filter system for the duration of the callback execution. Useful to + * prevent infinite loops when a filter needs to call the original method without re-triggering itself. + * + * Example usage: + * $ydb = yourls_get_db('write-get_from_cache'); + * $result = $ydb->withoutFilters(function($db) use ($method, $args) { + * return $db->fetch_wrapper($method, ...$args); + * }); + * + * @since 1.10.4 + * @param callable $callback + * @return mixed + */ + public function without_filters(callable $callback): mixed { + $this->bypass_shunt_filter = true; + try { + return $callback($this); + } finally { + $this->bypass_shunt_filter = false; + } + } } diff --git a/includes/class-mysql.php b/includes/class-mysql.php index 603c87c..6443636 100644 --- a/includes/class-mysql.php +++ b/includes/class-mysql.php @@ -4,7 +4,7 @@ * Connect to DB * * @since 1.0 - * @param string $context Optional context. Default: ''. + * @param string $context Optional context. Default: ''. See yourls_get_db() * @return \YOURLS\Database\YDB */ function yourls_db_connect($context = '') { @@ -60,11 +60,7 @@ function yourls_db_connect($context = '') { $ydb->init(); // Past this point, we're connected - $msg = 'Connected to ' . $dsn; - if ($context !== '') { - $msg .= ', context: ' . $context; - } - yourls_debug_log( $msg ); + yourls_debug_log( 'Connected to ' . $dsn ); yourls_debug_mode( YOURLS_DEBUG ); @@ -96,8 +92,8 @@ function yourls_db_connect($context = '') { */ function yourls_get_db($context = '') { // Allow plugins to short-circuit the whole function - $pre = yourls_apply_filter( 'shunt_get_db', false, $context ); - if ( false !== $pre ) { + $pre = yourls_apply_filter( 'shunt_get_db', yourls_shunt_default(), $context ); + if ( yourls_shunt_default() !== $pre ) { return $pre; } diff --git a/includes/functions-auth.php b/includes/functions-auth.php index cadf538..8a702b8 100644 --- a/includes/functions-auth.php +++ b/includes/functions-auth.php @@ -25,8 +25,8 @@ function yourls_maybe_require_auth() { */ function yourls_is_valid_user() { // Allow plugins to short-circuit the whole function - $pre = yourls_apply_filter( 'shunt_is_valid_user', null ); - if ( null !== $pre ) { + $pre = yourls_apply_filter( 'shunt_is_valid_user', yourls_shunt_default() ); + if ( yourls_shunt_default() !== $pre ) { return $pre; } @@ -702,14 +702,13 @@ function yourls_verify_nonce($action, $nonce = false, $user = false, $return = ' } /** - * Check if YOURLS_USER comes from environment variables + * Check if user credentials comes from environment variables * * @since 1.8.2 - * @return bool true if YOURLS_USER and YOURLS_PASSWORD are defined as environment variables + * @return bool true if credentials are defined as environment variables */ function yourls_is_user_from_env() { - return yourls_apply_filter('is_user_from_env', getenv('YOURLS_USER') && getenv('YOURLS_PASSWORD')); - + return yourls_apply_filter('is_user_from_env', getenv('YOURLS_PASSWORD') || getenv('YOURLS_PASS') || getenv('YOURLS_PASS_FILE')); } /** diff --git a/includes/functions-debug.php b/includes/functions-debug.php index cac4f2d..20f5178 100644 --- a/includes/functions-debug.php +++ b/includes/functions-debug.php @@ -6,16 +6,19 @@ /** * Add a message to the debug log * - * When in debug mode (YOURLS_DEBUG == true) the debug log is echoed in yourls_html_footer() + * When in debug mode (YOURLS_DEBUG == true at startup or yourls_debug_mode() set to true later on), the debug log is + * echoed in yourls_html_footer(). * Log messages are appended to $ydb->debug_log array, which is instantiated within class Database\YDB * * @since 1.7 * @param string $msg Message to add to the debug log * @return string The message itself */ -function yourls_debug_log( $msg ) { - yourls_do_action( 'debug_log', $msg ); - yourls_get_db('read-debug_log')->getProfiler()->getLogger()->log('debug', $msg); +function yourls_debug_log(string $msg): string { + if (yourls_get_debug_mode()) { + yourls_do_action('debug_log', $msg); + yourls_get_db('read-debug_log')->getProfiler()->getLogger()->log('debug', $msg); + } return $msg; } @@ -25,7 +28,7 @@ function yourls_debug_log( $msg ) { * @since 1.7.3 * @return array */ -function yourls_get_debug_log() { +function yourls_get_debug_log(): array { return yourls_get_db('read-get_debug_log')->getProfiler()->getLogger()->getMessages(); } @@ -34,7 +37,7 @@ function yourls_get_debug_log() { * * @return int */ -function yourls_get_num_queries() { +function yourls_get_num_queries(): int { return yourls_apply_filter( 'get_num_queries', yourls_get_db('read-get_num_queries')->get_num_queries() ); } @@ -45,7 +48,7 @@ function yourls_get_num_queries() { * @param bool $bool Debug on or off * @return void */ -function yourls_debug_mode( $bool ) { +function yourls_debug_mode(bool $bool): void { // log queries if true yourls_get_db('read-debug_mode')->getProfiler()->setActive( (bool)$bool ); @@ -60,6 +63,6 @@ function yourls_debug_mode( $bool ) { * @since 1.7.7 * @return bool */ -function yourls_get_debug_mode() { - return defined( 'YOURLS_DEBUG' ) && YOURLS_DEBUG; +function yourls_get_debug_mode(): bool { + return yourls_get_db('read-debug_mode')->getProfiler()->isActive(); } diff --git a/includes/functions-geo.php b/includes/functions-geo.php index 2dd7898..e93e959 100644 --- a/includes/functions-geo.php +++ b/includes/functions-geo.php @@ -14,11 +14,13 @@ */ function yourls_geo_ip_to_countrycode( $ip = '', $default = '' ) { // Allow plugins to short-circuit the Geo IP API - $location = yourls_apply_filter( 'shunt_geo_ip_to_countrycode', false, $ip, $default ); // at this point $ip can be '', check if your plugin hooks in here - if ( false !== $location ) { + $location = yourls_apply_filter( 'shunt_geo_ip_to_countrycode', yourls_shunt_default(), $ip, $default ); // at this point $ip can be '', check if your plugin hooks in here + if ( yourls_shunt_default() !== $location ) { return $location; } + $location = false; + if ( yourls_apply_filter( 'geo_use_cloudflare', true ) && !empty( $_SERVER[ 'HTTP_CF_IPCOUNTRY' ] ) && preg_match( '/^[A-Z]{2}$/', $_SERVER[ 'HTTP_CF_IPCOUNTRY' ] ) ) { return $_SERVER[ 'HTTP_CF_IPCOUNTRY' ]; @@ -74,11 +76,13 @@ function yourls_geo_ip_to_countrycode( $ip = '', $default = '' ) { */ function yourls_geo_countrycode_to_countryname( $code ) { // Allow plugins to short-circuit the function - $country = yourls_apply_filter( 'shunt_geo_countrycode_to_countryname', false, $code ); - if ( false !== $country ) { + $country = yourls_apply_filter( 'shunt_geo_countrycode_to_countryname', yourls_shunt_default(), $code ); + if ( yourls_shunt_default() !== $country ) { return $country; } + $country = false; + // Weeeeeeeeeeee $countries = [ 'A1' => 'Anonymous Proxy', 'A2' => 'Satellite Provider', 'AD' => 'Andorra', 'AE' => 'United Arab Emirates', 'AF' => 'Afghanistan', diff --git a/includes/functions-html.php b/includes/functions-html.php index aadf6f6..4fe0a05 100644 --- a/includes/functions-html.php +++ b/includes/functions-html.php @@ -181,8 +181,8 @@ function yourls_html_footer($can_query = true) { * @return void */ function yourls_html_addnew( $url = '', $keyword = '' ) { - $pre = yourls_apply_filter( 'shunt_html_addnew', false, $url, $keyword ); - if ( false !== $pre ) { + $pre = yourls_apply_filter( 'shunt_html_addnew', yourls_shunt_default(), $url, $keyword ); + if ( yourls_shunt_default() !== $pre ) { return $pre; } ?> @@ -439,9 +439,10 @@ function yourls_share_box( $longurl, $shorturl, $title = '', $text='', $shortlin $share_title = '
%1$s hit on %2$s', '%1$s hits on %2$s', $best['max'] ), $best['max'], yourls_date_i18n( yourls_get_date_format("F j, Y"), strtotime( $best['day'] ) ) ); ?>. +
%1$s hit on %2$s', '%1$s hits on %2$s', $best['max'] ), $best['max'], yourls_date_i18n( yourls_get_date_format(yourls__("F j, Y")), strtotime( $best['day'] ) ) ); ?>.