1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Andreas Gohr <andi@splitbrain.org> 5 */ 6// must be run within Dokuwiki 7if(!defined('DOKU_INC')) die(); 8 9require_once(DOKU_PLUGIN . 'syntax.php'); 10require_once(DOKU_INC . 'inc/infoutils.php'); 11 12/** 13 * This is the base class for all syntax classes, providing some general stuff 14 */ 15class helper_plugin_dataau extends DokuWiki_Plugin { 16 17 /** 18 * @var helper_plugin_sqlite initialized via _getDb() 19 */ 20 protected $db = null; 21 22 /** 23 * @var array stores the alias definitions 24 */ 25 protected $aliases = null; 26 27 /** 28 * @var array stores custom key localizations 29 */ 30 protected $locs = array(); 31 32 /** 33 * Constructor 34 * 35 * Loads custom translations 36 */ 37 public function __construct() { 38 $this->loadLocalizedLabels(); 39 } 40 41 private function loadLocalizedLabels() { 42 $lang = array(); 43 $path = DOKU_CONF . '/lang/en/dataau-plugin.php'; 44 if(file_exists($path)) include($path); 45 $path = DOKU_CONF . '/lang/' . $this->determineLang() . '/dataau-plugin.php'; 46 if(file_exists($path)) include($path); 47 foreach($lang as $key => $val) { 48 $lang[utf8_strtolower($key)] = $val; 49 } 50 $this->locs = $lang; 51 } 52 53 /** 54 * Return language code 55 * 56 * @return mixed 57 */ 58 protected function determineLang() { 59 /** @var helper_plugin_translation $trans */ 60 $trans = plugin_load('helper', 'translation'); 61 if($trans) { 62 $value = $trans->getLangPart(getID()); 63 if($value) return $value; 64 } 65 global $conf; 66 return $conf['lang']; 67 } 68 69 /** 70 * Simple function to check if the database is ready to use 71 * 72 * @return bool 73 */ 74 public function ready() { 75 return (bool) $this->_getDB(); 76 } 77 78 /** 79 * load the sqlite helper 80 * 81 * @return helper_plugin_sqlite|false plugin or false if failed 82 */ 83 function _getDB() { 84 if($this->db === null) { 85 $this->db = plugin_load('helper', 'sqlite'); 86 if($this->db === null) { 87 msg('The dataau plugin needs the sqlite plugin', -1); 88 return false; 89 } 90 if(!$this->db->init('dataau', dirname(__FILE__) . '/db/')) { 91 $this->db = null; 92 return false; 93 } 94 $this->db->create_function('DATARESOLVE', array($this, '_resolveData'), 2); 95 } 96 return $this->db; 97 } 98 99 /** 100 * Makes sure the given data fits with the given type 101 * 102 * @param string $value 103 * @param string|array $type 104 * @return string 105 */ 106 function _cleanData($value, $type) { 107 $value = trim($value); 108 if(!$value AND $value !== '0') { 109 return ''; 110 } 111 if(is_array($type)) { 112 if(isset($type['enum']) && !preg_match('/(^|,\s*)' . preg_quote_cb($value) . '($|\s*,)/', $type['enum'])) { 113 return ''; 114 } 115 $type = $type['type']; 116 } 117 switch($type) { 118 case 'dt': 119 if(preg_match('/^(\d\d?)-(\d\d?)-(\d\d\d\d)$/', $value, $m)) { 120 return sprintf('%02d-%02d-%d', $m[1], $m[2], $m[3]); 121 } 122 if($value === '%now%') { 123 return $value; 124 } 125 return ''; 126 case 'url': 127 if(!preg_match('!^[a-z]+://!i', $value)) { 128 $value = 'http://' . $value; 129 } 130 return $value; 131 case 'mail': 132 $email = ''; 133 $name = ''; 134 $parts = preg_split('/\s+/', $value); 135 do { 136 $part = array_shift($parts); 137 if(!$email && mail_isvalid($part)) { 138 $email = strtolower($part); 139 continue; 140 } 141 $name .= $part . ' '; 142 } while($part); 143 144 return trim($email . ' ' . $name); 145 case 'page': 146 case 'nspage': 147 return cleanID($value); 148 default: 149 return $value; 150 } 151 } 152 153 /** 154 * Add pre and postfixs to the given value 155 * 156 * $type may be an column array with pre and postfixes 157 * 158 * @param string|array $type 159 * @param string $val 160 * @param string $pre 161 * @param string $post 162 * @return string 163 */ 164 function _addPrePostFixes($type, $val, $pre = '', $post = '') { 165 if(is_array($type)) { 166 if(isset($type['prefix'])) { 167 $pre = $type['prefix']; 168 } 169 if(isset($type['postfix'])) { 170 $post = $type['postfix']; 171 } 172 } 173 $val = $pre . $val . $post; 174 $val = $this->replacePlaceholders($val); 175 return $val; 176 } 177 178 /** 179 * Resolve a value according to its column settings 180 * 181 * This function is registered as a SQL function named DATARESOLVE 182 * 183 * @param string $value 184 * @param string $colname 185 * @return string 186 */ 187 function _resolveData($value, $colname) { 188 // resolve pre and postfixes 189 $column = $this->_column($colname); 190 $value = $this->_addPrePostFixes($column['type'], $value); 191 192 // for pages, resolve title 193 $type = $column['type']; 194 if(is_array($type)) { 195 $type = $type['type']; 196 } 197 if($type == 'title' || ($type == 'page' && useHeading('content'))) { 198 $id = $value; 199 if($type == 'title') { 200 list($id,) = explode('|', $value, 2); 201 } 202 //DATARESOLVE is only used with the 'LIKE' comparator, so concatenate the different strings is fine. 203 $value .= ' ' . p_get_first_heading($id); 204 } 205 return $value; 206 } 207 208 public function ensureAbsoluteId($id) { 209 if (substr($id,0,1) !== ':') { 210 $id = ':' . $id; 211 } 212 return $id; 213 } 214 215 /** 216 * Return XHTML formated data, depending on column type 217 * 218 * @param array $column 219 * @param string $value 220 * @param Doku_Renderer_xhtml $R 221 * @return string 222 */ 223 function _formatData($column, $value, Doku_Renderer_xhtml $R) { 224 global $conf; 225 $vals = explode("\n", $value); 226 $outs = array(); 227 228 //multivalued line from db result for pageid and wiki has only in first value the ID 229 $storedID = ''; 230 231 foreach($vals as $val) { 232 $val = trim($val); 233 if($val == '') continue; 234 235 $type = $column['type']; 236 if(is_array($type)) { 237 $type = $type['type']; 238 } 239 switch($type) { 240 case 'page': 241 $val = $this->_addPrePostFixes($column['type'], $val); 242 $val = $this->ensureAbsoluteId($val); 243 $outs[] = $R->internallink($val, null, null, true); 244 break; 245 case 'title': 246 list($id, $title) = explode('|', $val, 2); 247 $id = $this->_addPrePostFixes($column['type'], $id); 248 $id = $this->ensureAbsoluteId($id); 249 $outs[] = $R->internallink($id, $title, null, true); 250 break; 251 case 'pageid': 252 list($id, $title) = explode('|', $val, 2); 253 254 //use ID from first value of the multivalued line 255 if($title == null) { 256 $title = $id; 257 if(!empty($storedID)) { 258 $id = $storedID; 259 } 260 } else { 261 $storedID = $id; 262 } 263 264 $id = $this->_addPrePostFixes($column['type'], $id); 265 266 $outs[] = $R->internallink($id, $title, null, true); 267 break; 268 case 'nspage': 269 // no prefix/postfix here 270 $val = ':' . $column['key'] . ":$val"; 271 272 $outs[] = $R->internallink($val, null, null, true); 273 break; 274 case 'mail': 275 list($id, $title) = explode(' ', $val, 2); 276 $id = $this->_addPrePostFixes($column['type'], $id); 277 $id = obfuscate(hsc($id)); 278 if(!$title) { 279 $title = $id; 280 } else { 281 $title = hsc($title); 282 } 283 if($conf['mailguard'] == 'visible') { 284 $id = rawurlencode($id); 285 } 286 $outs[] = '<a href="mailto:' . $id . '" class="mail" title="' . $id . '">' . $title . '</a>'; 287 break; 288 case 'url': 289 $val = $this->_addPrePostFixes($column['type'], $val); 290 $outs[] = $this->external_link($val, false, 'urlextern'); 291 break; 292 case 'tag': 293 // per default use keyname as target page, but prefix on aliases 294 if(!is_array($column['type'])) { 295 $target = $column['key'] . ':'; 296 } else { 297 $target = $this->_addPrePostFixes($column['type'], ''); 298 } 299 300 $outs[] = '<a href="' . wl(str_replace('/', ':', cleanID($target)), $this->_getTagUrlparam($column, $val)) 301 . '" title="' . sprintf($this->getLang('tagfilter'), hsc($val)) 302 . '" class="wikilink1">' . hsc($val) . '</a>'; 303 break; 304 case 'timestamp': 305 $outs[] = dformat($val); 306 break; 307 case 'wiki': 308 global $ID; 309 $oldid = $ID; 310 list($ID, $dataau) = explode('|', $val, 2); 311 312 //use ID from first value of the multivalued line 313 if($dataau == null) { 314 $dataau = $ID; 315 $ID = $storedID; 316 } else { 317 $storedID = $ID; 318 } 319 $dataau = $this->_addPrePostFixes($column['type'], $dataau); 320 321 // Trim document_{start,end}, p_{open,close} from instructions 322 $allinstructions = p_get_instructions($dataau); 323 $wraps = 1; 324 if(isset($allinstructions[1]) && $allinstructions[1][0] == 'p_open') { 325 $wraps ++; 326 } 327 $instructions = array_slice($allinstructions, $wraps, -$wraps); 328 329 $outs[] = p_render('xhtml', $instructions, $byref_ignore); 330 $ID = $oldid; 331 break; 332 default: 333 $val = $this->_addPrePostFixes($column['type'], $val); 334 //type '_img' or '_img<width>' 335 if(substr($type, 0, 3) == 'img') { 336 $width = (int) substr($type, 3); 337 if(!$width) { 338 $width = $this->getConf('image_width'); 339 } 340 341 list($mediaid, $title) = explode('|', $val, 2); 342 if($title === null) { 343 $title = $column['key'] . ': ' . basename(str_replace(':', '/', $mediaid)); 344 } else { 345 $title = trim($title); 346 } 347 348 if(media_isexternal($val)) { 349 $html = $R->externalmedia($mediaid, $title, $align = null, $width, $height = null, $cache = null, $linking = 'direct', true); 350 } else { 351 $html = $R->internalmedia($mediaid, $title, $align = null, $width, $height = null, $cache = null, $linking = 'direct', true); 352 } 353 if(strpos($html, 'mediafile') === false) { 354 $html = str_replace('href', 'rel="lightbox" href', $html); 355 } 356 357 $outs[] = $html; 358 } else { 359 $outs[] = hsc($val); 360 } 361 } 362 } 363 return join(', ', $outs); 364 } 365 366 /** 367 * Split a column name into its parts 368 * 369 * @param string $col column name 370 * @returns array with key, type, ismulti, title, opt 371 */ 372 function _column($col) { 373 preg_match('/^([^_]*)(?:_(.*))?((?<!s)|s)$/', $col, $matches); 374 $column = array( 375 'colname' => $col, 376 'multi' => ($matches[3] === 's'), 377 'key' => utf8_strtolower($matches[1]), 378 'origkey' => $matches[1], //similar to key, but stores upper case 379 'title' => $matches[1], 380 'type' => utf8_strtolower($matches[2]) 381 ); 382 383 // fix title for special columns 384 static $specials = array( 385 '%title%' => array('page', 'title'), 386 '%pageid%' => array('title', 'page'), 387 '%class%' => array('class'), 388 '%lastmod%' => array('lastmod', 'timestamp') 389 ); 390 if(isset($specials[$column['title']])) { 391 $s = $specials[$column['title']]; 392 $column['title'] = $this->getLang($s[0]); 393 if($column['type'] === '' && isset($s[1])) { 394 $column['type'] = $s[1]; 395 } 396 } 397 398 // check if the type is some alias 399 $aliases = $this->_aliases(); 400 if(isset($aliases[$column['type']])) { 401 $column['origtype'] = $column['type']; 402 $column['type'] = $aliases[$column['type']]; 403 } 404 405 // use custom localization for keys 406 if(isset($this->locs[$column['key']])) { 407 $column['title'] = $this->locs[$column['key']]; 408 } 409 410 return $column; 411 } 412 413 /** 414 * Load defined type aliases 415 * 416 * @return array 417 */ 418 function _aliases() { 419 if(!is_null($this->aliases)) return $this->aliases; 420 421 $sqlite = $this->_getDB(); 422 if(!$sqlite) return array(); 423 424 $this->aliases = array(); 425 $res = $sqlite->query("SELECT * FROM aliases"); 426 $rows = $sqlite->res2arr($res); 427 foreach($rows as $row) { 428 $name = $row['name']; 429 unset($row['name']); 430 $this->aliases[$name] = array_filter(array_map('trim', $row)); 431 if(!isset($this->aliases[$name]['type'])) { 432 $this->aliases[$name]['type'] = ''; 433 } 434 } 435 return $this->aliases; 436 } 437 438 /** 439 * Parse a filter line into an array 440 * 441 * @param $filterline 442 * @return array|bool - array on success, false on error 443 */ 444 function _parse_filter($filterline) { 445 //split filterline on comparator 446 if(preg_match('/^(.*?)([\*=<>!~]{1,2})(.*)$/', $filterline, $matches)) { 447 $column = $this->_column(trim($matches[1])); 448 449 $com = $matches[2]; 450 $aliasses = array( 451 '<>' => '!=', '=!' => '!=', '~!' => '!~', 452 '==' => '=', '~=' => '~', '=~' => '~' 453 ); 454 455 if(isset($aliasses[$com])) { 456 $com = $aliasses[$com]; 457 } elseif(!preg_match('/(!?[=~])|([<>]=?)|(\*~)/', $com)) { 458 msg('Failed to parse comparison "' . hsc($com) . '"', -1); 459 return false; 460 } 461 462 $val = trim($matches[3]); 463 464 if($com == '~~') { 465 $com = 'IN('; 466 } 467 if(strpos($com, '~') !== false) { 468 if($com === '*~') { 469 $val = '*' . $val . '*'; 470 $com = '~'; 471 } 472 $val = str_replace('*', '%', $val); 473 if($com == '!~') { 474 $com = 'NOT LIKE'; 475 } else { 476 $com = 'LIKE'; 477 } 478 } else { 479 // Clean if there are no asterisks I could kill 480 $val = $this->_cleanData($val, $column['type']); 481 } 482 $sqlite = $this->_getDB(); 483 if(!$sqlite) return false; 484 $val = $sqlite->escape_string($val); //pre escape 485 if($com == 'IN(') { 486 $val = explode(',', $val); 487 $val = array_map('trim', $val); 488 $val = implode("','", $val); 489 } 490 491 return array( 492 'key' => $column['key'], 493 'value' => $val, 494 'compare' => $com, 495 'colname' => $column['colname'], 496 'type' => $column['type'] 497 ); 498 } 499 msg('Failed to parse filter "' . hsc($filterline) . '"', -1); 500 return false; 501 } 502 503 /** 504 * Replace placeholders in sql 505 */ 506 function _replacePlaceholdersInSQL(&$dataau) { 507 global $USERINFO; 508 // allow current user name in filter: 509 $dataau['sql'] = str_replace('%user%', $_SERVER['REMOTE_USER'], $dataau['sql']); 510 $dataau['sql'] = str_replace('%groups%', implode("','", (array) $USERINFO['grps']), $dataau['sql']); 511 // allow current date in filter: 512 $dataau['sql'] = str_replace('%now%', dformat(null, '%d-%m-%Y'), $dataau['sql']); 513 514 // language filter 515 $dataau['sql'] = $this->makeTranslationReplacement($dataau['sql']); 516 } 517 518 /** 519 * Replace translation related placeholders in given string 520 * 521 * @param string $data 522 * @return string 523 */ 524 public function makeTranslationReplacement($dataau) { 525 global $conf; 526 global $ID; 527 528 $patterns[] = '%lang%'; 529 if(isset($conf['lang_before_translation'])) { 530 $values[] = $conf['lang_before_translation']; 531 } else { 532 $values[] = $conf['lang']; 533 } 534 535 // if translation plugin available, get current translation (empty for default lang) 536 $patterns[] = '%trans%'; 537 /** @var helper_plugin_translation $trans */ 538 $trans = plugin_load('helper', 'translation'); 539 if($trans) { 540 $local = $trans->getLangPart($ID); 541 if($local === '') { 542 $local = $conf['lang']; 543 } 544 $values[] = $local; 545 } else { 546 $values[] = ''; 547 } 548 return str_replace($patterns, $values, $dataau); 549 } 550 551 /** 552 * Get filters given in the request via GET or POST 553 * 554 * @return array 555 */ 556 function _get_filters() { 557 $filters = array(); 558 559 if(!isset($_REQUEST['dataflt'])) { 560 $flt = array(); 561 } elseif(!is_array($_REQUEST['dataflt'])) { 562 $flt = (array) $_REQUEST['dataflt']; 563 } else { 564 $flt = $_REQUEST['dataflt']; 565 } 566 foreach($flt as $key => $line) { 567 // we also take the column and filtertype in the key: 568 if(!is_numeric($key)) { 569 $line = $key . $line; 570 } 571 $f = $this->_parse_filter($line); 572 if(is_array($f)) { 573 $f['logic'] = 'AND'; 574 $filters[] = $f; 575 } 576 } 577 return $filters; 578 } 579 580 /** 581 * prepare an array to be passed through buildURLparams() 582 * 583 * @param string $name keyname 584 * @param string|array $array value or key-value pairs 585 * @return array 586 */ 587 function _a2ua($name, $array) { 588 $urlarray = array(); 589 foreach((array) $array as $key => $val) { 590 $urlarray[$name . '[' . $key . ']'] = $val; 591 } 592 return $urlarray; 593 } 594 595 /** 596 * get current URL parameters 597 * 598 * @param bool $returnURLparams 599 * @return array with dataflt, datasrt and dataofs parameters 600 */ 601 function _get_current_param($returnURLparams = true) { 602 $cur_params = array(); 603 if(isset($_REQUEST['dataflt'])) { 604 $cur_params = $this->_a2ua('dataflt', $_REQUEST['dataflt']); 605 } 606 if(isset($_REQUEST['dataausrt'])) { 607 $cur_params['dataausrt'] = $_REQUEST['dataausrt']; 608 } 609 if(isset($_REQUEST['dataauofs'])) { 610 $cur_params['dataauofs'] = $_REQUEST['dataauofs']; 611 } 612 613 //combine key and value 614 if(!$returnURLparams) { 615 $flat_param = array(); 616 foreach($cur_params as $key => $val) { 617 $flat_param[] = $key . $val; 618 } 619 $cur_params = $flat_param; 620 } 621 return $cur_params; 622 } 623 624 /** 625 * Get url parameters, remove all filters for given column and add filter for desired tag 626 * 627 * @param array $column 628 * @param string $tag 629 * @return array of url parameters 630 */ 631 function _getTagUrlparam($column, $tag) { 632 $param = array(); 633 634 if(isset($_REQUEST['dataflt'])) { 635 $param = (array) $_REQUEST['dataflt']; 636 637 //remove all filters equal to column 638 foreach($param as $key => $flt) { 639 if(!is_numeric($key)) { 640 $flt = $key . $flt; 641 } 642 $filter = $this->_parse_filter($flt); 643 if($filter['key'] == $column['key']) { 644 unset($param[$key]); 645 } 646 } 647 } 648 $param[] = $column['key'] . "_=$tag"; 649 $param = $this->_a2ua('dataflt', $param); 650 651 if(isset($_REQUEST['dataausrt'])) { 652 $param['dataausrt'] = $_REQUEST['dataausrt']; 653 } 654 if(isset($_REQUEST['dataauofs'])) { 655 $param['dataauofs'] = $_REQUEST['dataauofs']; 656 } 657 658 return $param; 659 } 660 661 /** 662 * Perform replacements on the output values 663 * 664 * @param string $value 665 * @return string 666 */ 667 private function replacePlaceholders($value) { 668 return $this->makeTranslationReplacement($value); 669 } 670} 671