      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: