xref: /plugin/dbquery/helper.php (revision 9730265cac55f032a41e08c80cc41610b870373b)
12e564e06SAndreas Gohr<?php
22e564e06SAndreas Gohr
3*9730265cSAndreas Gohruse dokuwiki\Extension\Plugin;
4*9730265cSAndreas 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 */
11*9730265cSAndreas 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    {
451a014136SAndreas Gohr        $dsn = $dsn ?: $this->getConf('dsn');
461a014136SAndreas Gohr        $user = $user ?: $this->getConf('user');
471a014136SAndreas Gohr        $pass = $pass ?: conf_decodeString($this->getConf('pass'));
481a014136SAndreas Gohr        $conid = md5($dsn . $user . $pass);
491a014136SAndreas Gohr
501a014136SAndreas Gohr        if (isset($this->pdo[$conid])) return $this->pdo[$conid];
511a014136SAndreas Gohr
526648d9d9SAndreas Gohr        $opts = [
536648d9d9SAndreas Gohr            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // always fetch as array
546648d9d9SAndreas Gohr            PDO::ATTR_EMULATE_PREPARES => true, // emulating prepares allows us to reuse param names
556648d9d9SAndreas Gohr            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // we want exceptions, not error codes
566648d9d9SAndreas Gohr        ];
576648d9d9SAndreas Gohr
581a014136SAndreas Gohr        $this->pdo[$conid] = new PDO($dsn, $user, $pass, $opts);
591a014136SAndreas Gohr        return $this->pdo[$conid];
606648d9d9SAndreas Gohr    }
616648d9d9SAndreas Gohr
626648d9d9SAndreas Gohr    /**
632e564e06SAndreas Gohr     * Opens a database connection, executes the query and returns the result
642e564e06SAndreas Gohr     *
652e564e06SAndreas Gohr     * @param string $query
662e564e06SAndreas Gohr     * @return array
672e564e06SAndreas Gohr     * @throws \PDOException
68584c7810SAndreas Gohr     * @throws Exception
692e564e06SAndreas Gohr     */
703113520cSAndreas Gohr    public function executeQuery($query, $dsnalias = null)
712e564e06SAndreas Gohr    {
72584c7810SAndreas Gohr        if (!preg_match('/^select /i', trim($query))) {
73584c7810SAndreas Gohr            throw new \Exception('For security reasons only SELECT statements are allowed in dbquery');
74584c7810SAndreas Gohr        }
75584c7810SAndreas Gohr
763113520cSAndreas Gohr        [$dsn, $user, $pass] = $this->getDSN($dsnalias);
773113520cSAndreas Gohr        $pdo = $this->getPDO($dsn, $user, $pass);
78be0b6729SAndreas Gohr        $params = $this->gatherVariables();
79be0b6729SAndreas Gohr        $sth = $this->prepareStatement($pdo, $query, $params);
80be0b6729SAndreas Gohr        $sth->execute();
81*9730265cSAndreas Gohr
822e564e06SAndreas Gohr        $data = $sth->fetchAll(PDO::FETCH_ASSOC);
832e564e06SAndreas Gohr        $sth->closeCursor();
842e564e06SAndreas Gohr
852e564e06SAndreas Gohr        return $data;
862e564e06SAndreas Gohr    }
87be0b6729SAndreas Gohr
88be0b6729SAndreas Gohr    /**
89be0b6729SAndreas Gohr     * Generate a prepared statement with bound parameters
90be0b6729SAndreas Gohr     *
91be0b6729SAndreas Gohr     * @param PDO $pdo
92be0b6729SAndreas Gohr     * @param string $sql
936648d9d9SAndreas Gohr     * @param array $parameters
94be0b6729SAndreas Gohr     * @return PDOStatement
95be0b6729SAndreas Gohr     */
96be0b6729SAndreas Gohr    public function prepareStatement(\PDO $pdo, $sql, $parameters)
97be0b6729SAndreas Gohr    {
986648d9d9SAndreas Gohr        // prepare the groups
996648d9d9SAndreas Gohr        $cnt = 0;
1006648d9d9SAndreas Gohr        $groupids = [];
1016648d9d9SAndreas Gohr        foreach ($parameters[':groups'] as $group) {
1026648d9d9SAndreas Gohr            $id = 'group' . $cnt++;
1036648d9d9SAndreas Gohr            $parameters[$id] = $group;
1046648d9d9SAndreas Gohr            $groupids[] = ":$id";
1056648d9d9SAndreas Gohr        }
1066648d9d9SAndreas Gohr        unset($parameters[':groups']);
107*9730265cSAndreas Gohr        $sql = str_replace(':groups', implode(',', $groupids), $sql);
108be0b6729SAndreas Gohr
1096648d9d9SAndreas Gohr        $sth = $pdo->prepare($sql);
110be0b6729SAndreas Gohr        foreach ($parameters as $key => $val) {
111be0b6729SAndreas Gohr            if (is_array($val)) continue;
112be0b6729SAndreas Gohr            if (is_object($val)) continue;
113*9730265cSAndreas Gohr            if (!str_contains($sql, $key)) continue; // skip if parameter is missing
114be0b6729SAndreas Gohr
115be0b6729SAndreas Gohr            if (is_int($val)) {
116be0b6729SAndreas Gohr                $sth->bindValue($key, $val, PDO::PARAM_INT);
117be0b6729SAndreas Gohr            } else {
118be0b6729SAndreas Gohr                $sth->bindValue($key, $val);
119be0b6729SAndreas Gohr            }
120be0b6729SAndreas Gohr        }
121be0b6729SAndreas Gohr
122be0b6729SAndreas Gohr        return $sth;
123be0b6729SAndreas Gohr    }
124be0b6729SAndreas Gohr
125be0b6729SAndreas Gohr    /**
126be0b6729SAndreas Gohr     * Get the standard replacement variables
127be0b6729SAndreas Gohr     *
128be0b6729SAndreas Gohr     * @return array
129be0b6729SAndreas Gohr     */
130be0b6729SAndreas Gohr    public function gatherVariables()
131be0b6729SAndreas Gohr    {
132be0b6729SAndreas Gohr        global $USERINFO;
133be0b6729SAndreas Gohr        global $INFO;
134be0b6729SAndreas Gohr        global $INPUT;
135be0b6729SAndreas Gohr
136be0b6729SAndreas Gohr        return [
137be0b6729SAndreas Gohr            ':user' => $INPUT->server->str('REMOTE_USER'),
1384515310dSAndreas Gohr            ':mail' => $USERINFO['mail'] ?? '',
1394515310dSAndreas Gohr            ':groups' => $USERINFO['grps'] ?? [],
1406648d9d9SAndreas Gohr            ':id' => ':' . $INFO['id'],
1416648d9d9SAndreas Gohr            ':page' => noNS($INFO['id']),
1426648d9d9SAndreas Gohr            ':ns' => ':' . getNS($INFO['id']),
143be0b6729SAndreas Gohr        ];
144be0b6729SAndreas Gohr    }
1453113520cSAndreas Gohr
1463113520cSAndreas Gohr    /**
1473113520cSAndreas Gohr     * Get the DSN, user and pass for a given alias
1483113520cSAndreas Gohr     *
1493113520cSAndreas Gohr     * @param string|null $alias null for default
1503113520cSAndreas Gohr     * @return [string, string, string] DSN, user, pass
1513113520cSAndreas Gohr     * @throws Exception
1523113520cSAndreas Gohr     */
1533113520cSAndreas Gohr    public function getDSN($alias = null)
1543113520cSAndreas Gohr    {
1553113520cSAndreas Gohr        static $aliases = null;
1563113520cSAndreas Gohr        if ($aliases === null) {
1573113520cSAndreas Gohr            $aliases = $this->getDsnAliases(
1583113520cSAndreas Gohr                $this->getConf('dsn'),
1593113520cSAndreas Gohr                $this->getConf('user'),
1603113520cSAndreas Gohr                $this->getConf('pass')
1613113520cSAndreas Gohr            );
1623113520cSAndreas Gohr        }
1633113520cSAndreas Gohr
1643113520cSAndreas Gohr        if ($aliases === []) throw new \Exception('No DSN aliases defined');
1653113520cSAndreas Gohr
1663113520cSAndreas Gohr        if ($alias === null || !isset($aliases[$alias])) {
1673113520cSAndreas Gohr            $alias = '_';
1683113520cSAndreas Gohr        }
1693113520cSAndreas Gohr
1703113520cSAndreas Gohr        return [$aliases[$alias]['dsn'], $aliases[$alias]['user'], $aliases[$alias]['pass']];
1713113520cSAndreas Gohr    }
1723113520cSAndreas Gohr
1733113520cSAndreas Gohr    /**
1743113520cSAndreas Gohr     * Load and parse the DSN configuration
1753113520cSAndreas Gohr     *
1763113520cSAndreas Gohr     * @param string $config
1773113520cSAndreas Gohr     * @param string $defaultuser
1783113520cSAndreas Gohr     * @param string $defaultpass
1793113520cSAndreas Gohr     * @return array
1803113520cSAndreas Gohr     */
1813113520cSAndreas Gohr    protected function getDsnAliases($config, $defaultuser, $defaultpass)
1823113520cSAndreas Gohr    {
1833113520cSAndreas Gohr        $aliases = [];
1843113520cSAndreas Gohr        $lines = explode("\n", $config);
1853113520cSAndreas Gohr        foreach ($lines as $line) {
1863113520cSAndreas Gohr            $line = trim($line);
1873113520cSAndreas Gohr            if (!$line) continue;
1883113520cSAndreas Gohr            $parts = preg_split('/\s+/', $line, 4);
1893113520cSAndreas Gohr
1903113520cSAndreas Gohr            if (count($parts) > 1) {
1913113520cSAndreas Gohr                $aliases[$parts[0]] = [
1923113520cSAndreas Gohr                    'dsn' => $parts[1],
1933113520cSAndreas Gohr                    'user' => $parts[2] ?? $defaultuser,
1943113520cSAndreas Gohr                    'pass' => $parts[3] ?? $defaultpass
1953113520cSAndreas Gohr                ];
1963113520cSAndreas Gohr            } else {
1973113520cSAndreas Gohr                $parts = ['', $parts[0]];
1983113520cSAndreas Gohr            }
1993113520cSAndreas Gohr
2003113520cSAndreas Gohr            if (!isset($aliases['_'])) {
2013113520cSAndreas Gohr                $aliases['_'] = [
2023113520cSAndreas Gohr                    'dsn' => $parts[1],
2033113520cSAndreas Gohr                    'user' => $parts[2] ?? $defaultuser,
2043113520cSAndreas Gohr                    'pass' => $parts[3] ?? $defaultpass
2053113520cSAndreas Gohr                ];
2063113520cSAndreas Gohr            }
2073113520cSAndreas Gohr        }
2083113520cSAndreas Gohr        return $aliases;
2093113520cSAndreas Gohr    }
2102e564e06SAndreas Gohr}
211