xref: /plugin/dbquery/helper.php (revision 3113520cf000e2a727ad7aa22d219ce725cbf5c9)
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