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