Fixed Broken Shit

This commit is contained in:
2022-10-30 14:32:20 -07:00
parent 7335796263
commit 4dabf5a6bf
635 changed files with 74885 additions and 17688 deletions

View File

@ -0,0 +1,108 @@
<?php
/**
* Custom logger for YOURLS that logs debug messages and queries.
*
* Based on \Aura\Sql\Profiler\MemoryLogger
*
* @since 1.7.10
*/
namespace YOURLS\Database;
use Psr\Log\AbstractLogger;
class Logger extends AbstractLogger {
/**
* Log messages.
*
* @var array
*/
protected $messages = [];
/**
* Logs a message.
*
* @param string $level The log level (ie type of message)
* @param string $message The log message.
* @param array $context Data to interpolate into the message.
*
* The logger receives the following:
*
* From yourls_debug("something went wrong") :
* $level : string 'debug'
* $message : string 'something went wrong'
* $context : array()
* See yourls_debug() in includes/functions-debug.php
*
* From a query that triggers the internal logging of Aura SQL :
* $level : string 'query'
* $message : string '{function} ({duration} seconds): {statement} {backtrace}'
* (which is the default $logFormat from Aura\Sql\Profiler\Profiler), we're not using it)
* $context : array(
* 'function' => string 'perform'
* 'duration' => float 0.0025360584259033
* 'statement' => string 'SELECT `keyword`,`url` FROM `yourls_url` WHERE `url` LIKE (:url)'
* 'values' => array('url' => '%rss%')
* )
* See finish() in Aura\Sql\Profiler\Profiler
*
* @return void
*/
public function log($level, $message, array $context = []) {
// if it's an internal SQL query, format the message, otherwise store a string
if($level === 'query') {
$this->messages[] = sprintf(
'SQL %s: %s (%s s)',
$context['function'],
$this->pretty_format($context['statement'], $context['values']),
number_format($context['duration'], 5)
);
} else {
$this->messages[] = (string)$message;
}
}
/**
* Returns the logged messages.
*
* @return array
*/
public function getMessages() {
return $this->messages;
}
/**
* Format PDO statement with bind/values replacement
*
* This replaces PDO binds such as 'key_name = :name' with corresponding array values, eg array('name'=>'some value')
* This is merely a cosmetic replacement to allow for readability: the result WILL NOT be valid SQL! (eg no proper quotes)
*
* @since 1.7.3
* @param string $statement SQL query with PDO style named placeholders
* @param array $values Optional array of values corresponding to placeholders
* @return string Readable SQL query with placeholders replaced
*/
public function pretty_format($statement, array $values = array() ) {
if (!$values) {
return $statement;
}
return preg_replace_callback(
'/:([^\s;)]*)/',
/**
* @param string $matches
*/
function ($matches) use ($values) {
$replacement = isset( $values[$matches[1]] ) ? $values[$matches[1]] : '';
if(is_array($replacement)) {
$replacement = implode(",", $replacement);
}
return "'$replacement'";
},
$statement
);
}
}

View File

@ -0,0 +1,254 @@
<?php
/**
* YOURLS Options
*
* Note to plugin authors: you most likely SHOULD NOT use directly methods and properties of this class. Use instead
* function wrappers (eg don't use $ydb->option, or $ydb->get(), use yourls_*_options() functions instead).
*
* Note to devs: this class internally uses function wrappers eg yourls_*_options() instead of direct methods, to
* comply to any filter set in the function wrappers (eg $this->update() uses yourls_get_option()).
* Maybe in the future this will look as a dumb idea?
* The alternative would be to move return filters from function wrappers to here, but I think this will make things
* less readable for users.
*
* @since 1.7.3
*/
namespace YOURLS\Database;
use YOURLS\Database\YDB;
use PDOException;
class Options {
/**
* Hold a copy of the all mighty $ydb global
*
* @var \YOURLS\Database\YDB
*/
protected $ydb;
public function __construct(YDB $ydb) {
$this->ydb = $ydb;
}
/**
* Read all options from DB at once, return bool
*
* @since 1.7.3
* @see yourls_get_all_options()
* @return bool True on success, false on failure (eg table missing or empty)
*/
public function get_all_options() {
// Get option values from DB
$table = YOURLS_DB_TABLE_OPTIONS;
$sql = "SELECT option_name, option_value FROM $table WHERE 1=1";
try {
$options = (array) $this->ydb->fetchPairs($sql);
} catch ( PDOException $e ) {
// We could not fetch value from the table. Let's check if the option table exists
try {
$check = $this->ydb->fetchAffected(sprintf("SHOW TABLES LIKE '%s'", $table));
// Table doesn't exist
if ($check ==0) {
return false;
}
// Error at this point means the database isn't readable
} catch ( PDOException $e ) {
$this->ydb->dead_or_error($e);
}
}
// Unlikely scenario, but who knows: table exists, but is empty
if (empty($options)) {
return false;
}
foreach ($options as $name => $value) {
$this->ydb->set_option($name, yourls_maybe_unserialize($value));
}
yourls_apply_filter('get_all_options', 'deprecated');
return true;
}
/**
* Get option value from DB (or from cache if available). Return value or $default if not found
*
* @since 1.7.3
* @see yourls_get_option()
* @param string $name Option name
* @param string $default Value to return if option doesn't exist
* @return mixed Value set for the option
*/
public function get($name, $default) {
$name = trim((string)$name);
if (empty($name)) {
return $default;
}
// Check if option value is cached already
if($this->ydb->has_option($name)) {
return $this->ydb->get_option($name);
}
// Get option value from DB
$table = YOURLS_DB_TABLE_OPTIONS;
$sql = "SELECT option_value FROM $table WHERE option_name = :option_name LIMIT 1";
$bind = array('option_name' => $name);
// Use fechOne() to get array('option_value'=>$value), or false if not found.
// This way, we can effectively store false as an option value, and not confuse with false as the default return value
$value = $this->ydb->fetchOne($sql, $bind);
if($value !== false) {
$value = yourls_maybe_unserialize( $value['option_value'] );
// Cache option value to save a DB query if needed later
$this->ydb->set_option($name, $value);
} else {
$value = $default;
}
/**
* We don't cache value if option is not set, to make a difference between "not found: returning false"
* and "found, and value is false".
* This way, we can:
* $check = yourls_get_option('doesnt_exist'); // false
* yourls_add_option('doesnt_exist', 'value'); // will work, because check on has_option() will return false
*/
return $value;
}
/**
* Update (add if doesn't exist) an option to DB
*
* @since 1.7.3
* @see yourls_update_option()
* @param string $name Option name. Expected to not be SQL-escaped.
* @param mixed $newvalue Option value.
* @return bool False if value was not updated, true otherwise.
*/
public function update($name, $newvalue) {
$name = trim((string)$name);
if (empty($name)) {
return false;
}
// Use clone to break object refs -- see commit 09b989d375bac65e692277f61a84fede2fb04ae3
if (is_object($newvalue)) {
$newvalue = clone $newvalue;
}
$oldvalue = yourls_get_option($name);
// If the new and old values are the same, no need to update.
if ($newvalue === $oldvalue) {
return false;
}
// If this is a new option, just add it
if (false === $oldvalue) {
return $this->add($name, $newvalue);
}
$_newvalue = yourls_maybe_serialize($newvalue);
$table = YOURLS_DB_TABLE_OPTIONS;
$sql = "UPDATE $table SET option_value = :value WHERE option_name = :name";
$bind = array('name' => $name, 'value' => $_newvalue);
$do = $this->ydb->fetchAffected($sql, $bind);
if($do !== 1) {
// Something went wrong :(
return false;
}
// Cache option value to save a DB query if needed later
$this->ydb->set_option($name, $newvalue);
yourls_do_action( 'update_option', $name, $oldvalue, $newvalue );
return true;
}
/**
* Add an option to the DB
*
* @since 1.7.3
* @see yourls_add_option()
* @param string $name Name of option to add. Expected to not be SQL-escaped.
* @param mixed $value Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @return bool False if option was not added (eg already exists), true otherwise.
*/
public function add($name, $value) {
$name = trim((string)$name);
if (empty($name)) {
return false;
}
// Use clone to break object refs -- see commit 09b989d375bac65e692277f61a84fede2fb04ae3
if (is_object($value)) {
$value = clone $value;
}
// Make sure the option doesn't already exist
if ($this->ydb->has_option($name)) {
return false;
}
// if (false !== yourls_get_option($name)) {
// return false;
// }
$table = YOURLS_DB_TABLE_OPTIONS;
$_value = yourls_maybe_serialize($value);
$sql = "INSERT INTO $table (option_name, option_value) VALUES (:name, :value)";
$bind = array('name' => $name, 'value' => $_value);
$do = $this->ydb->fetchAffected($sql, $bind);
if($do !== 1) {
// Something went wrong :(
return false;
}
// Cache option value to save a DB query if needed later
$this->ydb->set_option($name, $value);
yourls_do_action('add_option', $name, $_value);
return true;
}
/**
* Delete option from DB
*
* @since 1.7.3
* @see yourls_delete_option()
* @param string $name Option name to delete. Expected to not be SQL-escaped.
* @return bool False if option was not deleted (eg not found), true otherwise.
*/
public function delete($name) {
$name = trim((string)$name);
if (empty($name)) {
return false;
}
$table = YOURLS_DB_TABLE_OPTIONS;
$sql = "DELETE FROM $table WHERE option_name = :name";
$bind = array('name' => $name);
$do = $this->ydb->fetchAffected($sql, $bind);
if($do !== 1) {
// Something went wrong :(
return false;
}
yourls_do_action('delete_option', $name);
$this->ydb->delete_option($name);
return true;
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Custom profiler for YOURLS
*
* Based on \Aura\Sql\Profiler\Profiler to tweak function finish()
*
* @since 1.7.10
*/
namespace YOURLS\Database;
class Profiler extends \Aura\Sql\Profiler\Profiler {
/**
*
* Finishes and logs a profile entry.
*
* We're just overriding the original class finish() to
* - not throw an exception and collect a backtrace that will remain unused
* - not flatten the array of 'values' into a string
*
* @param string $statement The statement being profiled, if any.
* @param array $values The values bound to the statement, if any.
* @return void
*/
public function finish($statement = null, array $values = [])
{
if (! $this->active) {
return;
}
$this->context['duration'] = microtime(true) - $this->context['start'];
$this->context['statement'] = $statement;
$this->context['values'] = (array)$values;
$this->logger->log($this->logLevel, $this->logFormat, $this->context);
$this->context = [];
}
}

423
includes/Database/YDB.php Normal file
View File

@ -0,0 +1,423 @@
<?php
/**
* Aura SQL wrapper for YOURLS that creates the allmighty YDB object.
*
* A fine example of a "class that knows too much" (see https://en.wikipedia.org/wiki/God_object)
*
* Note to plugin authors: you most likely SHOULD NOT use directly methods and properties of this class. Use instead
* function wrappers (eg don't use $ydb->option, or $ydb->set_option(), use yourls_*_options() functions instead).
*
* @since 1.7.3
*/
namespace YOURLS\Database;
use \Aura\Sql\ExtendedPdo;
use \YOURLS\Database\Profiler;
use \YOURLS\Database\Logger;
use PDO;
class YDB extends ExtendedPdo {
/**
* Debug mode, default false
* @var bool
*/
protected $debug = false;
/**
* Page context (ie "infos", "bookmark", "plugins"...)
* @var string
*/
protected $context = '';
/**
* Information related to a short URL keyword (eg timestamp, long URL, ...)
*
* @var array
*
*/
protected $infos = [];
/**
* Is YOURLS installed and ready to run?
* @var bool
*/
protected $installed = false;
/**
* Options
* @var array
*/
protected $option = [];
/**
* Plugin admin pages informations
* @var array
*/
protected $plugin_pages = [];
/**
* Plugin informations
* @var array
*/
protected $plugins = [];
/**
* Are we emulating prepare statements ?
* @var bool
*/
protected $is_emulate_prepare;
/**
* @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
*/
public function __construct($dsn, $user, $pass, $options, $attributes) {
parent::__construct($dsn, $user, $pass, $options, $attributes);
}
/**
* Init everything needed
*
* Everything we need to set up is done here in init(), not in the constructor, so even
* when the connection fails (eg config error or DB dead), the constructor has worked
* and we have a $ydb object properly instantiated (and for instance yourls_die() can
* correctly die, even if using $ydb methods)
*
* @since 1.7.3
* @return void
*/
public function init() {
$this->connect_to_DB();
$this->set_emulate_state();
$this->start_profiler();
}
/**
* Check if we emulate prepare statements, and set bool flag accordingly
*
* Check if current driver can PDO::getAttribute(PDO::ATTR_EMULATE_PREPARES)
* Some combinations of PHP/MySQL don't support this function. See
* https://travis-ci.org/YOURLS/YOURLS/jobs/271423782#L481
*
* @since 1.7.3
* @return void
*/
public function set_emulate_state() {
try {
$this->is_emulate_prepare = $this->getAttribute(PDO::ATTR_EMULATE_PREPARES);
} catch (\PDOException $e) {
$this->is_emulate_prepare = false;
}
}
/**
* Get emulate status
*
* @since 1.7.3
* @return bool
*/
public function get_emulate_state() {
return $this->is_emulate_prepare;
}
/**
* Initiate real connection to DB server
*
* This is to check that the server is running and/or the config is OK
*
* @since 1.7.3
* @return void
* @throws \PDOException
*/
public function connect_to_DB() {
try {
$this->connect();
} catch ( \Exception $e ) {
$this->dead_or_error($e);
}
}
/**
* Die with an error message
*
* @since 1.7.3
*
* @param \Exception $exception
*
* @return void
*/
public function dead_or_error(\Exception $exception) {
// Use any /user/db_error.php file
if( file_exists( YOURLS_USERDIR . '/db_error.php' ) ) {
include_once( YOURLS_USERDIR . '/db_error.php' );
die();
}
$message = yourls__( 'Incorrect DB config, or could not connect to DB' );
$message .= '<br/>' . get_class($exception) .': ' . $exception->getMessage();
yourls_die( yourls__( $message ), yourls__( 'Fatal error' ), 503 );
die();
}
/**
* Start a Message Logger
*
* @since 1.7.3
* @see \Aura\Sql\Profiler\Profiler
* @see \Aura\Sql\Profiler\MemoryLogger
* @return void
*/
public function start_profiler() {
// Instantiate a custom logger and make it the profiler
$yourls_logger = new Logger();
$profiler = new Profiler($yourls_logger);
$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()
*/
$profiler->setLoglevel('query');
}
/**
* @param string $context
* @return void
*/
public function set_html_context($context) {
$this->context = $context;
}
/**
* @return string
*/
public function get_html_context() {
return $this->context;
}
// Options low level functions, see \YOURLS\Database\Options
/**
* @param string $name
* @param mixed $value
* @return void
*/
public function set_option($name, $value) {
$this->option[$name] = $value;
}
/**
* @param string $name
* @return bool
*/
public function has_option($name) {
return array_key_exists($name, $this->option);
}
/**
* @param string $name
* @return string
*/
public function get_option($name) {
return $this->option[$name];
}
/**
* @param string $name
* @return void
*/
public function delete_option($name) {
unset($this->option[$name]);
}
// Infos (related to keyword) low level functions
/**
* @param string $keyword
* @param mixed $infos
* @return void
*/
public function set_infos($keyword, $infos) {
$this->infos[$keyword] = $infos;
}
/**
* @param string $keyword
* @return bool
*/
public function has_infos($keyword) {
return array_key_exists($keyword, $this->infos);
}
/**
* @param string $keyword
* @return array
*/
public function get_infos($keyword) {
return $this->infos[$keyword];
}
/**
* @param string $keyword
* @return void
*/
public function delete_infos($keyword) {
unset($this->infos[$keyword]);
}
/**
* @todo: infos & options are working the same way here. Abstract this.
*/
// Plugin low level functions, see functions-plugins.php
/**
* @return array
*/
public function get_plugins() {
return $this->plugins;
}
/**
* @param array $plugins
* @return void
*/
public function set_plugins(array $plugins) {
$this->plugins = $plugins;
}
/**
* @param string $plugin plugin filename
* @return void
*/
public function add_plugin($plugin) {
$this->plugins[] = $plugin;
}
/**
* @param string $plugin plugin filename
* @return void
*/
public function remove_plugin($plugin) {
unset($this->plugins[$plugin]);
}
// Plugin Pages low level functions, see functions-plugins.php
/**
* @return array
*/
public function get_plugin_pages() {
return is_array( $this->plugin_pages ) ? $this->plugin_pages : [];
}
/**
* @param array $pages
* @return void
*/
public function set_plugin_pages(array $pages) {
$this->plugin_pages = $pages;
}
/**
* @param string $slug
* @param string $title
* @param callable $function
* @return void
*/
public function add_plugin_page( $slug, $title, $function ) {
$this->plugin_pages[ $slug ] = [
'slug' => $slug,
'title' => $title,
'function' => $function,
];
}
/**
* @param string $slug
* @return void
*/
public function remove_plugin_page( $slug ) {
unset( $this->plugin_pages[ $slug ] );
}
/**
* Return count of SQL queries performed
*
* @since 1.7.3
* @return int
*/
public function get_num_queries() {
return count( (array) $this->get_queries() );
}
/**
* Return SQL queries performed
*
* @since 1.7.3
* @return array
*/
public function get_queries() {
$queries = $this->getProfiler()->getLogger()->getMessages();
// Only keep messages that start with "SQL "
$queries = array_filter($queries, function($query) {return substr( $query, 0, 4 ) === "SQL ";});
return $queries;
}
/**
* Set YOURLS installed state
*
* @since 1.7.3
* @param bool $bool
* @return void
*/
public function set_installed($bool) {
$this->installed = $bool;
}
/**
* Get YOURLS installed state
*
* @since 1.7.3
* @return bool
*/
public function is_installed() {
return $this->installed;
}
/**
* Return standardized DB version
*
* 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
* follows after that.
* 'omgmysql-5.5-ubuntu-4.20' => '5.5'
* 'mysql5.5-ubuntu-4.20' => '5.5'
* '5.5-ubuntu-4.20' => '5.5'
* '5.5-beta2' => '5.5'
* '5.5' => '5.5'
*
* @since 1.7.3
* @return string
*/
public function mysql_version() {
$version = $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
return $version;
}
}