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: */
198: public function allowedExtensions ( $extn, $error="This file type cannot be uploaded" )
199: {
200: $this->_extns = $extn;
201: $this->_extnError = $error;
202:
203: return $this;
204: }
205:
206:
207: /**
208: * Database configuration method. When used, this method will tell Editor
209: * what information you want written to a database on file upload, should
210: * you wish to store relational information about your file on the database
211: * (this is generally recommended).
212: *
213: * @param string $table The name of the table where the file information
214: * should be stored
215: * @param string $pkey Primary key column name. The `Upload` class
216: * requires that the database table have a single primary key so each
217: * row can be uniquely identified.
218: * @param array $fields A list of the fields to be written to on upload.
219: * The property names are the database columns and the values can be
220: * defined by the constants of this class. The value can also be a
221: * string or a closure function if you wish to send custom information
222: * to the database.
223: * @return self Current instance, used for chaining
224: */
225: public function db ( $table, $pkey, $fields )
226: {
227: $this->_dbTable = $table;
228: $this->_dbPKey = $pkey;
229: $this->_dbFields = $fields;
230:
231: return $this;
232: }
233:
234:
235: /**
236: * Set a callback function that is used to remove files which no longer have
237: * a reference in a source table.
238: *
239: * @param callable $callback Function that will be executed on clean. It is
240: * given an array of information from the database about the orphaned
241: * rows, and can return true to indicate that the rows should be
242: * removed from the database. Any other return value (including none)
243: * will result in the records being retained.
244: * @return self Current instance, used for chaining
245: */
246: public function dbClean( $tableField, $callback=null )
247: {
248: // Argument swapping
249: if ( $callback === null ) {
250: $callback = $tableField;
251: $tableField = null;
252: }
253:
254: $this->_dbCleanCallback = $callback;
255: $this->_dbCleanTableField = $tableField;
256:
257: return $this;
258: }
259:
260:
261: /**
262: * Add a validation method to check file uploads. Multiple validators can be
263: * added by calling this method multiple times - they will be executed in
264: * sequence when a file has been uploaded.
265: *
266: * @param callable $fn Validation function. A PHP `$_FILES` parameter is
267: * passed in for the uploaded file and the return is either a string
268: * (validation failed and error message), or `null` (validation passed).
269: * @return self Current instance, used for chaining
270: */
271: public function validator ( $fn )
272: {
273: $this->_validators[] = $fn;
274:
275: return $this;
276: }
277:
278:
279: /**
280: * Add a condition to the data to be retrieved from the database. This
281: * must be given as a function to be executed (usually anonymous) and
282: * will be passed in a single argument, the `Query` object, to which
283: * conditions can be added. Multiple calls to this method can be made.
284: *
285: * @param callable $fn Where function.
286: * @return self Current instance, used for chaining
287: */
288: public function where ( $fn )
289: {
290: $this->_where[] = $fn;
291:
292: return $this;
293: }
294:
295:
296: /* * * * * * * * * * * * * * * * * * * * * * * * *
297: * Internal methods
298: */
299:
300: /**
301: * Get database information data from the table
302: *
303: * @param \DataTables\Database $db Database
304: * @param number [$id=null] Limit to a specific id
305: * @return array Database information
306: * @internal
307: */
308: public function data ( $db, $id=null )
309: {
310: if ( ! $this->_dbTable ) {
311: return null;
312: }
313:
314: // Select the details requested, for the columns requested
315: $q = $db
316: ->query( 'select' )
317: ->table( $this->_dbTable )
318: ->get( $this->_dbPKey );
319:
320: foreach ( $this->_dbFields as $column => $prop ) {
321: if ( $prop !== self::DB_CONTENT ) {
322: $q->get( $column );
323: }
324: }
325:
326: if ( $id !== null ) {
327: $q->where( $this->_dbPKey, $id );
328: }
329:
330: for ( $i=0, $ien=count($this->_where) ; $i<$ien ; $i++ ) {
331: $q->where( $this->_where[$i] );
332: }
333:
334: $result = $q->exec()->fetchAll();
335: $out = array();
336:
337: for ( $i=0, $ien=count($result) ; $i<$ien ; $i++ ) {
338: $out[ $result[$i][ $this->_dbPKey ] ] = $result[$i];
339: }
340:
341: return $out;
342: }
343:
344:
345: /**
346: * Clean the database
347: * @param \DataTables\Editor $editor Calling Editor instance
348: * @param Field $field Host field
349: * @internal
350: */
351: public function dbCleanExec ( $editor, $field )
352: {
353: // Database and file system clean up BEFORE adding the new file to
354: // the db, otherwise it will be removed immediately
355: $tables = $editor->table();
356: $this->_dbClean( $editor->db(), $tables[0], $field->dbField() );
357: }
358:
359:
360: /**
361: * Get the set error message
362: *
363: * @return string Class error
364: * @internal
365: */
366: public function error ()
367: {
368: return $this->_error;
369: }
370:
371:
372: /**
373: * Execute an upload
374: *
375: * @param \DataTables\Editor $editor Calling Editor instance
376: * @return int Primary key value
377: * @internal
378: */
379: public function exec ( $editor )
380: {
381: $id = null;
382: $upload = $_FILES['upload'];
383:
384: // Validation - PHP standard validation
385: if ( $upload['error'] !== UPLOAD_ERR_OK ) {
386: if ( $upload['error'] === UPLOAD_ERR_INI_SIZE ) {
387: $this->_error = "File exceeds maximum file upload size";
388: }
389: else {
390: $this->_error = "There was an error uploading the file (".$upload['error'].")";
391: }
392: return false;
393: }
394:
395: // Validation - acceptable file extensions
396: if ( is_array( $this->_extns ) ) {
397: $extn = pathinfo($upload['name'], PATHINFO_EXTENSION);
398:
399: if ( in_array( strtolower($extn), array_map( 'strtolower', $this->_extns ) ) === false ) {
400: $this->_error = $this->_extnError;
401: return false;
402: }
403: }
404:
405: // Validation - custom callback
406: for ( $i=0, $ien=count($this->_validators) ; $i<$ien ; $i++ ) {
407: $res = $this->_validators[$i]( $upload );
408:
409: if ( is_string( $res ) ) {
410: $this->_error = $res;
411: return false;
412: }
413: }
414:
415: // Database
416: if ( $this->_dbTable ) {
417: foreach ( $this->_dbFields as $column => $prop ) {
418: // We can't know what the path is, if it has moved into place
419: // by an external function - throw an error if this does happen
420: if ( ! is_string( $this->_action ) &&
421: ($prop === self::DB_SYSTEM_PATH || $prop === self::DB_WEB_PATH )
422: ) {
423: $this->_error = "Cannot set path information in database ".
424: "if a custom method is used to save the file.";
425:
426: return false;
427: }
428: }
429:
430: // Commit to the database
431: $id = $this->_dbExec( $editor->db() );
432: }
433:
434: // Perform file system actions
435: return $this->_actionExec( $id );
436: }
437:
438:
439: /**
440: * Get the primary key column for the table
441: *
442: * @return string Primary key column name
443: * @internal
444: */
445: public function pkey ()
446: {
447: return $this->_dbPKey;
448: }
449:
450:
451: /**
452: * Get the db table name
453: *
454: * @return string DB table name
455: * @internal
456: */
457: public function table ()
458: {
459: return $this->_dbTable;
460: }
461:
462:
463:
464: /* * * * * * * * * * * * * * * * * * * * * * * * *
465: * Private methods
466: */
467:
468: /**
469: * Execute the configured action for the upload
470: *
471: * @param int $id Primary key value
472: * @return int File identifier - typically the primary key
473: */
474: private function _actionExec ( $id )
475: {
476: $upload = $_FILES['upload'];
477:
478: if ( ! is_string( $this->_action ) ) {
479: // Custom function
480: $action = $this->_action;
481: return $action( $upload, $id );
482: }
483:
484: // Default action - move the file to the location specified by the
485: // action string
486: $to = $this->_path( $upload['name'], $id );
487: $res = move_uploaded_file( $upload['tmp_name'], $to );
488:
489: if ( $res === false ) {
490: $this->_error = "An error occurred while moving the uploaded file.";
491: return false;
492: }
493:
494: return $id !== null ?
495: $id :
496: $to;
497: }
498:
499: /**
500: * Perform the database clean by first getting the information about the
501: * orphaned rows and then calling the callback function. The callback can
502: * then instruct the rows to be removed through the return value.
503: *
504: * @param \DataTables\Database $db Database instance
505: * @param string $editorTable Editor Editor instance table name
506: * @param string $fieldName Host field's name
507: */
508: private function _dbClean ( $db, $editorTable, $fieldName )
509: {
510: $callback = $this->_dbCleanCallback;
511:
512: if ( ! $this->_dbTable || ! $callback ) {
513: return;
514: }
515:
516: // If there is a table / field that we should use to check if the value
517: // is in use, then use that. Otherwise we'll try to use the information
518: // from the Editor / Field instance.
519: if ( $this->_dbCleanTableField ) {
520: $fieldName = $this->_dbCleanTableField;
521: }
522:
523: $a = explode('.', $fieldName);
524: if ( count($a) === 1 ) {
525: $table = $editorTable;
526: $field = $a[0];
527: }
528: else if ( count($a) === 2 ) {
529: $table = $a[0];
530: $field = $a[1];
531: }
532: else {
533: $table = $a[1];
534: $field = $a[2];
535: }
536:
537: // Select the details requested, for the columns requested
538: $q = $db
539: ->query( 'select' )
540: ->table( $this->_dbTable )
541: ->get( $this->_dbPKey );
542:
543: foreach ( $this->_dbFields as $column => $prop ) {
544: if ( $prop !== self::DB_CONTENT ) {
545: $q->get( $column );
546: }
547: }
548:
549: $q->where( $this->_dbPKey, '(SELECT '.$field.' FROM '.$table.' WHERE '.$field.' IS NOT NULL)', 'NOT IN', false );
550:
551: $data = $q->exec()->fetchAll();
552:
553: if ( count( $data ) === 0 ) {
554: return;
555: }
556:
557: $result = $callback( $data );
558:
559: // Delete the selected rows, iff the developer says to do so with the
560: // returned value (i.e. acknowledge that the files have be removed from
561: // the file system)
562: if ( $result === true ) {
563: $qDelete = $db
564: ->query( 'delete' )
565: ->table( $this->_dbTable );
566:
567: for ( $i=0, $ien=count( $data ) ; $i<$ien ; $i++ ) {
568: $qDelete->or_where( $this->_dbPKey, $data[$i][ $this->_dbPKey ] );
569: }
570:
571: $qDelete->exec();
572: }
573: }
574:
575: /**
576: * Add a record to the database for a newly uploaded file
577: *
578: * @param \DataTables\Database $db Database instance
579: * @return int Primary key value for the newly uploaded file
580: */
581: private function _dbExec ( $db )
582: {
583: $upload = $_FILES['upload'];
584: $pathFields = array();
585:
586: // Insert the details requested, for the columns requested
587: $q = $db
588: ->query( 'insert' )
589: ->table( $this->_dbTable )
590: ->pkey( $this->_dbPKey );
591:
592: foreach ( $this->_dbFields as $column => $prop ) {
593: switch ( $prop ) {
594: case self::DB_READ_ONLY:
595: break;
596:
597: case self::DB_CONTENT:
598: $q->set( $column, file_get_contents($upload['tmp_name']) );
599: break;
600:
601: case self::DB_CONTENT_TYPE:
602: case self::DB_MIME_TYPE:
603: $finfo = finfo_open(FILEINFO_MIME);
604: $mime = finfo_file($finfo, $upload['tmp_name']);
605: finfo_close($finfo);
606:
607: $q->set( $column, $mime );
608: break;
609:
610: case self::DB_EXTN:
611: $extn = pathinfo($upload['name'], PATHINFO_EXTENSION);
612: $q->set( $column, $extn );
613: break;
614:
615: case self::DB_FILE_NAME:
616: $q->set( $column, $upload['name'] );
617: break;
618:
619: case self::DB_FILE_SIZE:
620: $q->set( $column, $upload['size'] );
621: break;
622:
623: case self::DB_SYSTEM_PATH:
624: $pathFields[ $column ] = self::DB_SYSTEM_PATH;
625: $q->set( $column, '-' ); // Use a temporary value to avoid cases
626: break; // where the db will reject empty values
627:
628: case self::DB_WEB_PATH:
629: $pathFields[ $column ] = self::DB_WEB_PATH;
630: $q->set( $column, '-' ); // Use a temporary value (as above)
631: break;
632:
633: default:
634: if ( is_callable($prop) && is_object($prop) ) { // is a closure
635: $q->set( $column, $prop( $db, $upload ) );
636: }
637: else {
638: $q->set( $column, $prop );
639: }
640:
641: break;
642: }
643: }
644:
645: $res = $q->exec();
646: $id = $res->insertId();
647:
648: // Update the newly inserted row with the path information. We have to
649: // use a second statement here as we don't know in advance what the
650: // database schema is and don't want to prescribe that certain triggers
651: // etc be created. It makes it a bit less efficient but much more
652: // compatible
653: if ( count( $pathFields ) ) {
654: // For this to operate the action must be a string, which is
655: // validated in the `exec` method
656: $path = $this->_path( $upload['name'], $id );
657: $webPath = str_replace($_SERVER['DOCUMENT_ROOT'], '', $path);
658: $q = $db
659: ->query( 'update' )
660: ->table( $this->_dbTable )
661: ->where( $this->_dbPKey, $id );
662:
663: foreach ( $pathFields as $column => $type ) {
664: $q->set( $column, $type === self::DB_WEB_PATH ? $webPath : $path );
665: }
666:
667: $q->exec();
668: }
669:
670: return $id;
671: }
672:
673:
674: /**
675: * Apply macros to a user specified path
676: *
677: * @param string $name File path
678: * @param int $id Primary key value for the file
679: * @return string Resolved path
680: */
681: private function _path ( $name, $id )
682: {
683: $extn = pathinfo( $name, PATHINFO_EXTENSION );
684:
685: $to = $this->_action;
686: $to = str_replace( "__NAME__", $name, $to );
687: $to = str_replace( "__ID__", $id, $to );
688: $to = str_replace( "__EXTN__", $extn, $to );
689:
690: return $to;
691: }
692: }
693:
694: