*/ use dokuwiki\Extension\Plugin; use dokuwiki\ErrorHandler; use dokuwiki\plugin\sqlite\SQLiteDB; use dokuwiki\Utf8\PhpString; /** * This is the base class for all syntax classes, providing some general stuff */ class helper_plugin_data extends Plugin { /** * @var SQLiteDB initialized via _getDb() */ protected $db; /** * @var array stores the alias definitions */ protected $aliases; /** * @var array stores custom key localizations */ protected $locs = []; /** * Constructor * * Loads custom translations */ public function __construct() { $this->loadLocalizedLabels(); } private function loadLocalizedLabels() { $lang = []; $path = DOKU_CONF . '/lang/en/data-plugin.php'; if (file_exists($path)) include($path); $path = DOKU_CONF . '/lang/' . $this->determineLang() . '/data-plugin.php'; if (file_exists($path)) include($path); foreach ($lang as $key => $val) { $lang[PhpString::strtolower($key)] = $val; } $this->locs = $lang; } /** * Return language code * * @return mixed */ protected function determineLang() { /** @var helper_plugin_translation $trans */ $trans = plugin_load('helper', 'translation'); if ($trans) { $value = $trans->getLangPart(getID()); if ($value) return $value; } global $conf; return $conf['lang']; } /** * Simple function to check if the database is ready to use * * @return bool */ public function ready() { return (bool)$this->getDB(); } /** * load the sqlite helper * * @return SQLiteDB|null SQLite class plugin or null if failed */ public function getDB() { if ($this->db === null) { try { $this->db = new SQLiteDB('data', __DIR__ . '/db/'); $this->db->getPdo()->sqliteCreateFunction('DATARESOLVE', [$this, 'resolveData'], 2); } catch (\Exception $exception) { if (defined('DOKU_UNITTEST')) throw new \RuntimeException('Could not load SQLite', 0, $exception); ErrorHandler::logException($exception); msg('Couldn\'t load sqlite.', -1); return null; } } return $this->db; } /** * Makes sure the given data fits with the given type * * @param string $value * @param string|array $type * @return string */ public function cleanData($value, $type) { $value = trim((string) $value); if (!$value && $value !== '0') { return ''; } if (is_array($type)) { if (isset($type['enum']) && !preg_match('/(^|,\s*)' . preg_quote_cb($value) . '($|\s*,)/', $type['enum'])) { return ''; } $type = $type['type']; } switch ($type) { case 'dt': if (preg_match('/^(\d\d\d\d)-(\d\d?)-(\d\d?)$/', $value, $m)) { return sprintf('%d-%02d-%02d', $m[1], $m[2], $m[3]); } if ($value === '%now%') { return $value; } return ''; case 'url': if (!preg_match('!^[a-z]+://!i', $value)) { $value = 'http://' . $value; } return $value; case 'mail': $email = ''; $name = ''; $parts = preg_split('/\s+/', $value); do { $part = array_shift($parts); if (!$email && mail_isvalid($part)) { $email = strtolower($part); continue; } $name .= $part . ' '; } while ($part); return trim($email . ' ' . $name); case 'page': case 'nspage': return cleanID($value); default: return $value; } } /** * Add pre and postfixs to the given value * * $type may be an column array with pre and postfixes * * @param string|array $type * @param string $val * @param string $pre * @param string $post * @return string */ public function addPrePostFixes($type, $val, $pre = '', $post = '') { if (is_array($type)) { if (isset($type['prefix'])) { $pre = $type['prefix']; } if (isset($type['postfix'])) { $post = $type['postfix']; } } $val = $pre . $val . $post; $val = $this->replacePlaceholders($val); return $val; } /** * Resolve a value according to its column settings * * This function is registered as a SQL function named DATARESOLVE * * @param string $value * @param string $colname * @return string */ public function resolveData($value, $colname) { // resolve pre and postfixes $column = $this->column($colname); $value = $this->addPrePostFixes($column['type'], $value); // for pages, resolve title $type = $column['type']; if (is_array($type)) { $type = $type['type']; } if ($type == 'title' || ($type == 'page' && useHeading('content'))) { $id = $value; if ($type == 'title') { [$id, ] = explode('|', $value, 2); } //DATARESOLVE is only used with the 'LIKE' comparator, so concatenate the different strings is fine. $value .= ' ' . p_get_first_heading($id); } return $value; } public function ensureAbsoluteId($id) { if (substr($id, 0, 1) !== ':') { $id = ':' . $id; } return $id; } /** * Return XHTML formated data, depending on column type * * @param array $column * @param string $value * @param Doku_Renderer_xhtml $R * @return string */ public function formatData($column, $value, Doku_Renderer_xhtml $R) { global $conf; $vals = explode("\n", $value); $outs = []; //multivalued line from db result for pageid and wiki has only in first value the ID $storedID = ''; foreach ($vals as $val) { $val = trim($val); if ($val == '') continue; $type = $column['type']; if (is_array($type)) { $type = $type['type']; } switch ($type) { case 'page': $val = $this->addPrePostFixes($column['type'], $val); $val = $this->ensureAbsoluteId($val); $outs[] = $R->internallink($val, null, null, true); break; case 'title': [$id, $title] = array_pad(explode('|', $val, 2), 2, null); $id = $this->addPrePostFixes($column['type'], $id); $id = $this->ensureAbsoluteId($id); $outs[] = $R->internallink($id, $title, null, true); break; case 'pageid': [$id, $title] = array_pad(explode('|', $val, 2), 2, null); //use ID from first value of the multivalued line if ($title == null) { $title = $id; if (!empty($storedID)) { $id = $storedID; } } else { $storedID = $id; } $id = $this->addPrePostFixes($column['type'], $id); $outs[] = $R->internallink($id, $title, null, true); break; case 'nspage': // no prefix/postfix here $val = ':' . $column['key'] . ":$val"; $outs[] = $R->internallink($val, null, null, true); break; case 'mail': [$id, $title] = array_pad(explode(' ', $val, 2), 2, null); $id = $this->addPrePostFixes($column['type'], $id); $id = obfuscate(hsc($id)); if (!$title) { $title = $id; } else { $title = hsc($title); } if ($conf['mailguard'] == 'visible') { $id = rawurlencode($id); } $outs[] = '' . $title . ''; break; case 'url': $val = $this->addPrePostFixes($column['type'], $val); $outs[] = $this->external_link($val, false, 'urlextern'); break; case 'tag': // per default use keyname as target page, but prefix on aliases if (!is_array($column['type'])) { $target = $column['key'] . ':'; } else { $target = $this->addPrePostFixes($column['type'], ''); } $outs[] = '' . hsc($val) . ''; break; case 'timestamp': $outs[] = dformat($val); break; case 'wiki': global $ID; $oldid = $ID; [$ID, $data] = explode('|', $val, 2); //use ID from first value of the multivalued line if ($data == null) { $data = $ID; $ID = $storedID; } else { $storedID = $ID; } $data = $this->addPrePostFixes($column['type'], $data); // Trim document_{start,end}, p_{open,close} from instructions $allinstructions = p_get_instructions($data); $wraps = 1; if (isset($allinstructions[1]) && $allinstructions[1][0] == 'p_open') { $wraps++; } $instructions = array_slice($allinstructions, $wraps, -$wraps); $outs[] = p_render('xhtml', $instructions, $byref_ignore); $ID = $oldid; break; default: $val = $this->addPrePostFixes($column['type'], $val); //type '_img' or '_img' if (substr($type, 0, 3) == 'img') { $width = (int)substr($type, 3); if (!$width) { $width = $this->getConf('image_width'); } [$mediaid, $title] = array_pad(explode('|', $val, 2), 2, null); if ($title === null) { $title = $column['key'] . ': ' . basename(str_replace(':', '/', $mediaid)); } else { $title = trim($title); } if (media_isexternal($val)) { $html = $R->externalmedia( $mediaid, $title, $align = null, $width, $height = null, $cache = null, $linking = 'direct', true ); } else { $html = $R->internalmedia( $mediaid, $title, $align = null, $width, $height = null, $cache = null, $linking = 'direct', true ); } if (strpos($html, 'mediafile') === false) { $html = str_replace('href', 'rel="lightbox" href', $html); } $outs[] = $html; } else { $outs[] = hsc($val); } } } return implode(', ', $outs); } /** * Split a column name into its parts * * @param string $col column name * @return array with key, type, ismulti, title, opt */ public function column($col) { preg_match('/^([^_]*)(?:_(.*))?((? $col, 'multi' => ($matches[3] === 's'), 'key' => PhpString::strtolower($matches[1]), 'origkey' => $matches[1], //similar to key, but stores upper case 'title' => $matches[1], 'type' => PhpString::strtolower($matches[2]), ]; // fix title for special columns static $specials = [ '%title%' => ['page', 'title'], '%pageid%' => ['title', 'page'], '%class%' => ['class'], '%lastmod%' => ['lastmod', 'timestamp'] ]; if (isset($specials[$column['title']])) { $s = $specials[$column['title']]; $column['title'] = $this->getLang($s[0]); if ($column['type'] === '' && isset($s[1])) { $column['type'] = $s[1]; } } // check if the type is some alias $aliases = $this->aliases(); if (isset($aliases[$column['type']])) { $column['origtype'] = $column['type']; $column['type'] = $aliases[$column['type']]; } // use custom localization for keys if (isset($this->locs[$column['key']])) { $column['title'] = $this->locs[$column['key']]; } return $column; } /** * Load defined type aliases * * @return array */ public function aliases() { if (!is_null($this->aliases)) return $this->aliases; $sqlite = $this->getDB(); if (!$sqlite) return []; $this->aliases = []; $rows = $sqlite->queryAll("SELECT * FROM aliases"); foreach ($rows as $row) { $name = $row['name']; unset($row['name']); $this->aliases[$name] = array_filter(array_map('trim', $row)); if (!isset($this->aliases[$name]['type'])) { $this->aliases[$name]['type'] = ''; } } return $this->aliases; } /** * Parse a filter line into an array * * @param $filterline * @return array|bool - array on success, false on error */ public function parseFilter($filterline) { //split filterline on comparator if (preg_match('/^(.*?)([\*=<>!~]{1,2})(.*)$/', $filterline, $matches)) { $column = $this->column(trim($matches[1])); $com = $matches[2]; $aliasses = ['<>' => '!=', '=!' => '!=', '~!' => '!~', '==' => '=', '~=' => '~', '=~' => '~']; if (isset($aliasses[$com])) { $com = $aliasses[$com]; } elseif (!preg_match('/(!?[=~])|([<>]=?)|(\*~)/', $com)) { msg('Failed to parse comparison "' . hsc($com) . '"', -1); return false; } $val = trim($matches[3]); if ($com == '~~') { $com = 'IN('; } if (strpos($com, '~') !== false) { if ($com === '*~') { $val = '*' . $val . '*'; $com = '~'; } $val = str_replace('*', '%', $val); if ($com == '!~') { $com = 'NOT LIKE'; } else { $com = 'LIKE'; } } else { // Clean if there are no asterisks I could kill $val = $this->cleanData($val, $column['type']); } $sqlite = $this->getDB(); if (!$sqlite) return false; if ($com == 'IN(') { $val = explode(',', $val); $val = array_map('trim', $val); $val = array_map([$sqlite->getPdo(), 'quote'], $val); $val = implode(",", $val); } else { $val = $sqlite->getPdo()->quote($val); } return [ 'key' => $column['key'], 'value' => $val, 'compare' => $com, 'colname' => $column['colname'], 'type' => $column['type'] ]; } msg('Failed to parse filter "' . hsc($filterline) . '"', -1); return false; } /** * Replace placeholders in sql * * @param $data */ public function replacePlaceholdersInSQL(&$data) { global $USERINFO; global $INPUT; // allow current user name in filter: $data['sql'] = str_replace('%user%', $INPUT->server->str('REMOTE_USER'), $data['sql']); $data['sql'] = str_replace('%groups%', implode("','", $USERINFO['grps'] ?? []), $data['sql']); // allow current date in filter: $data['sql'] = str_replace('%now%', dformat(null, '%Y-%m-%d'), $data['sql']); // language filter $data['sql'] = $this->makeTranslationReplacement($data['sql']); } /** * Replace translation related placeholders in given string * * @param string $data * @return string */ public function makeTranslationReplacement($data) { global $conf; global $ID; $patterns[] = '%lang%'; if (isset($conf['lang_before_translation'])) { $values[] = $conf['lang_before_translation']; } else { $values[] = $conf['lang']; } // if translation plugin available, get current translation (empty for default lang) $patterns[] = '%trans%'; /** @var helper_plugin_translation $trans */ $trans = plugin_load('helper', 'translation'); if ($trans) { $local = $trans->getLangPart($ID); if ($local === '') { $local = $conf['lang']; } $values[] = $local; } else { $values[] = ''; } return str_replace($patterns, $values, $data); } /** * Get filters given in the request via GET or POST * * @return array */ public function getFilters() { $filters = []; if (!isset($_REQUEST['dataflt'])) { $flt = []; } elseif (!is_array($_REQUEST['dataflt'])) { $flt = (array)$_REQUEST['dataflt']; } else { $flt = $_REQUEST['dataflt']; } foreach ($flt as $key => $line) { // we also take the column and filtertype in the key: if (!is_numeric($key)) { $line = $key . $line; } $f = $this->parseFilter($line); if (is_array($f)) { $f['logic'] = 'AND'; $filters[] = $f; } } return $filters; } /** * prepare an array to be passed through buildURLparams() * * @param string $name keyname * @param string|array $array value or key-value pairs * @return array */ public function a2ua($name, $array) { $urlarray = []; foreach ((array)$array as $key => $val) { $urlarray[$name . '[' . $key . ']'] = $val; } return $urlarray; } /** * get current URL parameters * * @param bool $returnURLparams * @return array with dataflt, datasrt and dataofs parameters */ public function getPurrentParam($returnURLparams = true) { $cur_params = []; if (isset($_REQUEST['dataflt'])) { $cur_params = $this->a2ua('dataflt', $_REQUEST['dataflt']); } if (isset($_REQUEST['datasrt'])) { $cur_params['datasrt'] = $_REQUEST['datasrt']; } if (isset($_REQUEST['dataofs'])) { $cur_params['dataofs'] = $_REQUEST['dataofs']; } //combine key and value if (!$returnURLparams) { $flat_param = []; foreach ($cur_params as $key => $val) { $flat_param[] = $key . $val; } $cur_params = $flat_param; } return $cur_params; } /** * Get url parameters, remove all filters for given column and add filter for desired tag * * @param array $column * @param string $tag * @return array of url parameters */ public function getTagUrlparam($column, $tag) { $param = []; if (isset($_REQUEST['dataflt'])) { $param = (array)$_REQUEST['dataflt']; //remove all filters equal to column foreach ($param as $key => $flt) { if (!is_numeric($key)) { $flt = $key . $flt; } $filter = $this->parseFilter($flt); if ($filter['key'] == $column['key']) { unset($param[$key]); } } } $param[] = $column['key'] . "_=$tag"; $param = $this->a2ua('dataflt', $param); if (isset($_REQUEST['datasrt'])) { $param['datasrt'] = $_REQUEST['datasrt']; } if (isset($_REQUEST['dataofs'])) { $param['dataofs'] = $_REQUEST['dataofs']; } return $param; } /** * Perform replacements on the output values * * @param string $value * @return string */ private function replacePlaceholders($value) { return $this->makeTranslationReplacement($value); } }