1<?php 2 3/** 4 * DokuWiki Plugin dbquery (Helper Component) 5 * 6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 7 * @author Andreas Gohr <dokuwiki@cosmocode.de> 8 */ 9class helper_plugin_dbquery extends dokuwiki\Extension\Plugin 10{ 11 /** @var PDO[] do not access directly, use getPDO instead */ 12 protected $pdo = []; 13 14 /** 15 * @param string $name Page name of the query 16 * @return array 17 * @throws \Exception 18 */ 19 public function loadDataFromPage($name) 20 { 21 22 $name = cleanID($name); 23 $id = $this->getConf('namespace') . ':' . $name; 24 if (!page_exists($id)) throw new \Exception("No query named '$name' found"); 25 26 $doc = p_cached_output(wikiFN($id), 'dbquery'); 27 28 return json_decode($doc, true); 29 } 30 31 /** 32 * Return the PDO object and cache it for the request 33 * 34 * Connections data can be null to use the info from the config 35 * 36 * @param string|null $dsn 37 * @param string|null $user 38 * @param string|null $pass 39 * @return PDO 40 */ 41 public function getPDO($dsn = null, $user = null, $pass = null) 42 { 43 $dsn = $dsn ?: $this->getConf('dsn'); 44 $user = $user ?: $this->getConf('user'); 45 $pass = $pass ?: conf_decodeString($this->getConf('pass')); 46 $conid = md5($dsn . $user . $pass); 47 48 if (isset($this->pdo[$conid])) return $this->pdo[$conid]; 49 50 $opts = [ 51 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // always fetch as array 52 PDO::ATTR_EMULATE_PREPARES => true, // emulating prepares allows us to reuse param names 53 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // we want exceptions, not error codes 54 ]; 55 56 $this->pdo[$conid] = new PDO($dsn, $user, $pass, $opts); 57 return $this->pdo[$conid]; 58 } 59 60 /** 61 * Opens a database connection, executes the query and returns the result 62 * 63 * @param string $query 64 * @return array 65 * @throws \PDOException 66 * @throws Exception 67 */ 68 public function executeQuery($query, $dsnalias = null) 69 { 70 if (!preg_match('/^select /i', trim($query))) { 71 throw new \Exception('For security reasons only SELECT statements are allowed in dbquery'); 72 } 73 74 [$dsn, $user, $pass] = $this->getDSN($dsnalias); 75 $pdo = $this->getPDO($dsn, $user, $pass); 76 $params = $this->gatherVariables(); 77 $sth = $this->prepareStatement($pdo, $query, $params); 78 $sth->execute(); 79 $data = $sth->fetchAll(PDO::FETCH_ASSOC); 80 $sth->closeCursor(); 81 82 return $data; 83 } 84 85 /** 86 * Generate a prepared statement with bound parameters 87 * 88 * @param PDO $pdo 89 * @param string $sql 90 * @param array $parameters 91 * @return PDOStatement 92 */ 93 public function prepareStatement(\PDO $pdo, $sql, $parameters) 94 { 95 // prepare the groups 96 $cnt = 0; 97 $groupids = []; 98 foreach ($parameters[':groups'] as $group) { 99 $id = 'group' . $cnt++; 100 $parameters[$id] = $group; 101 $groupids[] = ":$id"; 102 } 103 unset($parameters[':groups']); 104 $sql = str_replace(':groups', join(',', $groupids), $sql); 105 106 $sth = $pdo->prepare($sql); 107 foreach ($parameters as $key => $val) { 108 if (is_array($val)) continue; 109 if (is_object($val)) continue; 110 if (strpos($sql, $key) === false) continue; // skip if parameter is missing 111 112 if (is_int($val)) { 113 $sth->bindValue($key, $val, PDO::PARAM_INT); 114 } else { 115 $sth->bindValue($key, $val); 116 } 117 } 118 119 return $sth; 120 } 121 122 /** 123 * Get the standard replacement variables 124 * 125 * @return array 126 */ 127 public function gatherVariables() 128 { 129 global $USERINFO; 130 global $INFO; 131 global $INPUT; 132 133 return [ 134 ':user' => $INPUT->server->str('REMOTE_USER'), 135 ':mail' => $USERINFO['mail'] ?? '', 136 ':groups' => $USERINFO['grps'] ?? [], 137 ':id' => ':' . $INFO['id'], 138 ':page' => noNS($INFO['id']), 139 ':ns' => ':' . getNS($INFO['id']), 140 ]; 141 } 142 143 /** 144 * Get the DSN, user and pass for a given alias 145 * 146 * @param string|null $alias null for default 147 * @return [string, string, string] DSN, user, pass 148 * @throws Exception 149 */ 150 public function getDSN($alias = null) 151 { 152 static $aliases = null; 153 if ($aliases === null) { 154 $aliases = $this->getDsnAliases( 155 $this->getConf('dsn'), 156 $this->getConf('user'), 157 $this->getConf('pass') 158 ); 159 } 160 161 if ($aliases === []) throw new \Exception('No DSN aliases defined'); 162 163 if ($alias === null || !isset($aliases[$alias])) { 164 $alias = '_'; 165 } 166 167 return [$aliases[$alias]['dsn'], $aliases[$alias]['user'], $aliases[$alias]['pass']]; 168 } 169 170 /** 171 * Load and parse the DSN configuration 172 * 173 * @param string $config 174 * @param string $defaultuser 175 * @param string $defaultpass 176 * @return array 177 */ 178 protected function getDsnAliases($config, $defaultuser, $defaultpass) 179 { 180 $aliases = []; 181 $lines = explode("\n", $config); 182 foreach ($lines as $line) { 183 $line = trim($line); 184 if (!$line) continue; 185 $parts = preg_split('/\s+/', $line, 4); 186 187 if (count($parts) > 1) { 188 $aliases[$parts[0]] = [ 189 'dsn' => $parts[1], 190 'user' => $parts[2] ?? $defaultuser, 191 'pass' => $parts[3] ?? $defaultpass 192 ]; 193 } else { 194 $parts = ['', $parts[0]]; 195 } 196 197 if (!isset($aliases['_'])) { 198 $aliases['_'] = [ 199 'dsn' => $parts[1], 200 'user' => $parts[2] ?? $defaultuser, 201 'pass' => $parts[3] ?? $defaultpass 202 ]; 203 } 204 } 205 return $aliases; 206 } 207} 208