       1: <?php
       2: /**
       3:  * DataTables PHP libraries.
       4:  *
       5:  * PHP libraries for DataTables and DataTables Editor, utilising PHP 5.3+.
       6:  *
       7:  *  @author    SpryMedia
       8:  *  @version   __VERSION__
       9:  *  @copyright 2012 SpryMedia ( http://sprymedia.co.uk )
      10:  *  @license   http://editor.datatables.net/license DataTables Editor
      11:  *  @link      http://editor.datatables.net
      12:  */
      13:
      14: namespace DataTables;
      15: if (!defined('DATATABLES')) exit();
      16:
      17: use
      18:     DataTables,
      19:     DataTables\Editor\Join,
      20:     DataTables\Editor\Field;
      21:
      22:
      23: /**
      24:  * DataTables Editor base class for creating editable tables.
      25:  *
      26:  * Editor class instances are capable of servicing all of the requests that
      27:  * DataTables and Editor will make from the client-side - specifically:
      28:  *
      29:  * * Get data
      30:  * * Create new record
      31:  * * Edit existing record
      32:  * * Delete existing records
      33:  *
      34:  * The Editor instance is configured with information regarding the
      35:  * database table fields that you wish to make editable, and other information
      36:  * needed to read and write to the database (table name for example!).
      37:  *
      38:  * This documentation is very much focused on describing the API presented
      39:  * by these DataTables Editor classes. For a more general overview of how
      40:  * the Editor class is used, and how to install Editor on your server, please
      41:  * refer to the {@link https://editor.datatables.net/manual Editor manual}.
      42:  *
      43:  *  @example
      44:  *    A very basic example of using Editor to create a table with four fields.
      45:  *    This is all that is needed on the server-side to create a editable
      46:  *    table - the {@link process} method determines what action DataTables /
      47:  *    Editor is requesting from the server-side and will correctly action it.
      48:  *    <code>
      49:  *      Editor::inst( $db, 'browsers' )
      50:  *          ->fields(
      51:  *              Field::inst( 'first_name' )->validator( Validate::required() ),
      52:  *              Field::inst( 'last_name' )->validator( Validate::required() ),
      53:  *              Field::inst( 'country' ),
      54:  *              Field::inst( 'details' )
      55:  *          )
      56:  *          ->process( $_POST )
      57:  *          ->json();
      58:  *    </code>
      59:  */
      60: class Editor extends Ext {
      61:     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
      62:      * Statics
      63:      */
      64:
      65:     /** Request type - read */
      66:     const ACTION_READ = 'read';
      67:
      68:     /** Request type - create */
      69:     const ACTION_CREATE = 'create';
      70:
      71:     /** Request type - edit */
      72:     const ACTION_EDIT = 'edit';
      73:
      74:     /** Request type - delete */
      75:     const ACTION_DELETE = 'remove';
      76:
      77:     /** Request type - upload */
      78:     const ACTION_UPLOAD = 'upload';
      79:
      80:
      81:     /**
      82:      * Determine the request type from an HTTP request.
      83:      *
      84:      * @param array $http Typically $_POST, but can be any array used to carry
      85:      *   an Editor payload
      86:      * @return string `Editor::ACTION_READ`, `Editor::ACTION_CREATE`,
      87:      *   `Editor::ACTION_EDIT` or `Editor::ACTION_DELETE` indicating the request
      88:      *   type.
      89:      */
      90:     static public function action ( $http )
      91:     {
      92:         if ( ! isset( $http['action'] ) ) {
      93:             return self::ACTION_READ;
      94:         }
      95:
      96:         switch ( $http['action'] ) {
      97:             case 'create':
      98:                 return self::ACTION_CREATE;
      99:
     100:             case 'edit':
     101:                 return self::ACTION_EDIT;
     102:
     103:             case 'remove':
     104:                 return self::ACTION_DELETE;
     105:
     106:             case 'upload':
     107:                 return self::ACTION_UPLOAD;
     108:
     109:             default:
     110:                 throw new \Exception("Unknown Editor action: ".$http['action']);
     111:         }
     112:     }
     113:
     114:
     115:     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     116:      * Constructor
     117:      */
     118:
     119:     /**
     120:      * Constructor.
     121:      *  @param Database $db An instance of the DataTables Database class that we can
     122:      *    use for the DB connection. Can be given here or with the 'db' method.
     123:      *    <code>
     124:      *      456
     125:      *    </code>
     126:      *  @param string|array $table The table name in the database to read and write
     127:      *    information from and to. Can be given here or with the 'table' method.
     128:      *  @param string|array $pkey Primary key column name in the table given in
     129:      *    the $table parameter. Can be given here or with the 'pkey' method.
     130:      */
     131:     function __construct( $db=null, $table=null, $pkey=null )
     132:     {
     133:         // Set constructor parameters using the API - note that the get/set will
     134:         // ignore null values if they are used (i.e. not passed in)
     135:         $this->db( $db );
     136:         $this->table( $table );
     137:         $this->pkey( $pkey );
     138:     }
     139:
     140:
     141:     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     142:      * Public properties
     143:      */
     144:
     145:     /** @var string */
     146:     public $version = '1.9.0';
     147:
     148:
     149:
     150:     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     151:      * Private properties
     152:      */
     153:
     154:     /** @var DataTables\Database */
     155:     private $_db = null;
     156:
     157:     /** @var DataTables\Editor\Field[] */
     158:     private $_fields = array();
     159:
     160:     /** @var array */
     161:     private $_formData;
     162:
     163:     /** @var array */
     164:     private $_processData;
     165:
     166:     /** @var string */
     167:     private $_idPrefix = 'row_';
     168:
     169:     /** @var DataTables\Editor\Join[] */
     170:     private $_join = array();
     171:
     172:     /** @var array */
     173:     private $_pkey = array('id');
     174:
     175:     /** @var string[] */
     176:     private $_table = array();
     177:
     178:     /** @var string[] */
     179:     private $_readTableNames = array();
     180:
     181:     /** @var boolean */
     182:     private $_transaction = true;
     183:
     184:     /** @var array */
     185:     private $_where = array();
     186:
     187:     /** @var array */
     188:     private $_leftJoin = array();
     189:
     190:     /** @var boolean - deprecated */
     191:     private $_whereSet = false;
     192:
     193:     /** @var array */
     194:     private $_out = array();
     195:
     196:     /** @var array */
     197:     private $_events = array();
     198:
     199:     /** @var boolean */
     200:     private $_debug = false;
     201:
     202:     /** @var array */
     203:     private $_debugInfo = array();
     204:
     205:     /** @var string Log output path */
     206:     private $_debugLog = '';
     207:
     208:     /** @var callback */
     209:     private $_validator = array();
     210:
     211:     /** @var boolean Enable true / catch when processing */
     212:     private $_tryCatch = true;
     213:
     214:     /** @var boolean Enable / disable delete on left joined tables */
     215:     private $_leftJoinRemove = false;
     216:
     217:
     218:
     219:     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     220:      * Public methods
     221:      */
     222:
     223:     /**
     224:      * Get the data constructed in this instance.
     225:      *
     226:      * This will get the PHP array of data that has been constructed for the
     227:      * command that has been processed by this instance. Therefore only useful after
     228:      * process has been called.
     229:      *  @return array Processed data array.
     230:      */
     231:     public function data ()
     232:     {
     233:         return $this->_out;
     234:     }
     235:
     236:
     237:     /**
     238:      * Get / set the DB connection instance
     239:      *  @param Database $_ DataTable's Database class instance to use for database
     240:      *    connectivity. If not given, then used as a getter.
     241:      *  @return Database|self The Database connection instance if no parameter
     242:      *    is given, or self if used as a setter.
     243:      */
     244:     public function db ( $_=null )
     245:     {
     246:         return $this->_getSet( $this->_db, $_ );
     247:     }
     248:
     249:
     250:     /**
     251:      * Get / set debug mode and set a debug message.
     252:      *
     253:      * It can be useful to see the SQL statements that Editor is using. This
     254:      * method enables that ability. Information about the queries used is
     255:      * automatically added to the output data array / JSON under the property
     256:      * name `debugSql`.
     257:      *
     258:      * This method can also be called with a string parameter, which will be
     259:      * added to the debug information sent back to the client-side. This can
     260:      * be useful when debugging event listeners, etc.
     261:      *
     262:      *  @param boolean|mixed $_ Debug mode state. If not given, then used as a
     263:      *    getter. If given as anything other than a boolean, it will be added
     264:      *    to the debug information sent back to the client.
     265:      *  @param string [$path=null] Set an output path to log debug information
     266:      *  @return boolean|self Debug mode state if no parameter is given, or
     267:      *    self if used as a setter or when adding a debug message.
     268:      */
     269:     public function debug ( $_=null, $path=null )
     270:     {
     271:         if ( ! is_bool( $_ ) ) {
     272:             $this->_debugInfo[] = $_;
     273:
     274:             return $this;
     275:         }
     276:
     277:         if ( $path ) {
     278:             $this->_debugLog = $path;
     279:         }
     280:
     281:         return $this->_getSet( $this->_debug, $_ );
     282:     }
     283:
     284:
     285:     /**
     286:      * Get / set field instance.
     287:      *
     288:      * The list of fields designates which columns in the table that Editor will work
     289:      * with (both get and set).
     290:      *  @param Field|string $_... This parameter effects the return value of the
     291:      *      function:
     292:      *
     293:      *      * `null` - Get an array of all fields assigned to the instance
     294:      *      * `string` - Get a specific field instance whose 'name' matches the
     295:      *           field passed in
     296:      *      * {@link Field} - Add a field to the instance's list of fields. This
     297:      *           can be as many fields as required (i.e. multiple arguments)
     298:      *      * `array` - An array of {@link Field} instances to add to the list
     299:      *        of fields.
     300:      *  @return Field|Field[]|Editor The selected field, an array of fields, or
     301:      *      the Editor instance for chaining, depending on the input parameter.
     302:      *  @throws \Exception Unkown field error
     303:      *  @see {@link Field} for field documentation.
     304:      */
     305:     public function field ( $_=null )
     306:     {
     307:         if ( is_string( $_ ) ) {
     308:             for ( $i=0, $ien=count($this->_fields) ; $i<$ien ; $i++ ) {
     309:                 if ( $this->_fields[$i]->name() === $_ ) {
     310:                     return $this->_fields[$i];
     311:                 }
     312:             }
     313:
     314:             throw new \Exception('Unknown field: '.$_);
     315:         }
     316:
     317:         if ( $_ !== null && !is_array($_) ) {
     318:             $_ = func_get_args();
     319:         }
     320:         return $this->_getSet( $this->_fields, $_, true );
     321:     }
     322:
     323:
     324:     /**
     325:      * Get / set field instances.
     326:      *
     327:      * An alias of {@link field}, for convenience.
     328:      *  @param Field $_... Instances of the {@link Field} class, given as a single
     329:      *    instance of {@link Field}, an array of {@link Field} instances, or multiple
     330:      *    {@link Field} instance parameters for the function.
     331:      *  @return Field[]|self Array of fields, or self if used as a setter.
     332:      *  @see {@link Field} for field documentation.
     333:      */
     334:     public function fields ( $_=null )
     335:     {
     336:         if ( $_ !== null && !is_array($_) ) {
     337:             $_ = func_get_args();
     338:         }
     339:         return $this->_getSet( $this->_fields, $_, true );
     340:     }
     341:
     342:
     343:     /**
     344:      * Get / set the DOM prefix.
     345:      *
     346:      * Typically primary keys are numeric and this is not a valid ID value in an
     347:      * HTML document - is also increases the likelihood of an ID clash if multiple
     348:      * tables are used on a single page. As such, a prefix is assigned to the
     349:      * primary key value for each row, and this is used as the DOM ID, so Editor
     350:      * can track individual rows.
     351:      *  @param string $_ Primary key's name. If not given, then used as a getter.
     352:      *  @return string|self Primary key value if no parameter is given, or
     353:      *    self if used as a setter.
     354:      */
     355:     public function idPrefix ( $_=null )
     356:     {
     357:         return $this->_getSet( $this->_idPrefix, $_ );
     358:     }
     359:
     360:
     361:     /**
     362:      * Get the data that is being processed by the Editor instance. This is only
     363:      * useful once the `process()` method has been called, and is available for
     364:      * use in validation and formatter methods.
     365:      *
     366:      *   @return array Data given to `process()`.
     367:      */
     368:     public function inData ()
     369:     {
     370:         return $this->_processData;
     371:     }
     372:
     373:
     374:     /**
     375:      * Get / set join instances. Note that for the majority of use cases you
     376:      * will want to use the `leftJoin()` method. It is significantly easier
     377:      * to use if you are just doing a simple left join!
     378:      *
     379:      * The list of Join instances that Editor will join the parent table to
     380:      * (i.e. the one that the {@link table} and {@link fields} methods refer to
     381:      * in this class instance).
     382:      *
     383:      *  @param Join $_,... Instances of the {@link Join} class, given as a
     384:      *    single instance of {@link Join}, an array of {@link Join} instances,
     385:      *    or multiple {@link Join} instance parameters for the function.
     386:      *  @return Join[]|self Array of joins, or self if used as a setter.
     387:      *  @see {@link Join} for joining documentation.
     388:      */
     389:     public function join ( $_=null )
     390:     {
     391:         if ( $_ !== null && !is_array($_) ) {
     392:             $_ = func_get_args();
     393:         }
     394:         return $this->_getSet( $this->_join, $_, true );
     395:     }
     396:
     397:
     398:     /**
     399:      * Get the JSON for the data constructed in this instance.
     400:      *
     401:      * Basically the same as the {@link data} method, but in this case we echo, or
     402:      * return the JSON string of the data.
     403:      *  @param boolean $print Echo the JSON string out (true, default) or return it
     404:      *    (false).
     405:      *  @return string|self self if printing the JSON, or JSON representation of
     406:      *    the processed data if false is given as the first parameter.
     407:      */
     408:     public function json ( $print=true )
     409:     {
     410:         if ( $print ) {
     411:             $json = json_encode( $this->_out );
     412:
     413:             if ( $json !== false ) {
     414:                 echo $json;
     415:             }
     416:             else {
     417:                 echo json_encode( array(
     418:                     "error" => "JSON encoding error: ".json_last_error_msg()
     419:                 ) );
     420:             }
     421:
     422:             return $this;
     423:         }
     424:         return json_encode( $this->_out );
     425:     }
     426:
     427:
     428:     /**
     429:      * Echo out JSONP for the data constructed and processed in this instance.
     430:      * This is basically the same as {@link json} but wraps the return in a
     431:      * JSONP callback.
     432:      *
     433:      * @param string $callback The callback function name to use. If not given
     434:      *    or `null`, then `$_GET['callback']` is used (the jQuery default).
     435:      * @return self Self for chaining.
     436:      * @throws \Exception JSONP function name validation
     437:      */
     438:     public function jsonp ( $callback=null )
     439:     {
     440:         if ( ! $callback ) {
     441:             $callback = $_GET['callback'];
     442:         }
     443:
     444:         if ( preg_match('/[^a-zA-Z0-9_]/', $callback) ) {
     445:             throw new \Exception("Invalid JSONP callback function name");
     446:         }
     447:
     448:         echo $callback.'('.json_encode( $this->_out ).');';
     449:         return $this;
     450:     }
     451:
     452:
     453:     /**
     454:      * Add a left join condition to the Editor instance, allowing it to operate
     455:      * over multiple tables. Multiple `leftJoin()` calls can be made for a
     456:      * single Editor instance to join multiple tables.
     457:      *
     458:      * A left join is the most common type of join that is used with Editor
     459:      * so this method is provided to make its use very easy to configure. Its
     460:      * parameters are basically the same as writing an SQL left join statement,
     461:      * but in this case Editor will handle the create, update and remove
     462:      * requirements of the join for you:
     463:      *
     464:      * * Create - On create Editor will insert the data into the primary table
     465:      *   and then into the joined tables - selecting the required data for each
     466:      *   table.
     467:      * * Edit - On edit Editor will update the main table, and then either
     468:      *   update the existing rows in the joined table that match the join and
     469:      *   edit conditions, or insert a new row into the joined table if required.
     470:      * * Remove - On delete Editor will remove the main row and then loop over
     471:      *   each of the joined tables and remove the joined data matching the join
     472:      *   link from the main table.
     473:      *
     474:      * Please note that when using join tables, Editor requires that you fully
     475:      * qualify each field with the field's table name. SQL can result table
     476:      * names for ambiguous field names, but for Editor to provide its full CRUD
     477:      * options, the table name must also be given. For example the field
     478:      * `first_name` in the table `users` would be given as `users.first_name`.
     479:      *
     480:      * @param string $table Table name to do a join onto
     481:      * @param string $field1 Field from the parent table to use as the join link
     482:      * @param string $operator Join condition (`=`, '<`, etc)
     483:      * @param string $field2 Field from the child table to use as the join link
     484:      * @return self Self for chaining.
     485:      *
     486:      * @example
     487:      *    Simple join:
     488:      *    <code>
     489:      *        ->field(
     490:      *          Field::inst( 'users.first_name as myField' ),
     491:      *          Field::inst( 'users.last_name' ),
     492:      *          Field::inst( 'users.dept_id' ),
     493:      *          Field::inst( 'dept.name' )
     494:      *        )
     495:      *        ->leftJoin( 'dept', 'users.dept_id', '=', 'dept.id' )
     496:      *        ->process($_POST)
     497:      *        ->json();
     498:      *    </code>
     499:      *
     500:      *    This is basically the same as the following SQL statement:
     501:      *
     502:      *    <code>
     503:      *      SELECT users.first_name, users.last_name, user.dept_id, dept.name
     504:      *      FROM users
     505:      *      LEFT JOIN dept ON users.dept_id = dept.id
     506:      *    </code>
     507:      */
     508:     public function leftJoin ( $table, $field1, $operator, $field2 )
     509:     {
     510:         $this->_leftJoin[] = array(
     511:             "table"    => $table,
     512:             "field1"   => $field1,
     513:             "field2"   => $field2,
     514:             "operator" => $operator
     515:         );
     516:
     517:         return $this;
     518:     }
     519:
     520:
     521:     /**
     522:      * Indicate if a remove should be performed on left joined tables when deleting
     523:      * from the parent row. Note that this is disabled by default and will be
     524:      * removed completely in v2. Use `ON DELETE CASCADE` in your database instead.
     525:      *
     526:      *  @deprecated
     527:      *  @param boolean $_ Value to set. If not given, then used as a getter.
     528:      *  @return boolean|self Value if no parameter is given, or
     529:      *    self if used as a setter.
     530:      */
     531:     public function leftJoinRemove ( $_=null )
     532:     {
     533:         return $this->_getSet( $this->_leftJoinRemove, $_ );
     534:     }
     535:
     536:
     537:     /**
     538:      * Add an event listener. The `Editor` class will trigger an number of
     539:      * events that some action can be taken on.
     540:      *
     541:      * @param  string $name     Event name
     542:      * @param  callable $callback Callback function to execute when the event
     543:      *     occurs
     544:      * @return self Self for chaining.
     545:      */
     546:     public function on ( $name, $callback )
     547:     {
     548:         if ( ! isset( $this->_events[ $name ] ) ) {
     549:             $this->_events[ $name ] = array();
     550:         }
     551:
     552:         $this->_events[ $name ][] = $callback;
     553:
     554:         return $this;
     555:     }
     556:
     557:
     558:     /**
     559:      * Get / set the primary key.
     560:      *
     561:      * The primary key must be known to Editor so it will know which rows are being
     562:      * edited / deleted upon those actions. The default value is ['id'].
     563:      *
     564:      *  @param string|array $_ Primary key's name. If not given, then used as a
     565:      *    getter. An array of column names can be given to allow composite keys to
     566:      *    be used.
     567:      *  @return string|self Primary key value if no parameter is given, or
     568:      *    self if used as a setter.
     569:      */
     570:     public function pkey ( $_=null )
     571:     {
     572:         if ( is_string( $_ ) ) {
     573:             $this->_pkey = array( $_ );
     574:             return $this;
     575:         }
     576:         return $this->_getSet( $this->_pkey, $_ );
     577:     }
     578:
     579:
     580:     /**
     581:      * Convert a primary key array of field values to a combined value.
     582:      *
     583:      * @param  string  $row   The row of data that the primary key value should
     584:      *   be extracted from.
     585:      * @param  boolean $flat  Flag to indicate if the given array is flat
     586:      *   (useful for `where` conditions) or nested for join tables.
     587:      * @return string The created primary key value.
     588:      * @throws \Exception If one of the values that the primary key is made up
     589:      *    of cannot be found in the data set given, an Exception will be thrown.
     590:      */
     591:     public function pkeyToValue ( $row, $flat=false )
     592:     {
     593:         $pkey = $this->_pkey;
     594:         $id = array();
     595:
     596:         for ( $i=0, $ien=count($pkey) ; $i<$ien ; $i++ ) {
     597:             $column = $pkey[ $i ];
     598:
     599:             if ( $flat ) {
     600:                 if ( isset( $row[ $column ] ) ) {
     601:                     if ( $row[ $column ] === null ) {
     602:                         throw new \Exception("Primary key value is null.", 1);
     603:                     }
     604:                     $val = $row[ $column ];
     605:                 }
     606:                 else {
     607:                     $val = null;
     608:                 }
     609:             }
     610:             else {
     611:                 $val = $this->_readProp( $column, $row );
     612:             }
     613:
     614:             if ( $val === null ) {
     615:                 throw new \Exception("Primary key element is not available in data set.", 1);
     616:             }
     617:
     618:             $id[] = $val;
     619:         }
     620:
     621:         return implode( $this->_pkey_separator(), $id );
     622:     }
     623:
     624:
     625:     /**
     626:      * Convert a primary key combined value to an array of field values.
     627:      *
     628:      * @param  string  $value The id that should be split apart
     629:      * @param  boolean $flat  Flag to indicate if the returned array should be
     630:      *   flat (useful for `where` conditions) or nested for join tables.
     631:      * @param  string[] $pkey The primary key name - will use the instance value
     632:      *   if not given
     633:      * @return array          Array of field values that the id was made up of.
     634:      * @throws \Exception If the primary key value does not match the expected
     635:      *   length based on the primary key configuration, an exception will be
     636:      *   thrown.
     637:      */
     638:     public function pkeyToArray ( $value, $flat=false, $pkey=null )
     639:     {
     640:         $arr = array();
     641:         $value = str_replace( $this->idPrefix(), '', $value );
     642:         $idParts = explode( $this->_pkey_separator(), $value );
     643:
     644:         if ( $pkey === null ) {
     645:             $pkey = $this->_pkey;
     646:         }
     647:
     648:         if ( count($pkey) !== count($idParts) ) {
     649:             throw new \Exception("Primary key data doesn't match submitted data", 1);
     650:         }
     651:
     652:         for ( $i=0, $ien=count($idParts) ; $i<$ien ; $i++ ) {
     653:             if ( $flat ) {
     654:                 $arr[ $pkey[$i] ] = $idParts[$i];
     655:             }
     656:             else {
     657:                 $this->_writeProp( $arr, $pkey[$i], $idParts[$i] );
     658:             }
     659:         }
     660:
     661:         return $arr;
     662:     }
     663:
     664:
     665:     /**
     666:      * Process a request from the Editor client-side to get / set data.
     667:      *
     668:      *  @param array $data Typically $_POST or $_GET as required by what is sent
     669:      *  by Editor
     670:      *  @return self
     671:      */
     672:     public function process ( $data )
     673:     {
     674:         if ( $this->_debug ) {
     675:             $debugInfo = &$this->_debugInfo;
     676:             $debugVal = $this->_db->debug( function ( $mess ) use ( &$debugInfo ) {
     677:                 $debugInfo[] = $mess;
     678:             } );
     679:         }
     680:
     681:         if ( $this->_tryCatch ) {
     682:             try {
     683:                 $this->_process( $data );
     684:             }
     685:             catch (\Exception $e) {
     686:                 // Error feedback
     687:                 $this->_out['error'] = $e->getMessage();
     688:
     689:                 if ( $this->_transaction ) {
     690:                     $this->_db->rollback();
     691:                 }
     692:             }
     693:         }
     694:         else {
     695:             $this->_process( $data );
     696:         }
     697:
     698:         if ( $this->_debug ) {
     699:             $this->_out['debug'] = $this->_debugInfo;
     700:
     701:             // Save to a log file
     702:             if ( $this->_debugLog ) {
     703:                 file_put_contents( $this->_debugLog, json_encode( $this->_debugInfo )."\n", FILE_APPEND );
     704:             }
     705:
     706:             $this->_db->debug( false );
     707:         }
     708:
     709:         return $this;
     710:     }
     711:
     712:
     713:     /**
     714:      * The CRUD read table name. If this method is used, Editor will create from the
     715:      * table name(s) given rather than those given by `Editor->table()`. This can be
     716:      * a useful distinction to allow a read from a VIEW (which could make use of a
     717:      * complex SELECT) while writing to a different table.
     718:      *
     719:      *  @param string|array $_,... Read table names given as a single string, an array
     720:      *    of strings or multiple string parameters for the function.
     721:      *  @return string[]|self Array of read tables names, or self if used as a setter.
     722:      */
     723:     public function readTable ( $_=null )
     724:     {
     725:         if ( $_ !== null && !is_array($_) ) {
     726:             $_ = func_get_args();
     727:         }
     728:         return $this->_getSet( $this->_readTableNames, $_, true );
     729:     }
     730:
     731:
     732:     /**
     733:      * Get / set the table name.
     734:      *
     735:      * The table name designated which DB table Editor will use as its data
     736:      * source for working with the database. Table names can be given with an
     737:      * alias, which can be used to simplify larger table names. The field
     738:      * names would also need to reflect the alias, just like an SQL query. For
     739:      * example: `users as a`.
     740:      *
     741:      *  @param string|array $_,... Table names given as a single string, an array of
     742:      *    strings or multiple string parameters for the function.
     743:      *  @return string[]|self Array of tables names, or self if used as a setter.
     744:      */
     745:     public function table ( $_=null )
     746:     {
     747:         if ( $_ !== null && !is_array($_) ) {
     748:             $_ = func_get_args();
     749:         }
     750:         return $this->_getSet( $this->_table, $_, true );
     751:     }
     752:
     753:
     754:     /**
     755:      * Get / set transaction support.
     756:      *
     757:      * When enabled (which it is by default) Editor will use an SQL transaction
     758:      * to ensure data integrity while it is performing operations on the table.
     759:      * This can be optionally disabled using this method, if required by your
     760:      * database configuration.
     761:      *
     762:      *  @param boolean $_ Enable (`true`) or disabled (`false`) transactions.
     763:      *    If not given, then used as a getter.
     764:      *  @return boolean|self Transactions enabled flag, or self if used as a
     765:      *    setter.
     766:      */
     767:     public function transaction ( $_=null )
     768:     {
     769:         return $this->_getSet( $this->_transaction, $_ );
     770:     }
     771:
     772:
     773:     /**
     774:      * Enable / try catch when `process()` is called. Disabling this can be
     775:      * useful for debugging, but is not recommended for production.
     776:      *
     777:      * @param  boolean $_ `true` to enable (default), otherwise false to disable
     778:      * @return boolean|Editor Value if used as a getter, otherwise `$this` when
     779:      *   used as a setter.
     780:      */
     781:     public function tryCatch ( $_=null )
     782:     {
     783:         return $this->_getSet( $this->_tryCatch, $_ );
     784:     }
     785:
     786:
     787:     /**
     788:      * Perform validation on a data set.
     789:      *
     790:      * Note that validation is performed on data only when the action is
     791:      * `create` or `edit`. Additionally, validation is performed on the _wire
     792:      * data_ - i.e. that which is submitted from the client, without formatting.
     793:      * Any formatting required by `setFormatter` is performed after the data
     794:      * from the client has been validated.
     795:      *
     796:      *  @param array $errors Output array to which field error information will
     797:      *      be written. Each element in the array represents a field in an error
     798:      *      condition. These elements are themselves arrays with two properties
     799:      *      set; `name` and `status`.
     800:      *  @param array $data The format data to check
     801:      *  @return boolean `true` if the data is valid, `false` if not.
     802:      */
     803:     public function validate ( &$errors, $data )
     804:     {
     805:         // Validation is only performed on create and edit
     806:         if ( $data['action'] != "create" && $data['action'] != "edit" ) {
     807:             return true;
     808:         }
     809:
     810:         foreach( $data['data'] as $id => $values ) {
     811:             for ( $i=0 ; $i<count($this->_fields) ; $i++ ) {
     812:                 $field = $this->_fields[$i];
     813:                 $validation = $field->validate( $values, $this,
     814:                     str_replace( $this->idPrefix(), '', $id )
     815:                 );
     816:
     817:                 if ( $validation !== true ) {
     818:                     $errors[] = array(
     819:                         "name" => $field->name(),
     820:                         "status" => $validation
     821:                     );
     822:                 }
     823:             }
     824:
     825:             // MJoin validation
     826:             for ( $i=0 ; $i<count($this->_join) ; $i++ ) {
     827:                 $this->_join[$i]->validate( $errors, $this, $values, $data['action'] );
     828:             }
     829:         }
     830:
     831:         return count( $errors ) > 0 ? false : true;
     832:     }
     833:
     834:
     835:     /**
     836:      * Get / set a global validator that will be triggered for the create, edit
     837:      * and remove actions performed from the client-side. Multiple validators
     838:      * can be added.
     839:      *
     840:      * @param  callable $_ Function to execute when validating the input data.
     841:      *   It is passed three parameters: 1. The editor instance, 2. The action
     842:      *   and 3. The values.
     843:      * @return Editor|callback Editor instance if called as a setter, or the
     844:      *   validator function if not.
     845:      */
     846:     public function validator ( $_=null )
     847:     {
     848:         return $this->_getSet( $this->_validator, $_, true );
     849:     }
     850:
     851:
     852:     /**
     853:      * Where condition to add to the query used to get data from the database.
     854:      *
     855:      * Can be used in two different ways:
     856:      *
     857:      * * Simple case: `where( field, value, operator )`
     858:      * * Complex: `where( fn )`
     859:      *
     860:      * The simple case is fairly self explanatory, a condition is applied to the
     861:      * data that looks like `field operator value` (e.g. `name = 'Allan'`). The
     862:      * complex case allows full control over the query conditions by providing a
     863:      * closure function that has access to the database Query that Editor is
     864:      * using, so you can use the `where()`, `or_where()`, `and_where()` and
     865:      * `where_group()` methods as you require.
     866:      *
     867:      * Please be very careful when using this method! If an edit made by a user
     868:      * using Editor removes the row from the where condition, the result is
     869:      * undefined (since Editor expects the row to still be available, but the
     870:      * condition removes it from the result set).
     871:      *
     872:      * @param string|callable $key   Single field name or a closure function
     873:      * @param string          $value Single field value.
     874:      * @param string          $op    Condition operator: <, >, = etc
     875:      * @return string[]|self Where condition array, or self if used as a setter.
     876:      */
     877:     public function where ( $key=null, $value=null, $op='=' )
     878:     {
     879:         if ( $key === null ) {
     880:             return $this->_where;
     881:         }
     882:
     883:         if ( is_callable($key) && is_object($key) ) {
     884:             $this->_where[] = $key;
     885:         }
     886:         else {
     887:             $this->_where[] = array(
     888:                 "key"   => $key,
     889:                 "value" => $value,
     890:                 "op"    => $op
     891:             );
     892:         }
     893:
     894:         return $this;
     895:     }
     896:
     897:
     898:     /**
     899:      * Get / set if the WHERE conditions should be included in the create and
     900:      * edit actions.
     901:      *
     902:      *  @param boolean $_ Include (`true`), or not (`false`)
     903:      *  @return boolean Current value
     904:      *  @deprecated Note that `whereSet` is now deprecated and replaced with the
     905:      *    ability to set values for columns on create and edit. The C# libraries
     906:      *    do not support this option at all.
     907:      */
     908:     public function whereSet ( $_=null )
     909:     {
     910:         return $this->_getSet( $this->_whereSet, $_ );
     911:     }
     912:
     913:
     914:
     915:     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     916:      * Private methods
     917:      */
     918:
     919:     /**
     920:      * Process a request from the Editor client-side to get / set data.
     921:      *
     922:      *  @param array $data Data to process
     923:      *  @private
     924:      */
     925:     private function _process( $data )
     926:     {
     927:         $this->_out = array(
     928:             "fieldErrors" => array(),
     929:             "error" => "",
     930:             "data" => array(),
     931:             "ipOpts" => array(),
     932:             "cancelled" => array()
     933:         );
     934:
     935:         $this->_processData = $data;
     936:         $this->_formData = isset($data['data']) ? $data['data'] : null;
     937:         $validators = $this->_validator;
     938:
     939:         if ( $this->_transaction ) {
     940:             $this->_db->transaction();
     941:         }
     942:
     943:         $this->_prepJoin();
     944:
     945:         if ( $validators ) {
     946:             for ( $i=0 ; $i<count($validators) ; $i++ ) {
     947:                 $validator = $validators[$i];
     948:                 $ret = $validator( $this, !isset($data['action']) ? self::ACTION_READ : $data['action'], $data );
     949:
     950:                 if ( is_string($ret) ) {
     951:                     $this->_out['error'] = $ret;
     952:                     break;
     953:                 }
     954:             }
     955:         }
     956:
     957:         if ( ! $this->_out['error'] ) {
     958:             if ( ! isset($data['action']) ) {
     959:                 /* Get data */
     960:                 $this->_out = array_merge( $this->_out, $this->_get( null, $data ) );
     961:             }
     962:             else if ( $data['action'] == "upload" ) {
     963:                 /* File upload */
     964:                 $this->_upload( $data );
     965:             }
     966:             else if ( $data['action'] == "remove" ) {
     967:                 /* Remove rows */
     968:                 $this->_remove( $data );
     969:                 $this->_fileClean();
     970:             }
     971:             else {
     972:                 /* Create or edit row */
     973:                 // Pre events so they can occur before the validation
     974:                 foreach ($data['data'] as $idSrc => &$values) {
     975:                     $cancel = null;
     976:
     977:                     if ( $data['action'] == 'create' ) {
     978:                         $cancel = $this->_trigger( 'preCreate', $values );
     979:                     }
     980:                     else {
     981:                         $id = str_replace( $this->_idPrefix, '', $idSrc );
     982:                         $cancel = $this->_trigger( 'preEdit', $id, $values );
     983:                     }
     984:
     985:                     // One of the event handlers returned false - don't continue
     986:                     if ( $cancel === false ) {
     987:                         // Remove the data from the data set so it won't be processed
     988:                         unset( $data['data'][$idSrc] );
     989:
     990:                         // Tell the client-side we aren't updating this row
     991:                         $this->_out['cancelled'][] = $idSrc;
     992:                     }
     993:                 }
     994:
     995:                 // Validation
     996:                 $valid = $this->validate( $this->_out['fieldErrors'], $data );
     997:
     998:                 if ( $valid ) {
     999:                     foreach ($data['data'] as $id => &$values) {
    1000:                         $d = $data['action'] == "create" ?
    1001:                             $this->_insert( $values ) :
    1002:                             $this->_update( $id, $values );
    1003:
    1004:                         if ( $d !== null ) {
    1005:                             $this->_out['data'][] = $d;
    1006:                         }
    1007:                     }
    1008:                 }
    1009:
    1010:                 $this->_fileClean();
    1011:             }
    1012:         }
    1013:
    1014:         if ( $this->_transaction ) {
    1015:             $this->_db->commit();
    1016:         }
    1017:
    1018:         // Tidy up the reply
    1019:         if ( count( $this->_out['fieldErrors'] ) === 0 ) {
    1020:             unset( $this->_out['fieldErrors'] );
    1021:         }
    1022:
    1023:         if ( $this->_out['error'] === '' ) {
    1024:             unset( $this->_out['error'] );
    1025:         }
    1026:
    1027:         if ( count( $this->_out['ipOpts'] ) === 0 ) {
    1028:             unset( $this->_out['ipOpts'] );
    1029:         }
    1030:
    1031:         if ( count( $this->_out['cancelled'] ) === 0 ) {
    1032:             unset( $this->_out['cancelled'] );
    1033:         }
    1034:     }
    1035:
    1036:
    1037:     /**
    1038:      * Get an array of objects from the database to be given to DataTables as a
    1039:      * result of an sAjaxSource request, such that DataTables can display the information
    1040:      * from the DB in the table.
    1041:      *
    1042:      *  @param integer|string $id Primary key value to get an individual row
    1043:      *    (after create or update operations). Gets the full set if not given.
    1044:      *    If a compound key is being used, this should be the string
    1045:      *    representation of it (i.e. joined together) rather than an array form.
    1046:      *  @param array $http HTTP parameters from GET or POST request (so we can service
    1047:      *    server-side processing requests from DataTables).
    1048:      *  @return array DataTables get information
    1049:      *  @throws \Exception Error on SQL execution
    1050:      *  @private
    1051:      */
    1052:     private function _get( $id=null, $http=null )
    1053:     {
    1054:
    1055:         $cancel = $this->_trigger( 'preGet', $id );
    1056:         if ( $cancel === false ) {
    1057:             return array();
    1058:         }
    1059:
    1060:         $query = $this->_db
    1061:             ->query('select')
    1062:             ->table( $this->_read_table() )
    1063:             ->get( $this->_pkey );
    1064:
    1065:         // Add all fields that we need to get from the database
    1066:         foreach ($this->_fields as $field) {
    1067:             // Don't reselect a pkey column if it was already added
    1068:             if ( in_array( $field->dbField(), $this->_pkey ) ) {
    1069:                 continue;
    1070:             }
    1071:
    1072:             if ( $field->apply('get') && $field->getValue() === null ) {
    1073:                 $query->get( $field->dbField() );
    1074:             }
    1075:         }
    1076:
    1077:         $this->_get_where( $query );
    1078:         $this->_perform_left_join( $query );
    1079:         $ssp = $this->_ssp_query( $query, $http );
    1080:
    1081:         if ( $id !== null ) {
    1082:             $query->where( $this->pkeyToArray( $id, true ) );
    1083:         }
    1084:
    1085:         $res = $query->exec();
    1086:         if ( ! $res ) {
    1087:             throw new \Exception('Error executing SQL for data get. Enable SQL debug using `->debug(true)`');
    1088:         }
    1089:
    1090:         $out = array();
    1091:         while ( $row=$res->fetch() ) {
    1092:             $inner = array();
    1093:             $inner['DT_RowId'] = $this->_idPrefix . $this->pkeyToValue( $row, true );
    1094:
    1095:             foreach ($this->_fields as $field) {
    1096:                 if ( $field->apply('get') ) {
    1097:                     $field->write( $inner, $row );
    1098:                 }
    1099:             }
    1100:
    1101:             $out[] = $inner;
    1102:         }
    1103:
    1104:         // Field options
    1105:         $options = array();
    1106:
    1107:         if ( $id === null ) {
    1108:             foreach ($this->_fields as $field) {
    1109:                 $opts = $field->optionsExec( $this->_db );
    1110:
    1111:                 if ( $opts !== false ) {
    1112:                     $options[ $field->name() ] = $opts;
    1113:                 }
    1114:             }
    1115:         }
    1116:
    1117:         // Row based "joins"
    1118:         for ( $i=0 ; $i<count($this->_join) ; $i++ ) {
    1119:             $this->_join[$i]->data( $this, $out, $options );
    1120:         }
    1121:
    1122:         $this->_trigger( 'postGet', $out, $id );
    1123:
    1124:         return array_merge(
    1125:             array(
    1126:                 'data'    => $out,
    1127:                 'options' => $options,
    1128:                 'files'   => $this->_fileData( null, null, $out )
    1129:             ),
    1130:             $ssp
    1131:         );
    1132:     }
    1133:
    1134:
    1135:     /**
    1136:      * Insert a new row in the database
    1137:      *  @private
    1138:      */
    1139:     private function _insert( $values )
    1140:     {
    1141:         // Only allow a composite insert if the values for the key are
    1142:         // submitted. This is required because there is no reliable way in MySQL
    1143:         // to return the newly inserted row, so we can't know any newly
    1144:         // generated values.
    1145:         $this->_pkey_validate_insert( $values );
    1146:
    1147:         // Insert the new row
    1148:         $id = $this->_insert_or_update( null, $values );
    1149:
    1150:         if ( $id === null ) {
    1151:             return null;
    1152:         }
    1153:
    1154:         // Was the primary key altered as part of the edit, if so use the
    1155:         // submitted values
    1156:         $id = count( $this->_pkey ) > 1 ?
    1157:             $this->pkeyToValue( $values ) :
    1158:             $this->_pkey_submit_merge( $id, $values );
    1159:
    1160:         // Join tables
    1161:         for ( $i=0 ; $i<count($this->_join) ; $i++ ) {
    1162:             $this->_join[$i]->create( $this, $id, $values );
    1163:         }
    1164:
    1165:         $this->_trigger( 'writeCreate', $id, $values );
    1166:
    1167:         // Full data set for the created row
    1168:         $row = $this->_get( $id );
    1169:         $row = count( $row['data'] ) > 0 ?
    1170:             $row['data'][0] :
    1171:             null;
    1172:
    1173:         $this->_trigger( 'postCreate', $id, $values, $row );
    1174:
    1175:         return $row;
    1176:     }
    1177:
    1178:
    1179:     /**
    1180:      * Update a row in the database
    1181:      *  @param string $id The DOM ID for the row that is being edited.
    1182:      *  @return array Row's data
    1183:      *  @private
    1184:      */
    1185:     private function _update( $id, $values )
    1186:     {
    1187:         $id = str_replace( $this->_idPrefix, '', $id );
    1188:
    1189:         // Update or insert the rows for the parent table and the left joined
    1190:         // tables
    1191:         $this->_insert_or_update( $id, $values );
    1192:
    1193:         // And the join tables
    1194:         for ( $i=0 ; $i<count($this->_join) ; $i++ ) {
    1195:             $this->_join[$i]->update( $this, $id, $values );
    1196:         }
    1197:
    1198:         // Was the primary key altered as part of the edit, if so use the
    1199:         // submitted values
    1200:         $getId = $this->_pkey_submit_merge( $id, $values );
    1201:
    1202:         $this->_trigger( 'writeEdit', $id, $values );
    1203:
    1204:         // Full data set for the modified row
    1205:         $row = $this->_get( $getId );
    1206:         $row = count( $row['data'] ) > 0 ?
    1207:             $row['data'][0] :
    1208:             null;
    1209:
    1210:         $this->_trigger( 'postEdit', $id, $values, $row );
    1211:
    1212:         return $row;
    1213:     }
    1214:
    1215:
    1216:     /**
    1217:      * Delete one or more rows from the database
    1218:      *  @private
    1219:      */
    1220:     private function _remove( $data )
    1221:     {
    1222:         $ids = array();
    1223:
    1224:         // Get the ids to delete from the data source
    1225:         foreach ($data['data'] as $idSrc => $rowData) {
    1226:             // Strip the ID prefix that the client-side sends back
    1227:             $id = str_replace( $this->_idPrefix, "", $idSrc );
    1228:
    1229:             $res = $this->_trigger( 'preRemove', $id, $rowData );
    1230:
    1231:             // Allow the event to be cancelled and inform the client-side
    1232:             if ( $res === false ) {
    1233:                 $this->_out['cancelled'][] = $idSrc;
    1234:             }
    1235:             else {
    1236:                 $ids[] = $id;
    1237:             }
    1238:         }
    1239:
    1240:         if ( count( $ids ) === 0 ) {
    1241:             return;
    1242:         }
    1243:
    1244:         // Row based joins - remove first as the host row will be removed which
    1245:         // is a dependency
    1246:         for ( $i=0 ; $i<count($this->_join) ; $i++ ) {
    1247:             $this->_join[$i]->remove( $this, $ids );
    1248:         }
    1249:
    1250:         // Remove from the left join tables
    1251:         if ( $this->_leftJoinRemove ) {
    1252:             for ( $i=0, $ien=count($this->_leftJoin) ; $i<$ien ; $i++ ) {
    1253:                 $join = $this->_leftJoin[$i];
    1254:                 $table = $this->_alias( $join['table'], 'orig' );
    1255:
    1256:                 // which side of the join refers to the parent table?
    1257:                 if ( strpos( $join['field1'], $join['table'] ) === 0 ) {
    1258:                     $parentLink = $join['field2'];
    1259:                     $childLink = $join['field1'];
    1260:                 }
    1261:                 else {
    1262:                     $parentLink = $join['field1'];
    1263:                     $childLink = $join['field2'];
    1264:                 }
    1265:
    1266:                 // Only delete on the primary key, since that is what the ids refer
    1267:                 // to - otherwise we'd be deleting random data! Note that this
    1268:                 // won't work with compound keys since the parent link would be
    1269:                 // over multiple fields.
    1270:                 if ( $parentLink === $this->_pkey[0] && count($this->_pkey) === 1 ) {
    1271:                     $this->_remove_table( $join['table'], $ids, array($childLink) );
    1272:                 }
    1273:             }
    1274:         }
    1275:
    1276:         // Remove from the primary tables
    1277:         for ( $i=0, $ien=count($this->_table) ; $i<$ien ; $i++ ) {
    1278:             $this->_remove_table( $this->_table[$i], $ids );
    1279:         }
    1280:
    1281:         foreach ($data['data'] as $idSrc => $rowData) {
    1282:             $id = str_replace( $this->_idPrefix, "", $idSrc );
    1283:
    1284:             $this->_trigger( 'postRemove', $id, $rowData );
    1285:         }
    1286:     }
    1287:
    1288:
    1289:     /**
    1290:      * File upload
    1291:      *  @param array $data Upload data
    1292:      *  @throws \Exception File upload name error
    1293:      *  @private
    1294:      */
    1295:     private function _upload( $data )
    1296:     {
    1297:         // Search for upload field in local fields
    1298:         $field = $this->_find_field( $data['uploadField'], 'name' );
    1299:         $fieldName = '';
    1300:
    1301:         if ( ! $field ) {
    1302:             // Perhaps it is in a join instance
    1303:             for ( $i=0 ; $i<count($this->_join) ; $i++ ) {
    1304:                 $join = $this->_join[$i];
    1305:                 $fields = $join->fields();
    1306:
    1307:                 for ( $j=0, $jen=count($fields) ; $j<$jen ; $j++ ) {
    1308:                     $joinField = $fields[ $j ];
    1309:                     $name = $join->name().'[].'.$joinField->name();
    1310:
    1311:                     if ( $name === $data['uploadField'] ) {
    1312:                         $field = $joinField;
    1313:                         $fieldName = $name;
    1314:                     }
    1315:                 }
    1316:             }
    1317:         }
    1318:         else {
    1319:             $fieldName = $field->name();
    1320:         }
    1321:
    1322:         if ( ! $field ) {
    1323:             throw new \Exception("Unknown upload field name submitted");
    1324:         }
    1325:
    1326:         $res = $this->_trigger( 'preUpload', $data );
    1327:
    1328:         // Allow the event to be cancelled and inform the client-side
    1329:         if ( $res === false ) {
    1330:             return;
    1331:         }
    1332:
    1333:         $upload = $field->upload();
    1334:         if ( ! $upload ) {
    1335:             throw new \Exception("File uploaded to a field that does not have upload options configured");
    1336:         }
    1337:
    1338:         $res = $upload->exec( $this );
    1339:
    1340:         if ( $res === false ) {
    1341:             $this->_out['fieldErrors'][] = array(
    1342:                 "name"   => $fieldName,      // field name can be just the field's
    1343:                 "status" => $upload->error() // name or a join combination
    1344:             );
    1345:         }
    1346:         else {
    1347:             $files = $this->_fileData( $upload->table(), array($res) );
    1348:
    1349:             $this->_out['files'] = $files;
    1350:             $this->_out['upload']['id'] = $res;
    1351:
    1352:             $this->_trigger( 'postUpload', $res, $files, $data );
    1353:         }
    1354:     }
    1355:
    1356:
    1357:     /**
    1358:      * Get information about the files that are detailed in the database for
    1359:      * the fields which have an upload method defined on them.
    1360:      *
    1361:      * @param  string [$limitTable=null] Limit the data gathering to a single
    1362:      *     table only
    1363:      * @param number[] [$id=null] Limit to a specific set of ids
    1364:      * @return array File information
    1365:      * @private
    1366:      */
    1367:     private function _fileData ( $limitTable=null, $ids=null, $data=null )
    1368:     {
    1369:         $files = array();
    1370:
    1371:         // The fields in this instance
    1372:         $this->_fileDataFields( $files, $this->_fields, $limitTable, $ids, $data );
    1373:
    1374:         // From joined tables
    1375:         for ( $i=0 ; $i<count($this->_join) ; $i++ ) {
    1376:             $joinData = null;
    1377:
    1378:             // If we have data from the get, it is nested from the join, so we need to
    1379:             // un-nest it (i.e. get the array of joined data for each row)
    1380:             if ( $data ) {
    1381:                 $joinData = array();
    1382:
    1383:                 for ( $j=0, $jen=count($data) ; $j<$jen ; $j++ ) {
    1384:                     $joinData = array_merge( $joinData, $data[$j][$this->_join[$i]->name()] );
    1385:                 }
    1386:             }
    1387:
    1388:             $this->_fileDataFields( $files, $this->_join[$i]->fields(), $limitTable, $ids, $joinData );
    1389:         }
    1390:
    1391:         return $files;
    1392:     }
    1393:
    1394:
    1395:     /**
    1396:      * Common file get method for any array of fields
    1397:      * @param  array &$files File output array
    1398:      * @param  Field[] $fields Fields to get file information about
    1399:      * @param  string[] $limitTable Limit the data gathering to a single table
    1400:      *     only
    1401:      * @private
    1402:      */
    1403:     private function _fileDataFields ( &$files, $fields, $limitTable, $ids=null, $data=null )
    1404:     {
    1405:         foreach ($fields as $field) {
    1406:             $upload = $field->upload();
    1407:
    1408:             if ( $upload ) {
    1409:                 $table = $upload->table();
    1410:
    1411:                 if ( ! $table ) {
    1412:                     continue;
    1413:                 }
    1414:
    1415:                 if ( $limitTable !== null && $table !== $limitTable ) {
    1416:                     continue;
    1417:                 }
    1418:
    1419:                 // Make a collection of the ids used in this data set to get a limited data set
    1420:                 // in return (security and performance)
    1421:                 if ( $ids === null ) {
    1422:                     $ids = array();
    1423:                 }
    1424:
    1425:                 if ( $data !== null ) {
    1426:                     for ( $i=0, $ien=count($data); $i<$ien ; $i++ ) {
    1427:                         $val = $field->val( 'set', $data[$i] );
    1428:
    1429:                         if ( $val ) {
    1430:                             $ids[] = $val;
    1431:                         }
    1432:                     }
    1433:
    1434:                     if ( count($ids) === 0 ) {
    1435:                         // If no data to fetch, then don't bother
    1436:                         return;
    1437:                     }
    1438:                     else if ( count($ids) > 1000 ) {
    1439:                         // Don't use `where_in` for really large data sets
    1440:                         $ids = array();
    1441:                     }
    1442:                 }
    1443:
    1444:                 $fileData = $upload->data( $this->_db, $ids );
    1445:
    1446:                 if ( $fileData !== null ) {
    1447:                     if ( isset($files[$table]) ) {
    1448:                         $files[$table] = $files[$table] + $fileData;
    1449:                     }
    1450:                     else {
    1451:                         $files[$table] = $fileData;
    1452:                     }
    1453:                 }
    1454:             }
    1455:         }
    1456:     }
    1457:
    1458:     /**
    1459:      * Run the file clean up
    1460:      *
    1461:      * @private
    1462:      */
    1463:     private function _fileClean ()
    1464:     {
    1465:         foreach ( $this->_fields as $field ) {
    1466:             $upload = $field->upload();
    1467:
    1468:             if ( $upload ) {
    1469:                 $upload->dbCleanExec( $this, $field );
    1470:             }
    1471:         }
    1472:
    1473:         for ( $i=0 ; $i<count($this->_join) ; $i++ ) {
    1474:             foreach ( $this->_join[$i]->fields() as $field ) {
    1475:                 $upload = $field->upload();
    1476:
    1477:                 if ( $upload ) {
    1478:                     $upload->dbCleanExec( $this, $field );
    1479:                 }
    1480:             }
    1481:         }
    1482:     }
    1483:
    1484:
    1485:     /*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
    1486:      * Server-side processing methods
    1487:      */
    1488:
    1489:     /**
    1490:      * When server-side processing is being used, modify the query with // the
    1491:      * required extra conditions
    1492:      *
    1493:      *  @param \DataTables\Database\Query $query Query instance to apply the SSP commands to
    1494:      *  @param array $http Parameters from HTTP request
    1495:      *  @return array Server-side processing information array
    1496:      *  @private
    1497:      */
    1498:     private function _ssp_query ( $query, $http )
    1499:     {
    1500:         if ( ! isset( $http['draw'] ) ) {
    1501:             return array();
    1502:         }
    1503:
    1504:         // Add the server-side processing conditions
    1505:         $this->_ssp_limit( $query, $http );
    1506:         $this->_ssp_sort( $query, $http );
    1507:         $this->_ssp_filter( $query, $http );
    1508:
    1509:         // Get the number of rows in the result set
    1510:         $ssp_set_count = $this->_db
    1511:             ->query('count')
    1512:             ->table( $this->_read_table() )
    1513:             ->get( $this->_pkey[0] );
    1514:         $this->_get_where( $ssp_set_count );
    1515:         $this->_ssp_filter( $ssp_set_count, $http );
    1516:         $this->_perform_left_join( $ssp_set_count );
    1517:         $ssp_set_count = $ssp_set_count->exec()->fetch();
    1518:
    1519:         // Get the number of rows in the full set
    1520:         $ssp_full_count = $this->_db
    1521:             ->query('count')
    1522:             ->table( $this->_read_table() )
    1523:             ->get( $this->_pkey[0] );
    1524:         $this->_get_where( $ssp_full_count );
    1525:         if ( count( $this->_where ) ) { // only needed if there is a where condition
    1526:             $this->_perform_left_join( $ssp_full_count );
    1527:         }
    1528:         $ssp_full_count = $ssp_full_count->exec()->fetch();
    1529:
    1530:         return array(
    1531:             "draw" => intval( $http['draw'] ),
    1532:             "recordsTotal" => $ssp_full_count['cnt'],
    1533:             "recordsFiltered" => $ssp_set_count['cnt']
    1534:         );
    1535:     }
    1536:
    1537:
    1538:     /**
    1539:      * Convert a column index to a database field name - used for server-side
    1540:      * processing requests.
    1541:      *  @param array $http HTTP variables (i.e. GET or POST)
    1542:      *  @param int $index Index in the DataTables' submitted data
    1543:      *  @returns string DB field name
    1544:      *  @throws \Exception Unknown fields
    1545:      *  @private Note that it is actually public for PHP 5.3 - thread 39810
    1546:      */
    1547:     public function _ssp_field( $http, $index )
    1548:     {
    1549:         $name = $http['columns'][$index]['data'];
    1550:         $field = $this->_find_field( $name, 'name' );
    1551:
    1552:         if ( ! $field ) {
    1553:             // Is it the primary key?
    1554:             if ( $name === 'DT_RowId' ) {
    1555:                 return $this->_pkey[0];
    1556:             }
    1557:
    1558:             throw new \Exception('Unknown field: '.$name .' (index '.$index.')');
    1559:         }
    1560:
    1561:         return $field->dbField();
    1562:     }
    1563:
    1564:
    1565:     /**
    1566:      * Sorting requirements to a server-side processing query.
    1567:      *  @param \DataTables\Database\Query $query Query instance to apply sorting to
    1568:      *  @param array $http HTTP variables (i.e. GET or POST)
    1569:      *  @private
    1570:      */
    1571:     private function _ssp_sort ( $query, $http )
    1572:     {
    1573:         if ( isset( $http['order'] ) ) {
    1574:             for ( $i=0 ; $i<count($http['order']) ; $i++ ) {
    1575:                 $order = $http['order'][$i];
    1576:
    1577:                 $query->order(
    1578:                     $this->_ssp_field( $http, $order['column'] ) .' '.
    1579:                     ($order['dir']==='asc' ? 'asc' : 'desc')
    1580:                 );
    1581:             }
    1582:         }
    1583:     }
    1584:
    1585:
    1586:     /**
    1587:      * Add DataTables' 'where' condition to a server-side processing query. This
    1588:      * works for both global and individual column filtering.
    1589:      *  @param \DataTables\Database\Query $query Query instance to apply the WHERE conditions to
    1590:      *  @param array $http HTTP variables (i.e. GET or POST)
    1591:      *  @private
    1592:      */
    1593:     private function _ssp_filter ( $query, $http )
    1594:     {
    1595:         $that = $this;
    1596:
    1597:         // Global filter
    1598:         $fields = $this->_fields;
    1599:
    1600:         // Global search, add a ( ... or ... ) set of filters for each column
    1601:         // in the table (not the fields, just the columns submitted)
    1602:         if ( $http['search']['value'] ) {
    1603:             $query->where( function ($q) use (&$that, &$fields, $http) {
    1604:                 for ( $i=0 ; $i<count($http['columns']) ; $i++ ) {
    1605:                     if ( $http['columns'][$i]['searchable'] == 'true' ) {
    1606:                         $field = $that->_ssp_field( $http, $i );
    1607:
    1608:                         if ( $field ) {
    1609:                             $q->or_where( $field, '%'.$http['search']['value'].'%', 'like' );
    1610:                         }
    1611:                     }
    1612:                 }
    1613:             } );
    1614:         }
    1615:
    1616:         // if ( $http['search']['value'] ) {
    1617:         //  $words = explode(" ", $http['search']['value']);
    1618:
    1619:         //  $query->where( function ($q) use (&$that, &$fields, $http, $words) {
    1620:         //      for ( $j=0, $jen=count($words) ; $j<$jen ; $j++ ) {
    1621:         //          if ( $words[$j] ) {
    1622:         //              $q->where_group( true );
    1623:
    1624:         //              for ( $i=0, $ien=count($http['columns']) ; $i<$ien ; $i++ ) {
    1625:         //                  if ( $http['columns'][$i]['searchable'] == 'true' ) {
    1626:         //                      $field = $that->_ssp_field( $http, $i );
    1627:
    1628:         //                      $q->or_where( $field, $words[$j].'%', 'like' );
    1629:         //                      $q->or_where( $field, '% '.$words[$j].'%', 'like' );
    1630:         //                  }
    1631:         //              }
    1632:
    1633:         //              $q->where_group( false );
    1634:         //          }
    1635:         //      }
    1636:         //  } );
    1637:         // }
    1638:
    1639:         // Column filters
    1640:         for ( $i=0, $ien=count($http['columns']) ; $i<$ien ; $i++ ) {
    1641:             $column = $http['columns'][$i];
    1642:             $search = $column['search']['value'];
    1643:
    1644:             if ( $search !== '' && $column['searchable'] == 'true' ) {
    1645:                 $query->where( $this->_ssp_field( $http, $i ), '%'.$search.'%', 'like' );
    1646:             }
    1647:         }
    1648:     }
    1649:
    1650:
    1651:     /**
    1652:      * Add a limit / offset to a server-side processing query
    1653:      *  @param \DataTables\Database\Query $query Query instance to apply the offset / limit to
    1654:      *  @param array $http HTTP variables (i.e. GET or POST)
    1655:      *  @private
    1656:      */
    1657:     private function _ssp_limit ( $query, $http )
    1658:     {
    1659:         if ( $http['length'] != -1 ) { // -1 is 'show all' in DataTables
    1660:             $query
    1661:                 ->offset( $http['start'] )
    1662:                 ->limit( $http['length'] );
    1663:         }
    1664:     }
    1665:
    1666:
    1667:     /*  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *  *
    1668:      * Internal helper methods
    1669:      */
    1670:
    1671:     /**
    1672:      * Add left join commands for the instance to a query.
    1673:      *
    1674:      *  @param \DataTables\Database\Query $query Query instance to apply the joins to
    1675:      *  @private
    1676:      */
    1677:     private function _perform_left_join ( $query )
    1678:     {
    1679:         if ( count($this->_leftJoin) ) {
    1680:             for ( $i=0, $ien=count($this->_leftJoin) ; $i<$ien ; $i++ ) {
    1681:                 $join = $this->_leftJoin[$i];
    1682:
    1683:                 $query->join( $join['table'], $join['field1'].' '.$join['operator'].' '.$join['field2'], 'LEFT' );
    1684:             }
    1685:         }
    1686:     }
    1687:
    1688:
    1689:     /**
    1690:      * Add local WHERE condition to query
    1691:      *  @param \DataTables\Database\Query $query Query instance to apply the WHERE conditions to
    1692:      *  @private
    1693:      */
    1694:     private function _get_where ( $query )
    1695:     {
    1696:         for ( $i=0 ; $i<count($this->_where) ; $i++ ) {
    1697:             if ( is_callable( $this->_where[$i] ) ) {
    1698:                 $this->_where[$i]( $query );
    1699:             }
    1700:             else {
    1701:                 $query->where(
    1702:                     $this->_where[$i]['key'],
    1703:                     $this->_where[$i]['value'],
    1704:                     $this->_where[$i]['op']
    1705:                 );
    1706:             }
    1707:         }
    1708:     }
    1709:
    1710:
    1711:     /**
    1712:      * Get a field instance from a known field name
    1713:      *
    1714:      *  @param string $name Field name
    1715:      *  @param string $type Matching name type
    1716:      *  @return Field Field instance
    1717:      *  @private
    1718:      */
    1719:     private function _find_field ( $name, $type )
    1720:     {
    1721:         for ( $i=0, $ien=count($this->_fields) ; $i<$ien ; $i++ ) {
    1722:             $field = $this->_fields[ $i ];
    1723:
    1724:             if ( $type === 'name' && $field->name() === $name ) {
    1725:                 return $field;
    1726:             }
    1727:             else if ( $type === 'db' && $field->dbField() === $name ) {
    1728:                 return $field;
    1729:             }
    1730:         }
    1731:
    1732:         return null;
    1733:     }
    1734:
    1735:
    1736:     /**
    1737:      * Insert or update a row for all main tables and left joined tables.
    1738:      *
    1739:      *  @param int|string $id ID to use to condition the update. If null is
    1740:      *      given, the first query performed is an insert and the inserted id
    1741:      *      used as the value should there be any subsequent tables to operate
    1742:      *      on. Mote that for compound keys, this should be the "joined" value
    1743:      *      (i.e. a single string rather than an array).
    1744:      *  @return \DataTables\Database\Result Result from the query or null if no
    1745:      *      query performed.
    1746:      *  @private
    1747:      */
    1748:     private function _insert_or_update ( $id, $values )
    1749:     {
    1750:         // Loop over all tables in _table, doing the insert or update as needed
    1751:         for ( $i=0, $ien=count( $this->_table ) ; $i<$ien ; $i++ ) {
    1752:             $res = $this->_insert_or_update_table(
    1753:                 $this->_table[$i],
    1754:                 $values,
    1755:                 $id !== null ?
    1756:                     $this->pkeyToArray( $id, true ) :
    1757:                     null
    1758:             );
    1759:
    1760:             // If we don't have an id yet, then the first insert will return
    1761:             // the id we want
    1762:             if ( $res !== null && $id === null ) {
    1763:                 $id = $res->insertId();
    1764:             }
    1765:         }
    1766:
    1767:         // And for the left join tables as well
    1768:         for ( $i=0, $ien=count( $this->_leftJoin ) ; $i<$ien ; $i++ ) {
    1769:             $join = $this->_leftJoin[$i];
    1770:
    1771:             // which side of the join refers to the parent table?
    1772:             $joinTable = $this->_alias( $join['table'], 'alias' );
    1773:             $tablePart = $this->_part( $join['field1'] );
    1774:
    1775:             if ( $this->_part( $join['field1'], 'db' ) ) {
    1776:                 $tablePart = $this->_part( $join['field1'], 'db' ).'.'.$tablePart;
    1777:             }
    1778:
    1779:             if ( $tablePart === $joinTable ) {
    1780:                 $parentLink = $join['field2'];
    1781:                 $childLink = $join['field1'];
    1782:             }
    1783:             else {
    1784:                 $parentLink = $join['field1'];
    1785:                 $childLink = $join['field2'];
    1786:             }
    1787:
    1788:             if ( $parentLink === $this->_pkey[0] && count($this->_pkey) === 1 ) {
    1789:                 $whereVal = $id;
    1790:             }
    1791:             else {
    1792:                 // We need submitted information about the joined data to be
    1793:                 // submitted as well as the new value. We first check if the
    1794:                 // host field was submitted
    1795:                 $field = $this->_find_field( $parentLink, 'db' );
    1796:
    1797:                 if ( ! $field || ! $field->apply( 'set', $values ) ) {
    1798:                     // If not, then check if the child id was submitted
    1799:                     $field = $this->_find_field( $childLink, 'db' );
    1800:
    1801:                     // No data available, so we can't do anything
    1802:                     if ( ! $field || ! $field->apply( 'set', $values ) ) {
    1803:                         continue;
    1804:                     }
    1805:                 }
    1806:
    1807:                 $whereVal = $field->val('set', $values);
    1808:             }
    1809:
    1810:             $whereName = $this->_part( $childLink, 'field' );
    1811:
    1812:             $this->_insert_or_update_table(
    1813:                 $join['table'],
    1814:                 $values,
    1815:                 array( $whereName => $whereVal )
    1816:             );
    1817:         }
    1818:
    1819:         return $id;
    1820:     }
    1821:
    1822:
    1823:     /**
    1824:      * Insert or update a row in a single database table, based on the data
    1825:      * given and the fields configured for the instance.
    1826:      *
    1827:      * The function will find the fields which are required for this specific
    1828:      * table, based on the names of fields and use only the appropriate data for
    1829:      * this table. Therefore the full submitted data set can be passed in.
    1830:      *
    1831:      *  @param string $table Database table name to use (can include an alias)
    1832:      *  @param array $where Update condition
    1833:      *  @return \DataTables\Database\Result Result from the query or null if no query
    1834:      *      performed.
    1835:      *  @throws \Exception Where set error
    1836:      *  @private
    1837:      */
    1838:     private function _insert_or_update_table ( $table, $values, $where=null )
    1839:     {
    1840:         $set = array();
    1841:         $action = ($where === null) ? 'create' : 'edit';
    1842:         $tableAlias = $this->_alias( $table, 'alias' );
    1843:
    1844:         for ( $i=0 ; $i<count($this->_fields) ; $i++ ) {
    1845:             $field = $this->_fields[$i];
    1846:             $tablePart = $this->_part( $field->dbField() );
    1847:
    1848:             if ( $this->_part( $field->dbField(), 'db' ) ) {
    1849:                 $tablePart = $this->_part( $field->dbField(), 'db' ).'.'.$tablePart;
    1850:             }
    1851:
    1852:             // Does this field apply to this table (only check when a join is
    1853:             // being used)
    1854:             if ( count($this->_leftJoin) && $tablePart !== $tableAlias ) {
    1855:                 continue;
    1856:             }
    1857:
    1858:             // Check if this field should be set, based on options and
    1859:             // submitted data
    1860:             if ( ! $field->apply( $action, $values ) ) {
    1861:                 continue;
    1862:             }
    1863:
    1864:             // Some db's (specifically postgres) don't like having the table
    1865:             // name prefixing the column name. Todo: it might be nicer to have
    1866:             // the db layer abstract this out?
    1867:             $fieldPart = $this->_part( $field->dbField(), 'field' );
    1868:             $set[ $fieldPart ] = $field->val( 'set', $values );
    1869:         }
    1870:
    1871:         // Add where fields if setting where values and required for this
    1872:         // table
    1873:         // Note that `whereSet` is now deprecated
    1874:         if ( $this->_whereSet ) {
    1875:             for ( $j=0, $jen=count($this->_where) ; $j<$jen ; $j++ ) {
    1876:                 $cond = $this->_where[$j];
    1877:
    1878:                 if ( ! is_callable( $cond ) ) {
    1879:                     // Make sure the value wasn't in the submitted data set,
    1880:                     // otherwise we would be overwriting it
    1881:                     if ( ! isset( $set[ $cond['key'] ] ) )
    1882:                     {
    1883:                         $whereTablePart = $this->_part( $cond['key'], 'table' );
    1884:
    1885:                         // No table part on the where condition to match against
    1886:                         // or table operating on matches table part from cond.
    1887:                         if ( ! $whereTablePart || $tableAlias == $whereTablePart ) {
    1888:                             $set[ $cond['key'] ] = $cond['value'];
    1889:                         }
    1890:                     }
    1891:                     else {
    1892:                         throw new \Exception( 'Where condition used as a setter, '.
    1893:                             'but value submitted for field: '.$cond['key']
    1894:                         );
    1895:                     }
    1896:                 }
    1897:             }
    1898:         }
    1899:
    1900:         // If nothing to do, then do nothing!
    1901:         if ( ! count( $set ) ) {
    1902:             return null;
    1903:         }
    1904:
    1905:         // Use pkey only for the host table
    1906:         $pkey = in_array( $table, $this->_table ) !== false ?
    1907:             $this->_pkey :
    1908:             '';
    1909:
    1910:         // Insert or update
    1911:         if ( $action === 'create' ) {
    1912:             return $this->_db->insert( $table, $set, $pkey );
    1913:         }
    1914:         else {
    1915:             return $this->_db->push( $table, $set, $where, $pkey );
    1916:         }
    1917:     }
    1918:
    1919:
    1920:     /**
    1921:      * Delete one or more rows from the database for an individual table
    1922:      *
    1923:      * @param string $table Database table name to use
    1924:      * @param array $ids Array of ids to remove
    1925:      * @param string $pkey Database column name to match the ids on for the
    1926:      *   delete condition. If not given the instance's base primary key is
    1927:      *   used.
    1928:      * @private
    1929:      */
    1930:     private function _remove_table ( $table, $ids, $pkey=null )
    1931:     {
    1932:         if ( $pkey === null ) {
    1933:             $pkey = $this->_pkey;
    1934:         }
    1935:
    1936:         // Check there is a field which has a set option for this table
    1937:         $count = 0;
    1938:
    1939:         foreach ($this->_fields as $field) {
    1940:             if ( strpos( $field->dbField(), '.') === false || (
    1941:                     $this->_part( $field->dbField(), 'table' ) === $table &&
    1942:                     $field->set() !== Field::SET_NONE
    1943:                 )
    1944:             ) {
    1945:                 $count++;
    1946:             }
    1947:         }
    1948:
    1949:         if ( $count > 0 ) {
    1950:             $q = $this->_db
    1951:                 ->query( 'delete' )
    1952:                 ->table( $table );
    1953:
    1954:             for ( $i=0, $ien=count($ids) ; $i<$ien ; $i++ ) {
    1955:                 $cond = $this->pkeyToArray( $ids[$i], true, $pkey );
    1956:
    1957:                 $q->or_where( function ($q2) use ($cond) {
    1958:                     $q2->where( $cond );
    1959:                 } );
    1960:             }
    1961:
    1962:             $q->exec();
    1963:         }
    1964:     }
    1965:
    1966:
    1967:     /**
    1968:      * Check the validity of the set options if  we are doing a join, since
    1969:      * there are some conditions for this state. Will throw an error if not
    1970:      * valid.
    1971:      *
    1972:      *  @private
    1973:      */
    1974:     private function _prepJoin ()
    1975:     {
    1976:         if ( count( $this->_leftJoin ) === 0 ) {
    1977:             return;
    1978:         }
    1979:
    1980:         // Check if the primary key has a table identifier - if not - add one
    1981:         for ( $i=0, $ien=count($this->_pkey) ; $i<$ien ; $i++ ) {
    1982:             $val = $this->_pkey[$i];
    1983:
    1984:             if ( strpos( $val, '.' ) === false ) {
    1985:                 $this->_pkey[$i] = $this->_alias( $this->_table[0], 'alias' ).'.'.$val;
    1986:             }
    1987:         }
    1988:
    1989:         // Check that all fields have a table selector, otherwise, we'd need to
    1990:         // know the structure of the tables, to know which fields belong in
    1991:         // which. This extra requirement on the fields removes that
    1992:         for ( $i=0, $ien=count($this->_fields) ; $i<$ien ; $i++ ) {
    1993:             $field = $this->_fields[$i];
    1994:             $name = $field->dbField();
    1995:
    1996:             if ( strpos( $name, '.' ) === false ) {
    1997:                 throw new \Exception( 'Table part of the field "'.$name.'" was not found. '.
    1998:                     'In Editor instances that use a join, all fields must have the '.
    1999:                     'database table set explicitly.'
    2000:                 );
    2001:             }
    2002:         }
    2003:     }
    2004:
    2005:
    2006:     /**
    2007:      * Get one side or the other of an aliased SQL field name.
    2008:      *
    2009:      *  @param string $name SQL field
    2010:      *  @param string $type Which part to get: `alias` (default) or `orig`.
    2011:      *  @returns string Alias
    2012:      *  @private
    2013:      */
    2014:     private function _alias ( $name, $type='alias' )
    2015:     {
    2016:         if ( stripos( $name, ' as ' ) !== false ) {
    2017:             $a = preg_split( '/ as /i', $name );
    2018:             return $type === 'alias' ?
    2019:                 $a[1] :
    2020:                 $a[0];
    2021:         }
    2022:
    2023:         if ( stripos( $name, ' ' ) !== false ) {
    2024:             $a = preg_split( '/ /i', $name );
    2025:             return $type === 'alias' ?
    2026:                 $a[1] :
    2027:                 $a[0];
    2028:         }
    2029:
    2030:         return $name;
    2031:     }
    2032:
    2033:
    2034:     /**
    2035:      * Get part of an SQL field definition regardless of how deeply defined it
    2036:      * is
    2037:      *
    2038:      *  @param string $name SQL field
    2039:      *  @param string $type Which part to get: `table` (default) or `db` or
    2040:      *      `column`
    2041:      *  @return string Part name
    2042:      *  @private
    2043:      */
    2044:     private function _part ( $name, $type='table' )
    2045:     {
    2046:         $db = null;
    2047:         $table = null;
    2048:         $column = null;
    2049:
    2050:         if ( strpos( $name, '.' ) !== false ) {
    2051:             $a = explode( '.', $name );
    2052:
    2053:             if ( count($a) === 3 ) {
    2054:                 $db = $a[0];
    2055:                 $table = $a[1];
    2056:                 $column = $a[2];
    2057:             }
    2058:             else if ( count($a) === 2 ) {
    2059:                 $table = $a[0];
    2060:                 $column = $a[1];
    2061:             }
    2062:         }
    2063:         else {
    2064:             $column = $name;
    2065:         }
    2066:
    2067:         if ( $type === 'db' ) {
    2068:             return $db;
    2069:         }
    2070:         else if ( $type === 'table' ) {
    2071:             return $table;
    2072:         }
    2073:         return $column;
    2074:     }
    2075:
    2076:
    2077:     /**
    2078:      * Trigger an event
    2079:      *
    2080:      * @private
    2081:      */
    2082:     private function _trigger ( $eventName, &$arg1=null, &$arg2=null, &$arg3=null, &$arg4=null, &$arg5=null )
    2083:     {
    2084:         $out = null;
    2085:         $argc = func_num_args();
    2086:         $args = array( $this );
    2087:
    2088:         // Hack to enable pass by reference with a "variable" number of parameters
    2089:         for ( $i=1 ; $i<$argc ; $i++ ) {
    2090:             $name = 'arg'.$i;
    2091:             $args[] = &$$name;
    2092:         }
    2093:
    2094:         if ( ! isset( $this->_events[ $eventName ] ) ) {
    2095:             return;
    2096:         }
    2097:
    2098:         $events = $this->_events[ $eventName ];
    2099:
    2100:         for ( $i=0, $ien=count($events) ; $i<$ien ; $i++ ) {
    2101:             $res = call_user_func_array( $events[$i], $args );
    2102:
    2103:             if ( $res !== null ) {
    2104:                 $out = $res;
    2105:             }
    2106:         }
    2107:
    2108:         return $out;
    2109:     }
    2110:
    2111:
    2112:     /**
    2113:      * Merge a primary key value with an updated data source.
    2114:      *
    2115:      * @param  string $pkeyVal Old primary key value to merge into
    2116:      * @param  array  $row     Data source for update
    2117:      * @return string          Merged value
    2118:      */
    2119:     private function _pkey_submit_merge ( $pkeyVal, $row )
    2120:     {
    2121:         $pkey = $this->_pkey;
    2122:         $arr = $this->pkeyToArray( $pkeyVal, true );
    2123:
    2124:         for ( $i=0, $ien=count($pkey) ; $i<$ien ; $i++ ) {
    2125:             $column = $pkey[ $i ];
    2126:             $field = $this->_find_field( $column, 'db' );
    2127:
    2128:             if ( $field && $field->apply( 'edit', $row ) ) {
    2129:                 $arr[ $column ] = $field->val( 'set', $row );
    2130:             }
    2131:         }
    2132:
    2133:         return $this->pkeyToValue( $arr, true );
    2134:     }
    2135:
    2136:
    2137:     /**
    2138:      * Validate that all primary key fields have values for create.
    2139:      *
    2140:      * @param  array $row Row's data
    2141:      * @return boolean    `true` if valid, `false` otherwise
    2142:      */
    2143:     private function _pkey_validate_insert ( $row )
    2144:     {
    2145:         $pkey = $this->_pkey;
    2146:
    2147:         if ( count( $pkey ) === 1 ) {
    2148:             return true;
    2149:         }
    2150:
    2151:         for ( $i=0, $ien=count($pkey) ; $i<$ien ; $i++ ) {
    2152:             $column = $pkey[ $i ];
    2153:             $field = $this->_find_field( $column, 'db' );
    2154:
    2155:             if ( ! $field || ! $field->apply("create", $row) ) {
    2156:                 throw new \Exception( "When inserting into a compound key table, ".
    2157:                     "all fields that are part of the compound key must be ".
    2158:                     "submitted with a specific value.", 1
    2159:                 );
    2160:             }
    2161:         }
    2162:
    2163:         return true;
    2164:     }
    2165:
    2166:
    2167:     /**
    2168:      * Create the separator value for a compound primary key.
    2169:      *
    2170:      * @return string Calculated separator
    2171:      */
    2172:     private function _pkey_separator ()
    2173:     {
    2174:         $str = implode(',', $this->_pkey);
    2175:
    2176:         return hash( 'crc32', $str );
    2177:     }
    2178:
    2179:     private function _read_table ()
    2180:     {
    2181:         return count($this->_readTableNames) ?
    2182:             $this->_readTableNames :
    2183:             $this->_table;
    2184:     }
    2185: }
    2186:
    2187: