* @version 0.2 * @copyright GPLv2 * */ if ( !defined( 'DB2_PATH' ) ) define( 'DB2_PATH', 'lib/plugins/database2/' ); /** * Implementation of database access feature. * * This class is integrated into DokuWiki syntax extension in file syntax.php * and provides the plugin's actual features like database interaction, table * browsing, data lookup and input ... * * @author Thomas Urban * @version 0.2 * @copyright GPLv2 * */ class Database2 { const maxSurroundingPagesCount = 3; const maxUploadSize = 2097152; // 2 MiB protected static $ioIndices = array(); /** * Renderer for producing any output. * * @var Doku_Renderer_xhtml */ public $renderer; /** * Link to database of current namespace. * * @var PDO */ protected $db; /** * Name of driver indicating type of DB currently connected to. * * @var string */ private $driver; /** * Name of table managed by current instance. * * @var string */ protected $table = null; /** * Meta information on/definition of current table as provided in tag value * * @var array */ protected $meta = array(); /** * Options additionally provided in opening tag's attributes. * * @var array */ protected $options = array(); /** * I/O-Index assigned for use on acting on table managed by current * instance. * * @var integer */ protected $ioIndex = null; /** * input data * * @var array */ private $input = null; /** * DSN used to connect to database server * * @var string */ private $dsn = null; /** * Name of slot in site configuration containing authentication data. * * @var string */ private $authSlot = null; /** * Reference to database syntax plugin object integrating this instance of * Database2. * * @var DokuWiki_Syntax_Plugin */ protected $integrator = null; /** * Page ID explicitly selected to use in current instance. * * This is used to supercede ID returned by DokuWiki's getID() and is * required in media.php using this class in a faked context. */ protected $explicitPageID = null; public function __construct( Doku_Renderer $renderer, DokuWiki_Syntax_Plugin $integrator ) { $this->renderer = $renderer; $this->db = null; $this->integrator = $integrator; $this->renderer->nocache(); } /** * Detects whether provided name is a valid name for a table or column. * * @param string $in name to test * @return boolean true if provided name might be used for tables/columns */ public static function isValidName( $in ) { return preg_match( '/^[_a-z][_a-z0-9]+$/i', $in ); } /** * Retrieves open link to current database file or null if not connected. * * @return PDO */ public function getLink() { return $this->db; } /** * Retrieves configuration setting using integrator's interface. * * @param string $name name of setting to retrieve * @return mixed retrieved configuration setting */ public function getConf( $name ) { global $conf; if ( $this->integrator instanceof DokuWiki_Syntax_Plugin ) { $value = $this->integrator->getConf( $name, null ); if ( is_null( $value ) ) if ( !is_null( $conf[$name] ) ) $value = $conf[$name]; return $value; } // fix for accessing configuration in media.php if ( isset( $conf['plugin']['database2'][$name] ) ) return $conf['plugin']['database2'][$name]; return $conf[$name]; } /** * Retrieves localized string. * * @param string $name name of localized string * @return mixed retrieved localized string */ public function getLang( $name ) { if ( $this->integrator instanceof DokuWiki_Syntax_Plugin ) return $this->integrator->getLang( $name ); // fix for accessing strings in media.php if ( !is_array( $this->integrator ) ) { $lang = array(); @include( dirname( __FILE__ ) . '/lang/en/lang.php' ); if ( $GLOBALS['conf']['lang'] != 'en' ) @include( dirname( __FILE__ ) . '/lang/' . $GLOBALS['conf']['lang'] . '/lang.php' ); $this->integrator = $lang; } return $this->integrator[$name]; } /** * Retrieves index to be used for parameters/fields passed in I/O on * currently processed integration of a table in current page. * * The current method is quite unstable, at least on editing arrangement * of database2 instances in a page, however it should work in a production * environment. * * @return integer numeric index */ protected function getIndex() { if ( is_null( $this->table ) ) throw new Exception( 'getIndex: missing name of managed table' ); if ( is_null( $this->ioIndex ) ) $this->ioIndex = self::$ioIndices[$tableName]++; return $this->ioIndex; } /** * Retrieves ID of current DokuWiki page. * * @return string */ protected function getPageID() { if ( !is_null( $this->explicitPageID ) ) return $this->explicitPageID; return getID(); } /** * Allocates separate section in session data for state of current * table instance. * * @return array */ protected function &getSession() { if ( !is_array( $_SESSION['database2'] ) ) $_SESSION['database2'] = array(); $id = $this->getPageID(); // if current page's source has changed ... $dates = p_get_metadata( $id, 'date' ); if ( is_array( $_SESSION['database2'][$id] ) ) if ( $_SESSION['database2'][$id]['onRevision'] != $dates['modified'] ) // ... it's related session-based data is dropped unset( $_SESSION['database2'][$id] ); if ( !is_array( $_SESSION['database2'][$id] ) ) $_SESSION['database2'][$id] = array( 'onRevision' => $dates['modified'], 'tables' => array(), ); $index = $this->getIndex(); if ( !is_array( $_SESSION['database2'][$id]['tables'][$index] ) ) $_SESSION['database2'][$id]['tables'][$index] = array(); return $_SESSION['database2'][$id]['tables'][$index]; } /** * Allocates separate section in session data for temporary content of * single-record editor. * * @return array */ protected function &getEditorSession() { $session =& $this->getSession(); if ( !is_array( $session['editors'] ) ) $session['editors'] = array(); return $session['editors']; } /** * Renders provided HTML code replacing database tag in current Wiki page. * * @param string $code HTML code to render */ protected function render( $code ) { $this->renderer->doc .= strval( $code ); } /** * Connects to database (external server, local SQLite DB file). * * @param string $dbPath database selector * @param string $authConfigSlot name of slot in site config containing * authentication data * @return boolean true on success, false on failure */ public function connect( $dbPath, $authConfigSlot = null ) { $dbPath = trim( $dbPath ); if ( $dbPath[0] == '@' ) $dsn = substr( $dbPath, 1 ); else if ( ( $dbPath[0] == '/' ) && !self::getConf( 'useslash' ) && is_dir( dirname( $dbPath ) ) && !preg_match( '#(\.\.)|(^\/(etc)\/)#', $dbPath ) ) $dsn = 'sqlite:' . $dbPath; else $dsn = 'sqlite:' . metaFN( $dbPath, '.db' ); try { // read username/password for authentication from optionally // selected slot in site's configuration if ( $authConfigSlot ) { $username = $password = ''; foreach ( explode( "\n", $this->getConf( 'authSlots' ) ) as $line ) { $line = trim( $line ); if ( ( $line[0] == '#' ) || ( ( $line[0] == '/' ) && ( $line[1] == '/' ) ) || ( $line === '' ) ) // skip comments and empty lines continue; // parse assignment $pos = 0; $temp = self::parseAssignment( $line, $pos ); if ( !is_array( $temp ) ) continue; list( $name, $value ) = $temp; if ( strcasecmp( $name, $authConfigSlot ) ) // not related to current authentication slot continue; // split value into username and password $value = trim( $value ); $sep = strcspn( $value, ':' ); $username = trim( substr( $value, 0, $sep ) ); $password = trim( substr( $value, $sep + 1 ) ); // done ... break; } if ( $username === '' ) unset( $username, $password ); else if ( $password === '' ) unset( $password ); } else unset( $username, $password ); // connect to database $this->db = new PDO( $dsn, $username, $password ); // request throwing exceptions on failure $this->db->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // cache used driver name $this->driver = strtolower( trim( $this->db->getAttribute( PDO::ATTR_DRIVER_NAME ) ) ); if ( strpos( $this->driver, 'mysql' ) !== false ) // ensure to use proper encoding on talking to MySQL RDBMSs // NOTE: according to server setup this may result in UTF-8 bytes // being UTF-8 encoded, thus resulting in usual "garbage" // TODO: add option for selecting whether using UTF8-mapping here $this->db->query( 'SET NAMES UTF8' ); // store how to connect to database for integrated retrieval of files $this->dsn = $dsn; $this->authSlot = $authConfigSlot; return true; } catch ( PDOException $e ) { $this->render( sprintf( $this->getLang( 'nodblink' ), $e->getMessage() ) ); $this->db = $this->driver = null; return false; } } /** * Reads all available input data extracting values related to this plugin. * * @return array set of input data related to current plugin */ protected function getInput() { if ( is_null( $this->input ) ) { $index = $this->getIndex(); $this->input = array(); $matchingSecTok = ( $_REQUEST['sectok'] == getSecurityToken() ); foreach ( $_REQUEST as $name => $value ) if ( preg_match( '/^db2do(.+?)(_[xy])?$/i', $name, $matches ) ) if ( $matchingSecTok || ( $_GET[$name] && preg_match( '/^(cmd|opt)/i', $matches[1] ) ) ) if ( is_null( $this->input[$matches[1]] ) ) if ( !is_array( $value ) || !is_null( $value[$index] ) ) $this->input[$matches[1]] = is_array( $value ) ? $value[$index] : $value; } return $this->input; } /** * Retrieves meta information on columns. * * @return array */ public function getColumnsMeta( $ignoreMissingMeta = false ) { if ( !is_array( $this->meta ) || empty( $this->meta ) ) { $session =& self::getSession(); if ( is_array( $session['definition'] ) ) $this->meta = $session['definition']; else if ( $ignoreMissingMeta ) return array(); // don't store permanently ... else throw new Exception( $this->getLang( 'nocolmeta' ) ); } return $this->meta; } /** * Renders name of form element to conform with input parser above. * * @param string $name internal name of element * @param index $rowid optional rowid element is related to * @return string external name of form element */ protected function varname( $name, $rowid = null ) { return 'db2do' . $name . ( $rowid ? $rowid : '' ) . '[' . $this->getIndex() . ']'; } /** * Processes occurrence of database tag in a wiki page. * * The tag's value (stuff between opening and closing tag) is passed in * $code. * * @param string $table name of table to work with * @param string $code code found between opening and closing tag * @param array $options additional options provided in tag attributes */ public function process( $table, $code, $options ) { // wrap all action on database in one exception handler try { // check whether or not database tags are enabled on this page if ( !$this->getConf( 'enableallpages' ) ) { $patterns = explode( "\n", trim( $this->getConf( 'enablepages' ) ) ); $enabled = false; $pageID = $this->getPageID(); foreach ( $patterns as $pattern ) { $pattern = trim( $pattern ); if ( preg_match( '#^/.+/\w*$#', $pattern ) ) $match = preg_match( $pattern, $pageID ); else $match = fnmatch( $pattern, $pageID ); if ( $match ) { $enabled = true; break; } } if ( !$enabled ) { // use of database tag is disabled $this->render( '
' . $this->getLang( 'tagdisabled' ) . '
' ); return; } } // normalize/validate table name $table = preg_replace( '/[^\w]/', '_', trim( $table ) ); if ( in_array( $table, array( '__keys', '__locks', '__log', ) ) ) throw new Exception( $this->getLang( 'restabnames' ) ); $this->table = $table; // select subset of input parameters $this->ioIndex = null; // drop to re-obtain new I/O index next $index = $this->getIndex(); // but ensure to obtain at all ... // install set of options $this->options = is_array( $options ) ? $options : array(); if ( trim( $this->options['mayview'] ) === '' ) $this->options['mayview'] = '@ALL'; if ( trim( $this->options['mayinspect'] ) === '' ) $this->options['mayinspect'] = '@ALL'; $this->options['view'] = trim( $this->options['view'] ); if ( !$this->getConf( 'customviews' ) ) $this->options['view'] = ''; else if ( !preg_match( '/^SELECT\s/i', trim( $this->options['view'] ) ) ) $this->options['view'] = ''; $this->options['wikimarkup'] = self::asBool( $this->options['wikimarkup'] ); $this->options['simplenav'] = self::asBool( $this->options['simplenav'] ); if ( ctype_digit( trim( $this->options['rowsperpage'] ) ) ) { $state =& $this->getSession(); if ( !is_integer( $state['num'] ) ) $state['num'] = intval( $this->options['rowsperpage'] ); } // parse code for contained definitions $this->parseDefinition( $code ); try { // look for available action to perform on selected table // support preventing CSRF ... foreach ( $this->getInput() as $key => $dummy ) { if ( !preg_match( '/^cmd([a-z]+)(\d*)(_x)?$/i', $key, $matches ) ) continue; $action = strtolower( trim( $matches[1] ) ); $rowid = intval( $matches[2] ); if ( $rowid ) $rowACL = $this->getRowACL( $rowid ); else $rowid = $rowACL = null; if ( $action === 'reset' ) { $state =& $this->getSession(); $state = array(); continue; } if ( !$this->isAuthorizedMulti( $rowACL, $this->options, 'may'.$action ) ) { // user isn't authorized to perform this action $this->render( '
' . sprintf( $this->getLang( 'accessdenied' ), $action, $this->table ) . '
' ); continue; } if ( ( $this->getSingleNumericPrimaryKey() !== false ) || in_array( $action, array( 'drop', ) ) ) // perform optionally requested action switch ( $action ) { case 'inspect' :// show record details (read-only) case 'insert' : // show record editor to insert case 'edit' : // show record editor to adjust do { if ( ( $action == 'insert' ) && $rowid ) { // insert record starting with duplicate // of existing one ... if ( !$this->isAuthorizedMulti( $rowACL, $this->options, 'mayinspect' ) ) { // user isn't authorized to perform this action $this->render( '
' . sprintf( $this->getLang( 'accessdenied' ), $action, $this->table ) . '
' ); break; } // use duplicate of selected record $duplicateRowID = $rowid; // but don't overwrite it! $rowid = null; } else $duplicateRowID = null; // invoke editor/single record view $readonly = ( $action == 'inspect' ); $result = $this->editRecord( $rowid, $readonly, $duplicateRowID, $rowACL ); if ( !$result ) // skip rendering table, rendered single return; if ( is_integer( $result ) ) { // switch to selected record $rowid = $result; continue; } break; } while ( true ); break; case 'delete' : $this->deleteRecord( $rowid ); break; case 'drop' : $this->dropTable(); break; default : $method = array( &$this, '__handle_'.$action ); if ( is_callable( $method ) ) if ( !call_user_func( $method, $rowid ) ) return; } } if ( !$this->exists( $this->table ) ) // (re-)create table as it is missing (e.g. after dropping) $this->createTable(); /* * finally render table */ // check user's authorization to view table if ( $this->isAuthorized( $this->options['mayview'] ) ) // user may view table $this->showTable( true, false, false, $this->options['view'] ); } catch ( PDOException $e ) { throw new Exception( sprintf( $this->getLang( 'badinteraction' ), $e->getMessage(), $e->getLine() ) ); } } catch ( Exception $e ) { $this->render( '
' . sprintf( $this->getLang( 'deferror' ), $e->getMessage(), $e->getLine(), $e->getFile() ) . '
' ); $resetCmd = $this->varname( 'cmdreset' ); $viewCmd = $this->varname( 'view' ); $btnSession = $this->getLang( 'btnResetSession' ); $btnTable = $this->getLang( 'btnViewTable' ); $this->render( $this->wrapInForm( << EOT ) ); } } protected function getACLCol() { foreach ( $this->meta as $name => $def ) if ( $def['isColumn'] && ( $def['format'] == 'acl' ) ) return $name; return null; } protected function getRowACL( $rowid ) { $session =& $this->getSession(); if ( !is_array( $session['rowACLs'] ) ) $session['rowACLs'] = array(); if ( !isset( $session['rowACLs'][$rowid] ) ) { $aclName = $this->getACLCol(); $idColumn = $this->getSingleNumericPrimaryKey(); if ( $aclName && $idColumn ) { $sql = sprintf( 'SELECT %s FROM %s WHERE %s=?', $aclName, $this->table, $idColumn ); $st = $this->db->prepare( $sql ); if ( !$st ) throw new PDOException( $this->getLang( 'aclprepare' ) ); if ( !$st->execute( array( $rowid ) ) ) throw new PDOException( $this->getLang( 'aclexecute' ) ); $row = $st->fetch( PDO::FETCH_NUM ); $st->closeCursor(); $session['rowACLs'][$rowid] = trim( $row[0] ); } } return $session['rowACLs'][$rowid] ? $session['rowACLs'][$rowid] : null; } protected function dropRowACL( $rowid ) { $session =& $this->getSession(); if ( $session['rowACLs'][$rowid] ) unset( $session['rowACLs'] ); } /** * Creates managed table on demand. * */ protected function createTable() { if ( empty( $this->meta ) ) throw new Exception( $this->getLang( 'defmissing' ) ); // extract all column definitions $cols = array_map( create_function( '$a','return $a[definition];' ), $this->meta ); // compile CREATE TABLE-statement using linebreaks as some versions // of SQLite engines cache it for schema representation, thus improving // human-readability ... $sql = "CREATE TABLE {$this->table}\n(\n\t" . implode( ",\n\t", $cols ). "\n)"; if ( $this->db->query( $sql ) === false ) throw new PDOException( sprintf( $this->getLang( 'nocreatetable' ), $this->table ) ); $this->log( 'create', $this->table ); } /** * Renders single record for editing (or inspecting if $readOnly is true). * * @param integer $rowid unique numeric ID of record to edit/inspect * @param boolean $readOnly if true, the record is rendered read-only * @param integer $duplicateOf unique numeric ID of record to duplicate * @return boolean if true, the table/list shouldn't be rendered */ protected function editRecord( $rowid, $readOnly, $duplicateOf = null, $rowACL = null ) { $ioIndex = $this->getIndex(); $input = $this->getInput(); $idColumn = $this->getSingleNumericPrimaryKey(); $isNew = !$rowid || $duplicateOf; /** * Obtain lock for exclusively accessing selected record */ if ( $rowid && !$readOnly && !$this->obtainLock( $this->table, $rowid ) ) { $this->render( '
' . $this->getLang( 'reclocked' ) . '
' ); return true; } /* * prepare session to contain data specific to this editor */ $state =& $this->getSession(); $store =& $this->getEditorSession(); $errors = array(); /* * process input data updating record and handle contained commands */ if ( $input && ( $input['____single'] === md5( $rowid ) ) ) { // select result to return depending on selected navigation mode if ( $input['____nav'] ) { if ( $input['____nav'][0] == 'P' ) $state['nav'] = 'previous'; else $state['nav'] = 'next'; $result = intval( substr( $input['____nav'], 1 ) ); if ( !$result ) $result = true; } else { unset( $state['nav'] ); $result = true; } if ( $input['____cancel'] ) { // cancel editing record if ( $rowid && !$readOnly && !$this->releaseLock( $this->table, $rowid ) ) $this->render( '
' . $this->getLang( 'editnorelease' ) . '
' ); // drop content of current editor session $store = array(); return $result; } /* * validate input data and store in session */ if ( !$readOnly ) foreach ( $this->meta as $column => $def ) if ( $def['isColumn'] && ( $column != $idColumn ) ) { $mayEdit = !$def['options']['mayedit'] || $this->isAuthorizedMulti( $rowACL, $def['options'], 'mayedit' ); if ( !$mayEdit ) continue; // user may edit this column ... $mayView = !$def['options']['mayview'] || $this->isAuthorizedMulti( $rowACL, $def['options'], 'mayview' ); if ( !$mayView ) // ... but mustn't view it ... if ( $rowid ) // it's an existing record -> reject editing field continue; // ELSE: editing new records doesn't actually imply // viewing something existing in this field $error = $this->checkValue( $rowid, $column, $input['data'.$column], $store[$column], $def ); if ( $error && $column ) { // something's wrong, but if it's a typo it's better // user may change his previous input next rather // than starting it all over again ... // --> store even malformed input in editor session $store[$column] = $input['data'.$column]; $errors[$column] = $error; } } if ( !$readOnly && empty( $errors ) && $input['____save'] ) { /* * write changed record to database */ if ( !$this->db->beginTransaction() ) $this->render( '
' . $this->getLang( 'editnotransact' ) . '
' ); else try { // convert record to be written to database next $record = array(); foreach ( $store as $column => $value ) if ( $column !== $idColumn ) if ( $this->meta[$column]['isColumn'] && !is_string( $this->meta[$column]['options']['aliasing'] ) ) { $value = $this->valueToDB( $rowid, $column, $value, $this->meta[$column] ); if ( $value !== false ) $record[$column] = $value; } if ( $isNew ) { if ( !( $record[$idColumn] = $this->nextID( $this->table, true ) ) ) throw new PDOException( $this->getLang( 'editnoid' ) ); $sql = sprintf( 'INSERT INTO %s (%s) VALUES (%s)', $this->table, implode( ',', array_keys( $record ) ), implode( ',', array_pad( array(), count( $record ), '?' ) ) ); $log = array( $record[$idColumn], 'insert' ); } else { $assignments = array(); foreach ( array_keys( $record ) as $column ) $assignments[] = $column . '=?'; $sql = sprintf( 'UPDATE %s SET %s WHERE %s=?', $this->table, implode( ',', $assignments ), $idColumn ); $record[$idColumn] = $rowid; $log = array( $rowid, 'update' ); } $st = $this->db->prepare( $sql ); if ( !$st ) throw new PDOException( $this->getLang( 'editprepare' ) ); if ( !$st->execute( array_values( $record ) ) ) throw new PDOException( $this->getLang( 'editexecute' ) ); $this->log( $log[1], $this->table, $log[0] ); if ( !$this->db->commit() ) throw new PDOException( $this->getLang( 'editcommit' ) ); /* * release lock on record */ if ( $rowid && !$readOnly && !$this->releaseLock( $this->table, $rowid, true ) ) throw new PDOException( $this->getLang( 'editnorelease' ) ); $store = array(); return $result; } catch ( PDOException $e ) { $this->render( '
' . sprintf( $this->getLang( 'editcantsave' ), $e->getMessage() ) . '
' ); if ( !$this->db->rollBack() ) $this->render( '
' . $this->getLang( 'editrollback' ) . '
' ); } } } else { /* * editor started ... load from DB or initialize */ if ( $isNew ) if ( $readOnly ) return; if ( $isNew && !$duplicateOf ) { $store = array(); foreach ( $this->meta as $column => $def ) if ( $def['isColumn'] && ( $column != $idColumn ) ) $store[$column] = $this->getInitialValue( $column, $def ); } else { // load record from table $cols = $this->__columnsList( false ); $cols = implode( ',', $cols ); // - get raw record if ( $this->options['view'] ) $sql = sprintf( '%s WHERE %s=?', $this->options['view'], $idColumn ); else $sql = sprintf( 'SELECT %s FROM %s WHERE %s=?', $cols, $this->table, $idColumn ); $st = $this->db->prepare( $sql ); if ( !$st ) throw new PDOException( $this->getLang( 'editloadprepare' ) ); if ( !$st->execute( array( $duplicateOf ? $duplicateOf : $rowid ) ) ) throw new PDOException( $this->getLang( 'editloadexecute' ) ); $record = $st->fetch( PDO::FETCH_ASSOC ); if ( !is_array( $record ) || empty( $record ) ) throw new PDOException( $this->getLang( 'notarecord' ) ); $st->closeCursor(); // drop contained ID column unset( $record[$idColumn] ); // on duplicating record reset some of the original record's // values current user isn't authorized to view foreach ( $record as $name => $value ) if ( !$this->isAuthorizedMulti( $rowACL, $this->meta[$name]['options'], 'mayview', null, true ) ) // user mustn't view this value of original record // --> reset to defined default value $record[$name] = $this->getInitialValue( $column, $def ); // transfer to temporary storage converting accordingly $store = $this->__sortRecord( $record ); // convert values from DB format to internal one foreach ( $store as $column => $value ) $store[$column] = $this->valueFromDB( $rowid, $column, $value, $this->meta[$column] ); } } /* * prepare to support navigation */ $nav = array(); if ( !$isNew ) { if ( !is_integer( $input['____idx'] ) ) $input['____idx'] = $this->recordI2X( $rowid ); if ( $input['____idx'] ) $nav[] = array( 'P' . $this->recordX2I( $input['____idx'] - 1 ), $this->getLang( 'navprevious' ), ( $state['nav'] === 'previous' ) ); $nextID = $this->recordX2I( $input['____idx'] + 1 ); if ( $nextID ) $nav[] = array( 'N' . $nextID, $this->getLang( 'navnext' ), ( $state['nav'] === 'next' ) ); if ( count( $nav ) ) array_unshift( $nav, array( 0, $this->getLang( 'navreturn' ) ) ); } if ( empty( $nav ) ) $nav = null; /* * Render single editor */ // compile form $elements = array(); $elements[] = $this->renderField( true, null, null, array(), null, $readOnly, $rowACL ); foreach ( $store as $column => $value ) $elements[] = $this->renderField( $rowid, $column, $value, $this->meta[$column], $errors[$column], $readOnly, $rowACL ); $elements[] = $this->renderField( false, $nav, null, array(), null, $readOnly, $rowACL ); if ( $readOnly && $rowid ) $cmdName = 'inspect' . $rowid; else if ( $rowid ) $cmdName = 'edit' . $rowid; else $cmdName = 'insert0'; // ensure to come back here on submitting form data $this->render( $this->wrapInForm( implode( '', $elements ), array( $this->varname( 'cmd' . $cmdName ) => '1', $this->varname( '____single' ) => md5( $rowid ), $this->varname( '____idx' ) => $input['____idx'], ), self::maxUploadSize, true ) ); // return and mark to prevent rendering data list return false; } /** * Sorts record according to tabindex order provided in definition. * * @param array $record unsorted record * @return array sorted record */ private function __sortRecord( $record ) { $in = $record; $index = array(); foreach ( $in as $column => $value ) { $tabindex = $this->meta[$column]['options']['tabindex']; if ( $tabindex > 0 ) { $index[$column] = intval( $tabindex ); unset( $in[$column] ); } } foreach ( $in as $column => $value ) $index[$column] = empty( $index ) ? 1 : ( max( $index ) + 1 ); // sort columns according to explicit/implicit tabindex asort( $index ); // sort record according to that index $out = array(); foreach ( $index as $column => $dummy ) if ( $this->meta[$column]['isColumn'] ) $out[$column] = $record[$column]; return $out; } /** * Deletes selected record from managed table. * * @param integer $rowid ID of row to delete */ protected function deleteRecord( $rowid ) { if ( !$rowid || !ctype_digit( trim( $rowid ) ) ) throw new Exception( $this->getLang( 'notarecord' ) ); if ( !$this->db->beginTransaction() ) throw new PDOException( $this->getLang( 'notransact' ) ); try { if ( !$this->obtainLock( $this->table, $rowid, true, true ) ) { $this->render( '
' . $this->getLang( 'reclocked' ) . '
' ); $this->db->rollback(); return true; } $idColumn = $this->getSingleNumericPrimaryKey(); $st = $this->db->prepare( 'DELETE FROM ' . $this->table . ' WHERE '. $idColumn . '=?' ); if ( !$st ) throw new PDOException( $this->getLang( 'delprepare' ) ); if ( !$st->execute( array( $rowid ) ) ) throw new PDOException( $this->getLang( 'delexecute' ) ); $this->log( 'delete', $this->table, $rowid ); if ( !$this->db->commit() ) throw new PDOException( $this->getLang( 'delcommit' ) ); } catch ( PDOException $e ) { $this->db->rollback(); throw $e; } } /** * Drops whole table. * */ protected function dropTable() { if ( !$this->db->beginTransaction() ) throw new PDOException( $this->getLang( 'notransact' ) ); try { if ( !$this->obtainLock( $this->table, null, true, true ) ) { $this->render( '
' . $this->getLang( 'tablelocked' ) . '
' ); $this->db->rollback(); return true; } if ( $this->db->query( 'DROP TABLE ' . $this->table ) === false ) throw new PDOException( sprintf( $this->getLang( 'nodrop' ), $this->table ) ); $this->log( 'drop', $this->table ); if ( !$this->db->commit() ) throw new PDOException( $this->getLang( 'dropcommit' ) ); } catch ( PDOException $e ) { $this->db->rollback(); throw $e; } } /** * Retrieves list of columns (optionally reduced to the set marked as * visible) in current table. * * @param boolean $visibleOnly if true return visible columns, only * @param boolean $printable if true, obey marks on columns being printable * @return array list of columns' names */ protected function __columnsList( $visibleOnly = true, $printable = false ) { $meta = $this->getColumnsMeta(); $idCol = $this->getSingleNumericPrimaryKey(); $cols = array(); if ( $visibleOnly ) foreach ( $meta as $colName => $def ) if ( $def['isColumn'] ) { if ( $def['options']['visible'] === 1 ) $use = true; else if ( $printable ) $use = $def['options']['print'] || ( $def['options']['visible'] && !$def['options']['noprint'] ); else $use = $def['options']['visible']; if ( $use ) $cols[] = $colName; } if ( !$visibleOnly || empty( $cols ) ) foreach ( $meta as $colName => $def ) if ( $def['isColumn'] ) $cols[] = $colName; foreach ( $cols as $index => $name ) if ( is_string( $meta[$name]['options']['aliasing'] ) ) $cols[$index] = $meta[$name]['options']['aliasing'] . ' AS ' . $name; if ( $idCol ) array_unshift( $cols, $idCol ); return $cols; } /** * Retrieves count of records in table obeying current state of filter. * * @return integer number of available records in table matching filter */ protected function __recordsCount( $customQuery = null ) { // get compiled filter list( $filter, $parameters ) = $this->getFilter(); $customQuery = trim( $customQuery ); if ( $customQuery === '' ) $query = "SELECT COUNT(*) FROM {$this->table}"; else { $query = preg_replace( '/^SELECT .+ (FROM .+)$/i', 'SELECT COUNT(*) \1', $customQuery ); if ( stripos( $query, ' WHERE ' ) !== false ) { $filter = ''; $parameters = array(); } } $st = $this->db->prepare( $query . $filter ); if ( !$st ) throw new PDOException( $this->getLang( 'countprepare' ) ); if ( !$st->execute( $parameters ) ) throw new PDOException( $this->getLang( 'countexecute' ) ); $count = $st->fetch( PDO::FETCH_NUM ); $st->closeCursor(); return intval( array_shift( $count ) ); } /** * Retrieves records from table. * * @param array/string $columns list of records to retrieve, "*" for all * @param boolean $obeyFilter if true, current state of filter is used to * retrieve matching records, only * @param string $sortColumn name of column to optionally sort by * @param boolean $sortAscendingly if true, request to sort ascendingly * @param integer $offset optional number of matching records to skip * @param integer $limit maximum number of matching records to retrieve * @return array excerpt of matching records according to given parameters */ public function __recordsList( $columns = '*', $obeyFilter = true, // $sortColumn = null, $sortAscendingly = true, $offset = 0, $limit = null, $customQuery = null ) { $config = $this->__configureSelect(); if ( is_array( $config ) ) { list( $filter, $parameters, $order, $cols ) = $config; } else { /* // prepare filter if ( $obeyFilter ) list( $filter, $parameters ) = $this->getFilter(); else { $filter = ''; $parameters = array(); } // prepare sorting if ( $sortColumn ) if ( !$this->meta[$sortColumn]['isColumn'] ) $sortColumn = null; if ( $sortColumn ) $order = ' ORDER BY ' . $sortColumn . ( $sortAscendingly ? ' ASC' : ' DESC' ); else $order = ''; */ $cols = array(); $filter = ''; $parameters = array(); $order = ''; } // prepare limits if ( ( $offset > 0 ) || ( $limit > 0 ) ) { $limit = ' LIMIT ' . ( ( $limit > 0 ) ? $limit : '10' ); if ( $offset > 0 ) $limit .= ' OFFSET ' . $offset; } // prepare columns selected for retrieval if ( is_array( $columns ) ) $cols = array_merge( $columns, $cols ); else $cols[] = $columns; $columns = implode( ',', $cols ); if ( trim( $customQuery ) === '' ) $query = 'SELECT ' . $columns . ' FROM ' . $this->table; else { $query = $customQuery; if ( stripos( $query, ' WHERE ' ) !== false ) { $filter = ''; $parameters = array(); } } // query for records returning whole resultset $st = $this->db->prepare( $query . $filter . $order . $limit ); if ( !$st ) throw new PDOException( $this->getLang( 'listprepare' ) ); if ( !$st->execute( $parameters ) ) throw new PDOException( $this->getLang( 'listexecute' ) ); return $st->fetchAll( PDO::FETCH_ASSOC ); } /** * Renders (excerpt of) managed table. * * @param boolean $expectInput if true, input is processed and controls * (filter+commands) are rendered * @param boolean $returnOutput if true, rendered table is returned rather * than enqueued for rendering in DokuWiki page * @param boolean $listAll if true, paging is disabled and all available/ * matching records are rendered * @param string $customQuery a custom query to use instead of managed one */ protected function showTable( $expectInput = true, $returnOutput = false, $listAll = false, $customQuery = null, $isPrintVersion = false ) { $customQuery = trim( $customQuery ); $meta = $this->getColumnsMeta( ( $customQuery !== '' ) ); /* * count all matching records in table */ $count = $this->__recordsCount( $customQuery ); /* * update view state according to available input data */ $state =& $this->getSession(); if ( trim( $state['sort'] ) === '' ) $state['sort'] = $this->options['sort']; $updated = array( 'skip' => intval( $state['skip'] ), 'num' => intval( $state['num'] ), 'sort' => trim( $state['sort'] ), ); if ( $expectInput ) { $input = $this->getInput(); foreach ( $input as $name => $dummy ) if ( preg_match( '/^(skip|num|sort)(.+)$/i', $name, $m ) ) { $name = strtolower( $m[1] ); $updated[$name] = ( $name == 'sort' ) ? trim( $m[2] ) : intval( $m[2] ); } } // keep values in range $updated['num'] = max( 10, $updated['num'] ); $updated['skip'] = max( 0, min( $count - $updated['num'], $updated['skip'] ) ); // save updated view state in session $state = array_merge( $state, $updated ); // load view state values for easier access extract( $updated ); /* * prepare information on requested sorting */ /* if ( $sort ) { $sortDescendingly = ( $sort[0] == '!' ); $sortCol = $sortDescendingly ? strtok( substr( $sort, 1 ), ',' ) : $sort; $sortCol = trim( $sortCol ); } */ /* * query to list matching records */ if ( $listAll ) unset( $skip, $num ); $cols = $this->__columnsList( true, $isPrintVersion ); $rows = $this->__recordsList( $cols, true, $skip, $num, $customQuery ); $idCol = $this->getSingleNumericPrimaryKey(); $code = $this->__renderTable( $idCol, $cols, $rows, $count, $num, $skip, $sort, $meta, $expectInput, $listAll ); if ( $returnOutput ) return $code; $this->render( $code ); } public function __renderTable( $idCol, $cols, $rows, $count, $num, $skip, $sort, $meta, $expectInput, $listAll ) { // required to check whether $idCol column has to be rendered or not $visibleIDCol = null; foreach ( $meta as $colName => $def ) if ( $def['isColumn'] ) { if ( is_null( $visibleIDCol ) && $def['options']['visible'] ) $visibleIDCol = ( $colName == $idCol ); else if ( $colName == $idCol ) $visibleIDCol = ( $def['options']['visible'] != false ); } if ( is_null( $visibleIDCol ) ) $visibleIDCol = $meta[$idCol] && !$meta[$idCol]['auto_id']; /* * - collect header row according to listed rows * - transform all listed rows to properly defined HTML table cells */ $headers = array(); $counter = $skip; foreach ( $rows as $nr => $row ) { // get record's rowid $rowid = $idCol ? intval( $row[$idCol] ) : 0; if ( !$visibleIDCol ) unset( $row[$idCol] ); // convert all values in current row to table cells $i = 0; if ( $this->options['aclColumn'] ) { $rowACL = $row[$this->options['aclColumn']]; if ( $meta[$this->options['aclColumn']]['options']['visible'] === 1 ) unset( $row[$this->options['aclColumn']] ); } else $rowACL = null; $clicks = array(); foreach ( $row as $column => $value ) { if ( !is_array( $meta[$column] ) ) $meta[$column] = array( 'readonly' => true, 'isColumn' => true, 'format' => 'text', 'label' => $column, ); $def = $meta[$column]; $headers[$column] = $def['label'] ? $def['label'] : $column; $class = $def['format'] ? $def['format'] : 'na'; $class .= ' col' . ++$i; $value = $this->valueFromDB( $rowid, $column, $value, $def ); $cell = $this->renderValue( $rowid, $column, $value, $def, false, false, $rowACL ); switch ( $clickAction = $def['options']['onclick'] ) { case 'edit' : if ( $this->options['view'] || !$this->isAuthorizedMulti( $rowACL, $this->options, 'may' . $clickAction ) ) $clickAction = 'inspect'; case 'inspect' : if ( $this->isAuthorizedMulti( $rowACL, $this->options, 'may' . $clickAction ) ) { $cell = '' . $cell . ''; $clicks[] = $clickAction; } break; default : $cell = $this->convertToLink( $clickAction, $cell, array( 'value' => $cell ) ); } $row[$column] = "" . $cell . "\n"; } // prepend cell for counter array_unshift( $row, ''.++$counter."\n" ); if ( $expectInput ) // append cell for row-related commands $row[] = '' . $this->getRecordCommands( $rowid, $rowACL, $clicks ) . ''; // convert set of values into HTML table row $classes = array(); if ( $nr == 0 ) $classes[] = 'first'; if ( $nr == count( $rows ) - 1 ) $classes[] = 'last'; $classes[] = ( $nr % 2 ) ? 'even' : 'odd'; $classes[] = 'row' . ( $nr + 1 ); $classes = implode( ' ', $classes ); $rows[$nr] = ''. implode( '', $row ) . "\n"; } // finally convert all HTML table rows into single HTML table body $rows = implode( '', $rows ); /* * compile header row */ // ensure to have row of headers (missing on an empty list of rows) if ( empty( $headers ) ) foreach ( $cols as $column ) { unset( $def ); if ( is_array( $meta[$column] ) ) $def = $meta[$column]; else { // missing meta information on current "column name" // --> might be an alias definition // --> extract originally selected column name from that $pos = strripos( $column, ' AS ' ); if ( $pos !== false ) { $temp = substr( $column, $pos + 4 ); if ( $meta[$temp] ) { // found definition on extracted column name $def = $meta[$temp]; $column = $temp; } } } $headers[$column] = $def['label'] ? $def['label'] : $column; } // next transform headers into table header cells including proper // controls for sorting etc. $sortDescendingly = ( $sort[0] == '!' ); if ( $sortDescendingly ) $sort = substr( $sort, 1 ); $sort = trim( strtok( $sort, ',' ) ); foreach ( $headers as $column => $label ) { if ( $meta[$column]['options']['headerlabel'] ) $label = trim( $meta[$column]['options']['headerlabel'] ); if ( ( $href = trim( $meta[$column]['options']['headerlink'] ) ) !== '' ) $label = $this->convertToLink( $href, $label ); if ( ( $sort == $column ) && $sortDescendingly ) { $name = $column; $icon = 'down'; $title = $this->getLang( 'hintsortasc' ); } else if ( $sort == $column ) { $name = '!' . $column; $icon = 'up'; $title = $this->getLang( 'hintsortdesc' ); } else { $name = $column; $icon = 'none'; $title = $this->getLang( 'hintsortasc' ); } if ( $expectInput ) $sorter = " "; else $sorter = ''; $headers[$column] = "$label$sorter\n"; } // compile row of header cells $headers = implode( '', $headers ); /* * check for available filter */ if ( $this->isAuthorized( $this->options['mayfilter'] ) && $expectInput ) { $filter = $this->renderFilter(); if ( $filter != '' ) $filter = '' . $filter . ''; } else $filter = ''; /* * compile pager */ list( $flipDown, $flipUp, $pages, $sizes, $stat ) = $this->getPagerElements( $skip, $num, $count ); if ( !$expectInput ) unset( $sizes ); $sepStat = $sizes ? ' — ' . $stat : $stat; /* * retrieve all available commands operating on whole table */ $globalCmds = $expectInput ? $this->getGlobalCommands() : ''; /* * render list of rows as HTML table */ $width = intval( $this->options['width'] ) ? ' width="' . $this->options['width'] . '"' : ''; $cmdHeader = $expectInput ? '' : ''; $trClass = $this->options['wikistyle'] ? '' : ' class="data-list"'; $tableClass = $this->options['wikistyle'] ? ' class="inline"' : ''; $table = << $headers $cmdHeader $rows $sizes$sepStat EOT; if ( $expectInput || !$listAll ) $table = << $filter $flipDown $pages $flipUp $table $globalCmds EOT; else $table = << $table EOT; return $expectInput ? $this->wrapInForm( $table ) : $table; } public function __csvLine( $fields ) { foreach ( $fields as &$field ) $field = '"' . strtr( $field, array( '"' => '""' ) ) . '"'; return implode( ';', $fields ) . "\n"; } protected function button( $name, $label ) { $args = func_get_args(); $args = array_filter( array_slice( $args, 2 ), create_function( '$a', 'return trim($a)!=="";' ) ); $disabled = in_array( 'disabled', $args ) ? ' disabled="disabled"' : ''; if ( !$icon ) $args[] = 'pure-text'; $classes = implode( ' ', $args ); return ''; } protected function imgbutton( $name, $label, $icon ) { $args = func_get_args(); $args = array_filter( array_slice( $args, 3 ), create_function( '$a', 'return trim($a)!=="";' ) ); $disabled = in_array( 'disabled', $args ) ? ' disabled="disabled"' : ''; $classes = implode( ' ', $args ); return ''; } /** * Compiles elements for flipping/selecting page and number of records per * page in listing table according to current context. * * @param integer $skip number of records to skip on listing * @param integer $num number of records to list per page at most * @param integer $count number of records in table * @return array five-element array containing buttons for flipping down, * flipping up, selecting page, selecting number of records * per page and for showing number of records and pages. */ protected function getPagerElements( $skip, $num, $count ) { // build list of skip-values for all pages $skips = array(); if ( $num > 0 ) { for ( $i = $skip; $i > 0; $i -= $num ) array_unshift( $skips, $i ); array_unshift( $skips, 0 ); for ( $i = $skip + $num; $i < $count - $num; $i += $num ) array_push( $skips, $i ); if ( $i < $count ) array_push( $skips, $count - $num ); } // detect index of currently visible page $page = array_search( $skip, $skips ); $minPage = max( 0, $page - self::maxSurroundingPagesCount ); $maxPage = min( count( $skips ), $page + self::maxSurroundingPagesCount ); /* * compile pager elements for ... */ if ( count( $skips ) <= 1 ) $backward = $forward = $pages = ''; else { // ... flipping down $backward = $pages = $forward = array(); if ( $page > 1 ) $backward[] = $this->imgbutton( 'skip0', $this->getLang( 'hintflipfirst' ), 'first' ); if ( $page > 0 ) $backward[] = $this->imgbutton( 'skip' . $skips[$page-1], $this->getLang( 'hintflipprevious' ), 'previous' ); $backward = implode( "\n", $backward ); // ... switching to near page for ( $i = $minPage; $i < $maxPage; $i++ ) $pages[] = $this->button( 'skip' . $skips[$i], $i + 1, ( $i == $page ? 'selected' : '' ) ); if ( $minPage > 0 ) array_unshift( $pages, '...' ); if ( $maxPage < count( $skips ) - 1 ) array_push( $pages, '...' ); $pages = implode( "\n", $pages ); // ... flipping up if ( $page < count( $skips ) - 1 ) $forward[] = $this->imgbutton( 'skip' . $skips[$page+1], $this->getLang( 'hintflipnext' ), 'next' ); if ( $page < count( $skips ) - 2 ) $forward[] = $this->imgbutton( 'skip' . ( $count - $num ), $this->getLang( 'hintfliplast' ), 'last' ); $forward = implode( "\n", $forward ); } // ... showing number of records/pages if ( $count === 1 ) $stat = sprintf( $this->getLang( 'recnumsingle' ), $count ); else if ( count( $skips ) <= 1 ) $stat = sprintf( $this->getLang( 'recnummulti' ), $count ); else $stat = sprintf( $this->getLang( 'recnummultipage' ), $count, count( $skips ) ); // ... selecting number of records per page foreach ( array( 10, 20, 50, 100, 200 ) as $size ) { $sizes[] = $this->button( 'num' . $size, $size, ( $size == $num ? 'selected' : '' ) ); if ( $size >= $count ) break; } $sizes = count( $sizes ) == 1 ? '' : implode( "\n", $sizes ); // return set of pager elements to caller return array( $backward, $forward, $pages, $sizes, $stat ); } /** * Wraps provided HTML code in a form sending form data to current wiki * page. * * @param string $code HTML code to embed * @param array $hiddens set of hidden values to be added * @param integer $maxUploadSize maximum size of supported uploads in bytes * @param boolean $isSingle set true on calling to render single-record editor * @return string */ protected function wrapInForm( $code, $hiddens = null, $maxUploadSize = null, $isSingle = false ) { include_once( DOKU_INC . 'inc/form.php' ); ob_start(); $id = 'database2_' . ( $isSingle ? 'single' : 'table' ) . '_' . $this->table . '_' . $this->getIndex(); if ( $maxUploadSize > 0 ) { $form = new Doku_Form( $id, false, 'POST', 'multipart/form-data' ); $form->addHidden( 'MAX_FILE_SIZE', intval( $maxUploadSize ) ); } else $form = new Doku_Form( $id ); $form->addHidden( 'id', $this->getPageID() ); if ( is_array( $hiddens ) ) foreach ( $hiddens as $name => $value ) $form->addHidden( $name, $value ); $form->addElement( $code ); $form->printForm(); return ob_get_clean(); } /** * Checks if current user is authorized according to given rule. * * The rule is a comma-separated list of usernames and groups (after * preceeding @ character), e.g. * * admin,@user * * authorizing user admin and every user in group "user". * * @param string $rule rule describing authorizations * @return boolean true if current user is authorized, false otherwise */ protected function isAuthorized( $rule ) { global $USERINFO; if ( auth_isadmin() ) return true; if ( $rule ) { $granted = true; foreach ( explode( ',', $rule ) as $role ) { $role = trim( $role ); if ( $role === '' ) continue; if ( !strcasecmp( $role, '@ALL' ) ) return true; if ( !strcasecmp( $role, '@NONE' ) ) return false; if ( $_SERVER['REMOTE_USER'] ) { if ( $role[0] == '!' ) { $role = substr( $role, 1 ); $match = false; } else $match = true; if ( $role[0] == '@' ) { if ( in_array( substr( $role, 1 ), $USERINFO['grps'] ) ) { if ( $match && $granted ) return true; if ( !$match ) $granted = false; } } else if ( $role == $_SERVER['REMOTE_USER'] ) { if ( $match && $granted ) return true; if ( !$match ) $granted = false; } } } } return false; } /** * Takes ACL rule set related to a single row and related to whole table * testing them either of them preferring the first one for authorizing * current user to do one of up to two sorts of access preferring the first * one again. * * @param string|array $rowACL row-related ACL rule set * @param array $tableACL table-related ACL rule set, used if $rowACL isn't * managing any of the given sorts of access * @param string $ruleName preferred sort of access to authorize for * @param string $optRuleName optional fallback sort of access if first * isn't managed in selected rule set * @param boolean $defaultGrant true to select granting access if neither * rule set is managing any selected sort of access * @return boolean true if user is authorized, false otherwise */ protected function isAuthorizedMulti( &$rowACL, $tableACL, $ruleName, $optRuleName = null, $defaultGrant = false ) { if ( is_string( $rowACL ) ) $rowACL = $this->parseACLRule( $rowACL, false, true ); // use row-related rule set if it's managing any given sort of access if ( $rowACL[$ruleName] ) $rule = $rowACL[$ruleName]; else if ( $rowACL[$optRuleName] ) $rule = $rowACL[$optRuleName]; else $rule = null; if ( $rule ) return $this->isAuthorized( $rule ); // use table-related rule set if it's managin any given sort of access if ( $tableACL[$ruleName] ) $rule = $tableACL[$ruleName]; else if ( $tableACL[$optRuleName] ) $rule = $tableACL[$optRuleName]; else // neither of them is managing given sorts of access // --> grant or revoke access depending on fifth argument if ( $defaultGrant ) return true; else $rule = ''; return $this->isAuthorized( $rule ); } /** * Gets HTML code of global commands current user is authorized to invoke. * * @return string HTML code */ protected function getGlobalCommands() { if ( $this->options['view'] ) return ''; $globalCmds = array( 'insert' => array( 'add', $this->getLang( 'cmdadd' ) ), 'drop' => array( 'drop', $this->getLang( 'cmddrop' ), true ), 'print' => array( 'print', $this->getLang( 'cmdprint' ), 'print', true, 'print' ), 'export.csv' => array( 'exportcsv', $this->getLang( 'cmdcsv' ), 'csv' ), // 'export.xml' => array( 'exportxml', $this->getLang( 'cmdxml' ), 'xml' ), 'viewlog' => array( 'exportlog', $this->getLang( 'cmdlog' ), 'log' ), ); if ( !$this->getSingleNumericPrimaryKey() ) unset( $globalCmds['insert'] ); foreach ( $globalCmds as $name => $def ) if ( $this->isAuthorized( $this->options['may'.($authz=strtok( $name, '.' ))] ) ) { if ( is_string( $def[2] ) ) { $href = $this->attachmentLink( $def[2], $authz, !$def[3] ); $globalCmds[$name] = '' .
										 $def[1] . ''; } else $globalCmds[$name] = 'getLang( 'confirmdrop' ) . "');" : '' ) . '" />'; } else unset( $globalCmds[$name] ); return implode( "\n", $globalCmds ); } /** * Gets HTML code of commands related to selected record current user is * authorized to invoke. * * @param integer $rowid ID of record * @return string HTML code */ protected function getRecordCommands( $rowid, $rowACL = null, $clickActions = null ) { $rowid = intval( $rowid ); if ( !$rowid ) // don't provide record management on complex/non-integer // primary keys return ''; if ( !$this->getSingleNumericPrimaryKey() ) return ''; $recordCmds = array( 'inspect' => array( 'view', $this->getLang( 'cmdview' ) ), 'edit' => array( 'edit', $this->getLang( 'cmdedit' ) ), 'insert' => array( 'copy', $this->getLang( 'cmdcopy' ) ), // 'insert' => array( 'insert', $this->getLang( 'cmdinsert' ) ), 'delete' => array( 'delete', $this->getLang( 'cmddelete' ), true ), ); if ( $this->options['view'] ) // it's a read-only view thus exclude commands for adjusting data unset( $recordCmds['edit'], $recordCmds['insert'], $recordCmds['delete'] ); if ( !is_array( $clickActions ) ) $clickActions = array(); foreach ( $recordCmds as $name => $def ) if ( $this->isAuthorizedMulti( $rowACL, $this->options, 'may' . $name ) ) { $idName = $this->varname( 'cmd' . $name, $rowid ); $class = ( !in_array( $name, $clickActions ) || $this->options['addonclick'] ) ? '' : ' hidden'; $recordCmds[$name] = 'getLang( 'confirmdelete' ) . "');" : '' ) . '" />'; } else unset( $recordCmds[$name] ); return implode( "\n", $recordCmds ); } protected function __configureSelect() { $idCol = $this->getSingleNumericPrimaryKey(); if ( $idCol === false ) return false; $cols = array( $idCol ); // prepare filter list( $filter, $parameters ) = $this->getFilter(); // prepare sorting $state =& $this->getSession(); $sort = preg_split( '/[,\s]+/', trim( $state['sort'] ) ); foreach ( $sort as $key => $desc ) { $dir = ( $desc[0] == '!' ); $col = $dir ? substr( $desc, 1 ) : $desc; $col = trim( $col ); if ( $this->meta[$col]['isColumn'] ) { if ( is_string( $this->meta[$col]['options']['aliasing'] ) ) $cols[] = $this->meta[$col]['options']['aliasing'] . ' AS ' . $col; else $cols[] = $col; $sort[$key] = $col . ( $dir ? ' DESC' : ' ASC' ); } else unset( $sort[$key] ); } $order = count( $sort ) ? ' ORDER BY ' . implode( ', ', $sort ) : ''; return array( $filter, $parameters, $order, $cols ); } protected function recordI2X( $rowid ) { $config = $this->__configureSelect(); if ( !is_array( $config ) ) return false; list( $filter, $parameters, $order, $cols ) = $config; // query for records returning whole resultset $st = $this->db->prepare( 'SELECT ' . implode( ',', $cols ) . ' FROM ' . $this->table . $filter . $order ); if ( !$st ) throw new PDOException( $this->getLang( 'listprepare' ) ); if ( !$st->execute( $parameters ) ) throw new PDOException( $this->getLang( 'listexecute' ) ); $index = 0; while ( ( $record = $st->fetch( PDO::FETCH_NUM ) ) !== false ) if ( $record[0] == $rowid ) { $st->closeCursor(); return $index; } else $index++; return null; } protected function recordX2I( $index ) { if ( !is_integer( $index ) || ( $index < 0 ) ) return false; $config = $this->__configureSelect(); if ( !is_array( $config ) ) return false; list( $filter, $parameters, $order, $cols ) = $config; // query for records returning whole resultset $st = $this->db->prepare( 'SELECT ' . implode( ',', $cols ) . ' FROM ' . $this->table . $filter . $order . ' LIMIT 1 OFFSET ' . $index ); if ( !$st ) throw new PDOException( $this->getLang( 'listprepare' ) ); if ( !$st->execute( $parameters ) ) throw new PDOException( $this->getLang( 'listexecute' ) ); $record = $st->fetch( PDO::FETCH_NUM ); if ( is_array( $record ) && count( $record ) ) return intval( $record[0] ); return null; } /** * Parses provided string for filter description consisting of one or more * components. * * @param string $in string containing filter definition * @return array list of successfully parsed filter components */ protected function parseFilterCode( $in ) { $in = trim( $in ); $out = array(); $prevMode = false; while ( $in !== '' ) { if ( preg_match( '/^(\w+)\s+(\w+)(.*)$/i', $in, $matches ) ) { // extract argument to current filter rule $tail = trim( $matches[3] ); if ( ( $tail[0] == '"' ) || ( $tail[0] == "'" ) ) { // argument is enclosed in quotes $pos = 0; $argument = $this->parseString( $tail, $pos ); } else { // argument take everything up to next space or separator $pos = strcspn( $tail, " \r\n\t\f&|" ); if ( $pos ) $argument = trim( substr( $tail, 0, $pos ) ); else $argument = ''; } $new = array( 'col' => $matches[1], 'op' => $matches[2], 'arg' => $this->replaceMarkup( $argument ), ); if ( $prevMode === '&' ) $new['mode'] = 'AND'; else if ( $prevMode === '|' ) $new['mode'] = 'OR'; else if ( $prevMode !== false ) // invalid pattern separator --> break parsing filter code break; $out[] = $new; $in = ltrim( substr( $tail, $pos ) ); } // invalid filter element --> break parsing filter code else break; $prevMode = $in[0]; $in = substr( $in, 1 ); } return $out; } /** * Manages current state of table's filter in session processing optional * modifications in current input data. * * @return array description of current state of filter */ protected function getFilterInput() { $input = $this->getInput(); $session =& $this->getSession(); if ( $input['searchdrop'] ) $session['search'] = array(); else { if ( !is_array( $session['search'] ) ) { $session['search'] = array(); if ( $this->options['basefilter'] ) // initialize filter using provided code return ( $session['search'] = $this->parseFilterCode( $this->options['basefilter'] ) ); } // parse filter input and transfer it to session foreach ( $input as $key => $value ) if ( preg_match( '/^search(col|op|arg)(\d*)$/', $key, $matches ) ) { $index = intval( $matches[2] ); if ( !is_array( $session['search'][$index] ) ) $session['search'][$index] = array(); $session['search'][$index][$matches[1]] = $value; } // drop incomplete filter components foreach ( $session['search'] as $index => $filter ) if ( !is_string( $filter['col'] ) || !is_string( $filter['op'] ) || !is_string( $filter['arg'] ) ) unset( $session['search'][$index] ); else if ( !in_array( $filter['mode'], array( 'AND', 'OR' ) ) ) if ( $index ) unset( $session['search'][$index] ); } return $session['search']; } /** * Gets WHERE clause and contained parameters to filter records in table. * * @return array two-element array, SQL-WHERE-clause with initial WHERE and * array with all values to be bound as parameters to clause */ protected function getFilter() { $filters = $this->getFilterInput(); $opMap = array( 'like' => ' %s ( %s like ? )', 'nlike' => ' %s ( %s not like ? )', 'lt' => ' %s ( %s < ? )', 'eq' => ' %s ( %s = ? )', 'gt' => ' %s ( %s > ? )', 'ne' => ' %s ( %s <> ? )', 'le' => ' %s ( %s <= ? )', 'ge' => ' %s ( %s >= ? )', 'isset' => ' %s ( ( %2$s = ? ) AND %2$s IS NOT NULL )', 'isclear' => ' %s ( ( %2$s <> ? ) OR %2$s IS NULL )', ); $meta = $this->getColumnsMeta(); $out = array( '', null ); foreach ( $filters as $index => $filter ) { $mode = ( $out[0] !== '' ) ? $filter['mode'] : 'WHERE'; $column = $meta[$filter['col']]; if ( $column && $column['isColumn'] ) { // 1) filter operates on valid column if ( $column['type'] == 'bool' ) { if ( in_array( $filter['op'], array( 'like', 'eq', 'le', 'ge', 'isset' ) ) ) $filter['op'] = 'isset'; else $filter['op'] = 'isclear'; switch ( $column['options']['booltype'] ) { case 'xmark' : $argument = 'x'; break; case 'yesno' : $argument = 'y'; break; case 'int' : $argument = '1'; break; } } else $argument = trim( $filter['arg'] ); if ( $argument !== '' ) // 2) filter operates with non-empty argument if ( $opMap[$filter['op']] ) { // 3) filter uses valid operation // ----> include it. if ( in_array( $filter['op'], array( 'like', 'nlike' ) ) ) if ( strpos( $argument, '%' ) === false ) $argument = '%' . $argument . '%'; $out[0] .= sprintf( $opMap[$filter['op']], $mode, $filter['col'] ); if ( is_array( $out[1] ) ) $out[1][] = $argument; else $out[1] = array( $argument ); } } } return $out; } /** * Renders filter on current table. * * @return string HTML-code representing filter on selected table */ protected function renderFilter() { /* * prepare entries for selecting column */ $meta = $this->getColumnsMeta(); $columns = array(); $mapType = array( 'integer' => 'numeric', 'real' => 'numeric', 'decimal' => 'numeric', 'text' => 'text', 'date' => 'date', 'enum' => 'enum', 'bool' => 'bool', ); $allVisible = true; foreach ( $meta as $column => $def ) if ( !is_null( $def['options']['visible'] ) || !is_null( $def['options']['filter'] ) ) { $allVisible = false; break; } foreach ( $meta as $column => $def ) if ( $def['isColumn'] && ( $allVisible || $def['options']['visible'] || $def['options']['filter'] ) ) { if ( $def['format'] == 'acl' ) continue; $class = $mapType[$def['type']]; if ( !$class ) continue; $label = $def['label'] ? $def['label'] : $column; $label = strtr( $label, array( '<' => '<' ) ); $head = "