1<?php 2 3 4/** 5 * Database interface implementation 6 * 7 * This file is part of DokuWiki plugin database2 and is available under 8 * GPL version 2. See the following URL for a copy of this license! 9 * 10 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 11 * 12 * 13 * @author Thomas Urban <soletan@nihilum.de> 14 * @version 0.2 15 * @copyright GPLv2 16 * 17 */ 18 19 20if ( !defined( 'DB2_PATH' ) ) 21 define( 'DB2_PATH', 'lib/plugins/database2/' ); 22 23 24/** 25 * Implementation of database access feature. 26 * 27 * This class is integrated into DokuWiki syntax extension in file syntax.php 28 * and provides the plugin's actual features like database interaction, table 29 * browsing, data lookup and input ... 30 * 31 * @author Thomas Urban <soletan@nihilum.de> 32 * @version 0.2 33 * @copyright GPLv2 34 * 35 */ 36 37class Database2 38{ 39 40 const maxSurroundingPagesCount = 3; 41 42 const maxUploadSize = 2097152; // 2 MiB 43 44 45 protected static $ioIndices = array(); 46 47 48 /** 49 * Renderer for producing any output. 50 * 51 * @var Doku_Renderer_xhtml 52 */ 53 54 public $renderer; 55 56 57 /** 58 * Link to database of current namespace. 59 * 60 * @var PDO 61 */ 62 63 protected $db; 64 65 66 /** 67 * Name of driver indicating type of DB currently connected to. 68 * 69 * @var string 70 */ 71 72 private $driver; 73 74 75 /** 76 * Name of table managed by current instance. 77 * 78 * @var string 79 */ 80 81 protected $table = null; 82 83 84 /** 85 * Meta information on/definition of current table as provided in tag value 86 * 87 * @var array 88 */ 89 90 protected $meta = array(); 91 92 93 /** 94 * Options additionally provided in opening tag's attributes. 95 * 96 * @var array 97 */ 98 99 protected $options = array(); 100 101 102 /** 103 * I/O-Index assigned for use on acting on table managed by current 104 * instance. 105 * 106 * @var integer 107 */ 108 109 protected $ioIndex = null; 110 111 112 /** 113 * input data 114 * 115 * @var array 116 */ 117 118 private $input = null; 119 120 121 /** 122 * DSN used to connect to database server 123 * 124 * @var string 125 */ 126 127 private $dsn = null; 128 129 130 /** 131 * Name of slot in site configuration containing authentication data. 132 * 133 * @var string 134 */ 135 136 private $authSlot = null; 137 138 139 /** 140 * Reference to database syntax plugin object integrating this instance of 141 * Database2. 142 * 143 * @var DokuWiki_Syntax_Plugin 144 */ 145 146 protected $integrator = null; 147 148 149 /** 150 * Page ID explicitly selected to use in current instance. 151 * 152 * This is used to supercede ID returned by DokuWiki's getID() and is 153 * required in media.php using this class in a faked context. 154 */ 155 156 protected $explicitPageID = null; 157 158 159 160 public function __construct( Doku_Renderer $renderer, 161 DokuWiki_Syntax_Plugin $integrator ) 162 { 163 164 $this->renderer = $renderer; 165 $this->db = null; 166 $this->integrator = $integrator; 167 168 $this->renderer->nocache(); 169 170 } 171 172 173 /** 174 * Detects whether provided name is a valid name for a table or column. 175 * 176 * @param string $in name to test 177 * @return boolean true if provided name might be used for tables/columns 178 */ 179 180 public static function isValidName( $in ) 181 { 182 return preg_match( '/^[_a-z][_a-z0-9]+$/i', $in ); 183 } 184 185 186 /** 187 * Retrieves open link to current database file or null if not connected. 188 * 189 * @return PDO 190 */ 191 192 public function getLink() 193 { 194 return $this->db; 195 } 196 197 198 /** 199 * Retrieves configuration setting using integrator's interface. 200 * 201 * @param string $name name of setting to retrieve 202 * @return mixed retrieved configuration setting 203 */ 204 205 public function getConf( $name ) 206 { 207 global $conf; 208 209 210 if ( $this->integrator instanceof DokuWiki_Syntax_Plugin ) 211 { 212 213 $value = $this->integrator->getConf( $name, null ); 214 if ( is_null( $value ) ) 215 if ( !is_null( $conf[$name] ) ) 216 $value = $conf[$name]; 217 218 return $value; 219 220 } 221 222 223 // fix for accessing configuration in media.php 224 if ( isset( $conf['plugin']['database2'][$name] ) ) 225 return $conf['plugin']['database2'][$name]; 226 227 return $conf[$name]; 228 229 } 230 231 232 /** 233 * Retrieves localized string. 234 * 235 * @param string $name name of localized string 236 * @return mixed retrieved localized string 237 */ 238 239 public function getLang( $name ) 240 { 241 242 if ( $this->integrator instanceof DokuWiki_Syntax_Plugin ) 243 return $this->integrator->getLang( $name ); 244 245 246 // fix for accessing strings in media.php 247 if ( !is_array( $this->integrator ) ) 248 { 249 250 $lang = array(); 251 252 @include( dirname( __FILE__ ) . '/lang/en/lang.php' ); 253 if ( $GLOBALS['conf']['lang'] != 'en' ) 254 @include( dirname( __FILE__ ) . '/lang/' . 255 $GLOBALS['conf']['lang'] . '/lang.php' ); 256 257 $this->integrator = $lang; 258 259 } 260 261 262 return $this->integrator[$name]; 263 264 } 265 266 267 /** 268 * Retrieves index to be used for parameters/fields passed in I/O on 269 * currently processed integration of a table in current page. 270 * 271 * The current method is quite unstable, at least on editing arrangement 272 * of database2 instances in a page, however it should work in a production 273 * environment. 274 * 275 * @return integer numeric index 276 */ 277 278 protected function getIndex() 279 { 280 281 if ( is_null( $this->table ) ) 282 throw new Exception( 'getIndex: missing name of managed table' ); 283 284 if ( is_null( $this->ioIndex ) ) 285 $this->ioIndex = self::$ioIndices[$tableName]++; 286 287 return $this->ioIndex; 288 289 } 290 291 292 /** 293 * Retrieves ID of current DokuWiki page. 294 * 295 * @return string 296 */ 297 298 protected function getPageID() 299 { 300 301 if ( !is_null( $this->explicitPageID ) ) 302 return $this->explicitPageID; 303 304 return getID(); 305 306 } 307 308 309 /** 310 * Allocates separate section in session data for state of current 311 * table instance. 312 * 313 * @return array 314 */ 315 316 protected function &getSession() 317 { 318 if ( !is_array( $_SESSION['database2'] ) ) 319 $_SESSION['database2'] = array(); 320 321 $id = $this->getPageID(); 322 323 324 // if current page's source has changed ... 325 $dates = p_get_metadata( $id, 'date' ); 326 if ( is_array( $_SESSION['database2'][$id] ) ) 327 if ( $_SESSION['database2'][$id]['onRevision'] != $dates['modified'] ) 328 // ... it's related session-based data is dropped 329 unset( $_SESSION['database2'][$id] ); 330 331 332 if ( !is_array( $_SESSION['database2'][$id] ) ) 333 $_SESSION['database2'][$id] = array( 334 'onRevision' => $dates['modified'], 335 'tables' => array(), 336 ); 337 338 $index = $this->getIndex(); 339 if ( !is_array( $_SESSION['database2'][$id]['tables'][$index] ) ) 340 $_SESSION['database2'][$id]['tables'][$index] = array(); 341 342 343 return $_SESSION['database2'][$id]['tables'][$index]; 344 345 } 346 347 348 /** 349 * Allocates separate section in session data for temporary content of 350 * single-record editor. 351 * 352 * @return array 353 */ 354 355 protected function &getEditorSession() 356 { 357 358 $session =& $this->getSession(); 359 360 if ( !is_array( $session['editors'] ) ) 361 $session['editors'] = array(); 362 363 return $session['editors']; 364 365 } 366 367 368 /** 369 * Renders provided HTML code replacing database tag in current Wiki page. 370 * 371 * @param string $code HTML code to render 372 */ 373 374 protected function render( $code ) 375 { 376 $this->renderer->doc .= strval( $code ); 377 } 378 379 380 /** 381 * Connects to database (external server, local SQLite DB file). 382 * 383 * @param string $dbPath database selector 384 * @param string $authConfigSlot name of slot in site config containing 385 * authentication data 386 * @return boolean true on success, false on failure 387 */ 388 389 public function connect( $dbPath, $authConfigSlot = null ) 390 { 391 392 $dbPath = trim( $dbPath ); 393 if ( $dbPath[0] == '@' ) 394 $dsn = substr( $dbPath, 1 ); 395 else if ( ( $dbPath[0] == '/' ) && !self::getConf( 'useslash' ) && 396 is_dir( dirname( $dbPath ) ) && 397 !preg_match( '#(\.\.)|(^\/(etc)\/)#', $dbPath ) ) 398 $dsn = 'sqlite:' . $dbPath; 399 else 400 $dsn = 'sqlite:' . metaFN( $dbPath, '.db' ); 401 402 403 try 404 { 405 406 // read username/password for authentication from optionally 407 // selected slot in site's configuration 408 409 if ( $authConfigSlot ) 410 { 411 412 $username = $password = ''; 413 414 foreach ( explode( "\n", $this->getConf( 'authSlots' ) ) as $line ) 415 { 416 417 $line = trim( $line ); 418 419 if ( ( $line[0] == '#' ) || ( ( $line[0] == '/' ) && 420 ( $line[1] == '/' ) ) || ( $line === '' ) ) 421 // skip comments and empty lines 422 continue; 423 424 425 // parse assignment 426 $pos = 0; 427 $temp = self::parseAssignment( $line, $pos ); 428 429 if ( !is_array( $temp ) ) 430 continue; 431 432 list( $name, $value ) = $temp; 433 434 if ( strcasecmp( $name, $authConfigSlot ) ) 435 // not related to current authentication slot 436 continue; 437 438 439 // split value into username and password 440 $value = trim( $value ); 441 $sep = strcspn( $value, ':' ); 442 443 $username = trim( substr( $value, 0, $sep ) ); 444 $password = trim( substr( $value, $sep + 1 ) ); 445 446 // done ... 447 break; 448 449 } 450 451 452 if ( $username === '' ) 453 unset( $username, $password ); 454 else if ( $password === '' ) 455 unset( $password ); 456 457 } 458 else 459 unset( $username, $password ); 460 461 462 // connect to database 463 $this->db = new PDO( $dsn, $username, $password ); 464 465 // request throwing exceptions on failure 466 $this->db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 467 468 // cache used driver name 469 $this->driver = strtolower( trim( $this->db->getAttribute( 470 PDO::ATTR_DRIVER_NAME ) ) ); 471 472 473 if ( strpos( $this->driver, 'mysql' ) !== false ) 474 // ensure to use proper encoding on talking to MySQL RDBMSs 475 // NOTE: according to server setup this may result in UTF-8 bytes 476 // being UTF-8 encoded, thus resulting in usual "garbage" 477 // TODO: add option for selecting whether using UTF8-mapping here 478 $this->db->query( 'SET NAMES UTF8' ); 479 480 481 // store how to connect to database for integrated retrieval of files 482 $this->dsn = $dsn; 483 $this->authSlot = $authConfigSlot; 484 485 486 return true; 487 488 } 489 catch ( PDOException $e ) 490 { 491 492 $this->render( sprintf( $this->getLang( 'nodblink' ), $e->getMessage() ) ); 493 494 $this->db = $this->driver = null; 495 496 return false; 497 498 } 499 } 500 501 502 /** 503 * Reads all available input data extracting values related to this plugin. 504 * 505 * @return array set of input data related to current plugin 506 */ 507 508 protected function getInput() 509 { 510 511 if ( is_null( $this->input ) ) 512 { 513 514 $index = $this->getIndex(); 515 516 $this->input = array(); 517 518 $matchingSecTok = ( $_REQUEST['sectok'] == getSecurityToken() ); 519 520 foreach ( $_REQUEST as $name => $value ) 521 if ( preg_match( '/^db2do(.+?)(_[xy])?$/i', $name, $matches ) ) 522 if ( $matchingSecTok || ( $_GET[$name] && preg_match( '/^(cmd|opt)/i', $matches[1] ) ) ) 523 if ( is_null( $this->input[$matches[1]] ) ) 524 if ( !is_array( $value ) || !is_null( $value[$index] ) ) 525 $this->input[$matches[1]] = is_array( $value ) ? $value[$index] : $value; 526 527 } 528 529 530 return $this->input; 531 532 } 533 534 535 /** 536 * Retrieves meta information on columns. 537 * 538 * @return array 539 */ 540 541 public function getColumnsMeta( $ignoreMissingMeta = false ) 542 { 543 544 if ( !is_array( $this->meta ) || empty( $this->meta ) ) 545 { 546 547 $session =& self::getSession(); 548 if ( is_array( $session['definition'] ) ) 549 $this->meta = $session['definition']; 550 else if ( $ignoreMissingMeta ) 551 return array(); // don't store permanently ... 552 else 553 throw new Exception( $this->getLang( 'nocolmeta' ) ); 554 555 } 556 557 558 return $this->meta; 559 560 } 561 562 563 /** 564 * Renders name of form element to conform with input parser above. 565 * 566 * @param string $name internal name of element 567 * @param index $rowid optional rowid element is related to 568 * @return string external name of form element 569 */ 570 571 protected function varname( $name, $rowid = null ) 572 { 573 return 'db2do' . $name . ( $rowid ? $rowid : '' ) . '[' . 574 $this->getIndex() . ']'; 575 } 576 577 578 /** 579 * Processes occurrence of database tag in a wiki page. 580 * 581 * The tag's value (stuff between opening and closing tag) is passed in 582 * $code. 583 * 584 * @param string $table name of table to work with 585 * @param string $code code found between opening and closing tag 586 * @param array $options additional options provided in tag attributes 587 */ 588 589 public function process( $table, $code, $options ) 590 { 591 592 // wrap all action on database in one exception handler 593 try 594 { 595 596 // check whether or not database tags are enabled on this page 597 if ( !$this->getConf( 'enableallpages' ) ) 598 { 599 600 $patterns = explode( "\n", trim( $this->getConf( 'enablepages' ) ) ); 601 $enabled = false; 602 603 $pageID = $this->getPageID(); 604 605 foreach ( $patterns as $pattern ) 606 { 607 608 $pattern = trim( $pattern ); 609 if ( preg_match( '#^/.+/\w*$#', $pattern ) ) 610 $match = preg_match( $pattern, $pageID ); 611 else 612 $match = fnmatch( $pattern, $pageID ); 613 614 if ( $match ) 615 { 616 $enabled = true; 617 break; 618 } 619 } 620 621 if ( !$enabled ) 622 { 623 // use of database tag is disabled 624 625 $this->render( '<div class="database2-disabled">' . 626 $this->getLang( 'tagdisabled' ) . '</div>' ); 627 628 return; 629 630 } 631 } 632 633 634 635 // normalize/validate table name 636 $table = preg_replace( '/[^\w]/', '_', trim( $table ) ); 637 638 if ( in_array( $table, array( '__keys', '__locks', '__log', ) ) ) 639 throw new Exception( $this->getLang( 'restabnames' ) ); 640 641 $this->table = $table; 642 643 // select subset of input parameters 644 $this->ioIndex = null; // drop to re-obtain new I/O index next 645 $index = $this->getIndex(); // but ensure to obtain at all ... 646 647 // install set of options 648 $this->options = is_array( $options ) ? $options : array(); 649 650 if ( trim( $this->options['mayview'] ) === '' ) 651 $this->options['mayview'] = '@ALL'; 652 653 if ( trim( $this->options['mayinspect'] ) === '' ) 654 $this->options['mayinspect'] = '@ALL'; 655 656 $this->options['view'] = trim( $this->options['view'] ); 657 if ( !$this->getConf( 'customviews' ) ) 658 $this->options['view'] = ''; 659 else if ( !preg_match( '/^SELECT\s/i', trim( $this->options['view'] ) ) ) 660 $this->options['view'] = ''; 661 662 $this->options['wikimarkup'] = self::asBool( $this->options['wikimarkup'] ); 663 $this->options['simplenav'] = self::asBool( $this->options['simplenav'] ); 664 665 if ( ctype_digit( trim( $this->options['rowsperpage'] ) ) ) 666 { 667 668 $state =& $this->getSession(); 669 if ( !is_integer( $state['num'] ) ) 670 $state['num'] = intval( $this->options['rowsperpage'] ); 671 672 } 673 674 // parse code for contained definitions 675 $this->parseDefinition( $code ); 676 677 678 try 679 { 680 681 // look for available action to perform on selected table 682 683 // support preventing CSRF ... 684 foreach ( $this->getInput() as $key => $dummy ) 685 { 686 687 if ( !preg_match( '/^cmd([a-z]+)(\d*)(_x)?$/i', $key, $matches ) ) 688 continue; 689 690 $action = strtolower( trim( $matches[1] ) ); 691 692 $rowid = intval( $matches[2] ); 693 if ( $rowid ) 694 $rowACL = $this->getRowACL( $rowid ); 695 else 696 $rowid = $rowACL = null; 697 698 699 if ( $action === 'reset' ) 700 { 701 $state =& $this->getSession(); 702 $state = array(); 703 continue; 704 } 705 706 707 if ( !$this->isAuthorizedMulti( $rowACL, $this->options, 'may'.$action ) ) 708 { 709 // user isn't authorized to perform this action 710 $this->render( '<div class="error">' . 711 sprintf( $this->getLang( 'accessdenied' ), 712 $action, $this->table ) . '</div>' ); 713 continue; 714 } 715 716 717 if ( ( $this->getSingleNumericPrimaryKey() !== false ) || 718 in_array( $action, array( 'drop', ) ) ) 719 // perform optionally requested action 720 switch ( $action ) 721 { 722 723 case 'inspect' :// show record details (read-only) 724 case 'insert' : // show record editor to insert 725 case 'edit' : // show record editor to adjust 726 do 727 { 728 729 if ( ( $action == 'insert' ) && $rowid ) 730 { 731 // insert record starting with duplicate 732 // of existing one ... 733 734 if ( !$this->isAuthorizedMulti( $rowACL, $this->options, 'mayinspect' ) ) 735 { 736 // user isn't authorized to perform this action 737 $this->render( '<div class="error">' . 738 sprintf( $this->getLang( 'accessdenied' ), 739 $action, $this->table ) . '</div>' ); 740 break; 741 } 742 743 // use duplicate of selected record 744 $duplicateRowID = $rowid; 745 746 // but don't overwrite it! 747 $rowid = null; 748 749 } 750 else 751 $duplicateRowID = null; 752 753 754 // invoke editor/single record view 755 $readonly = ( $action == 'inspect' ); 756 $result = $this->editRecord( $rowid, $readonly, 757 $duplicateRowID, 758 $rowACL ); 759 if ( !$result ) 760 // skip rendering table, rendered single 761 return; 762 763 if ( is_integer( $result ) ) 764 { 765 // switch to selected record 766 $rowid = $result; 767 continue; 768 } 769 770 break; 771 772 } 773 while ( true ); 774 775 break; 776 777 case 'delete' : 778 $this->deleteRecord( $rowid ); 779 break; 780 781 case 'drop' : 782 $this->dropTable(); 783 break; 784 785 default : 786 $method = array( &$this, '__handle_'.$action ); 787 if ( is_callable( $method ) ) 788 if ( !call_user_func( $method, $rowid ) ) 789 return; 790 791 } 792 } 793 794 795 if ( !$this->exists( $this->table ) ) 796 // (re-)create table as it is missing (e.g. after dropping) 797 $this->createTable(); 798 799 800 801 /* 802 * finally render table 803 */ 804 805 // check user's authorization to view table 806 if ( $this->isAuthorized( $this->options['mayview'] ) ) 807 // user may view table 808 $this->showTable( true, false, false, $this->options['view'] ); 809 810 } 811 catch ( PDOException $e ) 812 { 813 throw new Exception( sprintf( $this->getLang( 'badinteraction' ), 814 $e->getMessage(), $e->getLine() ) ); 815 } 816 817 } 818 catch ( Exception $e ) 819 { 820 821 $this->render( '<div class="error">' . 822 sprintf( $this->getLang( 'deferror' ), 823 $e->getMessage(), $e->getLine(), $e->getFile() ) . 824 '</div>' ); 825 826 $resetCmd = $this->varname( 'cmdreset' ); 827 $viewCmd = $this->varname( 'view' ); 828 829 $btnSession = $this->getLang( 'btnResetSession' ); 830 $btnTable = $this->getLang( 'btnViewTable' ); 831 832 $this->render( $this->wrapInForm( <<<EOT 833<input type="submit" name="$resetCmd" value="$btnSession" /> 834<input type="submit" name="$viewCmd" value="$btnTable" /> 835EOT 836 ) ); 837 838 } 839 } 840 841 842 protected function getACLCol() 843 { 844 845 foreach ( $this->meta as $name => $def ) 846 if ( $def['isColumn'] && ( $def['format'] == 'acl' ) ) 847 return $name; 848 849 return null; 850 851 } 852 853 854 protected function getRowACL( $rowid ) 855 { 856 857 $session =& $this->getSession(); 858 859 if ( !is_array( $session['rowACLs'] ) ) 860 $session['rowACLs'] = array(); 861 862 if ( !isset( $session['rowACLs'][$rowid] ) ) 863 { 864 865 $aclName = $this->getACLCol(); 866 $idColumn = $this->getSingleNumericPrimaryKey(); 867 868 if ( $aclName && $idColumn ) 869 { 870 871 $sql = sprintf( 'SELECT %s FROM %s WHERE %s=?', $aclName, 872 $this->table, $idColumn ); 873 874 $st = $this->db->prepare( $sql ); 875 if ( !$st ) 876 throw new PDOException( $this->getLang( 'aclprepare' ) ); 877 878 if ( !$st->execute( array( $rowid ) ) ) 879 throw new PDOException( $this->getLang( 'aclexecute' ) ); 880 881 $row = $st->fetch( PDO::FETCH_NUM ); 882 883 $st->closeCursor(); 884 885 886 $session['rowACLs'][$rowid] = trim( $row[0] ); 887 888 } 889 } 890 891 892 return $session['rowACLs'][$rowid] ? $session['rowACLs'][$rowid] : null; 893 894 } 895 896 897 protected function dropRowACL( $rowid ) 898 { 899 900 $session =& $this->getSession(); 901 902 if ( $session['rowACLs'][$rowid] ) 903 unset( $session['rowACLs'] ); 904 905 } 906 907 908 /** 909 * Creates managed table on demand. 910 * 911 */ 912 913 protected function createTable() 914 { 915 916 if ( empty( $this->meta ) ) 917 throw new Exception( $this->getLang( 'defmissing' ) ); 918 919 // extract all column definitions 920 $cols = array_map( create_function( '$a','return $a[definition];' ), 921 $this->meta ); 922 923 // compile CREATE TABLE-statement using linebreaks as some versions 924 // of SQLite engines cache it for schema representation, thus improving 925 // human-readability ... 926 $sql = "CREATE TABLE {$this->table}\n(\n\t" . implode( ",\n\t", $cols ). 927 "\n)"; 928 929 930 if ( $this->db->query( $sql ) === false ) 931 throw new PDOException( sprintf( $this->getLang( 'nocreatetable' ), 932 $this->table ) ); 933 934 935 $this->log( 'create', $this->table ); 936 937 } 938 939 940 /** 941 * Renders single record for editing (or inspecting if $readOnly is true). 942 * 943 * @param integer $rowid unique numeric ID of record to edit/inspect 944 * @param boolean $readOnly if true, the record is rendered read-only 945 * @param integer $duplicateOf unique numeric ID of record to duplicate 946 * @return boolean if true, the table/list shouldn't be rendered 947 */ 948 949 protected function editRecord( $rowid, $readOnly, $duplicateOf = null, 950 $rowACL = null ) 951 { 952 953 $ioIndex = $this->getIndex(); 954 $input = $this->getInput(); 955 $idColumn = $this->getSingleNumericPrimaryKey(); 956 957 $isNew = !$rowid || $duplicateOf; 958 959 960 961 /** 962 * Obtain lock for exclusively accessing selected record 963 */ 964 965 if ( $rowid && !$readOnly && !$this->obtainLock( $this->table, $rowid ) ) 966 { 967 $this->render( '<div class="error">' . $this->getLang( 'reclocked' ) . '</div>' ); 968 return true; 969 } 970 971 972 973 /* 974 * prepare session to contain data specific to this editor 975 */ 976 977 $state =& $this->getSession(); 978 $store =& $this->getEditorSession(); 979 980 $errors = array(); 981 982 983 984 /* 985 * process input data updating record and handle contained commands 986 */ 987 988 if ( $input && ( $input['____single'] === md5( $rowid ) ) ) 989 { 990 991 // select result to return depending on selected navigation mode 992 if ( $input['____nav'] ) 993 { 994 995 if ( $input['____nav'][0] == 'P' ) 996 $state['nav'] = 'previous'; 997 else 998 $state['nav'] = 'next'; 999 1000 $result = intval( substr( $input['____nav'], 1 ) ); 1001 if ( !$result ) 1002 $result = true; 1003 1004 } 1005 else 1006 { 1007 unset( $state['nav'] ); 1008 $result = true; 1009 } 1010 1011 1012 1013 if ( $input['____cancel'] ) 1014 { 1015 // cancel editing record 1016 1017 if ( $rowid && !$readOnly && !$this->releaseLock( $this->table, $rowid ) ) 1018 $this->render( '<div class="error">' . $this->getLang( 'editnorelease' ) . '</div>' ); 1019 1020 // drop content of current editor session 1021 $store = array(); 1022 1023 return $result; 1024 1025 } 1026 1027 1028 1029 /* 1030 * validate input data and store in session 1031 */ 1032 1033 if ( !$readOnly ) 1034 foreach ( $this->meta as $column => $def ) 1035 if ( $def['isColumn'] && ( $column != $idColumn ) ) 1036 { 1037 1038 $mayEdit = !$def['options']['mayedit'] || $this->isAuthorizedMulti( $rowACL, $def['options'], 'mayedit' ); 1039 if ( !$mayEdit ) 1040 continue; 1041 1042 // user may edit this column ... 1043 $mayView = !$def['options']['mayview'] || $this->isAuthorizedMulti( $rowACL, $def['options'], 'mayview' ); 1044 if ( !$mayView ) 1045 // ... but mustn't view it ... 1046 if ( $rowid ) 1047 // it's an existing record -> reject editing field 1048 continue; 1049 // ELSE: editing new records doesn't actually imply 1050 // viewing something existing in this field 1051 1052 $error = $this->checkValue( $rowid, $column, $input['data'.$column], $store[$column], $def ); 1053 if ( $error && $column ) 1054 { 1055 // something's wrong, but if it's a typo it's better 1056 // user may change his previous input next rather 1057 // than starting it all over again ... 1058 // --> store even malformed input in editor session 1059 $store[$column] = $input['data'.$column]; 1060 $errors[$column] = $error; 1061 } 1062 1063 } 1064 1065 1066 1067 if ( !$readOnly && empty( $errors ) && $input['____save'] ) 1068 { 1069 1070 /* 1071 * write changed record to database 1072 */ 1073 1074 if ( !$this->db->beginTransaction() ) 1075 $this->render( '<div class="error">' . $this->getLang( 'editnotransact' ) . '</div>' ); 1076 else try 1077 { 1078 1079 // convert record to be written to database next 1080 $record = array(); 1081 foreach ( $store as $column => $value ) 1082 if ( $column !== $idColumn ) 1083 if ( $this->meta[$column]['isColumn'] && 1084 !is_string( $this->meta[$column]['options']['aliasing'] ) ) 1085 { 1086 1087 $value = $this->valueToDB( $rowid, $column, $value, $this->meta[$column] ); 1088 1089 if ( $value !== false ) 1090 $record[$column] = $value; 1091 1092 } 1093 1094 1095 if ( $isNew ) 1096 { 1097 1098 if ( !( $record[$idColumn] = $this->nextID( $this->table, true ) ) ) 1099 throw new PDOException( $this->getLang( 'editnoid' ) ); 1100 1101 $sql = sprintf( 'INSERT INTO %s (%s) VALUES (%s)', 1102 $this->table, 1103 implode( ',', array_keys( $record ) ), 1104 implode( ',', array_pad( array(), 1105 count( $record ), '?' ) ) ); 1106 1107 $log = array( $record[$idColumn], 'insert' ); 1108 1109 } 1110 else 1111 { 1112 1113 $assignments = array(); 1114 foreach ( array_keys( $record ) as $column ) 1115 $assignments[] = $column . '=?'; 1116 1117 $sql = sprintf( 'UPDATE %s SET %s WHERE %s=?', 1118 $this->table, implode( ',', $assignments ), 1119 $idColumn ); 1120 1121 $record[$idColumn] = $rowid; 1122 1123 $log = array( $rowid, 'update' ); 1124 1125 } 1126 1127 1128 $st = $this->db->prepare( $sql ); 1129 if ( !$st ) 1130 throw new PDOException( $this->getLang( 'editprepare' ) ); 1131 1132 if ( !$st->execute( array_values( $record ) ) ) 1133 throw new PDOException( $this->getLang( 'editexecute' ) ); 1134 1135 1136 $this->log( $log[1], $this->table, $log[0] ); 1137 1138 1139 if ( !$this->db->commit() ) 1140 throw new PDOException( $this->getLang( 'editcommit' ) ); 1141 1142 1143 1144 /* 1145 * release lock on record 1146 */ 1147 1148 if ( $rowid && !$readOnly && !$this->releaseLock( $this->table, $rowid, true ) ) 1149 throw new PDOException( $this->getLang( 'editnorelease' ) ); 1150 1151 $store = array(); 1152 1153 return $result; 1154 1155 } 1156 catch ( PDOException $e ) 1157 { 1158 1159 $this->render( '<div class="error">' . 1160 sprintf( $this->getLang( 'editcantsave' ), 1161 $e->getMessage() ) . '</div>' ); 1162 1163 if ( !$this->db->rollBack() ) 1164 $this->render( '<div class="error">' . 1165 $this->getLang( 'editrollback' ) . 1166 '</div>' ); 1167 1168 1169 } 1170 } 1171 } 1172 else 1173 { 1174 1175 /* 1176 * editor started ... load from DB or initialize 1177 */ 1178 1179 if ( $isNew ) 1180 if ( $readOnly ) 1181 return; 1182 1183 1184 1185 if ( $isNew && !$duplicateOf ) 1186 { 1187 1188 $store = array(); 1189 1190 foreach ( $this->meta as $column => $def ) 1191 if ( $def['isColumn'] && ( $column != $idColumn ) ) 1192 $store[$column] = $this->getInitialValue( $column, $def ); 1193 1194 } 1195 else 1196 { 1197 // load record from table 1198 1199 $cols = $this->__columnsList( false ); 1200 $cols = implode( ',', $cols ); 1201 1202 // - get raw record 1203 if ( $this->options['view'] ) 1204 $sql = sprintf( '%s WHERE %s=?', 1205 $this->options['view'], $idColumn ); 1206 else 1207 $sql = sprintf( 'SELECT %s FROM %s WHERE %s=?', 1208 $cols, $this->table, $idColumn ); 1209 1210 $st = $this->db->prepare( $sql ); 1211 if ( !$st ) 1212 throw new PDOException( $this->getLang( 'editloadprepare' ) ); 1213 1214 if ( !$st->execute( array( $duplicateOf ? $duplicateOf : $rowid ) ) ) 1215 throw new PDOException( $this->getLang( 'editloadexecute' ) ); 1216 1217 $record = $st->fetch( PDO::FETCH_ASSOC ); 1218 if ( !is_array( $record ) || empty( $record ) ) 1219 throw new PDOException( $this->getLang( 'notarecord' ) ); 1220 1221 $st->closeCursor(); 1222 1223 1224 // drop contained ID column 1225 unset( $record[$idColumn] ); 1226 1227 1228 // on duplicating record reset some of the original record's 1229 // values current user isn't authorized to view 1230 foreach ( $record as $name => $value ) 1231 if ( !$this->isAuthorizedMulti( $rowACL, $this->meta[$name]['options'], 'mayview', null, true ) ) 1232 // user mustn't view this value of original record 1233 // --> reset to defined default value 1234 $record[$name] = $this->getInitialValue( $column, $def ); 1235 1236 1237 1238 1239 // transfer to temporary storage converting accordingly 1240 $store = $this->__sortRecord( $record ); 1241 1242 1243 // convert values from DB format to internal one 1244 foreach ( $store as $column => $value ) 1245 $store[$column] = $this->valueFromDB( $rowid, $column, $value, $this->meta[$column] ); 1246 1247 } 1248 } 1249 1250 1251 1252 /* 1253 * prepare to support navigation 1254 */ 1255 1256 $nav = array(); 1257 1258 if ( !$isNew ) 1259 { 1260 1261 if ( !is_integer( $input['____idx'] ) ) 1262 $input['____idx'] = $this->recordI2X( $rowid ); 1263 1264 1265 if ( $input['____idx'] ) 1266 $nav[] = array( 'P' . $this->recordX2I( $input['____idx'] - 1 ), 1267 $this->getLang( 'navprevious' ), 1268 ( $state['nav'] === 'previous' ) ); 1269 1270 $nextID = $this->recordX2I( $input['____idx'] + 1 ); 1271 if ( $nextID ) 1272 $nav[] = array( 'N' . $nextID, $this->getLang( 'navnext' ), 1273 ( $state['nav'] === 'next' ) ); 1274 1275 if ( count( $nav ) ) 1276 array_unshift( $nav, array( 0, $this->getLang( 'navreturn' ) ) ); 1277 1278 } 1279 1280 if ( empty( $nav ) ) 1281 $nav = null; 1282 1283 1284 1285 /* 1286 * Render single editor 1287 */ 1288 1289 // compile form 1290 $elements = array(); 1291 1292 $elements[] = $this->renderField( true, null, null, array(), null, $readOnly, $rowACL ); 1293 1294 foreach ( $store as $column => $value ) 1295 $elements[] = $this->renderField( $rowid, $column, $value, 1296 $this->meta[$column], 1297 $errors[$column], $readOnly, $rowACL ); 1298 1299 $elements[] = $this->renderField( false, $nav, null, array(), null, $readOnly, $rowACL ); 1300 1301 1302 if ( $readOnly && $rowid ) 1303 $cmdName = 'inspect' . $rowid; 1304 else if ( $rowid ) 1305 $cmdName = 'edit' . $rowid; 1306 else 1307 $cmdName = 'insert0'; 1308 1309 // ensure to come back here on submitting form data 1310 $this->render( $this->wrapInForm( implode( '', $elements ), array( 1311 $this->varname( 'cmd' . $cmdName ) => '1', 1312 $this->varname( '____single' ) => md5( $rowid ), 1313 $this->varname( '____idx' ) => $input['____idx'], 1314 ), self::maxUploadSize, true ) ); 1315 1316 1317 // return and mark to prevent rendering data list 1318 return false; 1319 1320 } 1321 1322 1323 /** 1324 * Sorts record according to tabindex order provided in definition. 1325 * 1326 * @param array $record unsorted record 1327 * @return array sorted record 1328 */ 1329 1330 private function __sortRecord( $record ) 1331 { 1332 1333 $in = $record; 1334 $index = array(); 1335 1336 foreach ( $in as $column => $value ) 1337 { 1338 1339 $tabindex = $this->meta[$column]['options']['tabindex']; 1340 if ( $tabindex > 0 ) 1341 { 1342 $index[$column] = intval( $tabindex ); 1343 unset( $in[$column] ); 1344 } 1345 } 1346 1347 foreach ( $in as $column => $value ) 1348 $index[$column] = empty( $index ) ? 1 : ( max( $index ) + 1 ); 1349 1350 1351 // sort columns according to explicit/implicit tabindex 1352 asort( $index ); 1353 1354 1355 // sort record according to that index 1356 $out = array(); 1357 foreach ( $index as $column => $dummy ) 1358 if ( $this->meta[$column]['isColumn'] ) 1359 $out[$column] = $record[$column]; 1360 1361 1362 return $out; 1363 1364 } 1365 1366 1367 /** 1368 * Deletes selected record from managed table. 1369 * 1370 * @param integer $rowid ID of row to delete 1371 */ 1372 1373 protected function deleteRecord( $rowid ) 1374 { 1375 1376 if ( !$rowid || !ctype_digit( trim( $rowid ) ) ) 1377 throw new Exception( $this->getLang( 'notarecord' ) ); 1378 1379 1380 if ( !$this->db->beginTransaction() ) 1381 throw new PDOException( $this->getLang( 'notransact' ) ); 1382 1383 try 1384 { 1385 1386 if ( !$this->obtainLock( $this->table, $rowid, true, true ) ) 1387 { 1388 $this->render( '<div class="error">' . $this->getLang( 'reclocked' ) . '</div>' ); 1389 $this->db->rollback(); 1390 return true; 1391 } 1392 1393 1394 $idColumn = $this->getSingleNumericPrimaryKey(); 1395 1396 $st = $this->db->prepare( 'DELETE FROM ' . $this->table . ' WHERE '. 1397 $idColumn . '=?' ); 1398 if ( !$st ) 1399 throw new PDOException( $this->getLang( 'delprepare' ) ); 1400 1401 if ( !$st->execute( array( $rowid ) ) ) 1402 throw new PDOException( $this->getLang( 'delexecute' ) ); 1403 1404 $this->log( 'delete', $this->table, $rowid ); 1405 1406 1407 if ( !$this->db->commit() ) 1408 throw new PDOException( $this->getLang( 'delcommit' ) ); 1409 1410 } 1411 catch ( PDOException $e ) 1412 { 1413 1414 $this->db->rollback(); 1415 1416 throw $e; 1417 1418 } 1419 } 1420 1421 1422 /** 1423 * Drops whole table. 1424 * 1425 */ 1426 1427 protected function dropTable() 1428 { 1429 1430 if ( !$this->db->beginTransaction() ) 1431 throw new PDOException( $this->getLang( 'notransact' ) ); 1432 1433 try 1434 { 1435 1436 if ( !$this->obtainLock( $this->table, null, true, true ) ) 1437 { 1438 $this->render( '<div class="error">' . $this->getLang( 'tablelocked' ) . '</div>' ); 1439 $this->db->rollback(); 1440 return true; 1441 } 1442 1443 1444 if ( $this->db->query( 'DROP TABLE ' . $this->table ) === false ) 1445 throw new PDOException( sprintf( $this->getLang( 'nodrop' ), $this->table ) ); 1446 1447 $this->log( 'drop', $this->table ); 1448 1449 1450 if ( !$this->db->commit() ) 1451 throw new PDOException( $this->getLang( 'dropcommit' ) ); 1452 1453 } 1454 catch ( PDOException $e ) 1455 { 1456 1457 $this->db->rollback(); 1458 1459 throw $e; 1460 1461 } 1462 } 1463 1464 1465 /** 1466 * Retrieves list of columns (optionally reduced to the set marked as 1467 * visible) in current table. 1468 * 1469 * @param boolean $visibleOnly if true return visible columns, only 1470 * @param boolean $printable if true, obey marks on columns being printable 1471 * @return array list of columns' names 1472 */ 1473 1474 protected function __columnsList( $visibleOnly = true, $printable = false ) 1475 { 1476 1477 $meta = $this->getColumnsMeta(); 1478 1479 $idCol = $this->getSingleNumericPrimaryKey(); 1480 $cols = array(); 1481 1482 if ( $visibleOnly ) 1483 foreach ( $meta as $colName => $def ) 1484 if ( $def['isColumn'] ) 1485 { 1486 1487 if ( $def['options']['visible'] === 1 ) 1488 $use = true; 1489 else if ( $printable ) 1490 $use = $def['options']['print'] || 1491 ( $def['options']['visible'] && 1492 !$def['options']['noprint'] ); 1493 else 1494 $use = $def['options']['visible']; 1495 1496 if ( $use ) 1497 $cols[] = $colName; 1498 1499 } 1500 1501 if ( !$visibleOnly || empty( $cols ) ) 1502 foreach ( $meta as $colName => $def ) 1503 if ( $def['isColumn'] ) 1504 $cols[] = $colName; 1505 1506 foreach ( $cols as $index => $name ) 1507 if ( is_string( $meta[$name]['options']['aliasing'] ) ) 1508 $cols[$index] = $meta[$name]['options']['aliasing'] . ' AS ' . 1509 $name; 1510 1511 1512 if ( $idCol ) 1513 array_unshift( $cols, $idCol ); 1514 1515 1516 return $cols; 1517 1518 } 1519 1520 1521 /** 1522 * Retrieves count of records in table obeying current state of filter. 1523 * 1524 * @return integer number of available records in table matching filter 1525 */ 1526 1527 protected function __recordsCount( $customQuery = null ) 1528 { 1529 1530 // get compiled filter 1531 list( $filter, $parameters ) = $this->getFilter(); 1532 1533 1534 $customQuery = trim( $customQuery ); 1535 if ( $customQuery === '' ) 1536 $query = "SELECT COUNT(*) FROM {$this->table}"; 1537 else 1538 { 1539 1540 $query = preg_replace( '/^SELECT .+ (FROM .+)$/i', 1541 'SELECT COUNT(*) \1', $customQuery ); 1542 1543 if ( stripos( $query, ' WHERE ' ) !== false ) 1544 { 1545 $filter = ''; 1546 $parameters = array(); 1547 } 1548 } 1549 1550 $st = $this->db->prepare( $query . $filter ); 1551 if ( !$st ) 1552 throw new PDOException( $this->getLang( 'countprepare' ) ); 1553 1554 if ( !$st->execute( $parameters ) ) 1555 throw new PDOException( $this->getLang( 'countexecute' ) ); 1556 1557 $count = $st->fetch( PDO::FETCH_NUM ); 1558 1559 $st->closeCursor(); 1560 1561 1562 return intval( array_shift( $count ) ); 1563 1564 } 1565 1566 1567 /** 1568 * Retrieves records from table. 1569 * 1570 * @param array/string $columns list of records to retrieve, "*" for all 1571 * @param boolean $obeyFilter if true, current state of filter is used to 1572 * retrieve matching records, only 1573 * @param string $sortColumn name of column to optionally sort by 1574 * @param boolean $sortAscendingly if true, request to sort ascendingly 1575 * @param integer $offset optional number of matching records to skip 1576 * @param integer $limit maximum number of matching records to retrieve 1577 * @return array excerpt of matching records according to given parameters 1578 */ 1579 1580 public function __recordsList( $columns = '*', $obeyFilter = true, 1581// $sortColumn = null, $sortAscendingly = true, 1582 $offset = 0, $limit = null, $customQuery = null ) 1583 { 1584 1585 $config = $this->__configureSelect(); 1586 if ( is_array( $config ) ) 1587 { 1588 1589 list( $filter, $parameters, $order, $cols ) = $config; 1590 1591 } 1592 else 1593 { 1594 1595/* 1596 // prepare filter 1597 if ( $obeyFilter ) 1598 list( $filter, $parameters ) = $this->getFilter(); 1599 else 1600 { 1601 $filter = ''; 1602 $parameters = array(); 1603 } 1604 1605 // prepare sorting 1606 if ( $sortColumn ) 1607 if ( !$this->meta[$sortColumn]['isColumn'] ) 1608 $sortColumn = null; 1609 1610 if ( $sortColumn ) 1611 $order = ' ORDER BY ' . $sortColumn . 1612 ( $sortAscendingly ? ' ASC' : ' DESC' ); 1613 else 1614 $order = ''; 1615 1616 1617*/ 1618 1619 $cols = array(); 1620 $filter = ''; 1621 $parameters = array(); 1622 $order = ''; 1623 1624 } 1625 1626 1627 // prepare limits 1628 if ( ( $offset > 0 ) || ( $limit > 0 ) ) 1629 { 1630 1631 $limit = ' LIMIT ' . ( ( $limit > 0 ) ? $limit : '10' ); 1632 1633 if ( $offset > 0 ) 1634 $limit .= ' OFFSET ' . $offset; 1635 1636 } 1637 1638 // prepare columns selected for retrieval 1639 if ( is_array( $columns ) ) 1640 $cols = array_merge( $columns, $cols ); 1641 else 1642 $cols[] = $columns; 1643 1644 $columns = implode( ',', $cols ); 1645 1646 1647 if ( trim( $customQuery ) === '' ) 1648 $query = 'SELECT ' . $columns . ' FROM ' . $this->table; 1649 else 1650 { 1651 1652 $query = $customQuery; 1653 1654 if ( stripos( $query, ' WHERE ' ) !== false ) 1655 { 1656 $filter = ''; 1657 $parameters = array(); 1658 } 1659 1660 } 1661 1662 1663 // query for records returning whole resultset 1664 $st = $this->db->prepare( $query . $filter . $order . $limit ); 1665 if ( !$st ) 1666 throw new PDOException( $this->getLang( 'listprepare' ) ); 1667 1668 if ( !$st->execute( $parameters ) ) 1669 throw new PDOException( $this->getLang( 'listexecute' ) ); 1670 1671 return $st->fetchAll( PDO::FETCH_ASSOC ); 1672 1673 } 1674 1675 1676 /** 1677 * Renders (excerpt of) managed table. 1678 * 1679 * @param boolean $expectInput if true, input is processed and controls 1680 * (filter+commands) are rendered 1681 * @param boolean $returnOutput if true, rendered table is returned rather 1682 * than enqueued for rendering in DokuWiki page 1683 * @param boolean $listAll if true, paging is disabled and all available/ 1684 * matching records are rendered 1685 * @param string $customQuery a custom query to use instead of managed one 1686 */ 1687 1688 protected function showTable( $expectInput = true, $returnOutput = false, 1689 $listAll = false, $customQuery = null, 1690 $isPrintVersion = false ) 1691 { 1692 1693 $customQuery = trim( $customQuery ); 1694 1695 $meta = $this->getColumnsMeta( ( $customQuery !== '' ) ); 1696 1697 1698 1699 /* 1700 * count all matching records in table 1701 */ 1702 1703 $count = $this->__recordsCount( $customQuery ); 1704 1705 1706 1707 /* 1708 * update view state according to available input data 1709 */ 1710 1711 $state =& $this->getSession(); 1712 1713 if ( trim( $state['sort'] ) === '' ) 1714 $state['sort'] = $this->options['sort']; 1715 1716 1717 $updated = array( 1718 'skip' => intval( $state['skip'] ), 1719 'num' => intval( $state['num'] ), 1720 'sort' => trim( $state['sort'] ), 1721 ); 1722 1723 if ( $expectInput ) 1724 { 1725 1726 $input = $this->getInput(); 1727 1728 foreach ( $input as $name => $dummy ) 1729 if ( preg_match( '/^(skip|num|sort)(.+)$/i', $name, $m ) ) 1730 { 1731 1732 $name = strtolower( $m[1] ); 1733 1734 $updated[$name] = ( $name == 'sort' ) ? trim( $m[2] ) 1735 : intval( $m[2] ); 1736 1737 } 1738 } 1739 1740 1741 // keep values in range 1742 $updated['num'] = max( 10, $updated['num'] ); 1743 $updated['skip'] = max( 0, min( $count - $updated['num'], $updated['skip'] ) ); 1744 1745 // save updated view state in session 1746 $state = array_merge( $state, $updated ); 1747 1748 // load view state values for easier access 1749 extract( $updated ); 1750 1751 1752 1753 /* 1754 * prepare information on requested sorting 1755 */ 1756 1757/* 1758 if ( $sort ) 1759 { 1760 1761 $sortDescendingly = ( $sort[0] == '!' ); 1762 $sortCol = $sortDescendingly ? strtok( substr( $sort, 1 ), ',' ) 1763 : $sort; 1764 $sortCol = trim( $sortCol ); 1765 1766 } 1767 1768*/ 1769 1770 1771 /* 1772 * query to list matching records 1773 */ 1774 1775 if ( $listAll ) 1776 unset( $skip, $num ); 1777 1778 $cols = $this->__columnsList( true, $isPrintVersion ); 1779 $rows = $this->__recordsList( $cols, true, $skip, $num, $customQuery ); 1780 1781 1782 $idCol = $this->getSingleNumericPrimaryKey(); 1783 $code = $this->__renderTable( $idCol, $cols, $rows, $count, $num, 1784 $skip, $sort, $meta, $expectInput, 1785 $listAll ); 1786 1787 if ( $returnOutput ) 1788 return $code; 1789 1790 $this->render( $code ); 1791 1792 } 1793 1794 1795 public function __renderTable( $idCol, $cols, $rows, $count, $num, $skip, 1796 $sort, $meta, $expectInput, $listAll ) 1797 { 1798 1799 // required to check whether $idCol column has to be rendered or not 1800 $visibleIDCol = null; 1801 1802 foreach ( $meta as $colName => $def ) 1803 if ( $def['isColumn'] ) 1804 { 1805 1806 if ( is_null( $visibleIDCol ) && $def['options']['visible'] ) 1807 $visibleIDCol = ( $colName == $idCol ); 1808 else if ( $colName == $idCol ) 1809 $visibleIDCol = ( $def['options']['visible'] != false ); 1810 1811 } 1812 1813 if ( is_null( $visibleIDCol ) ) 1814 $visibleIDCol = $meta[$idCol] && !$meta[$idCol]['auto_id']; 1815 1816 1817 1818 /* 1819 * - collect header row according to listed rows 1820 * - transform all listed rows to properly defined HTML table cells 1821 */ 1822 1823 $headers = array(); 1824 $counter = $skip; 1825 1826 foreach ( $rows as $nr => $row ) 1827 { 1828 1829 // get record's rowid 1830 $rowid = $idCol ? intval( $row[$idCol] ) : 0; 1831 1832 if ( !$visibleIDCol ) 1833 unset( $row[$idCol] ); 1834 1835 1836 // convert all values in current row to table cells 1837 $i = 0; 1838 1839 if ( $this->options['aclColumn'] ) 1840 { 1841 $rowACL = $row[$this->options['aclColumn']]; 1842 if ( $meta[$this->options['aclColumn']]['options']['visible'] === 1 ) 1843 unset( $row[$this->options['aclColumn']] ); 1844 } 1845 else 1846 $rowACL = null; 1847 1848 1849 $clicks = array(); 1850 1851 foreach ( $row as $column => $value ) 1852 { 1853 1854 if ( !is_array( $meta[$column] ) ) 1855 $meta[$column] = array( 1856 'readonly' => true, 1857 'isColumn' => true, 1858 'format' => 'text', 1859 'label' => $column, 1860 ); 1861 1862 1863 $def = $meta[$column]; 1864 1865 $headers[$column] = $def['label'] ? $def['label'] : $column; 1866 $class = $def['format'] ? $def['format'] : 'na'; 1867 $class .= ' col' . ++$i; 1868 1869 $value = $this->valueFromDB( $rowid, $column, $value, $def ); 1870 1871 $cell = $this->renderValue( $rowid, $column, $value, $def, 1872 false, false, $rowACL ); 1873 1874 switch ( $clickAction = $def['options']['onclick'] ) 1875 { 1876 1877 case 'edit' : 1878 if ( $this->options['view'] || 1879 !$this->isAuthorizedMulti( $rowACL, $this->options, 1880 'may' . $clickAction ) ) 1881 $clickAction = 'inspect'; 1882 1883 case 'inspect' : 1884 if ( $this->isAuthorizedMulti( $rowACL, $this->options, 1885 'may' . $clickAction ) ) 1886 { 1887 $cell = '<a href="#" onclick="return !!document.getElementById(\'' . 1888 $this->varname( 'cmd' . $clickAction, $rowid ) . 1889 '\').click();">' . $cell . '</a>'; 1890 $clicks[] = $clickAction; 1891 } 1892 break; 1893 1894 default : 1895 $cell = $this->convertToLink( $clickAction, $cell, 1896 array( 'value' => $cell ) ); 1897 1898 } 1899 1900 $row[$column] = "<td class=\"$class\">" . $cell . "</td>\n"; 1901 1902 } 1903 1904 // prepend cell for counter 1905 array_unshift( $row, '<td class="counter col0 rightalign">'.++$counter."</td>\n" ); 1906 1907 if ( $expectInput ) 1908 // append cell for row-related commands 1909 $row[] = '<td class="commands col' . ++$i . '">' . 1910 $this->getRecordCommands( $rowid, $rowACL, $clicks ) . 1911 '</td>'; 1912 1913 1914 // convert set of values into HTML table row 1915 $classes = array(); 1916 1917 if ( $nr == 0 ) 1918 $classes[] = 'first'; 1919 if ( $nr == count( $rows ) - 1 ) 1920 $classes[] = 'last'; 1921 1922 $classes[] = ( $nr % 2 ) ? 'even' : 'odd'; 1923 $classes[] = 'row' . ( $nr + 1 ); 1924 1925 $classes = implode( ' ', $classes ); 1926 1927 $rows[$nr] = '<tr class="' . $classes . '">'. 1928 implode( '', $row ) . "</tr>\n"; 1929 1930 } 1931 1932 // finally convert all HTML table rows into single HTML table body 1933 $rows = implode( '', $rows ); 1934 1935 1936 1937 /* 1938 * compile header row 1939 */ 1940 1941 // ensure to have row of headers (missing on an empty list of rows) 1942 if ( empty( $headers ) ) 1943 foreach ( $cols as $column ) 1944 { 1945 1946 unset( $def ); 1947 1948 if ( is_array( $meta[$column] ) ) 1949 $def = $meta[$column]; 1950 else 1951 { 1952 // missing meta information on current "column name" 1953 // --> might be an alias definition 1954 // --> extract originally selected column name from that 1955 1956 $pos = strripos( $column, ' AS ' ); 1957 if ( $pos !== false ) 1958 { 1959 1960 $temp = substr( $column, $pos + 4 ); 1961 if ( $meta[$temp] ) 1962 { 1963 // found definition on extracted column name 1964 1965 $def = $meta[$temp]; 1966 $column = $temp; 1967 1968 } 1969 } 1970 } 1971 1972 1973 $headers[$column] = $def['label'] ? $def['label'] : $column; 1974 1975 } 1976 1977 1978 // next transform headers into table header cells including proper 1979 // controls for sorting etc. 1980 $sortDescendingly = ( $sort[0] == '!' ); 1981 if ( $sortDescendingly ) 1982 $sort = substr( $sort, 1 ); 1983 1984 $sort = trim( strtok( $sort, ',' ) ); 1985 1986 1987 foreach ( $headers as $column => $label ) 1988 { 1989 1990 if ( $meta[$column]['options']['headerlabel'] ) 1991 $label = trim( $meta[$column]['options']['headerlabel'] ); 1992 1993 if ( ( $href = trim( $meta[$column]['options']['headerlink'] ) ) !== '' ) 1994 $label = $this->convertToLink( $href, $label ); 1995 1996 1997 if ( ( $sort == $column ) && $sortDescendingly ) 1998 { 1999 $name = $column; 2000 $icon = 'down'; 2001 $title = $this->getLang( 'hintsortasc' ); 2002 } 2003 else if ( $sort == $column ) 2004 { 2005 $name = '!' . $column; 2006 $icon = 'up'; 2007 $title = $this->getLang( 'hintsortdesc' ); 2008 } 2009 else 2010 { 2011 $name = $column; 2012 $icon = 'none'; 2013 $title = $this->getLang( 'hintsortasc' ); 2014 } 2015 2016 if ( $expectInput ) 2017 $sorter = " <input " . 2018 'type="image" name="' . $this->varname( 'sort' . $name ). 2019 '" src="' . DOKU_BASE . DB2_PATH . 2020 "icons/$icon.gif\" title=\"$title\"/>"; 2021 else 2022 $sorter = ''; 2023 2024 $headers[$column] = "<th class=\"label\">$label$sorter</th>\n"; 2025 2026 } 2027 2028 // compile row of header cells 2029 $headers = implode( '', $headers ); 2030 2031 2032 2033 /* 2034 * check for available filter 2035 */ 2036 2037 if ( $this->isAuthorized( $this->options['mayfilter'] ) && $expectInput ) 2038 { 2039 2040 $filter = $this->renderFilter(); 2041 if ( $filter != '' ) 2042 $filter = '<tr class="filter"><td colspan="3">' . $filter . '</td></tr>'; 2043 2044 } 2045 else 2046 $filter = ''; 2047 2048 2049 2050 /* 2051 * compile pager 2052 */ 2053 2054 list( $flipDown, $flipUp, $pages, $sizes, $stat ) = $this->getPagerElements( $skip, $num, $count ); 2055 2056 if ( !$expectInput ) 2057 unset( $sizes ); 2058 2059 $sepStat = $sizes ? ' — ' . $stat : $stat; 2060 2061 2062 2063 /* 2064 * retrieve all available commands operating on whole table 2065 */ 2066 2067 $globalCmds = $expectInput ? $this->getGlobalCommands() : ''; 2068 2069 2070 /* 2071 * render list of rows as HTML table 2072 */ 2073 2074 $width = intval( $this->options['width'] ) ? ' width="' . $this->options['width'] . '"' : ''; 2075 $cmdHeader = $expectInput ? '<th class="commands"></th>' : ''; 2076 2077 $trClass = $this->options['wikistyle'] ? '' : ' class="data-list"'; 2078 $tableClass = $this->options['wikistyle'] ? ' class="inline"' : ''; 2079 2080 2081 $table = <<<EOT 2082 <table width="100%"$tableClass> 2083 <thead> 2084 <tr class="row0"> 2085 <th class="counter"></th> 2086 $headers 2087 $cmdHeader 2088 </tr> 2089 </thead> 2090 <tbody> 2091 $rows 2092 </tbody> 2093 <caption> 2094 $sizes$sepStat 2095 </caption> 2096 </table> 2097EOT; 2098 2099 if ( $expectInput || !$listAll ) 2100 $table = <<<EOT 2101<table class="database2"$width> 2102 <tbody> 2103 $filter 2104 <tr class="upper-navigation"> 2105 <td align="left" width="33.3%"> 2106 $flipDown 2107 </td> 2108 <td align="center" width="33.3%"> 2109 $pages 2110 </td> 2111 <td align="right" width="33.3%"> 2112 $flipUp 2113 </td> 2114 </tr> 2115 <tr$trClass> 2116 <td colspan="3"> 2117 $table 2118 </td> 2119 </tr> 2120 <tr class="lower-navigation"> 2121 <td colspan="3"> 2122 $globalCmds 2123 </td> 2124 </tr> 2125 </tbody> 2126</table> 2127EOT; 2128 else 2129 $table = <<<EOT 2130<table class="database2"$width> 2131 <tbody> 2132 <tr$trClass> 2133 <td> 2134 $table 2135 </td> 2136 </tr> 2137 </tbody> 2138</table> 2139EOT; 2140 2141 return $expectInput ? $this->wrapInForm( $table ) : $table; 2142 2143 } 2144 2145 2146 public function __csvLine( $fields ) 2147 { 2148 2149 foreach ( $fields as &$field ) 2150 $field = '"' . strtr( $field, array( '"' => '""' ) ) . '"'; 2151 2152 return implode( ';', $fields ) . "\n"; 2153 2154 } 2155 2156 2157 protected function button( $name, $label ) 2158 { 2159 2160 $args = func_get_args(); 2161 $args = array_filter( array_slice( $args, 2 ), create_function( '$a', 'return trim($a)!=="";' ) ); 2162 2163 $disabled = in_array( 'disabled', $args ) ? ' disabled="disabled"' : ''; 2164 2165 if ( !$icon ) 2166 $args[] = 'pure-text'; 2167 2168 $classes = implode( ' ', $args ); 2169 2170 return '<input type="submit" name="' . $this->varname( $name ) . 2171 '" value="' . htmlentities( $label ) . '" class="' . 2172 $classes . '"' . $disabled . ' />'; 2173 2174 } 2175 2176 protected function imgbutton( $name, $label, $icon ) 2177 { 2178 2179 $args = func_get_args(); 2180 $args = array_filter( array_slice( $args, 3 ), create_function( '$a', 'return trim($a)!=="";' ) ); 2181 2182 $disabled = in_array( 'disabled', $args ) ? ' disabled="disabled"' : ''; 2183 2184 $classes = implode( ' ', $args ); 2185 2186 return '<input type="image" name="' . $this->varname( $name ) . 2187 '" title="' . htmlentities( $label, ENT_COMPAT, 'UTF-8' ) . 2188 '" class="' . $classes . '"' . $disabled .' src="' . 2189 DOKU_BASE . DB2_PATH . 'icons/' . $icon . '.gif" />'; 2190 2191 } 2192 2193 2194 /** 2195 * Compiles elements for flipping/selecting page and number of records per 2196 * page in listing table according to current context. 2197 * 2198 * @param integer $skip number of records to skip on listing 2199 * @param integer $num number of records to list per page at most 2200 * @param integer $count number of records in table 2201 * @return array five-element array containing buttons for flipping down, 2202 * flipping up, selecting page, selecting number of records 2203 * per page and for showing number of records and pages. 2204 */ 2205 2206 protected function getPagerElements( $skip, $num, $count ) 2207 { 2208 2209 // build list of skip-values for all pages 2210 $skips = array(); 2211 2212 if ( $num > 0 ) 2213 { 2214 2215 for ( $i = $skip; $i > 0; $i -= $num ) 2216 array_unshift( $skips, $i ); 2217 2218 array_unshift( $skips, 0 ); 2219 2220 for ( $i = $skip + $num; $i < $count - $num; $i += $num ) 2221 array_push( $skips, $i ); 2222 2223 if ( $i < $count ) 2224 array_push( $skips, $count - $num ); 2225 2226 } 2227 2228 2229 // detect index of currently visible page 2230 $page = array_search( $skip, $skips ); 2231 2232 $minPage = max( 0, $page - self::maxSurroundingPagesCount ); 2233 $maxPage = min( count( $skips ), $page + self::maxSurroundingPagesCount ); 2234 2235 2236 2237 /* 2238 * compile pager elements for ... 2239 */ 2240 2241 if ( count( $skips ) <= 1 ) 2242 $backward = $forward = $pages = ''; 2243 else 2244 { 2245 2246 // ... flipping down 2247 $backward = $pages = $forward = array(); 2248 2249 if ( $page > 1 ) 2250 $backward[] = $this->imgbutton( 'skip0', $this->getLang( 'hintflipfirst' ), 'first' ); 2251 2252 if ( $page > 0 ) 2253 $backward[] = $this->imgbutton( 'skip' . $skips[$page-1], $this->getLang( 'hintflipprevious' ), 'previous' ); 2254 2255 $backward = implode( "\n", $backward ); 2256 2257 2258 // ... switching to near page 2259 for ( $i = $minPage; $i < $maxPage; $i++ ) 2260 $pages[] = $this->button( 'skip' . $skips[$i], $i + 1, 2261 ( $i == $page ? 'selected' : '' ) ); 2262 2263 if ( $minPage > 0 ) 2264 array_unshift( $pages, '...' ); 2265 2266 if ( $maxPage < count( $skips ) - 1 ) 2267 array_push( $pages, '...' ); 2268 2269 $pages = implode( "\n", $pages ); 2270 2271 2272 // ... flipping up 2273 if ( $page < count( $skips ) - 1 ) 2274 $forward[] = $this->imgbutton( 'skip' . $skips[$page+1], $this->getLang( 'hintflipnext' ), 'next' ); 2275 2276 if ( $page < count( $skips ) - 2 ) 2277 $forward[] = $this->imgbutton( 'skip' . ( $count - $num ), $this->getLang( 'hintfliplast' ), 'last' ); 2278 2279 $forward = implode( "\n", $forward ); 2280 2281 } 2282 2283 2284 // ... showing number of records/pages 2285 if ( $count === 1 ) 2286 $stat = sprintf( $this->getLang( 'recnumsingle' ), $count ); 2287 else if ( count( $skips ) <= 1 ) 2288 $stat = sprintf( $this->getLang( 'recnummulti' ), $count ); 2289 else 2290 $stat = sprintf( $this->getLang( 'recnummultipage' ), $count, count( $skips ) ); 2291 2292 2293 // ... selecting number of records per page 2294 foreach ( array( 10, 20, 50, 100, 200 ) as $size ) 2295 { 2296 $sizes[] = $this->button( 'num' . $size, $size, 2297 ( $size == $num ? 'selected' : '' ) ); 2298 if ( $size >= $count ) 2299 break; 2300 } 2301 2302 $sizes = count( $sizes ) == 1 ? '' : implode( "\n", $sizes ); 2303 2304 2305 2306 // return set of pager elements to caller 2307 return array( $backward, $forward, $pages, $sizes, $stat ); 2308 2309 } 2310 2311 2312 /** 2313 * Wraps provided HTML code in a form sending form data to current wiki 2314 * page. 2315 * 2316 * @param string $code HTML code to embed 2317 * @param array $hiddens set of hidden values to be added 2318 * @param integer $maxUploadSize maximum size of supported uploads in bytes 2319 * @param boolean $isSingle set true on calling to render single-record editor 2320 * @return string 2321 */ 2322 2323 protected function wrapInForm( $code, $hiddens = null, 2324 $maxUploadSize = null, $isSingle = false ) 2325 { 2326 include_once( DOKU_INC . 'inc/form.php' ); 2327 2328 ob_start(); 2329 2330 $id = 'database2_' . ( $isSingle ? 'single' : 'table' ) . '_' . 2331 $this->table . '_' . $this->getIndex(); 2332 2333 if ( $maxUploadSize > 0 ) 2334 { 2335 $form = new Doku_Form( $id, false, 'POST', 'multipart/form-data' ); 2336 $form->addHidden( 'MAX_FILE_SIZE', intval( $maxUploadSize ) ); 2337 } 2338 else 2339 $form = new Doku_Form( $id ); 2340 2341 $form->addHidden( 'id', $this->getPageID() ); 2342 2343 if ( is_array( $hiddens ) ) 2344 foreach ( $hiddens as $name => $value ) 2345 $form->addHidden( $name, $value ); 2346 2347 $form->addElement( $code ); 2348 2349 $form->printForm(); 2350 2351 2352 return ob_get_clean(); 2353 2354 } 2355 2356 2357 /** 2358 * Checks if current user is authorized according to given rule. 2359 * 2360 * The rule is a comma-separated list of usernames and groups (after 2361 * preceeding @ character), e.g. 2362 * 2363 * admin,@user 2364 * 2365 * authorizing user admin and every user in group "user". 2366 * 2367 * @param string $rule rule describing authorizations 2368 * @return boolean true if current user is authorized, false otherwise 2369 */ 2370 2371 protected function isAuthorized( $rule ) 2372 { 2373 global $USERINFO; 2374 2375 2376 if ( auth_isadmin() ) 2377 return true; 2378 2379 if ( $rule ) 2380 { 2381 2382 $granted = true; 2383 2384 foreach ( explode( ',', $rule ) as $role ) 2385 { 2386 2387 $role = trim( $role ); 2388 if ( $role === '' ) 2389 continue; 2390 2391 if ( !strcasecmp( $role, '@ALL' ) ) 2392 return true; 2393 2394 if ( !strcasecmp( $role, '@NONE' ) ) 2395 return false; 2396 2397 2398 if ( $_SERVER['REMOTE_USER'] ) 2399 { 2400 2401 if ( $role[0] == '!' ) 2402 { 2403 $role = substr( $role, 1 ); 2404 $match = false; 2405 } 2406 else 2407 $match = true; 2408 2409 if ( $role[0] == '@' ) 2410 { 2411 if ( in_array( substr( $role, 1 ), $USERINFO['grps'] ) ) 2412 { 2413 if ( $match && $granted ) 2414 return true; 2415 if ( !$match ) 2416 $granted = false; 2417 } 2418 } 2419 else if ( $role == $_SERVER['REMOTE_USER'] ) 2420 { 2421 if ( $match && $granted ) 2422 return true; 2423 if ( !$match ) 2424 $granted = false; 2425 } 2426 2427 } 2428 } 2429 } 2430 2431 2432 return false; 2433 2434 } 2435 2436 2437 /** 2438 * Takes ACL rule set related to a single row and related to whole table 2439 * testing them either of them preferring the first one for authorizing 2440 * current user to do one of up to two sorts of access preferring the first 2441 * one again. 2442 * 2443 * @param string|array $rowACL row-related ACL rule set 2444 * @param array $tableACL table-related ACL rule set, used if $rowACL isn't 2445 * managing any of the given sorts of access 2446 * @param string $ruleName preferred sort of access to authorize for 2447 * @param string $optRuleName optional fallback sort of access if first 2448 * isn't managed in selected rule set 2449 * @param boolean $defaultGrant true to select granting access if neither 2450 * rule set is managing any selected sort of access 2451 * @return boolean true if user is authorized, false otherwise 2452 */ 2453 2454 protected function isAuthorizedMulti( &$rowACL, $tableACL, $ruleName, 2455 $optRuleName = null, 2456 $defaultGrant = false ) 2457 { 2458 2459 if ( is_string( $rowACL ) ) 2460 $rowACL = $this->parseACLRule( $rowACL, false, true ); 2461 2462 // use row-related rule set if it's managing any given sort of access 2463 if ( $rowACL[$ruleName] ) 2464 $rule = $rowACL[$ruleName]; 2465 else if ( $rowACL[$optRuleName] ) 2466 $rule = $rowACL[$optRuleName]; 2467 else 2468 $rule = null; 2469 2470 if ( $rule ) 2471 return $this->isAuthorized( $rule ); 2472 2473 2474 // use table-related rule set if it's managin any given sort of access 2475 if ( $tableACL[$ruleName] ) 2476 $rule = $tableACL[$ruleName]; 2477 else if ( $tableACL[$optRuleName] ) 2478 $rule = $tableACL[$optRuleName]; 2479 else 2480 // neither of them is managing given sorts of access 2481 // --> grant or revoke access depending on fifth argument 2482 if ( $defaultGrant ) 2483 return true; 2484 else 2485 $rule = ''; 2486 2487 2488 return $this->isAuthorized( $rule ); 2489 2490 } 2491 2492 2493 /** 2494 * Gets HTML code of global commands current user is authorized to invoke. 2495 * 2496 * @return string HTML code 2497 */ 2498 2499 protected function getGlobalCommands() 2500 { 2501 2502 if ( $this->options['view'] ) 2503 return ''; 2504 2505 2506 $globalCmds = array( 2507 'insert' => array( 'add', $this->getLang( 'cmdadd' ) ), 2508 'drop' => array( 'drop', $this->getLang( 'cmddrop' ), true ), 2509 'print' => array( 'print', $this->getLang( 'cmdprint' ), 'print', true, 'print' ), 2510 'export.csv' => array( 'exportcsv', $this->getLang( 'cmdcsv' ), 'csv' ), 2511// 'export.xml' => array( 'exportxml', $this->getLang( 'cmdxml' ), 'xml' ), 2512 'viewlog' => array( 'exportlog', $this->getLang( 'cmdlog' ), 'log' ), 2513 ); 2514 2515 2516 if ( !$this->getSingleNumericPrimaryKey() ) 2517 unset( $globalCmds['insert'] ); 2518 2519 2520 foreach ( $globalCmds as $name => $def ) 2521 if ( $this->isAuthorized( $this->options['may'.($authz=strtok( $name, '.' ))] ) ) 2522 { 2523 2524 if ( is_string( $def[2] ) ) 2525 { 2526 2527 $href = $this->attachmentLink( $def[2], $authz, !$def[3] ); 2528 $globalCmds[$name] = '<a href="' . $href . '" class="icon-cmd" target="' . $def[4] . 2529 '"><img src="' . 2530 DOKU_BASE . DB2_PATH . 'icons/' . $def[0] . 2531 '.gif" title="' . $def[1] . '" alt="' . 2532 $def[1] . '" /></a>'; 2533 2534 } 2535 else 2536 $globalCmds[$name] = '<input type="image" class="icon-cmd" name="' . 2537 $this->varname( 'cmd' . $name ) . 2538 '" title="' . $def[1] . '" src="' . 2539 DOKU_BASE . DB2_PATH . 'icons/' . $def[0] . 2540 '.gif" onclick="' . ( $def[2] ? "return confirm('" . $this->getLang( 'confirmdrop' ) . "');" : '' ) . '" />'; 2541 2542 } 2543 else 2544 unset( $globalCmds[$name] ); 2545 2546 2547 return implode( "\n", $globalCmds ); 2548 2549 } 2550 2551 2552 /** 2553 * Gets HTML code of commands related to selected record current user is 2554 * authorized to invoke. 2555 * 2556 * @param integer $rowid ID of record 2557 * @return string HTML code 2558 */ 2559 2560 protected function getRecordCommands( $rowid, $rowACL = null, $clickActions = null ) 2561 { 2562 2563 $rowid = intval( $rowid ); 2564 if ( !$rowid ) 2565 // don't provide record management on complex/non-integer 2566 // primary keys 2567 return ''; 2568 2569 2570 if ( !$this->getSingleNumericPrimaryKey() ) 2571 return ''; 2572 2573 2574 $recordCmds = array( 2575 'inspect' => array( 'view', $this->getLang( 'cmdview' ) ), 2576 'edit' => array( 'edit', $this->getLang( 'cmdedit' ) ), 2577 'insert' => array( 'copy', $this->getLang( 'cmdcopy' ) ), 2578// 'insert' => array( 'insert', $this->getLang( 'cmdinsert' ) ), 2579 'delete' => array( 'delete', $this->getLang( 'cmddelete' ), true ), 2580 ); 2581 2582 2583 if ( $this->options['view'] ) 2584 // it's a read-only view thus exclude commands for adjusting data 2585 unset( $recordCmds['edit'], $recordCmds['insert'], 2586 $recordCmds['delete'] ); 2587 2588 2589 if ( !is_array( $clickActions ) ) 2590 $clickActions = array(); 2591 2592 foreach ( $recordCmds as $name => $def ) 2593 if ( $this->isAuthorizedMulti( $rowACL, $this->options, 'may' . $name ) ) 2594 { 2595 2596 $idName = $this->varname( 'cmd' . $name, $rowid ); 2597 $class = ( !in_array( $name, $clickActions ) || 2598 $this->options['addonclick'] ) ? '' : ' hidden'; 2599 2600 $recordCmds[$name] = '<input type="image" class="icon-cmd' . 2601 $class . '" name="' . $idName . '" id="' . 2602 $idName . '" title="' . $def[1] . '" src="' . 2603 DOKU_BASE . DB2_PATH . 'icons/' . $def[0] . 2604 '.gif" onclick="' . ( $def[2] ? "return confirm('" . $this->getLang( 'confirmdelete' ) . "');" : '' ) . '" />'; 2605 2606 } 2607 else 2608 unset( $recordCmds[$name] ); 2609 2610 2611 return implode( "\n", $recordCmds ); 2612 2613 } 2614 2615 2616 protected function __configureSelect() 2617 { 2618 2619 $idCol = $this->getSingleNumericPrimaryKey(); 2620 if ( $idCol === false ) 2621 return false; 2622 2623 2624 $cols = array( $idCol ); 2625 2626 2627 // prepare filter 2628 list( $filter, $parameters ) = $this->getFilter(); 2629 2630 2631 // prepare sorting 2632 $state =& $this->getSession(); 2633 2634 $sort = preg_split( '/[,\s]+/', trim( $state['sort'] ) ); 2635 2636 foreach ( $sort as $key => $desc ) 2637 { 2638 2639 $dir = ( $desc[0] == '!' ); 2640 2641 $col = $dir ? substr( $desc, 1 ) : $desc; 2642 $col = trim( $col ); 2643 2644 if ( $this->meta[$col]['isColumn'] ) 2645 { 2646 2647 if ( is_string( $this->meta[$col]['options']['aliasing'] ) ) 2648 $cols[] = $this->meta[$col]['options']['aliasing'] . ' AS ' . $col; 2649 else 2650 $cols[] = $col; 2651 2652 $sort[$key] = $col . ( $dir ? ' DESC' : ' ASC' ); 2653 2654 } 2655 else 2656 unset( $sort[$key] ); 2657 2658 } 2659 2660 $order = count( $sort ) ? ' ORDER BY ' . implode( ', ', $sort ) : ''; 2661 2662 2663 2664 return array( $filter, $parameters, $order, $cols ); 2665 2666 } 2667 2668 protected function recordI2X( $rowid ) 2669 { 2670 2671 $config = $this->__configureSelect(); 2672 if ( !is_array( $config ) ) 2673 return false; 2674 2675 list( $filter, $parameters, $order, $cols ) = $config; 2676 2677 2678 // query for records returning whole resultset 2679 $st = $this->db->prepare( 'SELECT ' . implode( ',', $cols ) . 2680 ' FROM ' . $this->table . $filter . $order ); 2681 if ( !$st ) 2682 throw new PDOException( $this->getLang( 'listprepare' ) ); 2683 2684 if ( !$st->execute( $parameters ) ) 2685 throw new PDOException( $this->getLang( 'listexecute' ) ); 2686 2687 2688 $index = 0; 2689 while ( ( $record = $st->fetch( PDO::FETCH_NUM ) ) !== false ) 2690 if ( $record[0] == $rowid ) 2691 { 2692 $st->closeCursor(); 2693 return $index; 2694 } 2695 else 2696 $index++; 2697 2698 2699 return null; 2700 2701 } 2702 2703 2704 protected function recordX2I( $index ) 2705 { 2706 2707 if ( !is_integer( $index ) || ( $index < 0 ) ) 2708 return false; 2709 2710 2711 $config = $this->__configureSelect(); 2712 if ( !is_array( $config ) ) 2713 return false; 2714 2715 list( $filter, $parameters, $order, $cols ) = $config; 2716 2717 2718 // query for records returning whole resultset 2719 $st = $this->db->prepare( 'SELECT ' . implode( ',', $cols ) . 2720 ' FROM ' . $this->table . $filter . $order . 2721 ' LIMIT 1 OFFSET ' . $index ); 2722 if ( !$st ) 2723 throw new PDOException( $this->getLang( 'listprepare' ) ); 2724 2725 if ( !$st->execute( $parameters ) ) 2726 throw new PDOException( $this->getLang( 'listexecute' ) ); 2727 2728 2729 $record = $st->fetch( PDO::FETCH_NUM ); 2730 2731 if ( is_array( $record ) && count( $record ) ) 2732 return intval( $record[0] ); 2733 2734 return null; 2735 2736 } 2737 2738 2739 /** 2740 * Parses provided string for filter description consisting of one or more 2741 * components. 2742 * 2743 * @param string $in string containing filter definition 2744 * @return array list of successfully parsed filter components 2745 */ 2746 2747 protected function parseFilterCode( $in ) 2748 { 2749 2750 $in = trim( $in ); 2751 $out = array(); 2752 2753 $prevMode = false; 2754 2755 while ( $in !== '' ) 2756 { 2757 2758 if ( preg_match( '/^(\w+)\s+(\w+)(.*)$/i', $in, $matches ) ) 2759 { 2760 2761 // extract argument to current filter rule 2762 $tail = trim( $matches[3] ); 2763 if ( ( $tail[0] == '"' ) || ( $tail[0] == "'" ) ) 2764 { 2765 // argument is enclosed in quotes 2766 2767 $pos = 0; 2768 $argument = $this->parseString( $tail, $pos ); 2769 2770 } 2771 else 2772 { 2773 // argument take everything up to next space or separator 2774 2775 $pos = strcspn( $tail, " \r\n\t\f&|" ); 2776 if ( $pos ) 2777 $argument = trim( substr( $tail, 0, $pos ) ); 2778 else 2779 $argument = ''; 2780 2781 } 2782 2783 2784 $new = array( 2785 'col' => $matches[1], 2786 'op' => $matches[2], 2787 'arg' => $this->replaceMarkup( $argument ), 2788 ); 2789 2790 if ( $prevMode === '&' ) 2791 $new['mode'] = 'AND'; 2792 else if ( $prevMode === '|' ) 2793 $new['mode'] = 'OR'; 2794 else if ( $prevMode !== false ) 2795 // invalid pattern separator --> break parsing filter code 2796 break; 2797 2798 $out[] = $new; 2799 2800 $in = ltrim( substr( $tail, $pos ) ); 2801 2802 } 2803 // invalid filter element --> break parsing filter code 2804 else break; 2805 2806 2807 $prevMode = $in[0]; 2808 $in = substr( $in, 1 ); 2809 2810 } 2811 2812 2813 return $out; 2814 2815 } 2816 2817 2818 /** 2819 * Manages current state of table's filter in session processing optional 2820 * modifications in current input data. 2821 * 2822 * @return array description of current state of filter 2823 */ 2824 2825 protected function getFilterInput() 2826 { 2827 2828 $input = $this->getInput(); 2829 $session =& $this->getSession(); 2830 2831 2832 if ( $input['searchdrop'] ) 2833 $session['search'] = array(); 2834 else 2835 { 2836 2837 if ( !is_array( $session['search'] ) ) 2838 { 2839 2840 $session['search'] = array(); 2841 2842 if ( $this->options['basefilter'] ) 2843 // initialize filter using provided code 2844 return ( $session['search'] = $this->parseFilterCode( $this->options['basefilter'] ) ); 2845 2846 } 2847 2848 2849 // parse filter input and transfer it to session 2850 foreach ( $input as $key => $value ) 2851 if ( preg_match( '/^search(col|op|arg)(\d*)$/', $key, $matches ) ) 2852 { 2853 2854 $index = intval( $matches[2] ); 2855 2856 if ( !is_array( $session['search'][$index] ) ) 2857 $session['search'][$index] = array(); 2858 2859 $session['search'][$index][$matches[1]] = $value; 2860 2861 } 2862 2863 2864 // drop incomplete filter components 2865 foreach ( $session['search'] as $index => $filter ) 2866 if ( !is_string( $filter['col'] ) || 2867 !is_string( $filter['op'] ) || 2868 !is_string( $filter['arg'] ) ) 2869 unset( $session['search'][$index] ); 2870 else 2871 if ( !in_array( $filter['mode'], array( 'AND', 'OR' ) ) ) 2872 if ( $index ) 2873 unset( $session['search'][$index] ); 2874 2875 } 2876 2877 2878 return $session['search']; 2879 2880 } 2881 2882 2883 /** 2884 * Gets WHERE clause and contained parameters to filter records in table. 2885 * 2886 * @return array two-element array, SQL-WHERE-clause with initial WHERE and 2887 * array with all values to be bound as parameters to clause 2888 */ 2889 2890 protected function getFilter() 2891 { 2892 2893 $filters = $this->getFilterInput(); 2894 $opMap = array( 2895 'like' => ' %s ( %s like ? )', 2896 'nlike' => ' %s ( %s not like ? )', 2897 'lt' => ' %s ( %s < ? )', 2898 'eq' => ' %s ( %s = ? )', 2899 'gt' => ' %s ( %s > ? )', 2900 'ne' => ' %s ( %s <> ? )', 2901 'le' => ' %s ( %s <= ? )', 2902 'ge' => ' %s ( %s >= ? )', 2903 'isset' => ' %s ( ( %2$s = ? ) AND %2$s IS NOT NULL )', 2904 'isclear' => ' %s ( ( %2$s <> ? ) OR %2$s IS NULL )', 2905 ); 2906 2907 2908 $meta = $this->getColumnsMeta(); 2909 $out = array( '', null ); 2910 2911 foreach ( $filters as $index => $filter ) 2912 { 2913 2914 $mode = ( $out[0] !== '' ) ? $filter['mode'] : 'WHERE'; 2915 2916 $column = $meta[$filter['col']]; 2917 if ( $column && $column['isColumn'] ) 2918 { 2919 // 1) filter operates on valid column 2920 2921 if ( $column['type'] == 'bool' ) 2922 { 2923 2924 if ( in_array( $filter['op'], array( 'like', 'eq', 'le', 'ge', 'isset' ) ) ) 2925 $filter['op'] = 'isset'; 2926 else 2927 $filter['op'] = 'isclear'; 2928 2929 switch ( $column['options']['booltype'] ) 2930 { 2931 case 'xmark' : $argument = 'x'; break; 2932 case 'yesno' : $argument = 'y'; break; 2933 case 'int' : $argument = '1'; break; 2934 } 2935 } 2936 else 2937 $argument = trim( $filter['arg'] ); 2938 2939 if ( $argument !== '' ) 2940 // 2) filter operates with non-empty argument 2941 if ( $opMap[$filter['op']] ) 2942 { 2943 // 3) filter uses valid operation 2944 // ----> include it. 2945 2946 if ( in_array( $filter['op'], array( 'like', 'nlike' ) ) ) 2947 if ( strpos( $argument, '%' ) === false ) 2948 $argument = '%' . $argument . '%'; 2949 2950 $out[0] .= sprintf( $opMap[$filter['op']], $mode, 2951 $filter['col'] ); 2952 2953 if ( is_array( $out[1] ) ) 2954 $out[1][] = $argument; 2955 else 2956 $out[1] = array( $argument ); 2957 2958 } 2959 } 2960 } 2961 2962 2963 return $out; 2964 2965 } 2966 2967 2968 /** 2969 * Renders filter on current table. 2970 * 2971 * @return string HTML-code representing filter on selected table 2972 */ 2973 2974 protected function renderFilter() 2975 { 2976 2977 /* 2978 * prepare entries for selecting column 2979 */ 2980 2981 $meta = $this->getColumnsMeta(); 2982 2983 $columns = array(); 2984 $mapType = array( 2985 'integer' => 'numeric', 2986 'real' => 'numeric', 2987 'decimal' => 'numeric', 2988 'text' => 'text', 2989 'date' => 'date', 2990 'enum' => 'enum', 2991 'bool' => 'bool', 2992 ); 2993 2994 $allVisible = true; 2995 foreach ( $meta as $column => $def ) 2996 if ( !is_null( $def['options']['visible'] ) || 2997 !is_null( $def['options']['filter'] ) ) 2998 { 2999 $allVisible = false; 3000 break; 3001 } 3002 3003 foreach ( $meta as $column => $def ) 3004 if ( $def['isColumn'] && ( $allVisible || $def['options']['visible'] || $def['options']['filter'] ) ) 3005 { 3006 3007 if ( $def['format'] == 'acl' ) 3008 continue; 3009 3010 $class = $mapType[$def['type']]; 3011 if ( !$class ) 3012 continue; 3013 3014 $label = $def['label'] ? $def['label'] : $column; 3015 $label = strtr( $label, array( '<' => '<' ) ); 3016 3017 $head = "<option value=\"$column\" class=\"$class\""; 3018 $tail = ">$label</option>"; 3019 3020 $columns[$column] = strtr( $head, array( '%' => '%%' ) ) . '%s'. 3021 strtr( $tail, array( '%' => '%%' ) ); 3022 3023 } 3024 3025 if ( empty( $columns ) ) 3026 // not supporting to filter any column -> don't render filter at all 3027 return ''; 3028 3029 3030 3031 /* 3032 * prepare entries for selecting operator 3033 */ 3034 3035 $operators = array( 3036 'like' => array( $this->getLang( 'oplike' ), 'text', ), 3037 'nlike' => array( $this->getLang( 'opnotlike' ), 'text', ), 3038 'lt' => array( '<', 'text', 'numeric', 'date', ), 3039 'le' => array( '<=', 'text', 'numeric', 'date', ), 3040 'eq' => array( '=', 'text', 'numeric', 'date', 'enum', ), 3041 'ne' => array( '<>', 'text', 'numeric', 'date', 'enum', ), 3042 'ge' => array( '>=', 'text', 'numeric', 'date', ), 3043 'gt' => array( '>', 'text', 'numeric', 'date', ), 3044 'isset' => array( $this->getLang( 'opset' ), 'bool', ), 3045 'isclear' => array( $this->getLang( 'opclear' ), 'bool', ), 3046 ); 3047 3048 foreach ( $operators as $op => $def ) 3049 { 3050 3051 $label = array_shift( $def ); 3052 $class = implode( ' ', $def ); 3053 3054 $head = "<option value=\"$op\" class=\"$class\""; 3055 $tail = ">$label</option>"; 3056 3057 $operators[$op] = strtr( $head, array( '%' => '%%' ) ) . '%s' . 3058 strtr( $tail, array( '%' => '%%' ) ); 3059 3060 } 3061 3062 3063 3064 /* 3065 * separately render used filter components 3066 */ 3067 3068 $filters = $this->getFilterInput(); 3069 $input = $this->getInput(); 3070 3071 $modeMap = array( 3072 'AND' => '<span class="mode-and">' . $this->getLang( 'opand' ) . '</span>', 3073 'OR' => '<span class="mode-or">' . $this->getLang( 'opor' ) . '</span>', 3074 ); 3075 3076 if ( empty( $filters ) || $input['searchand'] || $input['searchor'] ) 3077 { 3078 // add new empty filter rule 3079 3080 $newFilter = array( 3081 'col' => '', 3082 'op' => '', 3083 'arg' => '', 3084 ); 3085 3086 if ( $input['searchand'] ) 3087 $newFilter['mode'] = 'AND'; 3088 else if ( $input['searchor'] ) 3089 $newFilter['mode'] = 'OR'; 3090 3091 $session =& $this->getSession(); 3092 $session['search'][] = $newFilter; 3093 $filters[] = $newFilter; 3094 3095 } 3096 3097 3098 foreach ( $filters as $index => $filter ) 3099 { 3100 3101 // update columns selector entries marking currently selected one 3102 $optColumns = $columns; 3103 foreach ( $optColumns as $column => $code ) 3104 $optColumns[$column] = sprintf( $code, ( $column == $filter['col'] ) ? ' selected="selected"' : '' ); 3105 3106 // update operators selector entries marking currently selected one 3107 $optOperators = $operators; 3108 foreach ( $optOperators as $operator => $code ) 3109 $optOperators[$operator] = sprintf( $code, ( $operator == $filter['op'] ) ? ' selected="selected"' : '' ); 3110 3111 3112 // prepare stuff for rendering code 3113 $optColumns = implode( "\n", $optColumns ); 3114 $optOperators = implode( "\n", $optOperators ); 3115 3116 $argument = strtr( $filter['arg'], array( '"' => '"' ) ); 3117 3118 $colname = $this->varname( 'searchcol' . $index ); 3119 $opname = $this->varname( 'searchop' . $index ); 3120 $argname = $this->varname( 'searcharg' . $index ); 3121 3122 $mode = $index ? $modeMap[$filter['mode']] : ''; 3123 3124 $mark = ( trim( $filter['arg'] ) === '' ) ? ' unused' : ''; 3125 3126 3127 // render code for single filter component 3128 $filters[$index] = <<<EOT 3129<span class="filter-component$mark"> 3130 $mode 3131 <select name="$colname" class="column" onchange="return database2_searchCol(this);">$optColumns</select> 3132 <select name="$opname" class="operator">$optOperators</select> 3133 <input type="text" name="$argname" size="10" value="$argument" class="argument text numeric date enum" /> 3134</span> 3135EOT; 3136 3137 } 3138 3139 3140 3141 $cmds = array(); 3142 3143 $cmds[] = $this->imgbutton( 'searchgo', $this->getLang( 'cmdfilterapply' ), 'filter' ); 3144 $cmds[] = $this->imgbutton( 'searchand', $this->getLang( 'cmdfilterintersect' ), 'filter-and' ); 3145 $cmds[] = $this->imgbutton( 'searchor', $this->getLang( 'cmdfilterunion' ), 'filter-or' ); 3146 3147 if ( ( count( $filters ) > 1 ) || ( trim( $argument ) !== '' ) || 3148 ( $filter['op'] && ( $filter['op'] !== 'like' ) ) ) 3149 $cmds[] = $this->imgbutton( 'searchdrop', 'Reset filter', 3150 'filter-drop' ); 3151 3152 $commands = '<span class="commands">' . implode( "\n", $cmds ) . '</span>'; 3153 3154 3155 $class = ( count( $filters ) > 1 ) ? 'multi-filter' : 'single-filter'; 3156 3157 3158 return '<div class="' . $class . '">' . 3159 implode( "\n", $filters ) . $commands . '</div>'; 3160 3161 } 3162 3163 3164 /** 3165 * Extracts quoted string starting with arbitrary quoting character at given 3166 * index. 3167 * 3168 * The provided index in $first is updated on return to point to first 3169 * character after extracted string. 3170 * 3171 * @param string $in haystack containing quoted string 3172 * @param integer $first index of character starting quoted string 3173 * @return string extracted string on success, false on error 3174 */ 3175 3176 public static function parseString( $in, &$first ) 3177 { 3178 3179 $pos = $first; 3180 3181 do 3182 { 3183 3184 // find next matching quote character marking end of string 3185 $end = strpos( $in, $in[$first], $pos + 1 ); 3186 if ( $end === false ) 3187 // didn't find any --> malformed string 3188 return false; 3189 3190 $count = 0; 3191 for ( $idx = $end - 1; $idx > $pos; $idx-- ) 3192 if ( $in[$idx] == '\\' ) 3193 $count++; 3194 else 3195 break; 3196 3197 if ( $count & 1 ) 3198 $pos = $end; 3199 else 3200 { 3201 3202 $string = substr( $in, $first + 1, $end - $first - 1 ); 3203 3204 $first = $end + 1; 3205 3206 return stripcslashes( $string ); 3207 3208 } 3209 3210 } 3211 while ( true ); 3212 3213 } 3214 3215 3216 public static function parseAssignment( $in, &$first ) 3217 { 3218 3219 $pos = $first; 3220 3221 3222 // skip any leading whitespace 3223 $pos += strspn( $in, " \t", $pos ); 3224 3225 3226 3227 // read and normalize name 3228 $end = $pos + strcspn( $in, " \t=", $pos ); 3229 $name = substr( $in, $pos, $end - $pos ); 3230 3231 if ( $name === '' ) 3232 // there is no (further) assignment in $in 3233 return null; 3234 3235 if ( ctype_digit( $name ) ) 3236 $name = intval( $name ); 3237 3238 3239 3240 // skip any whitespace found between name and assignment operator 3241 $pos = $end + strspn( $in, " \t", $end ); 3242 3243 if ( $in[$pos] !== '=' ) 3244 // option does not use assignment operator 3245 // --> it's a "shortcut option" 3246 $value = true; 3247 else 3248 { 3249 // expecting assigned value next 3250 3251 3252 // skip whitespace between assignment operator and value 3253 $pos += strspn( $in, " \t", $pos + 1 ) + 1; 3254 $end = $pos; 3255 3256 if ( $in[$pos] === '"' ) 3257 { 3258 // value is enclosed in quotes 3259 3260 $temp = self::parseString( $in, $end ); 3261 if ( $temp === false ) 3262 return false; 3263 3264 $value = $temp; 3265 3266 } 3267 else 3268 { 3269 3270 $end += strcspn( $in, " \t", $end ); 3271 3272 $value = substr( $in, $pos, $end - $pos ); 3273 3274 } 3275 } 3276 3277 $first = $end; 3278 3279 3280 return array( $name, $value ); 3281 3282 } 3283 3284 3285 public static function stripTags( $in, $tags = null ) 3286 { 3287 3288 if ( !is_array( $tags ) ) 3289 $tags = array( 'script', 'form', 'link', 'html', 'body', 'head', ); 3290 3291 3292 $pos = 0; 3293 3294 do 3295 { 3296 3297 // fast search for next opening tag 3298 $tag = strpos( $in, '<', $pos ); 3299 if ( $tag === false ) 3300 return $in; 3301 3302 if ( $in[$tag+1] == '?' ) 3303 { 3304 // detected start of PI ... skip completely 3305 3306 $end = strpos( $in, '?>', $tag + 2 ); 3307 if ( $end !== false ) 3308 $in = substr_replace( $in, '', $tag, $end - $tag + 2 ); 3309 else 3310 $in = substr_replace( $in, '', $tag ); 3311 3312 // fix for properly updating $pos below 3313 $tag--; 3314 3315 } 3316 else 3317 { 3318 // got tag ... check its name 3319 3320 $name = strtok( substr( $in, $tag + 1, 20 ), ' >' ); 3321 $name = strtolower( trim( $name ) ); 3322 3323 if ( array_search( $name, $tags ) !== false ) 3324 { 3325 // tag is marked for dropping 3326 3327 // slow, but convenient: find next end of tag and drop everything in between 3328 if ( preg_match( "#.+?</\s*$name\s*>#i", $in, $m, null, $tag ) ) 3329 $in = substr_replace( $in, '', $tag, strlen( $m[0] ) ); 3330 else 3331 $in = substr_replace( $in, '', $tag ); 3332 3333 $tag--; 3334 3335 } 3336 } 3337 3338 // update $pos to omit all previously processed part of $in 3339 $pos = $tag + 1; 3340 3341 } 3342 while ( true ); 3343 3344 } 3345 3346 3347 public static function splitDefinitionLine( $line ) 3348 { 3349 3350 $line = trim( $line ); 3351 3352 $parts = array(); 3353 $part = ''; 3354 3355 $pos = 0; 3356 $length = strlen( $line ); 3357 3358 3359 while ( ( $pos < $length ) && ( count( $parts ) < 3 ) ) 3360 { 3361 3362 $pos += strspn( $line, " \t", $pos ); 3363 $end = $pos; 3364 3365 if ( $line[$pos] === '"' ) 3366 { 3367 3368 $temp = self::parseString( $line, $end ); 3369 if ( $temp === false ) 3370 return false; 3371 3372 if ( $part !== '' ) 3373 $part .= ' '; 3374 3375 $part .= $temp; 3376 3377 } 3378 else if ( $line[$pos] === ',' ) 3379 { 3380 3381 $parts[] = $part; 3382 $part = ''; 3383 3384 $end++; 3385 3386 } 3387 else 3388 { 3389 3390 $end += strcspn( $line, " \t,", $end ); 3391 3392 if ( $part !== '' ) 3393 $part .= ' '; 3394 3395 $part .= trim( substr( $line, $pos, $end - $pos ) ); 3396 3397 } 3398 3399 $pos = $end; 3400 3401 } 3402 3403 3404 if ( $part !== '' ) 3405 $parts[] = $part; 3406 3407 3408 $parts = array_pad( $parts, 3, '' ); 3409 3410 3411 $options = array(); 3412 3413 if ( $pos < $length ) 3414 { 3415 3416 $name = ''; 3417 $value = ''; 3418 3419 while ( $pos < $length ) 3420 { 3421 3422 $temp = self::parseAssignment( $line, $pos ); 3423 if ( $temp === false ) 3424 return false; 3425 3426 if ( is_null( $temp ) ) 3427 break; 3428 3429 list( $name, $value ) = $temp; 3430 3431 $options[$name] = $value; 3432 3433 } 3434 } 3435 3436 $parts[] = $options; 3437 3438 3439 return $parts; 3440 3441 } 3442 3443 3444 protected function convertToLink( $href, $label, $varspace = array() ) 3445 { 3446 3447 $href = trim( $href ); 3448 if ( $href === '' ) 3449 return $label; 3450 3451 3452 if ( is_array( $varspace ) && count( $varspace ) ) 3453 $href = $this->replaceMarkup( $href, $varspace ); 3454 3455 3456 if ( strpos( $href, '://' ) !== false ) 3457 { 3458 // embed external link in header 3459 3460 // externallink() is adding to renderer->doc() ... 3461 // --> remove from doc afterwards, thus store its length now 3462 $length = strlen( $this->renderer->doc ); 3463 3464 $this->renderer->externallink( $href, $label ); 3465 3466 // --> now extract rendered link from doc 3467 $label = substr( $this->renderer->doc, $length ); 3468 $this->renderer->doc = substr_replace( $this->renderer->doc, '', 3469 $length ); 3470 3471 } 3472 else 3473 { 3474 // embed internal link in header 3475 3476 resolve_pageid( getNS( self::getPageID() ), $href, $exists ); 3477 $label = $this->renderer->internallink( $href, $label, NULL, true ); 3478 3479 } 3480 3481 3482 return $label; 3483 3484 } 3485 3486 3487 /** 3488 * Parses the code between opening and closing tag for data definition of 3489 * table to be managed/provided by tag. 3490 * 3491 * @throws Exception 3492 * 3493 * @param string $code data definition found in Wiki code 3494 */ 3495 3496 protected function parseDefinition( $code ) 3497 { 3498 3499 $failed = $out = $primaries = $uniques = $visibles = array(); 3500 $aclColumn = null; 3501 3502 // parse line by line 3503 foreach ( explode( "\n", $code ) as $index => $line ) 3504 { 3505 3506 // skip empty lines and comments 3507 $line = trim( $line ); 3508 if ( ( $line === '' ) || ( $line[0] == '#' ) || 3509 ( ( $line[0] == '/' ) && ( $line[1] == '/' ) ) ) 3510 // comment or empty line -> skip 3511 continue; 3512 3513 3514 // split line into at most 4 comma-separated fields with last 3515 // containing optional set of attributes/options 3516 $parsed = $this->splitDefinitionLine( $line ); 3517 if ( $parsed === false ) 3518 throw new Exception( sprintf( $this->getLang( 'definline' ), $index ) ); 3519 3520 list( $colName, $rawType, $label, $attributes ) = $parsed; 3521 3522 3523 // validate and normalize fields 3524 try 3525 { 3526 3527 // ***** 1st field: the column name ***** 3528 // normalize column name dropping invalid all invalid characters 3529 $colName = preg_replace( '/[^\w]/', '_', $colName ); 3530 3531 if ( $out[$colName] ) 3532 throw new Exception( sprintf( $this->getLang( 'defdouble' ), $colName ) ); 3533 3534 3535 // ***** 4th field: additional options ***** 3536 $options = array(); 3537 3538 foreach ( $attributes as $name => $value ) 3539 { 3540 3541 // process option 3542 $name = strtolower( $name ); 3543 switch ( $name ) 3544 { 3545 3546 // marks to demand a non-empty value on editing 3547 // (column in table is defined as NOT NULL) 3548 case 'required' : 3549 case 'req' : 3550 $options['required'] = self::asBool( $value ); 3551 break; 3552 3553 // marks column to be included in listing records 3554 // (if no column is marked visible this way, all columns 3555 // are visible by default) 3556 case 'visible' : 3557 if ( self::asBool( $value ) ) 3558 { 3559 $visibles[] = $colName; 3560 $options['visible'] = true; 3561 } 3562 break; 3563 3564 // selects column to be (part of) primary key index 3565 case 'primary' : 3566 if ( self::asBool( $value ) ) 3567 { 3568 $primaries[] = $colName; 3569 $options['primary'] = $options['required'] = true; 3570 } 3571 break; 3572 3573 // selects explicit index in order of fields/columns 3574 // (this is used on inspecting/editing records, only) 3575 case 'tabindex' : 3576 if ( ctype_digit( trim( $value ) ) ) 3577 $options['tabindex'] = intval( $value ); 3578 break; 3579 3580 // selects how to handle this column being defined as 3581 // boolean in 2nd field: 3582 // yesno - column is CHAR(1) with values 'y' or 'n' 3583 // xmark - column is CHAR(1) with values 'x' or ' ' 3584 // int - column is TINYINT with values 1 or 0 3585 // default is "yesno" ... selected below! 3586 case 'booltype' : 3587 $value = strtolower( $value ); 3588 if ( !in_array( $value, array( 'yesno', 'int', 'xmark' ) ) ) 3589 throw new Exception( $this->getLang( 'invalidbool' ) ); 3590 $options['booltype'] = $value; 3591 break; 3592 3593 case 'readonly' : 3594 // mark column as read-only 3595 // (so even admin mustn't edit it) 3596 $options['readonly'] = self::asBool( $value ); 3597 break; 3598 3599 case 'aliasing' : 3600 if ( !is_string( $value ) || 3601 ( trim( $value ) === '' ) ) 3602 throw new Exception( $this->getLang( 'noaliased' ) ); 3603 3604 // mark column as read-only 3605 // (so even admin mustn't edit it) 3606 $options['aliasing'] = $value; 3607 3608 // changing aliased term isn't expected to work 3609 // --> so implicitly mark column as read-only 3610 $options['readonly'] = true; 3611 break; 3612 3613 default : 3614 // support shortcurt for tabindex-definition: 3615 // "@<integer>" is same as "tabindex=<integer>" 3616 if ( preg_match( '/^@(\d+)$/', $name, $matches ) ) 3617 $options['tabindex'] = intval( $matches[1] ); 3618 else if ( substr( $name, 0, 6 ) == 'unique' ) 3619 { 3620 // column is (part of) one of several unique 3621 // indices 3622 // --> an optional integer after name "unique" 3623 // selects group of columns being part of 3624 // same unique index 3625 3626 $group = trim( substr( $name, 6 ) ); 3627 if ( ctype_digit( $group ) || ( $group === '' )) 3628 { 3629 3630 if ( !is_array( $uniques[$group] ) ) 3631 $uniques[$group] = array(); 3632 3633 $uniques[$group][] = $colName; 3634 3635 } 3636 } 3637 else if ( ctype_digit( trim( $name ) ) ) 3638 // raw digits as token are selecting length of 3639 // column (e.g. maximum length of stored text) 3640 $options['length'] = intval( $name ); 3641 else 3642 // all else single tokens are handled like 3643 // assigning boolean value true ... 3644 $options[$name] = $value; 3645 3646 } 3647 } 3648 3649 if ( $this->options['view'] ) 3650 $options['readonly'] = true; 3651 3652 3653 // ***** 2nd field: the column's type ***** 3654 $sqldef = $format = null; 3655 3656 // derive basic column type and its format from type definition 3657 $rawType = trim( $rawType ); 3658 $typeName = strtolower( trim( strtok( $rawType, ' ' ) ) ); 3659 3660 switch ( $typeName ) 3661 { 3662 3663 case 'int' : 3664 case 'integer' : 3665 $type = 'integer'; 3666 $format = 'integer'; 3667 break; 3668 3669 case 'image' : 3670 $type = 'data'; 3671 $format = 'image'; 3672 break; 3673 3674 case 'blob' : 3675 case 'binary' : 3676 case 'file' : 3677 case 'data' : 3678 $type = 'data'; 3679 $format = 'file'; 3680 break; 3681 3682 case 'real' : 3683 case 'float' : 3684 case 'double' : 3685 $type = 'real'; 3686 $format = 'real'; 3687 break; 3688 3689 case 'money' : 3690 case 'monetary' : 3691 $type = 'decimal'; 3692 $format = 'monetary'; 3693 break; 3694 3695 case 'numeric' : 3696 case 'decimal' : 3697 $type = 'decimal'; 3698 $format = 'real'; 3699 break; 3700 3701 case 'time' : 3702 $type = $rawType; 3703 $format = $typeName; 3704 break; 3705 3706 case 'date' : 3707 case 'datetime' : 3708 $type = $options['unixts'] ? 'integer' : $rawType; 3709 $format = $typeName; 3710 break; 3711 3712 case 'url' : 3713 case 'link' : 3714 case 'href' : 3715 $type = 'text'; 3716 $format = 'url'; 3717 break; 3718 3719 case 'email' : 3720 case 'mail' : 3721 $type = 'text'; 3722 $format = 'email'; 3723 break; 3724 3725 case 'phone' : 3726 case 'fax' : 3727 $type = 'text'; 3728 $format = $typeName; 3729 break; 3730 3731 case '' : 3732 case 'string' : 3733 case 'text' : 3734 case 'name' : 3735 case 'char' : 3736 $type = 'text'; 3737 $format = 'text'; 3738 break; 3739 3740 case 'acl' : 3741 if ( !is_null( $aclColumn ) ) 3742 throw new Exception( $this->getLang( 'multiacl' ) ); 3743 3744 $type = 'text'; 3745 $format = 'acl'; 3746 $aclColumn = $colName; 3747 break; 3748 3749 case 'check' : 3750 case 'mark' : 3751 case 'boolean' : 3752 case 'bool' : 3753 if ( !$options['booltype'] ) 3754 $options['booltype'] = 'yesno'; 3755 3756 if ( $options['booltype'] == 'int' ) 3757 { 3758 $type = 'integer'; 3759 $sqldef = 'tinyint'; 3760 } 3761 else 3762 { 3763 $type = 'bool'; 3764 $sqldef = 'char'; 3765 $options['length'] = 1; 3766 } 3767 3768 $format = 'bool'; 3769 break; 3770 3771 case 'enum' : 3772 // get set of selectable enumeration elements provided 3773 // after type name separated by slash or semicolon 3774 $options['selectables'] = preg_split( '#[/;]+#', strtok( '' ) ); 3775 3776 $max = 0; 3777 foreach ( $options['selectables'] as &$selectable ) 3778 { 3779 3780 $selectable = trim( $selectable ); 3781 3782 $max = max( $max, strlen( $selectable ) ); 3783 3784 } 3785 3786 if ( !$max ) 3787 throw new Exception( $this->getLang( 'emptyenum' ) ); 3788 3789 if ( !isset( $options['length'] ) ) 3790 $options['length'] = $max; 3791 3792 $type = 'enum'; 3793 $format = 'enum'; 3794 $sqldef = ( $max > 1 ) ? 'varchar' : 'char'; 3795 break; 3796 3797 case 'related' : 3798 // get statement for listing selectable options 3799 $readerSQL = trim( strtok( '' ) ); 3800 if ( !$this->getConf( 'customviews' ) ) 3801 throw new Exception( $this->getLang( 'readerdisabled' ) ); 3802 if ( !preg_match( '/^SELECT\s/i', $readerSQL ) ) 3803 throw new Exception( $this->getLang( 'invalidreader' ) ); 3804 3805 3806 // read selectable options querying provided statement 3807 $selectables = array(); 3808 3809 $resultset = $this->db->query( $readerSQL ); 3810 if ( $resultset ) 3811 while ( is_array( $related = $resultset->fetch( PDO::FETCH_NUM ) ) ) 3812 { 3813 3814 if ( !ctype_digit( trim( $related[0] ) ) ) 3815 throw new Exception( $this->getLang( 'invalidreader' ) ); 3816 3817 $selectables[intval( $related[0] )] = trim( $related[1] ); 3818 3819 } 3820 3821 // workaround for bug in PHP prior to 5.2.10 3822 // see http://bugs.php.net/bug.php?id=35793 3823 $resultset->closeCursor(); 3824 $resultset = null; 3825 3826 if ( empty( $selectables ) ) 3827 throw new Exception( $this->getLang( 'emptyenum' ) ); 3828 3829 $options['selectables'] = $selectables; 3830 3831 3832 $type = 'related'; 3833 $format = 'related'; 3834 $sqldef = 'integer'; 3835 break; 3836 3837 default : 3838 throw new Exception( sprintf( $this->getLang( 'badtype' ), 3839 $typeName ) ); 3840 3841 } 3842 3843 // derive SQL type definition from parsed type of column 3844 switch ( $type ) 3845 { 3846 3847 case 'data' : 3848 if ( $options['length'] > 0 ) 3849 $sqldef = 'varbinary'; 3850 else if ( $this->driver == 'mssql' ) 3851 // untested: is this proper name of driver?? 3852 $sqldef = 'varbinary'; 3853 else 3854 $sqldef = 'longblob'; 3855 break; 3856 3857 case 'text' : 3858 $sqldef = ( $options['length'] > 0 ) ? 'varchar' : 'text'; 3859 break; 3860 3861 case 'decimal' : 3862 $sqldef = ( $this->driver == 'sqlite' ) ? 'real' 3863 : 'decimal'; 3864 break; 3865 3866 case 'date' : 3867 case 'datetime' : 3868 case 'time' : 3869 default : 3870 if ( is_null( $sqldef ) ) 3871 $sqldef = $type; 3872 3873 } 3874 3875 3876 $sqldef = $colName . ' ' . strtoupper( $sqldef ); 3877 3878 if ( $options['length'] > 0 ) 3879 if ( in_array( $type, array( 'text', 'enum', 'integer', 'related' ) ) ) 3880 $sqldef .= '(' . $options['length'] . ')'; 3881 3882 $sqldef .= $options['required'] ? ' NOT NULL' : ' NULL'; 3883 3884 3885 3886 // add parsed definition to resulting set 3887 if ( $this->getConf( 'aliasing' ) || 3888 !is_string( $options['aliasing'] ) ) 3889 $out[$colName] = array( 3890 'column' => trim( $colName ), 3891 'type' => $type, 3892 'format' => $format, 3893 'definition' => $sqldef, 3894 'options' => $options, 3895 'label' => trim( $label ), 3896 'isColumn' => true, 3897 ); 3898 3899 } 3900 catch ( Exception $e ) 3901 { 3902 $failed[] = sprintf( $this->getLang( 'baddef' ), 3903 $index + 1, $e->getMessage() ); 3904 } 3905 } 3906 3907 3908 if ( empty( $failed ) ) 3909 { 3910 // post-process column definitions 3911 3912 if ( empty( $out ) ) 3913 throw new Exception( $this->getLang( 'emptydef' ) ); 3914 3915 3916 if ( empty( $visibles ) ) 3917 // no column is explicitly marked visible 3918 // --> make them all visible 3919 foreach ( $out as &$def ) 3920 $def['options']['visible'] = ( $def['format'] == 'acl' ) ? 1 : true; 3921 3922 3923 // append primary key - either as defined or automatically 3924 if ( empty( $primaries ) ) 3925 { 3926 // missing explicit definition of primary key 3927 3928 if ( $out['id'] ) 3929 { 3930 // choose column "id" and turn it into primary key 3931 3932 if ( !$out['id']['options']['required'] ) 3933 // declare it as NOT NULL explicitly 3934 $out['id']['definition'] .= ' NOT NULL'; 3935 3936 $out['id']['definition'] .= ' PRIMARY KEY'; 3937 3938 } 3939 else 3940 // there is no column "id" 3941 // --> PREPEND one automatically 3942 $out = array_merge( array( 'id' => array( 3943 'column' => 'id', 3944 'type' => 'integer', 3945 'format' => 'integer', 3946 'definition' => 'id INTEGER NOT ' . 3947 'NULL PRIMARY KEY', 3948 'options' => array(), 3949 'label' => '#', 3950 'isColumn' => true, 3951 'auto_id' => true, 3952 ) ), $out ); 3953 3954 } 3955 else 3956 // append definition of defined primary key index 3957 $out['.PRIMARY_KEYS'] = array( 3958 'definition' => 'PRIMARY KEY ( ' . 3959 implode( ', ', 3960 $primaries ) . ' )', 3961 'primaries' => $primaries 3962 ); 3963 3964 3965 // next ensure to properly include all uniqueness constraints 3966 if ( count( $uniques ) ) 3967 { 3968 3969 foreach ( $uniques as $i => $group ) 3970 if ( count( $group ) == 1 ) 3971 { 3972 // apply uniqueness constraint on single column 3973 $col = array_shift( $group ); 3974 $out[$col]['definition'] .= ' UNIQUE'; 3975 } 3976 else 3977 // append separate unique index on joined columns 3978 $out['.UNIQUE-' . $i] = array( 3979 'definition' => 'UNIQUE ( ' . 3980 implode( ', ', 3981 $group ) . ' )', 3982 ); 3983 3984 } 3985 3986 3987 $this->meta = $out; 3988 3989 $session =& self::getSession(); 3990 $session['definition'] = $this->meta; 3991 3992 } 3993 else 3994 // encountered one or more parser errors 3995 // --> throw exception 3996 throw new Exception( implode( "<br />\n", $failed ) ); 3997 3998 3999 $this->options['aclColumn'] = $aclColumn; 4000 4001 } 4002 4003 4004 /** 4005 * Parses provided value for containing some human-readable form of a 4006 * boolean value. 4007 * 4008 * @param mixed $in value to parse 4009 * @param boolean $nullIfUnparseable if true, method returns null if $in 4010 * can't be parsed as boolean value 4011 * @return boolean boolean counterpart of provided value 4012 */ 4013 4014 protected static function asBool( $in, $nullIfUnparseable = false ) 4015 { 4016 4017 if ( is_numeric( $in ) ) 4018 return ( $in != 0 ); 4019 4020 if ( is_string( $in ) ) 4021 { 4022 4023 if ( preg_match( '/^(n|no|f|false|off)$/i', trim( $in ) ) ) 4024 return false; 4025 4026 if ( preg_match( '/^(y|yes|t|true|on)$/i', trim( $in ) ) ) 4027 return true; 4028 4029 } 4030 4031 if ( ctype_digit( trim( $in ) ) ) 4032 return ( intval( $in ) != 0 ); 4033 4034 if ( ( $in === true ) || ( $in === false ) ) 4035 return $in; 4036 4037 return $nullIfUnparseable ? null : (bool) $in; 4038 4039 } 4040 4041 4042 /** 4043 * Retrieves list of columns included in table's primary key. 4044 * 4045 * @return array list of column names 4046 */ 4047 4048 protected function getPrimaryKeyColumns() 4049 { 4050 4051 if ( !$this->meta ) 4052 return array(); 4053 4054 if ( $this->meta['.PRIMARY_KEYS'] ) 4055 return $this->meta['.PRIMARY_KEYS']['primaries']; 4056 4057 return array( 'id' ); 4058 4059 } 4060 4061 4062 /** 4063 * Retrieves column name of single-column integer primary key or false. 4064 * 4065 * The method returns false if 4066 * - none or multiple columns are set as primary key 4067 * - single column isn't of type integer 4068 * 4069 * @return string/false name of column, false if condition does not match 4070 */ 4071 4072 protected function getSingleNumericPrimaryKey() 4073 { 4074 4075 $primaries = $this->getPrimaryKeyColumns(); 4076 4077 if ( count( $primaries ) != 1 ) 4078 return false; 4079 4080 $column = array_shift( $primaries ); 4081 4082 if ( isset( $this->meta[$column]['type'] ) ) 4083 if ( $this->meta[$column]['type'] != 'integer' ) 4084 return false; 4085 4086 4087 return $column; 4088 4089 } 4090 4091 4092 /** 4093 * Detects if either a table or a single column in a table exists or not. 4094 * 4095 * @param string $table name of table to test 4096 * @param string $column optional name of single column in table to test 4097 * @return boolean true if test succeeds, false otherwise 4098 */ 4099 4100 protected function exists( $table, $column = null ) 4101 { 4102 4103 if ( is_null( $column ) ) 4104 $sql = 'SELECT COUNT(*) FROM ' . $table; 4105 else 4106 $sql = 'SELECT COUNT(' . $column . ') FROM ' . $table; 4107 4108 try 4109 { 4110 4111 $s = $this->db->query( $sql ); 4112 4113 if ( $s instanceof PDOStatement ) 4114 $s->closeCursor(); 4115 4116 return true; 4117 4118 } 4119 catch ( PDOException $e ) 4120 { 4121 4122 if ( in_array( $e->getCode(), array( '42S02' ) ) ) 4123 return false; 4124 4125 if ( stripos( $e->getMessage(), 'no such table' ) !== false ) 4126 return false; 4127 4128 if ( !is_null( $column ) ) 4129 if ( stripos( $e->getMessage(), 'no such column' ) !== false ) 4130 return false; 4131 4132 throw $e; 4133 4134 } 4135 } 4136 4137 4138 /** 4139 * Obtains next ID for use in an "auto-incrementing ID" column. 4140 * 4141 * On every call this method provides another, recently unused ID for the 4142 * given table. This is achieved by using a separate table in current DB. 4143 * 4144 * @throws Exception 4145 * 4146 * @param string $table name of table 4147 * @param boolean $nestedTransaction set true, if you call in a transaction 4148 * @return integer next available ID for assigning 4149 */ 4150 4151 protected function nextID( $table, $nestedTransaction = false ) 4152 { 4153 4154 // automatically create pool for tracking auto-incrementing row IDs 4155 if ( !$this->exists( '__keys' ) ) 4156 if ( $this->db->query( <<<EOT 4157CREATE TABLE __keys ( 4158 tablename CHAR(64) NOT NULL PRIMARY KEY, 4159 recent INTEGER NOT NULL 4160) 4161EOT 4162 ) === false ) 4163 throw new PDOException( $this->getLang( 'idnotable' ) ); 4164 4165 4166 4167 if ( !$nestedTransaction && !$this->db->beginTransaction() ) 4168 throw new PDOException( $this->getLang( 'notransact' ) ); 4169 4170 try 4171 { 4172 4173 // read recently assigned auto-incrementing row ID on table 4174 $st = $this->db->prepare('SELECT recent FROM __keys WHERE tablename=?'); 4175 if ( !$st ) 4176 throw new PDOException( $this->getLang( 'idreadprepare' ) ); 4177 4178 if ( !$st->execute( array( $table ) ) ) 4179 throw new PDOException( $this->getLang( 'idreadexecute' ) ); 4180 4181 4182 $row = $st->fetch( PDO::FETCH_NUM ); 4183 if ( is_array( $row ) ) 4184 { 4185 // got record -> assigned ID before --> increment and update 4186 $sql = 'UPDATE __keys SET recent=? WHERE tablename=?'; 4187 $nextID = ++$row[0]; 4188 } 4189 else 4190 { 4191 // no record -> assigning ID for the first time --> start with 1 4192 $sql = 'INSERT INTO __keys (recent,tablename) VALUES (?,?)'; 4193 $nextID = 1; 4194 } 4195 4196 $st->closeCursor(); 4197 4198 4199 // write new/updated track of auto-incrementing ID on current table 4200 $st = $this->db->prepare( $sql ); 4201 if ( !$st ) 4202 throw new PDOException( $this->getLang( 'idwriteprepare' ) ); 4203 4204 if ( !$st->execute( array( $nextID, $table ) ) ) 4205 throw new PDOException( $this->getLang( 'idwriteexecute' ) ); 4206 4207 4208 4209 if ( !$nestedTransaction && !$this->db->commit() ) 4210 throw new PDOException( $this->getLang( 'idcommit' ) ); 4211 4212 4213 return $nextID; 4214 4215 } 4216 catch ( PDOException $e ) 4217 { 4218 4219 if ( !$nestedTransaction && !$this->db->rollBack() ) 4220 throw new PDOException( $this->getLang( 'idrollback' ) ); 4221 4222 throw new Exception( $this->getLang( 'idnoid' ) ); 4223 4224 } 4225 } 4226 4227 4228 /** 4229 * Retrieves name of current "user" (providing temporary name for guests) 4230 * 4231 * @throws Exception 4232 * @return string 4233 */ 4234 4235 protected static function currentUser() 4236 { 4237 4238 $currentUser = $_SERVER['REMOTE_USER']; 4239 if ( !$currentUser ) 4240 { 4241 // there is no authenticated user ... 4242 // --> try using user's sesion ID instead 4243 4244 if ( !session_id() ) 4245 throw new Exception( $this->getLang( 'userunknown' ) ); 4246 4247 $currentUser = '|' . session_id(); 4248 4249 } 4250 4251 4252 return $currentUser; 4253 4254 } 4255 4256 4257 /** 4258 * Adds entry to log of changes on a table and record. 4259 * 4260 * Omit $rowid to mark change of a whole table. 4261 * 4262 * @throws Exception 4263 * 4264 * @param string $action name of change action 4265 * @param string $table name of table 4266 * @param integer $rowid ID of record changed 4267 */ 4268 4269 protected function log( $action, $table, $rowid = null ) 4270 { 4271 4272 // automatically create log table in DB 4273 if ( !$this->exists( '__log' ) ) 4274 if ( $this->db->query( <<<EOT 4275CREATE TABLE __log ( 4276 tablename CHAR(64) NOT NULL, 4277 rowid INTEGER NULL, 4278 action CHAR(8) NOT NULL, 4279 username CHAR(64) NOT NULL, 4280 ctime INTEGER NOT NULL 4281) 4282EOT 4283 ) === false ) 4284 throw new PDOException( $this->getLang( 'lognotable' ) ); 4285 4286 4287 4288 // add entry to log 4289 $st = $this->db->prepare( 'INSERT INTO __log (tablename,rowid,action,' . 4290 'username,ctime) VALUES (?,?,?,?,?)' ); 4291 if ( !$st ) 4292 throw new PDOException( $this->getLang( 'logprepare' ) ); 4293 4294 if ( !$st->execute( array( $table, intval( $rowid ), $action, 4295 self::currentUser(), time() ) ) ) 4296 throw new PDOException( $this->getLang( 'logexecute' ) ); 4297 4298 4299 // in a local SQLite database: drop all log records older than 30 days 4300 if ( $this->driver == 'sqlite' ) 4301 $this->db->query( 'DELETE FROM __log WHERE ctime<'.(time()-30*86400)); 4302 4303 } 4304 4305 4306 /** 4307 * Obtains a lock. 4308 * 4309 * The lock is either related to a whole table (if $rowid is omitted or 4310 * null) or a single record in that table selected by its unique (!!) 4311 * numeric ID. Obtaining record-related lock is rejected if whole table is 4312 * currently locked by some other user. 4313 * 4314 * NOTE! Locking records basically works with unique (!!) numeric IDs, only. 4315 * 4316 * @param string $table name of table lock is related to 4317 * @param integer $rowid unique (!!) ID of record lock is related to, omit 4318 * or set 0/null for a table-related lock 4319 * @param boolean $inTransaction if true, the caller started transaction 4320 * @param boolean $checkOnly if true, an available lock isn't obtained 4321 * actually 4322 * @return boolean true on success, false on failure 4323 */ 4324 4325 protected function obtainLock( $table, $rowid = null, $inTransaction = false, $checkOnly = false, $innerTest = false ) 4326 { 4327 4328 // automatically create DB's pool of obtained locks 4329 if ( !$this->exists( '__locks' ) ) 4330 if ( $this->db->query( <<<EOT 4331CREATE TABLE __locks ( 4332 tablename CHAR(64) NOT NULL, 4333 record INTEGER NOT NULL, 4334 username CHAR(64) NOT NULL, 4335 obtained INTEGER NOT NULL, 4336 PRIMARY KEY ( tablename, record ) 4337) 4338EOT 4339 ) === false ) 4340 return false; 4341 4342 4343 4344 // get "name" of current user (supporting guests as well) 4345 $currentUser = self::currentUser(); 4346 4347 // normalize $rowid selecting single record or whole table (==0) 4348 $rowid = intval( $rowid ); 4349 4350 4351 if ( !$inTransaction && !$this->db->beginTransaction() ) 4352 return false; 4353 4354 try 4355 { 4356 4357 if ( !$innerTest ) 4358 { 4359 4360 if ( $rowid ) 4361 { 4362 // obtaining lock on record is rejected if whole table is locked 4363 4364 if ( !$this->obtainLock( $table, null, true, true, true ) ) 4365 throw new PDOException( $this->getLang( 'locksuperlocked' ) ); 4366 4367 } 4368 else 4369 { 4370 // obtaining lock on whole table is rejected if some other user 4371 // has locked at least one record 4372 4373 $st = $this->db->prepare( 'SELECT COUNT(*) FROM __locks ' . 4374 'WHERE tablename=? AND record<>0 ' . 4375 'AND username<>?' ); 4376 if ( !$st ) 4377 throw new PDOException( $this->getLang( 'locksubprepare' ) ); 4378 4379 if ( !$st->execute( array( $table, $currentUser ) ) ) 4380 throw new PDOException( $this->getLang( 'locksubexecute' ) ); 4381 4382 4383 $count = $st->fetch( PDO::FETCH_NUM ); 4384 if ( $count && ( $count[0] > 0 ) ) 4385 throw new PDOException( $this->getLang( 'locksublocked' ) ); 4386 4387 $st->closeCursor(); 4388 4389 } 4390 } 4391 4392 4393 4394 // check for existing lock on selected entity 4395 $st = $this->db->prepare( 'SELECT username,obtained FROM __locks ' . 4396 'WHERE tablename=? AND record=?' ); 4397 if ( !$st ) 4398 throw new PDOException( $this->getLang( 'lockreadprepare' ) ); 4399 4400 if ( !$st->execute( array( $table, $rowid ) ) ) 4401 throw new PDOException( $this->getLang( 'lockreadexecute' ) ); 4402 4403 $lock = $st->fetchAll( PDO::FETCH_NUM ); 4404 if ( is_array( $lock ) && count( $lock ) ) 4405 { 4406 // there is a lock 4407 4408 $lock = array_shift( $lock ); 4409 4410 $user = trim( $lock[0] ); 4411 if ( $user !== $currentUser ) 4412 { 4413 // lock is obtained by different user 4414 4415 // - check whether it's outdated (1 hour) or not 4416 if ( time() - intval( $lock[1] ) < $this->getConf( 'locktime' ) ) 4417 // no -> reject to obtain 4418 throw new PDOException( $this->getLang( 'locklocked' ) ); 4419 4420 } 4421 4422 $sql = 'UPDATE __locks SET username=?,obtained=? ' . 4423 'WHERE tablename=? AND record=?'; 4424 4425 } 4426 else 4427 // resource isn't locked -> obtain lock now 4428 $sql = 'INSERT INTO __locks (username,obtained,tablename,record) ' . 4429 'VALUES (?,?,?,?)'; 4430 4431 4432 if ( !$checkOnly ) 4433 { 4434 4435 $st = $this->db->prepare( $sql ); 4436 if ( !$st ) 4437 throw new PDOException( $this->getLang( 'lockwriteprepare' ) ); 4438 4439 if ( !$st->execute( array( $currentUser, time(), $table, $rowid ) )) 4440 throw new PDOException( $this->getLang( 'lockwriteexecute' ) ); 4441 4442 } 4443 4444 4445 if ( !$inTransaction && !$this->db->commit() ) 4446 throw new PDOException( $this->getLang( 'lockcommit' ) ); 4447 4448 return true; 4449 4450 } 4451 catch ( PDOException $e ) 4452 { 4453 4454 if ( !$inTransaction && !$this->db->rollBack() ) 4455 throw new PDOException( $this->getLang( 'lockrollback' ) ); 4456 4457 return false; 4458 4459 } 4460 } 4461 4462 4463 /** 4464 * Releases recently obtained lock. 4465 * 4466 * The lock is either related to a whole table (if $rowid is omitted or 4467 * null) or a single record in that table selected by its numeric ID. 4468 * 4469 * NOTE! Locking records basically works with unique (!!) numeric IDs, only. 4470 * 4471 * @param string $table name of table lock is related to 4472 * @param integer $rowid unique (!!) ID of record lock is related to, omit 4473 * or set 0/null for a table-related lock 4474 * @param boolean $inTransaction if true, the caller started transaction 4475 * @return boolean true on successfully releasing lock, false on failure 4476 */ 4477 4478 protected function releaseLock( $table, $rowid = null, $inTransaction = false ) 4479 { 4480 4481 if ( !$this->exists( '__locks' ) ) 4482 // didn't create pool of locks before 4483 // --> succeed to release without hassle 4484 return true; 4485 4486 4487 // get "name" of current user (supporting guests) 4488 $currentUser = self::currentUser(); 4489 4490 // $rowid is non-zero or zero for obtaining lock on whole table 4491 $rowid = intval( $rowid ); 4492 4493 4494 if ( !$inTransaction && !$this->db->beginTransaction() ) 4495 return false; 4496 4497 try 4498 { 4499 4500 if ( !$this->obtainLock( $table, $rowid, true, true, true ) ) 4501 // user didn't obtain that lock before ... succeed to release 4502 return true; 4503 4504 4505 // check for existing lock on selected entity 4506 $st = $this->db->prepare( 'DELETE FROM __locks WHERE tablename=? AND ' . 4507 'record=? AND username=?' ); 4508 if ( !$st ) 4509 throw new PDOException( $this->getLang( 'releaseprepare' ) ); 4510 4511 if ( !$st->execute( array( $table, $rowid, $currentUser ) ) ) 4512 throw new PDOException( $this->getLang( 'releaseexecute' ) ); 4513 4514 4515 if ( !$inTransaction && !$this->db->commit() ) 4516 throw new PDOException( $this->getLang( 'releasecommit' ) ); 4517 4518 4519 return true; 4520 4521 } 4522 catch ( PDOException $e ) 4523 { 4524 4525 if ( !$inTransaction && !$this->db->rollBack() ) 4526 throw new PDOException( $this->getLang( 'releaserollback' ) ); 4527 4528 return false; 4529 4530 } 4531 } 4532 4533 4534 /** 4535 * Provides link for retrieving media data in selected record's column. 4536 * 4537 * @throws Exception 4538 * 4539 * @param integer $rowid unique numeric ID of selected record 4540 * @param string $column name of column in record containing media to retrieve 4541 * @param boolean $forDownload if true the media is requested for download 4542 * @return string URL for retrieving media 4543 */ 4544 4545 final protected function mediaLink( $rowid, $column, $forDownload = false, 4546 $rowACL = null ) 4547 { 4548 4549 // validate media selected for external retrieval 4550 $rowid = intval( $rowid ); 4551 $column = trim( $column ); 4552 4553 if ( !$rowid || ( $this->meta[$column]['type'] != 'data' ) ) 4554 throw new Exception( sprintf( $this->getLang( 'medianomedia' ), $column ) ); 4555 4556 4557 $idColumn = $this->getSingleNumericPrimaryKey(); 4558 4559 if ( !$idColumn ) 4560 throw new Exception( $this->getLang( 'mediana' ) ); 4561 4562 4563 // gain access on pool of hashing salts in session space 4564 $session =& $this->getSession(); 4565 4566 if ( !is_array( $session['linkedMediaSalts'] ) ) 4567 $session['linkedMediaSalts'] = array(); 4568 4569 4570 // compile selector describing media to be retrieved 4571 $selector = array( '@'.$this->dsn, $this->authSlot, $this->table, 4572 $column, $idColumn, $rowid, $this->getPageID(), 4573 $this->getIndex(), self::currentUser(), 4574 $_SERVER['REMOTE_ADDR'] ); 4575 4576 // use unsalted hash to find salt in internal pool for salted hash 4577 $hash = sha1( implode( '/', $selector ) ); 4578 4579 4580 // check authorization to download file first 4581 if ( !$this->isAuthorizedMulti( $rowACL, $this->options, 'maydownload', 'mayview', true ) ) 4582 { 4583 // lacking authorization 4584 // --> drop salt used in media frontend to proof authorization 4585 unset( $session['linkedMediaSalts'][$hash] ); 4586 throw new Exception( $this->getLang( 'mediadenied' ) ); 4587 } 4588 4589 4590 // create salt on requesting media for the first time ... 4591 if ( !$session['linkedMediaSalts'][$hash] ) 4592 { 4593 mt_srand( intval( microtime( true ) * 1000 ) ); 4594 $session['linkedMediaSalts'][$hash] = uniqid( mt_rand(), true ); 4595 } 4596 4597 4598 // derive URL components 4599 $source = urlencode( base64_encode( gzcompress( $selector = serialize( $selector ) ) ) ); 4600 $hash = urlencode( base64_encode( self::ssha( $selector, $session['linkedMediaSalts'][$hash] ) ) ); 4601 4602 4603 // return URL for retrieving media 4604 return DOKU_BASE . DB2_PATH . "media.php?a=$source&b=$hash&d=" . ( $forDownload ? 1 : 0 ); 4605 4606 } 4607 4608 4609 /** 4610 * Provides link for retrieving files virtually attached to table 4611 * (e.g. CSV exports). 4612 * 4613 * @throws Exception 4614 * 4615 * @param boolean $forDownload if true the media is requested for download 4616 * @return string URL for retrieving media 4617 */ 4618 4619 final protected function attachmentLink( $mode, $authorization, $forDownload = true, $rowACL = null ) 4620 { 4621 4622 // gain access on pool of hashing salts in session space 4623 $session =& $this->getSession(); 4624 4625 if ( !is_array( $session['linkedMediaSalts'] ) ) 4626 $session['linkedMediaSalts'] = array(); 4627 4628 4629 // compile selector describing media to be retrieved 4630 $selector = array( '@'.$this->dsn, $this->authSlot, $this->table,'fake', 4631 'id', 1, $this->getPageID(), $this->getIndex(), 4632 self::currentUser(), $_SERVER['REMOTE_ADDR'] ); 4633 4634 // use unsalted hash to find salt in internal pool for salted hash 4635 $hash = sha1( implode( '/', $selector ) ); 4636 4637 4638 // check authorization to request attached file 4639 if ( !$this->isAuthorizedMulti( $rowACL, $this->options, 'may' . $authorization ) ) 4640 { 4641 // lacking authorization 4642 // --> drop salt used in media frontend to proof authorization 4643 unset( $session['linkedMediaSalts'][$hash] ); 4644 throw new Exception( $this->getLang( 'mediadenied' ) ); 4645 } 4646 4647 4648 // create salt on requesting media for the first time ... 4649 if ( !$session['linkedMediaSalts'][$hash] ) 4650 { 4651 mt_srand( intval( microtime( true ) * 1000 ) ); 4652 $session['linkedMediaSalts'][$hash] = uniqid( mt_rand(), true ); 4653 } 4654 4655 4656 // derive URL components 4657 $source = urlencode( base64_encode( gzcompress( $selector = serialize( $selector ) ) ) ); 4658 $hash = urlencode( base64_encode( self::ssha( $selector, $session['linkedMediaSalts'][$hash] ) ) ); 4659 4660 4661 // return URL for retrieving media 4662 return DOKU_BASE . DB2_PATH . "media.php?a=$source&b=$hash&m=$mode&d=" . ( $forDownload ? 1 : 0 ); 4663 4664 } 4665 4666 4667 /** 4668 * Provides external link for retrieving media in session 4669 * 4670 * @param string $sessionFileKey name of section in session containing file 4671 * @param boolean $forDownload if true, the link requests file for download 4672 * @return string URL for retrieving file 4673 */ 4674 4675 public function editorSessionMediaLink( $column, $forDownload = false ) 4676 { 4677 4678 $session =& $this->getEditorSession(); 4679 4680 if ( !is_array( $session[$column] ) ) 4681 throw new Exception( $this->getLang( 'medianoeditor' ) ); 4682 4683 4684 // compile selector describing media to be retrieved 4685 $selector = array( $this->getPageID(), $this->getIndex(), $column ); 4686 4687 // derive URL components 4688 $source = urlencode( base64_encode( serialize( $selector ) ) ); 4689 4690 4691 // return URL for retrieving media 4692 return DOKU_BASE . DB2_PATH . "media.php?s=$source&d=" . ( $forDownload ? 1 : 0 ); 4693 4694 } 4695 4696 4697 /** 4698 * Gets "SSHA1" hash without including the salt (in opposition to what is 4699 * usually done in RFC-conforming SSHA1 algorithm). 4700 * 4701 * @param string $data data to hash 4702 * @param string $salt salt to use on hashing 4703 * @return string salted hash on $data 4704 */ 4705 4706 final public static function ssha( $data, $salt ) 4707 { 4708 return sha1( $salt . $data . sha1( $data . $salt, true ) ); 4709 } 4710 4711 4712 4713 4714 4715 /** 4716 * Parses provided string for SQL-like Date/Time representation. 4717 * 4718 * @param string $in representation of Date/Time in SQL format 4719 * @param boolean $skipTime if true, the time information is dropped/skipped 4720 * @return integer UNIX timestamp for parsed date/time 4721 */ 4722 4723 protected static function parseDBDateTime( $in, $skipTime = false ) 4724 { 4725 4726 list( $date, $time ) = preg_split( '/t|(\s+)/i', trim( $in ) ); 4727 4728 list( $year, $month, $day ) = explode( '-', trim( $date ) ); 4729 list( $hour, $minute, $second ) = explode( ':', trim( $time ) ); 4730 4731 if ( ( intval( $hour ) == 0 ) && ( intval( $minute ) == 0 ) && 4732 ( intval( $second ) == 0 ) && ( intval( $year ) == 0 ) && 4733 ( intval( $month ) == 0 ) && ( intval( $day ) == 0 ) ) 4734 return 0; 4735 4736 if ( $skipTime ) 4737 { 4738 $hour = 12; 4739 $minute = $second = 0; 4740 } 4741 4742 return mktime( intval( $hour ), intval( $minute ), intval( $second ), 4743 intval( $month ), intval( $day ), intval( $year ) ); 4744 4745 } 4746 4747 4748 protected static function parseInternalDate( $in ) 4749 { 4750 4751 $formats = array( 4752 '#^(\d{4})/(\d+)/(\d+)$#' => array( 'year', 'month', 'day' ), 4753 '#^(\d+)/(\d+)/(\d+)$#' => array( 'month', 'day', 'year' ), 4754 '/^(\d+)-(\d+)-(\d+)$/' => array( 'year', 'month', 'day' ), 4755 '/^(\d+)\.(\d+)\.(\d+)$/' => array( 'day', 'month', 'year' ), 4756 ); 4757 4758 $in = preg_replace( '/\s+/', '', $in ); 4759 4760 foreach ( $formats as $pattern => $order ) 4761 if ( preg_match( $pattern, $in, $matches ) ) 4762 { 4763 4764 $out = array(); 4765 4766 foreach ( $order as $key => $value ) 4767 $out[$value] = intval( $matches[$key+1] ); 4768 4769 if ( $out['year'] < 100 ) 4770 { 4771 if ( $out['year'] > 40 ) 4772 $out['year'] += 1900; 4773 else 4774 $out['year'] += 2000; 4775 } 4776 4777 return $out; 4778 4779 } 4780 4781 4782 return false; 4783 4784 } 4785 4786 4787 protected static function parseInternalTime( $in ) 4788 { 4789 4790 $formats = array( 4791 '/^(\d+):(\d+)(:(\d+))?$/' => array( 'hour', 'minute', 3 => 'second' ), 4792 ); 4793 4794 $in = preg_replace( '/\s+/', '', $in ); 4795 4796 foreach ( $formats as $pattern => $order ) 4797 if ( preg_match( $pattern, $in, $matches ) ) 4798 { 4799 4800 $out = array(); 4801 4802 foreach ( $order as $key => $value ) 4803 $out[$value] = sprintf( '%02d', intval( $matches[$key+1] ) ); 4804 4805 return $out; 4806 4807 } 4808 4809 4810 return false; 4811 4812 } 4813 4814 4815 /** 4816 * Parses provided ACL rules definition returning contained rules as array. 4817 * 4818 */ 4819 4820 protected function parseACLRule( $in, $mayThrow = false, $useLabels = false ) 4821 { 4822 4823 $out = array(); 4824 4825 $rules = preg_split( '/\s*;\s*/', trim( $in ) ); 4826 foreach ( $rules as $major => $rule ) 4827 if ( !preg_match( '/^(may\S+)\s*=\s*(\S.*)$/i', $rule, $matches ) ) 4828 { 4829 4830 if ( $mayThrow ) 4831 throw new Exception( $this->getLang( 'badaclrule' ) ); 4832 4833 continue; 4834 4835 } 4836 else 4837 { 4838 4839 $objects = preg_split( '/\s*,\s*/', trim( $matches[2] ) ); 4840 4841 foreach ( $objects as $minor => $object ) 4842 if ( !preg_match( '/^(!?)\s*(\S+)$/', $object, $subs ) ) 4843 { 4844 4845 if ( $mayThrow ) 4846 throw new Exception( $this->getLang( 'badaclrule' ) ); 4847 4848 unset( $objects[$minor] ); 4849 4850 } 4851 else 4852 $objects[$minor] = $subs[1] . $subs[2]; 4853 4854 if ( empty( $objects ) ) 4855 unset( $rules[$major] ); 4856 else if ( $useLabels ) 4857 $out[strtolower($matches[1])] = implode( ',', $objects ); 4858 else 4859 $rules[$major] = strtolower( $matches[1] ) . '=' . 4860 implode( ',', $objects ); 4861 4862 } 4863 4864 4865 return $useLabels ? $out : $rules; 4866 4867 } 4868 4869 4870 /** 4871 * Serves in processing method replaceMarkup by replacing single occurrence 4872 * of markup sequence. 4873 * 4874 * An empty string is returned if markup sequence isn't detected. 4875 * 4876 * @internal 4877 * 4878 * @param array $matches set of matches according to used PCRE pattern 4879 * @return string replacement string 4880 */ 4881 4882 public function __replaceMarkupCB( $matches ) 4883 { 4884 4885 $keyword = strtolower( $matches[1] ); 4886 switch ( $keyword ) 4887 { 4888 4889 case 'wiki.user' : 4890 return $_SERVER['REMOTE_USER']; 4891 case 'wiki.groups' : 4892 return implode( ',', $GLOBALS['USERINFO']['grps'] ); 4893 case 'wiki.page' : 4894 return $this->getPageID(); 4895 4896 default : 4897 $group = trim( strtok( $matches[1], '.' ) ); 4898 $arg = trim( strtok( '' ) ); 4899 4900 switch ( strtolower( $group ) ) 4901 { 4902 4903 case 'date' : 4904 return ( $arg !== '' ) ? date( $arg ) : ''; 4905 4906 default : 4907 if ( is_array( $this->__replaceMarkupVarspace ) ) 4908 { 4909 4910 if ( isset( $this->__replaceMarkupVarspace[$keyword] ) ) 4911 return $this->__replaceMarkupVarspace[$keyword]; 4912 4913 if ( isset( $this->__replaceMarkupVarspace[$group][$arg] ) ) 4914 $this->__replaceMarkupVarspace[$group][$arg]; 4915 4916 } 4917 4918 return ''; 4919 4920 } 4921 4922 } 4923 } 4924 4925 4926 /** 4927 * Replaces all occurrences of %{whatever} by a value actually related to 4928 * the internally defined keyword "whatever". 4929 * 4930 * @param string $in string to parse for markup sequences to be replaced 4931 * @return string string with all markup replaced 4932 */ 4933 4934 public function replaceMarkup( $in, $varspace = array() ) 4935 { 4936 4937 if ( strpos( $in, '%{' ) !== false ) 4938 { 4939 4940 $this->__replaceMarkupVarspace = $varspace; 4941 4942 $in = preg_replace_callback( '/%{([^}]+)}/', array( &$this, 4943 '__replaceMarkupCB' ), $in ); 4944 4945 } 4946 4947 return $in; 4948 4949 } 4950 4951 4952 /** 4953 * Provides initial value of a column used on creating new record. 4954 * 4955 * @param string $column name of column 4956 * @param array $def definition of column 4957 * @return mixed value in internal format 4958 */ 4959 4960 protected function getInitialValue( $column, $def ) 4961 { 4962 4963 if ( $def['type'] == 'data' ) 4964 return null; 4965 4966 if ( $def['options']['nodefault'] ) 4967 return null; 4968 4969 4970 $default = $this->replaceMarkup( trim( $def['options']['default'] ) ); 4971 4972 switch ( $def['format'] ) 4973 { 4974 4975 case 'bool' : 4976 return self::asBool( $default ); 4977 4978 case 'enum' : 4979 case 'related' : 4980 $value = array_search( $default, $def['options']['selectables'] ); 4981 if ( $value === false ) 4982 { 4983 4984 $value = null; 4985 4986 if ( ctype_digit( $default ) ) 4987 { 4988 4989 $default = intval( $default ); 4990 4991 if ( ( $default > 0 ) && ( $default <= count( $def['options']['selectables'] ) ) ) 4992 { 4993 4994 if ( $def['format'] == 'enum' ) 4995 $value = $default - 1; 4996 else 4997 $value = $default; 4998 4999 } 5000 } 5001 } 5002 5003 return $value; 5004 5005 default : 5006 try 5007 { 5008 return $this->inputToInternal( $default, $def ); 5009 } 5010 catch ( Exception $e ) 5011 { 5012 return null; 5013 } 5014 5015 } 5016 } 5017 5018 5019 /** 5020 * Converts value from format used in DB into format used internally. 5021 * 5022 * @param integer $rowid ID of row containing given value 5023 * @param string $column name of column 5024 * @param mixed $value value in DB 5025 * @param array $def definition of column 5026 * @return mixed value in internal format 5027 */ 5028 5029 protected function valueFromDB( $rowid, $column, $value, $def ) 5030 { 5031 5032 switch ( $def['format'] ) 5033 { 5034 5035 case 'image' : 5036 case 'file' : 5037 if ( is_null( $value ) ) 5038 return null; 5039 5040 if ( $value === '||' ) 5041 return null; 5042 5043 // parse file for internally used structure 5044 $a = strpos( $value, '|' ); 5045 if ( !$a ) 5046 // externally provided file --> don't touch 5047 return ( strlen( $value ) > 0 ); 5048 5049 $b = strpos( $value, '|', $a + 1 ); 5050 if ( !$b ) 5051 // externally provided file --> don't touch 5052 return true; 5053 5054 $temp = array( 5055 'mime' => substr( $value, 0, $a ), 5056 'name' => substr( $value, $a + 1, $b - $a - 1 ), 5057 'file' => substr( $value, $b + 1 ), 5058 ); 5059 5060 if ( !preg_match( '#^[a-z0-9-]+/[+a-z0-9-]+$#i', $temp['mime'] )) 5061 // externally provided file --> don't touch 5062 return true; 5063 5064 if ( trim( $temp['name'] ) === '' ) 5065 // externally provided file --> don't touch 5066 return true; 5067 5068 return $temp; 5069 5070 case 'date' : 5071 if ( $def['options']['unixts'] ) 5072 return $value; 5073 5074 if ( ( trim( $value ) === '' ) || ( $value == '0000-00-00' ) ) 5075 return 0; 5076 5077 return self::parseDBDateTime( $value, true ); 5078 5079 case 'time' : 5080 return $value; 5081 5082 case 'datetime' : 5083 if ( $def['options']['unixts'] ) 5084 return $value; 5085 5086 $value = substr( $value, 0, 19 ); 5087 if ( ( trim( $value ) === '' ) || 5088 ( $value == '0000-00-00T00:00:00' ) || 5089 ( $value == '0000-00-00 00:00:00' ) ) 5090 return 0; 5091 5092 return self::parseDBDateTime( $value, false ); 5093 5094 case 'bool' : 5095 $value = trim( $value ); 5096 switch ( $def['options']['booltype'] ) 5097 { 5098 5099 case 'int' : 5100 return ( intval( $value ) != 0 ); 5101 5102 case 'xmark' : 5103 return ( strtolower( $value[0] ) == 'x' ); 5104 5105 case 'yesno' : 5106 default : 5107 return ( strtolower( $value[0] ) == 'y' ); 5108 5109 } 5110 5111 case 'enum' : 5112 $value = trim( $value ); 5113 $value = array_search( $value, $def['options']['selectables'] ); 5114 if ( $value === false ) 5115 $value = null; 5116 else 5117 $value = intval( $value ); 5118 5119 break; 5120 5121 case 'related' : 5122 if ( is_numeric( $value ) ) 5123 $value = intval( $value ); 5124 5125 break; 5126 5127 case 'monetary' : 5128 case 'real' : 5129 /** @todo manage decimal point conversions */ 5130 5131 case 'url' : 5132 case 'email' : 5133 case 'phone' : 5134 case 'fax' : 5135 case 'text' : 5136 case 'integer' : 5137 case 'acl' : 5138 // keep value as is ... 5139 5140 } 5141 5142 return $value; 5143 5144 } 5145 5146 5147 /** 5148 * Converts value from format used internally into format used in DB. 5149 * 5150 * @param integer $rowid ID of row containing given value 5151 * @param string $column name of column 5152 * @param mixed $value value in internal format 5153 * @param array $def definition of column 5154 * @return mixed value in DB format, false to omit this value on writing 5155 * back to database, null to store NULL 5156 */ 5157 5158 protected function valueToDB( $rowid, &$column, $value, $def ) 5159 { 5160 5161 if ( $def['options']['readonly'] ) 5162 // always omit writing columns marked as read-only 5163 return false; 5164 5165 5166 if ( is_null( $value ) && !$def['options']['notnull'] && 5167 ( $def['format'] != 'bool' ) ) 5168 return null; 5169 5170 5171 5172 switch ( $def['format'] ) 5173 { 5174 5175 case 'image' : 5176 case 'file' : 5177 if ( is_bool( $value ) ) 5178 // don't change this file ... 5179 return false; 5180 5181 if ( is_string( $value ) || is_null( $value ) ) 5182 // got a raw file or nothing ... write as is 5183 return strval( $value ); 5184 5185 if ( !is_array( $value ) || !is_string( $value['file'] ) ) 5186 throw new Exception( $this->getLang( 'fileinvalid' ) ); 5187 5188 // internally managed files are serialized prior to saving 5189 return "$value[mime]|$value[name]|$value[file]"; 5190 5191 case 'date' : 5192 if ( !$value ) 5193 { 5194 5195 if ( $def['options']['unixts'] ) 5196 return 0; 5197 5198 return $def['options']['notnull'] ? '0000-00-00' : null; 5199 5200 } 5201 5202 return $def['options']['unixts'] ? $value : date( 'Y-m-d',intval( $value ) ); 5203 5204 case 'time' : 5205 $time = self::parseInternalTime( $value ); 5206 return $time ? "$time[hour]:$time[minute]:$time[second]" : ''; 5207 5208 case 'datetime' : 5209 if ( !$value ) 5210 { 5211 5212 if ( $def['options']['unixts'] ) 5213 return 0; 5214 5215 return $def['options']['notnull'] ? '0000-00-00T00:00:00' : null; 5216 5217 } 5218 5219 return $def['options']['unixts'] ? $value : date( 'Y-m-d\TH:i:s', intval( $value ) ); 5220 5221 case 'bool' : 5222 switch ( $def['options']['booltype'] ) 5223 { 5224 5225 case 'xmark' : 5226 return $value ? 'x' : ' '; 5227 5228 case 'int' : 5229 return $value ? 1 : 0; 5230 5231 case 'yesno' : 5232 default : 5233 return $value ? 'y' : 'n'; 5234 5235 } 5236 5237 case 'enum' : 5238 $value = $def['options']['selectables'][$value]; 5239 break; 5240 5241 case 'monetary' : 5242 case 'real' : 5243 /** @todo manage decimal point conversions */ 5244 if ( is_null( $value ) ) 5245 $value = '0.00'; 5246 break; 5247 5248 case 'acl' : 5249 $this->dropRowACL( $rowid ); 5250 5251 case 'url' : 5252 case 'email' : 5253 case 'phone' : 5254 case 'fax' : 5255 case 'text' : 5256 if ( is_null( $value ) ) 5257 $value = ''; 5258 break; 5259 5260 case 'related' : 5261 case 'integer' : 5262 $value = intval( $value ); 5263 break; 5264 5265 } 5266 5267 return $value; 5268 5269 } 5270 5271 5272 /** 5273 * Processes and validates input value on selected column. 5274 * 5275 * Set $column false to skip transferring this input value into session 5276 * storage, e.g. to skip overwriting mark on externally provided file for 5277 * keeping it untouched. 5278 * 5279 * @param integer $rowid ID of row containing given value 5280 * @param string $column name of column 5281 * @param mixed $value input value, optionally adjusted on return 5282 * @param mixed $inStore value stored in editor's session 5283 * @param array $def definition of column 5284 * @return string error message to be rendered next to field, null if okay 5285 */ 5286 5287 protected function checkValue( $rowid, &$column, $value, &$inStore, $def ) 5288 { 5289 5290 // pre-validate some selected formats 5291 switch ( $def['format'] ) 5292 { 5293 5294 case 'image' : 5295 case 'file' : 5296 if ( is_bool( $inStore ) ) 5297 // don't touch externally provided files 5298 return; 5299 5300 if ( trim( $value ) !== '' ) 5301 { 5302 // handle request for dropping file here 5303 // --> reset value in store prior to processing any upload 5304 $inStore = null; 5305 $value = null; 5306 } 5307 5308 5309 // check for available upload 5310 5311 $upload = $_FILES['db2dodata'.$column]; 5312 5313 // reduce array (shared by all currently open single-record editors) 5314 $in = array(); 5315 $idx = $this->getIndex(); 5316 5317 if ( is_array( $upload ) ) 5318 foreach ( $upload as $key => $list ) 5319 if ( is_array( $list ) && isset( $list[$idx] ) ) 5320 $in[$key] = $list[$idx]; 5321 5322 5323 if ( !is_array( $in ) || ( $in['error'] == UPLOAD_ERR_NO_FILE ) ) 5324 { 5325 // there is no upload for current field 5326 // --> keep existing value/state 5327 $value = $inStore; 5328 break; 5329 } 5330 5331 if ( $in['error'] !== UPLOAD_ERR_OK ) 5332 return sprintf( $this->getLang( 'filebadupload' ), $in['error'] ); 5333 5334 if ( $in['size'] === 0 ) 5335 return $this->getLang( 'filenoupload' ); 5336 5337 if ( $def['options']['accept'] ) 5338 { 5339 5340 if ( !preg_match( $def['options']['accept'], $in['type'] ) ) 5341 return $this->getLang( 'filebadmime' ); 5342 5343 } 5344 else if ( $def['format'] == 'image' ) 5345 { 5346 5347 list( $major, $minor ) = explode( '/', $in['type'] ); 5348 5349 if ( strtolower( trim( $major ) ) !== 'image' ) 5350 return $this->getLang( 'filenoimage' ); 5351 5352 } 5353 5354 5355 $data = file_get_contents( $in['tmp_name'] ); 5356 if ( strlen( $data ) != $in['size'] ) 5357 return $this->getLang( 'fileincomplete' ); 5358 5359 // finally accept upload replacing any existing file in session 5360 $value = array( 5361 'name' => $in['name'], 5362 'mime' => $in['type'], 5363 'file' => $data, 5364 ); 5365 5366 break; 5367 5368 case 'bool' : 5369 $value = ( $value != false ); 5370 5371 if ( !$value && $def['options']['required'] ) 5372 return $this->getLang( 'markrequired' ); 5373 5374 break; 5375 5376 case 'enum' : 5377 case 'related' : 5378 $value = trim( $value ); 5379 5380 // options of an enumeration are selected by 5381 // a) an integer index starting at 1 for first defined option 5382 // b) the option's label requiring case-sensitive match 5383 5384 if ( $value === '' ) 5385 $value = null; 5386 else 5387 { 5388 // test for selection by an option's label, first 5389 5390 if ( ctype_digit( trim( $value ) ) ) 5391 $new = false; 5392 else 5393 $new = array_search( $value, $def['options']['selectables'] ); 5394 5395 if ( $new !== false ) 5396 $value = $new; 5397 else 5398 { 5399 // no match -> test for selection by integer index, then 5400 5401 if ( !ctype_digit( $value ) ) 5402 return $this->getLang( 'badenum' ); 5403 5404 $value = intval( $value ); 5405 5406 5407 if ( $def['format'] == 'related' ) 5408 { 5409 // integers select related by unique ID 5410 5411 if ( !array_key_exists( $value, $def['options']['selectables'] ) ) 5412 return $this->getLang( 'badenum' ); 5413 5414 } 5415 else if ( intval( $value ) > 0 ) 5416 { 5417 // integers select option by 1-based index 5418 5419 if ( intval( $value ) > count( $def['options']['selectables'] ) ) 5420 // index out of range 5421 return $this->getLang( 'badenum' ); 5422 5423 $value--; 5424 5425 } 5426 else 5427 // not matching any defined option's label or index 5428 return $this->getLang( 'badenum' ); 5429 5430 } 5431 } 5432 5433 break; 5434 5435 } 5436 5437 5438 5439 try 5440 { 5441 5442 // transform value to internally used format 5443 $value = $this->inputToInternal( $value, $def ); 5444 5445 5446 // enforce to get value if required 5447 if ( $def['options']['required'] ) 5448 if ( is_null( $value ) ) 5449 throw new Exception( $this->getLang( 'required' ) ); 5450 5451 5452 // save input value in editor's session 5453 $inStore = $value; 5454 5455 } 5456 catch ( Exception $e ) 5457 { 5458 return $e->getMessage(); 5459 } 5460 } 5461 5462 5463 public function inputToInternal( $value, $def ) 5464 { 5465 5466 switch ( $def['format'] ) 5467 { 5468 5469 case 'image' : 5470 case 'file' : 5471 if ( !is_bool( $value ) ) 5472 if ( !is_array( $value ) || ( trim( implode( '', $value ) ) === '' ) ) 5473 $value = null; 5474 5475 break; 5476 5477 case 'integer' : 5478 $value = trim( $value ); 5479 if ( $value === '' ) 5480 $value = null; 5481 else 5482 { 5483 5484 if ( $value !== '' ) 5485 if ( !preg_match( '/^[+-]?\d+$/', $value ) ) 5486 throw new Exception( $this->getLang( 'badinteger' ) ); 5487 5488 $value = intval( $value ); 5489 5490 } 5491 break; 5492 5493 case 'date' : 5494 $value = strtolower( trim( $value ) ); 5495 if ( $value === '' ) 5496 $value = null; 5497 else if ( in_array( $value, array( $this->getLang( 'today' ), $this->getLang( 'now' ) ) ) ) 5498 $value = time(); 5499 else 5500 { 5501 5502 $value = self::parseInternalDate( $value ); 5503 if ( $value === false ) 5504 throw new Exception( $this->getLang( 'baddate' ) ); 5505 5506 $value = mktime( 12, 0, 0, $value['month'], $value['day'], $value['year'] ); 5507 5508 } 5509 break; 5510 5511 case 'time' : 5512 $value = strtolower( trim( $value ) ); 5513 if ( $value !== '' ) 5514 { 5515 5516 if ( $value == $this->getLang( 'now' ) ) 5517 $value = date( 'H:i:s' ); 5518 else if ( !self::parseInternalTime( $value ) ) 5519 throw new Exception( $this->getLang( 'badtime' ) ); 5520 5521 } 5522 else 5523 $value = null; 5524 5525 break; 5526 5527 case 'datetime' : 5528 $value = strtolower( trim( $value ) ); 5529 if ( $value === '' ) 5530 $value = null; 5531 else if ( $value === $this->getLang( 'now' ) ) 5532 $value = time(); 5533 else 5534 { 5535 5536 list( $date, $time, $tail ) = preg_split( '/[\s,;]+/', $value ); 5537 5538 $date = trim( $date ); 5539 $time = trim( $time ); 5540 $tail = trim( $tail ); 5541 5542 if ( ( $date === '' ) || ( $time === '' ) ) 5543 throw new Exception( $this->getLang( 'baddatetime' ) ); 5544 if ( $tail !== '' ) 5545 throw new Exception( $this->getLang( 'baddatetimetail' ) ); 5546 5547 if ( trim( $date ) === $this->getLang( 'today' ) ) 5548 { 5549 $date = array( 5550 'year' => idate( 'Y' ), 5551 'month' => idate( 'm' ), 5552 'day' => idate( 'd' ), 5553 ); 5554 } 5555 else 5556 { 5557 $date = self::parseInternalDate( $date ); 5558 if ( $date === false ) 5559 throw new Exception( $this->getLang( 'baddate' ) ); 5560 } 5561 5562 $time = self::parseInternalTime( $time ); 5563 if ( $time === false ) 5564 throw new Exception( $this->getLang( 'badtime' ) ); 5565 5566 $value = mktime( $time['hour'], $time['minute'], 5567 $time['second'], $date['month'], 5568 $date['day'], $date['year'] ); 5569 5570 } 5571 break; 5572 5573 case 'phone' : 5574 case 'fax' : 5575 $value = trim( $value ); 5576 if ( $value !== '' ) 5577 { 5578 5579 $temp = preg_replace( '/\s+/', '', $value ); 5580 $temp = preg_replace( '/\(([^)]+)\)/', '\1', $temp ); 5581 5582 if ( !preg_match( '#^\+?(\d+(([-/]|/-)\d+)*)+$#', $temp ) ) 5583 throw new Exception( $this->getLang( 'badphonefax' ) ); 5584 5585 } 5586 else 5587 $value = null; 5588 5589 break; 5590 5591 case 'monetary' : 5592 $value = trim( $value ); 5593 if ( $value !== '' ) 5594 { 5595 5596 $valuePattern = '/[+-]?\d+([.,]\d)?/'; 5597 5598 if ( !preg_match( $valuePattern, $value ) ) 5599 throw new Exception( $this->getLang( 'badmoney' ) ); 5600 5601 // validate to have one out of these formats: 5602 // 0,34 or "USD 34,00" or "5 EUR" ... 5603 $temp = preg_split( $valuePattern, $value ); 5604 5605 if ( trim( $temp[1] ) === '' ) unset( $temp[1] ); 5606 if ( trim( $temp[0] ) === '' ) unset( $temp[0] ); 5607 5608 if ( count( $temp ) > 1 ) 5609 throw new Exception( $this->getLang( 'badmoneytail' ) ); 5610 5611 } 5612 else 5613 $value = null; 5614 5615 break; 5616 5617 case 'real' : 5618 $value = trim( $value ); 5619 if ( $value === '' ) 5620 $value = null; 5621 else 5622 { 5623 5624 if ( !preg_match( '/^[+-]?\d+([.,]\d+)?$/', $value ) ) 5625 throw new Exception( $this->getLang( 'badfloat' ) ); 5626 5627 $value = doubleval( strtr( $value, ',', '.' ) ); 5628 5629 } 5630 5631 break; 5632 5633 case 'url' : 5634 $value = trim( $value ); 5635 if ( $value !== '' ) 5636 { 5637 5638 $info = parse_url( $value ); 5639 if ( !is_array( $info ) ) 5640 throw new Exception( $this->getLang( 'badurl' ) ); 5641 if ( ( $value !== '' ) && !$info['scheme'] ) 5642 throw new Exception( $this->getLang( 'badurlnoabs' ) ); 5643 5644 } 5645 else 5646 $value = null; 5647 5648 break; 5649 5650 case 'email' : 5651 $value = trim( $value ); 5652 if ( $value !== '' ) 5653 { 5654 5655 if ( !mail_isvalid( $value ) ) 5656 throw new Exception( $this->getLang( 'badmail' ) ); 5657 5658 if ( $this->getConf( 'checkmaildomains' ) != false ) 5659 { 5660 5661 list( $box, $domain ) = explode( '@', $value ); 5662 $ip = gethostbyname( $domain ); 5663 if ( ( $ip === $domain ) || ( ip2long( $ip ) === false ) ) 5664 if ( !getmxrr( $domain, $dummy ) ) 5665 throw new Exception( $this->getLang( 'badmailunknown' ) ); 5666 5667 } 5668 } 5669 else 5670 $value = null; 5671 5672 break; 5673 5674 case 'acl' : 5675 // row-based ACL rule 5676 if ( $this->isAuthorized( $this->options['mayadmin'] ) ) 5677 $value = implode( ';', $this->parseACLRule( trim( $value ), true ) ); 5678 else 5679 $value = null; 5680 5681 break; 5682 5683 case 'text' : 5684 // everything's fine here 5685 5686 default : 5687 if ( trim( $value ) === '' ) 5688 $value = null; 5689 5690 } 5691 5692 return $value; 5693 5694 } 5695 5696 5697 /** 5698 * Renders field for single-record editor. 5699 * 5700 * To keep rendering form in a single place this method is called to render 5701 * code for opening ($rowid === true) and closing ($rowid === false) form, 5702 * as well. All other arguments contain no useful information then. 5703 * 5704 * On closing form two buttons should be included for saving and cancelling. 5705 * The buttons' names are ____save and ____cancel accordingly. 5706 * 5707 * @param integer $rowid ID of row containing given value 5708 * @param string $column name of column 5709 * @param mixed $value input value 5710 * @param array $def definition of column 5711 * @param string $error optional error message to be rendered next to field 5712 * @param boolean $readOnly if true, the field should be rendered read-only 5713 * @return string HTML code representing single form field 5714 */ 5715 5716 protected function renderField( $rowid, $column, $value, $def, $error, $readOnly, &$rowACL ) 5717 { 5718 5719 if ( $rowid === true ) 5720 { 5721 // opening form 5722 5723 return <<<EOT 5724<table class="database2-single-editor"> 5725 <tbody> 5726EOT; 5727 5728 } 5729 else if ( $rowid === false ) 5730 { 5731 // close form 5732 if ( $readOnly && $this->input['optnoctl'] ) 5733 { 5734 // caller requested to hide controls 5735 5736 $buttons = ''; 5737 5738 } 5739 else 5740 { 5741 5742 $buttons = array(); 5743 if ( !$readOnly ) 5744 $buttons[] = '<input type="submit" name="' . 5745 $this->varname( '____save' ) . 5746 '" value="' . $this->getLang( 'cmdsave' ) . '" />'; 5747 5748 if ( is_array( $column ) && count( $column ) && !$this->options['simplenav'] ) 5749 { 5750 5751 foreach ( $column as $k => $option ) 5752 $column[$k] = '<option value="' . $option[0] . '"' . 5753 ( $option[2] ? ' selected="selected"' : '' ) . 5754 '>' . $option[1] . '</option>'; 5755 5756 $mode = '<select name="' . $this->varname( '____nav' ) . 5757 '">' . implode( "\n", $column ) . '</select>'; 5758 5759 $name = $this->getLang( 'cmdokay' ); 5760 5761 } 5762 else 5763 { 5764 $mode = null; 5765 $name = $this->getLang( 'cmdreturn' ); 5766 } 5767 5768 5769 $buttons[] = '<input type="submit" name="' . 5770 $this->varname( '____cancel' ) . 5771 '" value="' . ( $readOnly ? $name : ( $mode ? $this->getLang( 'cmdnosave' ) : $this->getLang( 'cmdcancel' ) ) ) . 5772 '" />'; 5773 5774 if ( $mode ) 5775 $buttons[] = $mode; 5776 5777 5778 $buttons = implode( ' ', $buttons ); 5779 5780 $buttons = <<<EOT 5781 <tr class="buttons"> 5782 <td class="label"></td> 5783 <td class="field"> 5784 $buttons 5785 </td> 5786 </tr> 5787EOT; 5788 5789 } 5790 5791 return <<<EOT 5792 $buttons 5793 </tbody> 5794</table> 5795EOT; 5796 5797 } 5798 else if ( $def['options']['readonly'] || $readOnly ) 5799 { 5800 5801 $code = $this->renderValue( $rowid, $column, $value, $def, true, true, $rowACL ); 5802 if ( is_null( $code ) && ( !$def['options']['alwaysshow'] || 5803 ( $def['format'] == 'acl' ) ) ) 5804 return null; 5805 5806 $label = $def['label'] ? $def['label'] : $column; 5807 5808 return <<<EOT 5809 <tr> 5810 <td class="label">$label:</td> 5811 <td class="field">$code</td> 5812 </tr>$error 5813EOT; 5814 5815 } 5816 else 5817 { 5818 // single form element 5819 5820 $mayView = $this->isAuthorizedMulti( $rowACL, $def['options'], 'mayview', null, true ); 5821 5822 if ( $rowACL['mayedit'] || $def['options']['mayedit'] ) 5823 if ( !$this->isAuthorizedMulti( $rowACL, $def['options'], 'mayedit' ) ) 5824 return $this->renderField( $rowid, $column, $value, $def, '', true, $rowACL ); 5825 5826 // user may edit this column ... 5827 if ( !$mayView ) 5828 // ... but mustn't view it ... 5829 if ( $rowid ) 5830 // it's an existing record -> reject editing field 5831 return null; 5832 // ELSE: editing new records doesn't actually imply 5833 // viewing something existing in this field 5834 5835 5836 5837 $name = $this->varname( 'data' . $column ); 5838 $selector = ''; 5839 $code = null; 5840 5841 if ( $error ) 5842 $error = <<<EOT 5843 <tr class="local-error"> 5844 <td class="label"></td> 5845 <td class="field local-error"> 5846 $error 5847 </td> 5848 </tr> 5849EOT; 5850 5851 5852 switch ( $def['format'] ) 5853 { 5854 5855 case 'image' : 5856 case 'file' : 5857 if ( ( $value === true ) || ( $value === false ) ) 5858 $code = '<span class="disabled">' . $this->getLang( 'fileexternal' ) . '</span>'; 5859 else 5860 { 5861 5862 $current = $this->renderValue( $rowid, $column, $value, $def, true, true, $rowACL ); 5863 if ( !is_null( $current ) ) 5864 $current = "<input type=\"submit\" name=\"$name\" value=\"" . $this->getLang( 'cmddropfile' ) . "\" onclick=\"return confirm('" . strtr( $this->getLang( 'confirmdropfile' ), array( '"' => '"' ) ) . "');\"/></td></tr><tr><td class=\"label\" /><td class=\"field\">$current"; 5865 5866 $code = "<input type=\"file\" name=\"$name\" />$current"; 5867 5868 } 5869 break; 5870 5871 case 'acl' : 5872 if ( !$this->isAuthorized( $this->options['mayadmin'] ) ) 5873 return null; 5874 5875 case 'text' : 5876 if ( !isset( $def['options']['length'] ) || 5877 ( $def['options']['length'] > 255 ) ) 5878 { 5879 $value = strtr( $value, array( '<' => '<' ) ); 5880 $code = "<textarea name=\"$name\" class=\"text\" rows=\"5\" cols=\"50\">$value</textarea>"; 5881 } 5882 break; 5883 5884 case 'bool' : 5885 $checked = $value ? ' checked="checked"' : ''; 5886 $code = "<input name=\"$name\" type=\"checkbox\" value=\"1\"$checked />"; 5887 break; 5888 5889 case 'enum' : 5890 case 'related' : 5891 $options = $def['options']['selectables']; 5892 $selectedAny = false; 5893 5894 foreach ( $options as $index => $option ) 5895 { 5896 5897 $selected = ( $index === $value ) ? ' selected="selected"' : ''; 5898 $selectedAny |= $selected; 5899 5900 if ( $def['format'] == 'enum' ) 5901 $key = $index + 1; 5902 else 5903 $key = $index; 5904 5905 $options[$index] = "<option value=\"$key\"$selected>$option</option>"; 5906 5907 } 5908 5909 if ( !$def['options']['required'] || !$selectedAny ) 5910 array_unshift( $options, '<option value="">' . $this->getLang( 'enumunselected' ) . '</option>' ); 5911 5912 $options = implode( "\n", $options ); 5913 5914 $code = "<select name=\"$name\">$options</select>"; 5915 5916 break; 5917 5918 case 'date' : 5919 if ( !$value ) 5920 $value = ''; 5921 else if ( preg_match( '/-?\d+/', trim( $value ) ) ) 5922 $value = strftime( strtok( trim( $this->getConf( 'dformat' ) ), ' ' ), intval( $value ) ); 5923 break; 5924 5925 case 'datetime' : 5926 if ( !$value ) 5927 $value = ''; 5928 else if ( preg_match( '/-?\d+/', trim( $value ) ) ) 5929 $value = strftime( $this->getConf( 'dformat' ), intval( $value ) ); 5930 break; 5931 5932 case 'time' : 5933 5934 case 'url' : 5935 case 'email' : 5936 case 'phone' : 5937 case 'fax' : 5938 case 'monetary' : 5939 case 'real' : 5940 case 'integer' : 5941 default : 5942 5943 } 5944 5945 if ( is_null( $code ) ) 5946 { 5947 5948 $value = strtr( $value, array( '"' => '"' ) ); 5949 5950 5951 $defaultSizes = array( 5952 'date' => strlen( strftime( $this->getConf( 'dformat' ) ) ), 5953 'datetime' => strlen( strftime( $this->getConf( 'dformat' ) ) ), 5954 'time' => 8, 5955 'url' => 255, 5956 'email' => 255, 5957 'phone' => 16, 5958 'fax' => 16, 5959 'monetary' => 16, 5960 'real' => 16, 5961 'integer' => 10, 5962 ); 5963 5964 if ( $def['options']['length'] ) 5965 $maxlen = $def['options']['length']; 5966 else if ( $defaultSizes[$def['format']] ) 5967 $maxlen = $defaultSizes[$def['format']]; 5968 else 5969 $maxlen = 30; 5970 5971 $size = min( $maxlen, 50 ); 5972 $code = "<input name=\"$name\" type=\"text\" value=\"$value\" size=\"$size\" maxlength=\"$maxlen\" class=\"text $def[format]\" />$selector"; 5973 5974 } 5975 5976 5977 $label = $def['label'] ? $def['label'] : $column; 5978 5979 return <<<EOT 5980 <tr> 5981 <td class="label">$label:</td> 5982 <td class="field">$code</td> 5983 </tr>$error 5984EOT; 5985 5986 } 5987 5988 } 5989 5990 5991 /** 5992 * Renders internally formatted value for read-only output (e.g. on listing 5993 * table). 5994 * 5995 * @param integer $rowid ID of row containing given value 5996 * @param string $column name of column 5997 * @param mixed $in value returned from DB 5998 * @param string $type defined type of value 5999 * @param boolean $mayBeSkipped if true, the method should return null if 6000 * value is unset 6001 * @param boolean $inEditor if true, the value comes from an editor's 6002 * session (important for rendering attached 6003 * files/images) 6004 * @return string code describing value for read-only output 6005 */ 6006 6007 protected function renderValue( $rowid, $column, $value, $def, 6008 $mayBeSkipped = false, $inEditor = false, $rowACL = null ) 6009 { 6010 6011 if ( !$this->isAuthorizedMulti( $rowACL, $def['options'], 'mayview', null, true ) ) 6012 return $mayBeSkipped ? null : '<em>' . $this->getLang( 'hidden' ) . '</em>'; 6013 6014 if ( is_null( $value ) && ( $def['type'] != 'data' ) ) 6015 return $mayBeSkipped ? null : ''; 6016 6017 6018 switch( $def['format'] ) 6019 { 6020 6021 case 'text' : 6022 if ( ( trim( $value ) === '' ) && $mayBeSkipped ) 6023 return null; 6024 6025 if ( $this->options['wikimarkup'] || $def['options']['wikimarkup'] ) 6026 { 6027 // apply processing of contained wiki markup 6028 6029 $value = p_render( 'xhtml', p_get_instructions( $value ), $info ); 6030 6031 if ( preg_match( '#^<p>((.|\n)+)</p>$#i', trim( $value ), $matches ) ) 6032 $value = $matches[1]; 6033 6034 } 6035 6036 // strip some special tags from text (preventing some XSS attacks) 6037 $value = $this->stripTags( $value ); 6038 6039 return trim( $value ); 6040 6041 case 'image' : 6042 case 'file' : 6043 if ( $value === true ) 6044 return '<span class="info">' . $this->getLang( 'fileexternalfound' ) . '</span>'; 6045 else if ( $value === false ) 6046 return '<span class="info">' . $this->getLang( 'fileexternalnotfound' ) . '</span>'; 6047 else 6048 { 6049 6050 $temp = is_array( $value ) ? trim( implode( '', $value ) ) 6051 : ''; 6052 if ( $temp === '' ) 6053 return $mayBeSkipped ? null : '<em>' . $this->getLang( 'none' ) . '</em>'; 6054 6055 if ( $inEditor ) 6056 $url = $this->editorSessionMediaLink( $column, ( $def['format'] != 'image' ) ); 6057 else 6058 $url = $this->mediaLink( $rowid, $column, ( $def['format'] != 'image' ) ); 6059 6060 if ( !$inEditor ) 6061 $url .= '&thumb=150'; 6062 6063 if ( $def['format'] === 'image' ) 6064 return "<img src=\"$url\" alt=\"" . sprintf( $this->getLang( 'fileimagealt' ), $column, $value['mime'] ) . "\" />"; 6065 6066 $mayDownload = $this->isAuthorizedMulti( $rowACL, $this->options, 'maydownload' ); 6067 if ( !$mayDownload ) 6068 return '<em>' . $this->getLang( 'filedenied' ) . '</em>'; 6069 6070 return "<a href=\"$url\" title=\"" . 6071 $this->getLang( 'filedlhint' ) . '">' . 6072 $this->getLang( 'cmddl' ) . '</a>'; 6073 6074 } 6075 break; 6076 6077 case 'email' : 6078 if ( ( trim( $value ) === '' ) && $mayBeSkipped ) 6079 return null; 6080 6081 return DokuWiki_Plugin::email( $value, $email ); 6082 6083 case 'url' : 6084 if ( ( trim( $value ) === '' ) && $mayBeSkipped ) 6085 return null; 6086 6087 return DokuWiki_Plugin::external_link( $value ); 6088 6089 case 'phone' : 6090 case 'fax' : 6091 if ( ( trim( $value ) === '' ) && $mayBeSkipped ) 6092 return null; 6093 6094 return $value; 6095 6096 case 'bool' : 6097 if ( !$value && $mayBeSkipped ) 6098 return null; 6099 return $value ? '✗' : '–'; 6100 6101 case 'date' : 6102 if ( !$value ) 6103 return $mayBeSkipped ? null : ''; 6104 6105 return strftime( strtok( trim( $this->getConf( 'dformat' ) ), ' ' ), $value ); 6106 6107 case 'datetime' : 6108 if ( !$value ) 6109 return $mayBeSkipped ? null : ''; 6110 6111 return strftime( $this->getConf( 'dformat' ), $value ); 6112 6113 case 'time' : 6114 if ( $value && ( substr( $value, -3 ) == ':00' ) && 6115 ( strlen( $value ) > 5 ) ) 6116 $value = substr( $value, 0, -3 ); 6117 6118 return $value; 6119 6120 case 'integer' : 6121 case 'monetary' : 6122 case 'real' : 6123 if ( !$value && $mayBeSkipped ) 6124 return null; 6125 6126 return $value; 6127 6128 case 'enum' : 6129 case 'related' : 6130 if ( is_integer( $value ) ) 6131 $value = $def['options']['selectables'][$value]; 6132 6133 return $value; 6134 6135 case 'acl' : 6136 if ( !$this->isAuthorized( $this->options['mayadmin'] ) ) 6137 // admins may see this value, only 6138 return null; 6139 6140 return $value; 6141 6142 default : 6143 return $value; 6144 6145 } 6146 } 6147} 6148 6149 6150class Database2_Admin extends Database2 6151{ 6152 6153 public function __construct( DokuWiki_Admin_Plugin $integrator ) 6154 { 6155 $this->renderer = ''; 6156 $this->integrator = $integrator; 6157 } 6158 6159 6160 protected function getIndex() 6161 { 6162 return 0; 6163 } 6164 6165 6166 public function getColumnsMeta( $ignoreMissingMeta = false ) 6167 { 6168 return array(); 6169 } 6170 6171 6172 protected function render( $code ) 6173 { 6174 $this->renderer .= $code; 6175 } 6176 6177 6178 public function getCode() 6179 { 6180 return $this->renderer; 6181 } 6182 6183 6184 public function getDB() 6185 { 6186 return $this->db; 6187 } 6188} 6189 6190// vim:ts=4:sw=4:et: 6191