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