12e564e06SAndreas Gohr<?php 22e564e06SAndreas Gohr 39730265cSAndreas Gohruse dokuwiki\Extension\Plugin; 49730265cSAndreas Gohr 52e564e06SAndreas Gohr/** 62e564e06SAndreas Gohr * DokuWiki Plugin dbquery (Helper Component) 72e564e06SAndreas Gohr * 82e564e06SAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 92e564e06SAndreas Gohr * @author Andreas Gohr <dokuwiki@cosmocode.de> 102e564e06SAndreas Gohr */ 119730265cSAndreas Gohrclass helper_plugin_dbquery extends Plugin 122e564e06SAndreas Gohr{ 131a014136SAndreas Gohr /** @var PDO[] do not access directly, use getPDO instead */ 141a014136SAndreas Gohr protected $pdo = []; 152e564e06SAndreas Gohr 162e564e06SAndreas Gohr /** 172e564e06SAndreas Gohr * @param string $name Page name of the query 186adcc9adSAndreas Gohr * @return array 192e564e06SAndreas Gohr * @throws \Exception 202e564e06SAndreas Gohr */ 21c64c1748SAndreas Gohr public function loadDataFromPage($name) 222e564e06SAndreas Gohr { 232e564e06SAndreas Gohr 242e564e06SAndreas Gohr $name = cleanID($name); 252e564e06SAndreas Gohr $id = $this->getConf('namespace') . ':' . $name; 262e564e06SAndreas Gohr if (!page_exists($id)) throw new \Exception("No query named '$name' found"); 272e564e06SAndreas Gohr 282e564e06SAndreas Gohr $doc = p_cached_output(wikiFN($id), 'dbquery'); 292e564e06SAndreas Gohr 306adcc9adSAndreas Gohr return json_decode($doc, true); 312e564e06SAndreas Gohr } 322e564e06SAndreas Gohr 332e564e06SAndreas Gohr /** 341a014136SAndreas Gohr * Return the PDO object and cache it for the request 351a014136SAndreas Gohr * 361a014136SAndreas Gohr * Connections data can be null to use the info from the config 371a014136SAndreas Gohr * 381a014136SAndreas Gohr * @param string|null $dsn 391a014136SAndreas Gohr * @param string|null $user 401a014136SAndreas Gohr * @param string|null $pass 416648d9d9SAndreas Gohr * @return PDO 426648d9d9SAndreas Gohr */ 431a014136SAndreas Gohr public function getPDO($dsn = null, $user = null, $pass = null) 446648d9d9SAndreas Gohr { 45*b2694ee4SAndreas Gohr global $conf; 46*b2694ee4SAndreas Gohr 471a014136SAndreas Gohr $dsn = $dsn ?: $this->getConf('dsn'); 481a014136SAndreas Gohr $user = $user ?: $this->getConf('user'); 491a014136SAndreas Gohr $pass = $pass ?: conf_decodeString($this->getConf('pass')); 50*b2694ee4SAndreas Gohr // placeholders for use with sqlite 51*b2694ee4SAndreas Gohr $dsn = str_replace( 52*b2694ee4SAndreas Gohr ['%DATA_DIR%', '%META_DIR%'], 53*b2694ee4SAndreas Gohr [fullpath($conf['savedir']), fullpath($conf['metadir'])], 54*b2694ee4SAndreas Gohr $dsn 55*b2694ee4SAndreas Gohr ); 561a014136SAndreas Gohr $conid = md5($dsn . $user . $pass); 571a014136SAndreas Gohr 581a014136SAndreas Gohr if (isset($this->pdo[$conid])) return $this->pdo[$conid]; 591a014136SAndreas Gohr 606648d9d9SAndreas Gohr $opts = [ 616648d9d9SAndreas Gohr PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // always fetch as array 626648d9d9SAndreas Gohr PDO::ATTR_EMULATE_PREPARES => true, // emulating prepares allows us to reuse param names 636648d9d9SAndreas Gohr PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // we want exceptions, not error codes 646648d9d9SAndreas Gohr ]; 656648d9d9SAndreas Gohr 661a014136SAndreas Gohr $this->pdo[$conid] = new PDO($dsn, $user, $pass, $opts); 671a014136SAndreas Gohr return $this->pdo[$conid]; 686648d9d9SAndreas Gohr } 696648d9d9SAndreas Gohr 706648d9d9SAndreas Gohr /** 712e564e06SAndreas Gohr * Opens a database connection, executes the query and returns the result 722e564e06SAndreas Gohr * 732e564e06SAndreas Gohr * @param string $query 742e564e06SAndreas Gohr * @return array 752e564e06SAndreas Gohr * @throws \PDOException 76584c7810SAndreas Gohr * @throws Exception 772e564e06SAndreas Gohr */ 783113520cSAndreas Gohr public function executeQuery($query, $dsnalias = null) 792e564e06SAndreas Gohr { 80d7913160SAndreas Gohr if (!preg_match('/^select(\s|$)/i', trim($query))) { 81584c7810SAndreas Gohr throw new \Exception('For security reasons only SELECT statements are allowed in dbquery'); 82584c7810SAndreas Gohr } 83584c7810SAndreas Gohr 843113520cSAndreas Gohr [$dsn, $user, $pass] = $this->getDSN($dsnalias); 853113520cSAndreas Gohr $pdo = $this->getPDO($dsn, $user, $pass); 86be0b6729SAndreas Gohr $params = $this->gatherVariables(); 87be0b6729SAndreas Gohr $sth = $this->prepareStatement($pdo, $query, $params); 88be0b6729SAndreas Gohr $sth->execute(); 899730265cSAndreas Gohr 902e564e06SAndreas Gohr $data = $sth->fetchAll(PDO::FETCH_ASSOC); 912e564e06SAndreas Gohr $sth->closeCursor(); 922e564e06SAndreas Gohr 932e564e06SAndreas Gohr return $data; 942e564e06SAndreas Gohr } 95be0b6729SAndreas Gohr 96be0b6729SAndreas Gohr /** 97be0b6729SAndreas Gohr * Generate a prepared statement with bound parameters 98be0b6729SAndreas Gohr * 99be0b6729SAndreas Gohr * @param PDO $pdo 100be0b6729SAndreas Gohr * @param string $sql 1016648d9d9SAndreas Gohr * @param array $parameters 102be0b6729SAndreas Gohr * @return PDOStatement 103be0b6729SAndreas Gohr */ 104be0b6729SAndreas Gohr public function prepareStatement(\PDO $pdo, $sql, $parameters) 105be0b6729SAndreas Gohr { 1066648d9d9SAndreas Gohr // prepare the groups 1076648d9d9SAndreas Gohr $cnt = 0; 1086648d9d9SAndreas Gohr $groupids = []; 1096648d9d9SAndreas Gohr foreach ($parameters[':groups'] as $group) { 1106648d9d9SAndreas Gohr $id = 'group' . $cnt++; 1116648d9d9SAndreas Gohr $parameters[$id] = $group; 1126648d9d9SAndreas Gohr $groupids[] = ":$id"; 1136648d9d9SAndreas Gohr } 1146648d9d9SAndreas Gohr unset($parameters[':groups']); 1159730265cSAndreas Gohr $sql = str_replace(':groups', implode(',', $groupids), $sql); 116be0b6729SAndreas Gohr 1176648d9d9SAndreas Gohr $sth = $pdo->prepare($sql); 118be0b6729SAndreas Gohr foreach ($parameters as $key => $val) { 119be0b6729SAndreas Gohr if (is_array($val)) continue; 120be0b6729SAndreas Gohr if (is_object($val)) continue; 1219730265cSAndreas Gohr if (!str_contains($sql, $key)) continue; // skip if parameter is missing 122be0b6729SAndreas Gohr 123be0b6729SAndreas Gohr if (is_int($val)) { 124be0b6729SAndreas Gohr $sth->bindValue($key, $val, PDO::PARAM_INT); 125be0b6729SAndreas Gohr } else { 126be0b6729SAndreas Gohr $sth->bindValue($key, $val); 127be0b6729SAndreas Gohr } 128be0b6729SAndreas Gohr } 129be0b6729SAndreas Gohr 130be0b6729SAndreas Gohr return $sth; 131be0b6729SAndreas Gohr } 132be0b6729SAndreas Gohr 133be0b6729SAndreas Gohr /** 134be0b6729SAndreas Gohr * Get the standard replacement variables 135be0b6729SAndreas Gohr * 136be0b6729SAndreas Gohr * @return array 137be0b6729SAndreas Gohr */ 138be0b6729SAndreas Gohr public function gatherVariables() 139be0b6729SAndreas Gohr { 140be0b6729SAndreas Gohr global $USERINFO; 141be0b6729SAndreas Gohr global $INFO; 142be0b6729SAndreas Gohr global $INPUT; 143be0b6729SAndreas Gohr 144be0b6729SAndreas Gohr return [ 145be0b6729SAndreas Gohr ':user' => $INPUT->server->str('REMOTE_USER'), 1464515310dSAndreas Gohr ':mail' => $USERINFO['mail'] ?? '', 1474515310dSAndreas Gohr ':groups' => $USERINFO['grps'] ?? [], 1486648d9d9SAndreas Gohr ':id' => ':' . $INFO['id'], 1496648d9d9SAndreas Gohr ':page' => noNS($INFO['id']), 1506648d9d9SAndreas Gohr ':ns' => ':' . getNS($INFO['id']), 151be0b6729SAndreas Gohr ]; 152be0b6729SAndreas Gohr } 1533113520cSAndreas Gohr 1543113520cSAndreas Gohr /** 1553113520cSAndreas Gohr * Get the DSN, user and pass for a given alias 1563113520cSAndreas Gohr * 1573113520cSAndreas Gohr * @param string|null $alias null for default 1583113520cSAndreas Gohr * @return [string, string, string] DSN, user, pass 1593113520cSAndreas Gohr * @throws Exception 1603113520cSAndreas Gohr */ 1613113520cSAndreas Gohr public function getDSN($alias = null) 1623113520cSAndreas Gohr { 1633113520cSAndreas Gohr static $aliases = null; 1643113520cSAndreas Gohr if ($aliases === null) { 1653113520cSAndreas Gohr $aliases = $this->getDsnAliases( 1663113520cSAndreas Gohr $this->getConf('dsn'), 1673113520cSAndreas Gohr $this->getConf('user'), 1683113520cSAndreas Gohr $this->getConf('pass') 1693113520cSAndreas Gohr ); 1703113520cSAndreas Gohr } 1713113520cSAndreas Gohr 1723113520cSAndreas Gohr if ($aliases === []) throw new \Exception('No DSN aliases defined'); 1733113520cSAndreas Gohr 1743113520cSAndreas Gohr if ($alias === null || !isset($aliases[$alias])) { 1753113520cSAndreas Gohr $alias = '_'; 1763113520cSAndreas Gohr } 1773113520cSAndreas Gohr 1783113520cSAndreas Gohr return [$aliases[$alias]['dsn'], $aliases[$alias]['user'], $aliases[$alias]['pass']]; 1793113520cSAndreas Gohr } 1803113520cSAndreas Gohr 1813113520cSAndreas Gohr /** 1823113520cSAndreas Gohr * Load and parse the DSN configuration 1833113520cSAndreas Gohr * 1843113520cSAndreas Gohr * @param string $config 1853113520cSAndreas Gohr * @param string $defaultuser 1863113520cSAndreas Gohr * @param string $defaultpass 1873113520cSAndreas Gohr * @return array 1883113520cSAndreas Gohr */ 1893113520cSAndreas Gohr protected function getDsnAliases($config, $defaultuser, $defaultpass) 1903113520cSAndreas Gohr { 1913113520cSAndreas Gohr $aliases = []; 1923113520cSAndreas Gohr $lines = explode("\n", $config); 1933113520cSAndreas Gohr foreach ($lines as $line) { 1943113520cSAndreas Gohr $line = trim($line); 1953113520cSAndreas Gohr if (!$line) continue; 1963113520cSAndreas Gohr $parts = preg_split('/\s+/', $line, 4); 1973113520cSAndreas Gohr 1983113520cSAndreas Gohr if (count($parts) > 1) { 1993113520cSAndreas Gohr $aliases[$parts[0]] = [ 2003113520cSAndreas Gohr 'dsn' => $parts[1], 2013113520cSAndreas Gohr 'user' => $parts[2] ?? $defaultuser, 2023113520cSAndreas Gohr 'pass' => $parts[3] ?? $defaultpass 2033113520cSAndreas Gohr ]; 2043113520cSAndreas Gohr } else { 2053113520cSAndreas Gohr $parts = ['', $parts[0]]; 2063113520cSAndreas Gohr } 2073113520cSAndreas Gohr 2083113520cSAndreas Gohr if (!isset($aliases['_'])) { 2093113520cSAndreas Gohr $aliases['_'] = [ 2103113520cSAndreas Gohr 'dsn' => $parts[1], 2113113520cSAndreas Gohr 'user' => $parts[2] ?? $defaultuser, 2123113520cSAndreas Gohr 'pass' => $parts[3] ?? $defaultpass 2133113520cSAndreas Gohr ]; 2143113520cSAndreas Gohr } 2153113520cSAndreas Gohr } 2163113520cSAndreas Gohr return $aliases; 2173113520cSAndreas Gohr } 2182e564e06SAndreas Gohr} 219