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: * @copyright 2015 SpryMedia ( http://sprymedia.co.uk )
9: * @license http://editor.datatables.net/license DataTables Editor
10: * @link http://editor.datatables.net
11: */
12:
13: namespace DataTables\Editor;
14: if (!defined('DATATABLES')) exit();
15:
16: use DataTables;
17:
18:
19: /**
20: * Upload class for Editor. This class provides the ability to easily specify
21: * file upload information, specifically how the file should be recorded on
22: * the server (database and file system).
23: *
24: * An instance of this class is attached to a field using the {@link
25: * Field.upload} method. When Editor detects a file upload for that file the
26: * information provided for this instance is executed.
27: *
28: * The configuration is primarily driven through the {@link db} and {@link
29: * action} methods:
30: *
31: * * {@link db} Describes how information about the uploaded file is to be
32: * stored on the database.
33: * * {@link action} Describes where the file should be stored on the file system
34: * and provides the option of specifying a custom action when a file is
35: * uploaded.
36: *
37: * Both methods are optional - you can store the file on the server using the
38: * {@link db} method only if you want to store the file in the database, or if
39: * you don't want to store relational data on the database us only {@link
40: * action}. However, the majority of the time it is best to use both - store
41: * information about the file on the database for fast retrieval (using a {@link
42: * Editor.leftJoin()} for example) and the file on the file system for direct
43: * web access.
44: *
45: * @example
46: * Store information about a file in a table called `files` and the actual
47: * file in an `uploads` directory.
48: * <code>
49: * Field::inst( 'imageId' )
50: * ->upload(
51: * Upload::inst( $_SERVER['DOCUMENT_ROOT'].'/uploads/__ID__.__EXTN__' )
52: * ->db( 'files', 'id', array(
53: * 'webPath' => Upload::DB_WEB_PATH,
54: * 'fileName' => Upload::DB_FILE_NAME,
55: * 'fileSize' => Upload::DB_FILE_SIZE,
56: * 'systemPath' => Upload::DB_SYSTEM_PATH
57: * ) )
58: * ->allowedExtensions( array( 'png', 'jpg' ), "Please upload an image file" )
59: * )
60: * </code>
61: *
62: * @example
63: * As above, but with PHP 5.4 (which allows chaining from new instances of a
64: * class)
65: * <code>
66: * newField( 'imageId' )
67: * ->upload(
68: * new Upload( $_SERVER['DOCUMENT_ROOT'].'/uploads/__ID__.__EXTN__' )
69: * ->db( 'files', 'id', array(
70: * 'webPath' => Upload::DB_WEB_PATH,
71: * 'fileName' => Upload::DB_FILE_NAME,
72: * 'fileSize' => Upload::DB_FILE_SIZE,
73: * 'systemPath' => Upload::DB_SYSTEM_PATH
74: * ) )
75: * ->allowedExtensions( array( 'png', 'jpg' ), "Please upload an image file" )
76: * )
77: * </code>
78: */
79: class Upload extends DataTables\Ext {
80: /* * * * * * * * * * * * * * * * * * * * * * * * *
81: * Constants
82: */
83:
84: /** Database value option (`Db()`) - File content. This should be written to
85: * a blob. Typically this should be avoided and the file saved on the file
86: * system, but there are cases where it can be useful to store the file in
87: * the database.
88: */
89: const DB_CONTENT = 'editor-content';
90:
91: /** Database value option (`Db()`) - Content type */
92: const DB_CONTENT_TYPE = 'editor-contentType';
93:
94: /** Database value option (`Db()`) - File extension */
95: const DB_EXTN = 'editor-extn';
96:
97: /** Database value option (`Db()`) - File name (with extension) */
98: const DB_FILE_NAME = 'editor-fileName';
99:
100: /** Database value option (`Db()`) - File size (bytes) */
101: const DB_FILE_SIZE = 'editor-fileSize';
102:
103: /** Database value option (`Db()`) - MIME type */
104: const DB_MIME_TYPE = 'editor-mimeType';
105:
106: /** Database value option (`Db()`) - Full system path to the file */
107: const DB_SYSTEM_PATH = 'editor-systemPath';
108:
109: /** Database value option (`Db()`) - HTTP path to the file. This is derived
110: * from the system path by removing `$_SERVER['DOCUMENT_ROOT']`. If your
111: * images live outside of the document root a custom value would be to be
112: * used.
113: */
114: const DB_WEB_PATH = 'editor-webPath';
115:
116: /** Read from the database - don't write to it
117: */
118: const DB_READ_ONLY = 'editor-readOnly';
119:
120:
121: /* * * * * * * * * * * * * * * * * * * * * * * * *
122: * Private parameters
123: */
124:
125: private $_action = null;
126: private $_dbCleanCallback = null;
127: private $_dbCleanTableField = null;
128: private $_dbTable = null;
129: private $_dbPKey = null;
130: private $_dbFields = null;
131: private $_extns = null;
132: private $_extnError = null;
133: private $_error = null;
134: private $_validators = array();
135: private $_where = array();
136:
137:
138: /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
139: * Constructor
140: */
141:
142: /**
143: * Upload instance constructor
144: * @param string|callable $action Action to take on upload - this is applied
145: * directly to {@link action}.
146: */
147: function __construct( $action=null )
148: {
149: if ( $action ) {
150: $this->action( $action );
151: }
152: }
153:
154:
155: /* * * * * * * * * * * * * * * * * * * * * * * * *
156: * Public methods
157: */
158:
159: /**
160: * Set the action to take when a file is uploaded. This can be either of:
161: *
162: * * A string - the value given is the full system path to where the
163: * uploaded file is written to. The value given can include three "macros"
164: * which are replaced by the script dependent on the uploaded file:
165: * * `__EXTN__` - the file extension
166: * * `__NAME__` - the uploaded file's name (including the extension)
167: * * `__ID__` - Database primary key value if the {@link db} method is
168: * used.
169: * * A closure - if a function is given the responsibility of what to do
170: * with the uploaded file is transferred to this function. That will
171: * typically involve writing it to the file system so it can be used
172: * later.
173: *
174: * @param string|callable $action Action to take - see description above.
175: * @return self Current instance, used for chaining
176: */
177: public function action ( $action )
178: {
179: $this->_action = $action;
180:
181: return $this;
182: }
183:
184:
185: /**
186: * An array of valid file extensions that can be uploaded. This is for
187: * simple validation that the file is of the expected type - for example you
188: * might use `[ 'png', 'jpg', 'jpeg', 'gif' ]` for images. The check is
189: * case-insensitive. If no extensions are given, no validation is performed
190: * on the file extension.
191: *
192: * @param string[] $extn List of file extensions that are allowable for
193: * the upload
194: * @param string $error Error message if a file is uploaded that doesn't
195: * match the valid list of extensions.
196: * @return self Current instance, used for chaining
197: * @deprecated Use Validate::fileExtensions
198: */
199: public function allowedExtensions ( $extn, $error="This file type cannot be uploaded" )
200: {
201: $this->_extns = $extn;
202: $this->_extnError = $error;
203:
204: return $this;
205: }
206:
207:
208: /**
209: * Database configuration method. When used, this method will tell Editor
210: * what information you want written to a database on file upload, should
211: * you wish to store relational information about your file on the database
212: * (this is generally recommended).
213: *
214: * @param string $table The name of the table where the file information
215: * should be stored
216: * @param string $pkey Primary key column name. The `Upload` class
217: * requires that the database table have a single primary key so each
218: * row can be uniquely identified.
219: * @param array $fields A list of the fields to be written to on upload.
220: * The property names are the database columns and the values can be
221: * defined by the constants of this class. The value can also be a
222: * string or a closure function if you wish to send custom information
223: * to the database.
224: * @return self Current instance, used for chaining
225: */
226: public function db ( $table, $pkey, $fields )
227: {
228: $this->_dbTable = $table;
229: $this->_dbPKey = $pkey;
230: $this->_dbFields = $fields;
231:
232: return $this;
233: }
234:
235:
236: /**
237: * Set a callback function that is used to remove files which no longer have
238: * a reference in a source table.
239: *
240: * @param callable $callback Function that will be executed on clean. It is
241: * given an array of information from the database about the orphaned
242: * rows, and can return true to indicate that the rows should be
243: * removed from the database. Any other return value (including none)
244: * will result in the records being retained.
245: * @return self Current instance, used for chaining
246: */
247: public function dbClean( $tableField, $callback=null )
248: {
249: // Argument swapping
250: if ( $callback === null ) {
251: $callback = $tableField;
252: $tableField = null;
253: }
254:
255: $this->_dbCleanCallback = $callback;
256: $this->_dbCleanTableField = $tableField;
257:
258: return $this;
259: }
260:
261:
262: /**
263: * Add a validation method to check file uploads. Multiple validators can be
264: * added by calling this method multiple times - they will be executed in
265: * sequence when a file has been uploaded.
266: *
267: * @param callable $fn Validation function. A PHP `$_FILES` parameter is
268: * passed in for the uploaded file and the return is either a string
269: * (validation failed and error message), or `null` (validation passed).
270: * @return self Current instance, used for chaining
271: */
272: public function validator ( $fn )
273: {
274: $this->_validators[] = $fn;
275:
276: return $this;
277: }
278:
279:
280: /**
281: * Add a condition to the data to be retrieved from the database. This
282: * must be given as a function to be executed (usually anonymous) and
283: * will be passed in a single argument, the `Query` object, to which
284: * conditions can be added. Multiple calls to this method can be made.
285: *
286: * @param callable $fn Where function.
287: * @return self Current instance, used for chaining
288: */
289: public function where ( $fn )
290: {
291: $this->_where[] = $fn;
292:
293: return $this;
294: }
295:
296:
297: /* * * * * * * * * * * * * * * * * * * * * * * * *
298: * Internal methods
299: */
300:
301: /**
302: * Get database information data from the table
303: *
304: * @param \DataTables\Database $db Database
305: * @param number[] [$ids=null] Limit to a specific set of ids
306: * @return array Database information
307: * @internal
308: */
309: public function data ( $db, $ids=null )
310: {
311: if ( ! $this->_dbTable ) {
312: return null;
313: }
314:
315: // Select the details requested, for the columns requested
316: $q = $db
317: ->query( 'select' )
318: ->table( $this->_dbTable )
319: ->get( $this->_dbPKey );
320:
321: foreach ( $this->_dbFields as $column => $prop ) {
322: if ( $prop !== self::DB_CONTENT ) {
323: $q->get( $column );
324: }
325: }
326:
327: if ( $ids !== null ) {
328: $q->where_in( $this->_dbPKey, $ids );
329: }
330:
331: for ( $i=0, $ien=count($this->_where) ; $i<$ien ; $i++ ) {
332: $q->where( $this->_where[$i] );
333: }
334:
335: $result = $q->exec()->fetchAll();
336: $out = array();
337:
338: for ( $i=0, $ien=count($result) ; $i<$ien ; $i++ ) {
339: $out[ $result[$i][ $this->_dbPKey ] ] = $result[$i];
340: }
341:
342: return $out;
343: }
344:
345:
346: /**
347: * Clean the database
348: * @param \DataTables\Editor $editor Calling Editor instance
349: * @param Field $field Host field
350: * @internal
351: */
352: public function dbCleanExec ( $editor, $field )
353: {
354: // Database and file system clean up BEFORE adding the new file to
355: // the db, otherwise it will be removed immediately
356: $tables = $editor->table();
357: $this->_dbClean( $editor->db(), $tables[0], $field->dbField() );
358: }
359:
360:
361: /**
362: * Get the set error message
363: *
364: * @return string Class error
365: * @internal
366: */
367: public function error ()
368: {
369: return $this->_error;
370: }
371:
372:
373: /**
374: * Execute an upload
375: *
376: * @param \DataTables\Editor $editor Calling Editor instance
377: * @return int Primary key value
378: * @internal
379: */
380: public function exec ( $editor )
381: {
382: $id = null;
383: $upload = $_FILES['upload'];
384:
385: // Validation - PHP standard validation
386: if ( $upload['error'] !== UPLOAD_ERR_OK ) {
387: if ( $upload['error'] === UPLOAD_ERR_INI_SIZE ) {
388: $this->_error = "File exceeds maximum file upload size";
389: }
390: else {
391: $this->_error = "There was an error uploading the file (".$upload['error'].")";
392: }
393: return false;
394: }
395:
396: // Validation - acceptable file extensions
397: if ( is_array( $this->_extns ) ) {
398: $extn = pathinfo($upload['name'], PATHINFO_EXTENSION);
399:
400: if ( in_array( strtolower($extn), array_map( 'strtolower', $this->_extns ) ) === false ) {
401: $this->_error = $this->_extnError;
402: return false;
403: }
404: }
405:
406: // Validation - custom callback
407: for ( $i=0, $ien=count($this->_validators) ; $i<$ien ; $i++ ) {
408: $res = $this->_validators[$i]( $upload );
409:
410: if ( is_string( $res ) ) {
411: $this->_error = $res;
412: return false;
413: }
414: }
415:
416: // Database
417: if ( $this->_dbTable ) {
418: foreach ( $this->_dbFields as $column => $prop ) {
419: // We can't know what the path is, if it has moved into place
420: // by an external function - throw an error if this does happen
421: if ( ! is_string( $this->_action ) &&
422: ($prop === self::DB_SYSTEM_PATH || $prop === self::DB_WEB_PATH )
423: ) {
424: $this->_error = "Cannot set path information in database ".
425: "if a custom method is used to save the file.";
426:
427: return false;
428: }
429: }
430:
431: // Commit to the database
432: $id = $this->_dbExec( $editor->db() );
433: }
434:
435: // Perform file system actions
436: return $this->_actionExec( $id );
437: }
438:
439:
440: /**
441: * Get the primary key column for the table
442: *
443: * @return string Primary key column name
444: * @internal
445: */
446: public function pkey ()
447: {
448: return $this->_dbPKey;
449: }
450:
451:
452: /**
453: * Get the db table name
454: *
455: * @return string DB table name
456: * @internal
457: */
458: public function table ()
459: {
460: return $this->_dbTable;
461: }
462:
463:
464:
465: /* * * * * * * * * * * * * * * * * * * * * * * * *
466: * Private methods
467: */
468:
469: /**
470: * Execute the configured action for the upload
471: *
472: * @param int $id Primary key value
473: * @return int File identifier - typically the primary key
474: */
475: private function _actionExec ( $id )
476: {
477: $upload = $_FILES['upload'];
478:
479: if ( ! is_string( $this->_action ) ) {
480: // Custom function
481: $action = $this->_action;
482: return $action( $upload, $id );
483: }
484:
485: // Default action - move the file to the location specified by the
486: // action string
487: $to = $this->_path( $upload['name'], $id );
488: $res = move_uploaded_file( $upload['tmp_name'], $to );
489:
490: if ( $res === false ) {
491: $this->_error = "An error occurred while moving the uploaded file.";
492: return false;
493: }
494:
495: return $id !== null ?
496: $id :
497: $to;
498: }
499:
500: /**
501: * Perform the database clean by first getting the information about the
502: * orphaned rows and then calling the callback function. The callback can
503: * then instruct the rows to be removed through the return value.
504: *
505: * @param \DataTables\Database $db Database instance
506: * @param string $editorTable Editor Editor instance table name
507: * @param string $fieldName Host field's name
508: */
509: private function _dbClean ( $db, $editorTable, $fieldName )
510: {
511: $callback = $this->_dbCleanCallback;
512:
513: if ( ! $this->_dbTable || ! $callback ) {
514: return;
515: }
516:
517: // If there is a table / field that we should use to check if the value
518: // is in use, then use that. Otherwise we'll try to use the information
519: // from the Editor / Field instance.
520: if ( $this->_dbCleanTableField ) {
521: $fieldName = $this->_dbCleanTableField;
522: }
523:
524: $a = explode('.', $fieldName);
525: if ( count($a) === 1 ) {
526: $table = $editorTable;
527: $field = $a[0];
528: }
529: else if ( count($a) === 2 ) {
530: $table = $a[0];
531: $field = $a[1];
532: }
533: else {
534: $table = $a[1];
535: $field = $a[2];
536: }
537:
538: // Select the details requested, for the columns requested
539: $q = $db
540: ->query( 'select' )
541: ->table( $this->_dbTable )
542: ->get( $this->_dbPKey );
543:
544: foreach ( $this->_dbFields as $column => $prop ) {
545: if ( $prop !== self::DB_CONTENT ) {
546: $q->get( $column );
547: }
548: }
549:
550: $q->where( $this->_dbPKey, '(SELECT '.$field.' FROM '.$table.' WHERE '.$field.' IS NOT NULL)', 'NOT IN', false );
551:
552: $data = $q->exec()->fetchAll();
553:
554: if ( count( $data ) === 0 ) {
555: return;
556: }
557:
558: $result = $callback( $data );
559:
560: // Delete the selected rows, iff the developer says to do so with the
561: // returned value (i.e. acknowledge that the files have be removed from
562: // the file system)
563: if ( $result === true ) {
564: $qDelete = $db
565: ->query( 'delete' )
566: ->table( $this->_dbTable );
567:
568: for ( $i=0, $ien=count( $data ) ; $i<$ien ; $i++ ) {
569: $qDelete->or_where( $this->_dbPKey, $data[$i][ $this->_dbPKey ] );
570: }
571:
572: $qDelete->exec();
573: }
574: }
575:
576: /**
577: * Add a record to the database for a newly uploaded file
578: *
579: * @param \DataTables\Database $db Database instance
580: * @return int Primary key value for the newly uploaded file
581: */
582: private function _dbExec ( $db )
583: {
584: $upload = $_FILES['upload'];
585: $pathFields = array();
586:
587: // Insert the details requested, for the columns requested
588: $q = $db
589: ->query( 'insert' )
590: ->table( $this->_dbTable )
591: ->pkey( $this->_dbPKey );
592:
593: foreach ( $this->_dbFields as $column => $prop ) {
594: switch ( $prop ) {
595: case self::DB_READ_ONLY:
596: break;
597:
598: case self::DB_CONTENT:
599: $q->set( $column, file_get_contents($upload['tmp_name']) );
600: break;
601:
602: case self::DB_CONTENT_TYPE:
603: case self::DB_MIME_TYPE:
604: $finfo = finfo_open(FILEINFO_MIME);
605: $mime = finfo_file($finfo, $upload['tmp_name']);
606: finfo_close($finfo);
607:
608: $q->set( $column, $mime );
609: break;
610:
611: case self::DB_EXTN:
612: $extn = pathinfo($upload['name'], PATHINFO_EXTENSION);
613: $q->set( $column, $extn );
614: break;
615:
616: case self::DB_FILE_NAME:
617: $q->set( $column, $upload['name'] );
618: break;
619:
620: case self::DB_FILE_SIZE:
621: $q->set( $column, $upload['size'] );
622: break;
623:
624: case self::DB_SYSTEM_PATH:
625: $pathFields[ $column ] = self::DB_SYSTEM_PATH;
626: $q->set( $column, '-' ); // Use a temporary value to avoid cases
627: break; // where the db will reject empty values
628:
629: case self::DB_WEB_PATH:
630: $pathFields[ $column ] = self::DB_WEB_PATH;
631: $q->set( $column, '-' ); // Use a temporary value (as above)
632: break;
633:
634: default:
635: if ( is_callable($prop) && is_object($prop) ) { // is a closure
636: $q->set( $column, $prop( $db, $upload ) );
637: }
638: else {
639: $q->set( $column, $prop );
640: }
641:
642: break;
643: }
644: }
645:
646: $res = $q->exec();
647: $id = $res->insertId();
648:
649: // Update the newly inserted row with the path information. We have to
650: // use a second statement here as we don't know in advance what the
651: // database schema is and don't want to prescribe that certain triggers
652: // etc be created. It makes it a bit less efficient but much more
653: // compatible
654: if ( count( $pathFields ) ) {
655: // For this to operate the action must be a string, which is
656: // validated in the `exec` method
657: $path = $this->_path( $upload['name'], $id );
658: $webPath = str_replace($_SERVER['DOCUMENT_ROOT'], '', $path);
659: $q = $db
660: ->query( 'update' )
661: ->table( $this->_dbTable )
662: ->where( $this->_dbPKey, $id );
663:
664: foreach ( $pathFields as $column => $type ) {
665: $q->set( $column, $type === self::DB_WEB_PATH ? $webPath : $path );
666: }
667:
668: $q->exec();
669: }
670:
671: return $id;
672: }
673:
674:
675: /**
676: * Apply macros to a user specified path
677: *
678: * @param string $name File path
679: * @param int $id Primary key value for the file
680: * @return string Resolved path
681: */
682: private function _path ( $name, $id )
683: {
684: $extn = pathinfo( $name, PATHINFO_EXTENSION );
685:
686: $to = $this->_action;
687: $to = str_replace( "__NAME__", $name, $to );
688: $to = str_replace( "__ID__", $id, $to );
689: $to = str_replace( "__EXTN__", $extn, $to );
690:
691: return $to;
692: }
693: }
694:
695: