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