file.inc 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. <?php
  2. /**
  3. * @file
  4. * Support for file entity as destination. Note that File Fields have their
  5. * own destination in fields.inc
  6. */
  7. /**
  8. * Interface for taking some value representing a file and returning
  9. * a Drupal file entity (creating the entity if necessary).
  10. */
  11. interface MigrateFileInterface {
  12. /**
  13. * Return a list of subfields and options specific to this implementation,
  14. * keyed by name.
  15. */
  16. public static function fields();
  17. /**
  18. * Create or link to a Drupal file entity.
  19. *
  20. * @param $value
  21. * A class-specific value (URI, pre-existing file ID, file blob, ...)
  22. * representing file content.
  23. *
  24. * @param $owner
  25. * uid of an account to be recorded as the file owner.
  26. *
  27. * @return object
  28. * File entity being created or referenced.
  29. */
  30. public function processFile($value, $owner);
  31. }
  32. abstract class MigrateFileBase implements MigrateFileInterface {
  33. /**
  34. * Extension of the core FILE_EXISTS_* constants, offering an alternative to
  35. * reuse the existing file if present as-is (core only offers the options of
  36. * replacing it or renaming to avoid collision).
  37. */
  38. const FILE_EXISTS_REUSE = -1;
  39. /**
  40. * An optional file object to use as a default starting point for building the
  41. * file entity.
  42. *
  43. * @var stdClass
  44. */
  45. protected $defaultFile;
  46. /**
  47. * How to handle destination filename collisions.
  48. *
  49. * @var int
  50. */
  51. protected $fileReplace = FILE_EXISTS_RENAME;
  52. /**
  53. * Set to TRUE to prevent file deletion on rollback.
  54. *
  55. * @var bool
  56. */
  57. protected $preserveFiles = FALSE;
  58. public function __construct($arguments = array(), $default_file = NULL) {
  59. if (isset($arguments['preserve_files'])) {
  60. $this->preserveFiles = $arguments['preserve_files'];
  61. }
  62. if (isset($arguments['file_replace'])) {
  63. $this->fileReplace = $arguments['file_replace'];
  64. }
  65. if ($default_file) {
  66. $this->defaultFile = $default_file;
  67. }
  68. else {
  69. $this->defaultFile = new stdClass;
  70. }
  71. }
  72. /**
  73. * Default implementation of MigrateFileInterface::fields().
  74. *
  75. * @return array
  76. */
  77. static public function fields() {
  78. return array(
  79. 'preserve_files' => t('Option: <a href="@doc">Boolean indicating whether files should be preserved or deleted on rollback</a>',
  80. array('@doc' => 'http://drupal.org/node/1540106#preserve_files')),
  81. );
  82. }
  83. /**
  84. * Setup a file entity object suitable for saving.
  85. *
  86. * @param $destination
  87. * Path to the Drupal copy of the file.
  88. * @param $owner
  89. * Uid of the file owner.
  90. * @return stdClass
  91. * A file object ready to be saved.
  92. */
  93. protected function createFileEntity($destination, $owner) {
  94. $file = clone $this->defaultFile;
  95. $file->uri = $destination;
  96. $file->uid = $owner;
  97. if (!isset($file->filename)) {
  98. $file->filename = drupal_basename($destination);
  99. }
  100. if (!isset($file->filemime)) {
  101. $file->filemime = file_get_mimetype(urldecode($destination));
  102. }
  103. if (!isset($file->status)) {
  104. $file->status = FILE_STATUS_PERMANENT;
  105. }
  106. if (empty($file->type) || $file->type == 'file') {
  107. // Try to determine the file type.
  108. if (module_exists('file_entity')) {
  109. $type = file_get_type($file);
  110. }
  111. elseif ($slash_pos = strpos($file->filemime, '/')) {
  112. $type = substr($file->filemime, 0, $slash_pos);
  113. }
  114. $file->type = isset($type) ? $type : 'file';
  115. }
  116. // If we are replacing or reusing an existing filesystem entry,
  117. // also re-use its database record.
  118. if ($this->fileReplace == FILE_EXISTS_REPLACE ||
  119. $this->fileReplace == self::FILE_EXISTS_REUSE) {
  120. $existing_files = file_load_multiple(array(), array('uri' => $destination));
  121. if (count($existing_files)) {
  122. $existing = reset($existing_files);
  123. $file->fid = $existing->fid;
  124. $file->filename = $existing->filename;
  125. }
  126. }
  127. return $file;
  128. }
  129. /**
  130. * If asked to preserve files from deletion on rollback, add a file_usage
  131. * entry.
  132. *
  133. * @param $fid
  134. */
  135. protected function markForPreservation($fid) {
  136. if (!empty($this->preserveFiles)) {
  137. // We do this directly instead of calling file_usage_add, to force the
  138. // count to 1 - otherwise, updates will increment the counter and the file
  139. // will never be deletable
  140. db_merge('file_usage')
  141. ->key(array(
  142. 'fid' => $fid,
  143. 'module' => 'migrate',
  144. 'type' => 'file',
  145. 'id' => $fid,
  146. ))
  147. ->fields(array('count' => 1))
  148. ->execute();
  149. }
  150. }
  151. }
  152. /**
  153. * The simplest possible file class - where the value is a remote URI which
  154. * simply needs to be saved as the URI on the destination side, with no attempt
  155. * to copy or otherwise use it.
  156. */
  157. class MigrateFileUriAsIs extends MigrateFileBase {
  158. public function processFile($value, $owner) {
  159. $file = file_save($this->createFileEntity($value, $owner));
  160. return $file;
  161. }
  162. }
  163. /**
  164. * Handle the degenerate case where we already have a file ID.
  165. */
  166. class MigrateFileFid extends MigrateFileBase {
  167. /**
  168. * Implementation of MigrateFileInterface::processFile().
  169. *
  170. * @param $value
  171. * An existing file entity ID (fid).
  172. * @param $owner
  173. * User ID (uid) to be the owner of the file. Ignored in this case.
  174. * @return int
  175. * The file entity corresponding to the fid that was passed in.
  176. */
  177. public function processFile($value, $owner) {
  178. $this->markForPreservation($value);
  179. return file_load($value);
  180. }
  181. }
  182. /**
  183. * Base class for creating core file entities.
  184. */
  185. abstract class MigrateFile extends MigrateFileBase {
  186. /**
  187. * The destination directory within Drupal.
  188. *
  189. * @var string
  190. */
  191. protected $destinationDir = 'public://';
  192. /**
  193. * The filename relative to destinationDir to which to save the current file.
  194. *
  195. * @var string
  196. */
  197. protected $destinationFile = '';
  198. public function __construct($arguments = array(), $default_file = NULL) {
  199. parent::__construct($arguments, $default_file);
  200. if (isset($arguments['destination_dir'])) {
  201. $this->destinationDir = $arguments['destination_dir'];
  202. }
  203. if (isset($arguments['destination_file'])) {
  204. $this->destinationFile = $arguments['destination_file'];
  205. }
  206. }
  207. /**
  208. * Implementation of MigrateFileInterface::fields().
  209. *
  210. * @return array
  211. */
  212. static public function fields() {
  213. return parent::fields() + array(
  214. 'destination_dir' => t('Subfield: <a href="@doc">Path within Drupal files directory to store file</a>',
  215. array('@doc' => 'http://drupal.org/node/1540106#destination_dir')),
  216. 'destination_file' => t('Subfield: <a href="@doc">Path within destination_dir to store the file.</a>',
  217. array('@doc' => 'http://drupal.org/node/1540106#destination_file')),
  218. 'file_replace' => t('Option: <a href="@doc">Value of $replace in that file function. Defaults to FILE_EXISTS_RENAME.</a>',
  219. array('@doc' => 'http://drupal.org/node/1540106#file_replace')),
  220. );
  221. }
  222. /**
  223. * By whatever appropriate means, put the file in the right place.
  224. *
  225. * @param $destination
  226. * Destination path within Drupal.
  227. * @return bool
  228. * TRUE if the file is successfully saved, FALSE otherwise.
  229. */
  230. abstract protected function copyFile($destination);
  231. /**
  232. * Default implementation of MigrateFileInterface::processFiles().
  233. *
  234. * @param $value
  235. * The URI or local filespec of a file to be imported.
  236. * @param $owner
  237. * User ID (uid) to be the owner of the file.
  238. * @return object
  239. * The file entity being created or referenced.
  240. */
  241. public function processFile($value, $owner) {
  242. $migration = Migration::currentMigration();
  243. // Determine the final path we want in Drupal - start with our preferred path.
  244. $destination = file_stream_wrapper_uri_normalize(
  245. $this->destinationDir . '/' .
  246. ltrim($this->destinationFile, "/\\"));
  247. // Our own file_replace behavior - if the file exists, use it without
  248. // replacing it
  249. if ($this->fileReplace == self::FILE_EXISTS_REUSE) {
  250. // See if we this file already (we'll reuse and resave a file entity if it exists).
  251. if (file_exists($destination)) {
  252. $file = $this->createFileEntity($destination, $owner);
  253. $file = file_save($file);
  254. $this->markForPreservation($file->fid);
  255. return $file;
  256. }
  257. // No existing one to reuse, reset to REPLACE
  258. $this->fileReplace = FILE_EXISTS_REPLACE;
  259. }
  260. // Prepare the destination directory.
  261. $destdir = drupal_dirname($destination);
  262. if (!file_prepare_directory($destdir,
  263. FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
  264. $migration->saveMessage(t('Could not create destination directory for !dest',
  265. array('!dest' => $destination)));
  266. return FALSE;
  267. }
  268. // Determine whether we can perform this operation based on overwrite rules.
  269. $destination = file_destination($destination, $this->fileReplace);
  270. if ($destination === FALSE) {
  271. $migration->saveMessage(t('The file could not be copied because file %dest already exists in the destination directory.',
  272. array('%dest' => $destination)));
  273. return FALSE;
  274. }
  275. // Make sure the .htaccess files are present.
  276. file_ensure_htaccess();
  277. // Put the file where it needs to be.
  278. if (!$this->copyFile($destination)) {
  279. return FALSE;
  280. }
  281. // Set the permissions on the new file.
  282. drupal_chmod($destination);
  283. // Create and save the file entity.
  284. $file = file_save($this->createFileEntity($destination, $owner));
  285. // Prevent deletion of the file on rollback if requested.
  286. if (is_object($file)) {
  287. $this->markForPreservation($file->fid);
  288. return $file;
  289. }
  290. else {
  291. return FALSE;
  292. }
  293. }
  294. }
  295. /**
  296. * Handle cases where we're handed a URI, or local filespec, representing a file
  297. * to be imported to Drupal.
  298. */
  299. class MigrateFileUri extends MigrateFile {
  300. /**
  301. * The source directory for the file, relative to which the value (source
  302. * file) will be taken.
  303. *
  304. * @var string
  305. */
  306. protected $sourceDir = '';
  307. /**
  308. * The full path to the source file.
  309. *
  310. * @var string
  311. */
  312. protected $sourcePath = '';
  313. /**
  314. * Whether to apply rawurlencode to the components of an incoming file path.
  315. */
  316. protected $urlEncode = TRUE;
  317. public function __construct($arguments = array(), $default_file = NULL) {
  318. parent::__construct($arguments, $default_file);
  319. if (isset($arguments['source_dir'])) {
  320. $this->sourceDir = rtrim($arguments['source_dir'], "/\\");
  321. }
  322. if (isset($arguments['urlencode'])) {
  323. $this->urlEncode = $arguments['urlencode'];
  324. }
  325. }
  326. /**
  327. * Implementation of MigrateFileInterface::fields().
  328. *
  329. * @return array
  330. */
  331. static public function fields() {
  332. return parent::fields() +
  333. array(
  334. 'source_dir' => t('Subfield: <a href="@doc">Path to source file.</a>',
  335. array('@doc' => 'http://drupal.org/node/1540106#source_dir')),
  336. 'urlencode' => t('Option: <a href="@doc">Encode all segments of the incoming path (defaults to TRUE).</a>',
  337. array('@doc' => 'http://drupal.org/node/1540106#urlencode')),
  338. );
  339. }
  340. /**
  341. * Implementation of MigrateFile::copyFile().
  342. *
  343. * @param $destination
  344. * Destination within Drupal.
  345. *
  346. * @return bool
  347. * TRUE if the copy succeeded, FALSE otherwise.
  348. */
  349. protected function copyFile($destination) {
  350. if ($this->urlEncode) {
  351. // Perform the copy operation, with a cleaned-up path.
  352. $this->sourcePath = self::urlencode($this->sourcePath);
  353. }
  354. try {
  355. copy($this->sourcePath, $destination);
  356. return TRUE;
  357. }
  358. catch (Exception $e) {
  359. $migration = Migration::currentMigration();
  360. $migration->saveMessage(t('The specified file %file could not be copied to %destination: "%exception_msg"',
  361. array('%file' => $this->sourcePath, '%destination' => $destination, '%exception_msg' => $e->getMessage())));
  362. return FALSE;
  363. }
  364. }
  365. /**
  366. * Urlencode all the components of a remote filename.
  367. *
  368. * @param $filename
  369. *
  370. * @return string
  371. */
  372. static public function urlencode($filename) {
  373. // Only apply to a full URL
  374. if (strpos($filename, '://')) {
  375. $components = explode('/', $filename);
  376. foreach ($components as $key => $component) {
  377. $components[$key] = rawurlencode($component);
  378. }
  379. $filename = implode('/', $components);
  380. // Actually, we don't want certain characters encoded
  381. $filename = str_replace('%3A', ':', $filename);
  382. $filename = str_replace('%3F', '?', $filename);
  383. $filename = str_replace('%26', '&', $filename);
  384. }
  385. return $filename;
  386. }
  387. /**
  388. * Implementation of MigrateFileInterface::processFiles().
  389. *
  390. * @param $value
  391. * The URI or local filespec of a file to be imported.
  392. * @param $owner
  393. * User ID (uid) to be the owner of the file.
  394. * @return object
  395. * The file entity being created or referenced.
  396. */
  397. public function processFile($value, $owner) {
  398. // Identify the full path to the source file
  399. if (!empty($this->sourceDir)) {
  400. $this->sourcePath = rtrim($this->sourceDir, "/\\") . '/' . ltrim($value, "/\\");
  401. }
  402. else {
  403. $this->sourcePath = $value;
  404. }
  405. if (empty($this->destinationFile)) {
  406. $this->destinationFile = basename($this->sourcePath);
  407. }
  408. // MigrateFile has most of the smarts - the key is that it will call back
  409. // to our copyFile() implementation.
  410. $file = parent::processFile($value, $owner);
  411. return $file;
  412. }
  413. }
  414. /**
  415. * Handle cases where we're handed a blob (i.e., the actual contents of a file,
  416. * such as image data) to be stored as a real file in Drupal.
  417. */
  418. class MigrateFileBlob extends MigrateFile {
  419. /**
  420. * The file contents we will be writing to a real file.
  421. *
  422. * @var
  423. */
  424. protected $fileContents;
  425. /**
  426. * Implementation of MigrateFile::copyFile().
  427. *
  428. * @param $destination
  429. * Drupal destination path.
  430. * @return bool
  431. * TRUE if the file contents were successfully written, FALSE otherwise.
  432. */
  433. protected function copyFile($destination) {
  434. if (file_put_contents($destination, $this->fileContents)) {
  435. return TRUE;
  436. }
  437. else {
  438. $migration = Migration::currentMigration();
  439. $migration->saveMessage(t('Failed to write blob data to %destination',
  440. array('%destination' => $destination)));
  441. return FALSE;
  442. }
  443. }
  444. /**
  445. * Implementation of MigrateFileInterface::processFile().
  446. *
  447. * @param $value
  448. * The file contents to be saved as a file.
  449. * @param $owner
  450. * User ID (uid) to be the owner of the file.
  451. * @return object
  452. * File entity being created or referenced.
  453. */
  454. public function processFile($value, $owner) {
  455. $this->fileContents = $value;
  456. $file = parent::processFile($value, $owner);
  457. return $file;
  458. }
  459. }
  460. /**
  461. * Destination class implementing migration into the files table.
  462. */
  463. class MigrateDestinationFile extends MigrateDestinationEntity {
  464. /**
  465. * File class (MigrateFileUri etc.) doing the dirty wrk.
  466. *
  467. * @var string
  468. */
  469. protected $fileClass;
  470. public function setFileClass($file_class) {
  471. $this->fileClass = $file_class;
  472. }
  473. /**
  474. * Boolean indicating whether we should avoid deleting the actual file on
  475. * rollback.
  476. *
  477. * @var bool
  478. */
  479. protected $preserveFiles = FALSE;
  480. /**
  481. * Implementation of MigrateDestination::getKeySchema().
  482. *
  483. * @return array
  484. */
  485. static public function getKeySchema() {
  486. return array(
  487. 'fid' => array(
  488. 'type' => 'int',
  489. 'unsigned' => TRUE,
  490. 'description' => 'file_managed ID',
  491. ),
  492. );
  493. }
  494. /**
  495. * Basic initialization
  496. *
  497. * @param array $options
  498. * Options applied to files.
  499. */
  500. public function __construct($bundle = 'file', $file_class = 'MigrateFileUri',
  501. $options = array()) {
  502. parent::__construct('file', $bundle, $options);
  503. $this->fileClass = $file_class;
  504. }
  505. /**
  506. * Returns a list of fields available to be mapped for the entity type (bundle)
  507. *
  508. * @param Migration $migration
  509. * Optionally, the migration containing this destination.
  510. * @return array
  511. * Keys: machine names of the fields (to be passed to addFieldMapping)
  512. * Values: Human-friendly descriptions of the fields.
  513. */
  514. public function fields($migration = NULL) {
  515. $fields = array();
  516. // First the core properties
  517. $fields['fid'] = t('Existing file ID');
  518. $fields['uid'] = t('Uid of user associated with file');
  519. $fields['value'] = t('Representation of the source file (usually a URI)');
  520. $fields['timestamp'] = t('UNIX timestamp for the date the file was added');
  521. // Then add in anything provided by handlers
  522. $fields += migrate_handler_invoke_all('Entity', 'fields', $this->entityType, $this->bundle, $migration);
  523. $fields += migrate_handler_invoke_all('File', 'fields', $this->entityType, $this->bundle, $migration);
  524. // Plus anything provided by the file class
  525. $fields += call_user_func(array($this->fileClass, 'fields'));
  526. return $fields;
  527. }
  528. /**
  529. * Delete a file entry.
  530. *
  531. * @param array $fid
  532. * Fid to delete, arrayed.
  533. */
  534. public function rollback(array $fid) {
  535. migrate_instrument_start('file_load');
  536. $file = file_load(reset($fid));
  537. migrate_instrument_stop('file_load');
  538. if ($file) {
  539. migrate_instrument_start('file_delete');
  540. // If we're preserving files, roll our own version of file_delete() to make
  541. // sure we don't delete them. If we're not, make sure we do the job completely.
  542. $migration = Migration::currentMigration();
  543. $mappings = $migration->getFieldMappings();
  544. if (isset($mappings['preserve_files'])) {
  545. // Assumes it's set using defaultValue
  546. $preserve_files = $mappings['preserve_files']->getDefaultValue();
  547. }
  548. else {
  549. $preserve_files = FALSE;
  550. }
  551. $this->prepareRollback($fid);
  552. if ($preserve_files) {
  553. $this->fileDelete($file);
  554. }
  555. else {
  556. file_delete($file, TRUE);
  557. }
  558. $this->completeRollback($fid);
  559. migrate_instrument_stop('file_delete');
  560. }
  561. }
  562. /**
  563. * Delete database references to a file without deleting the file itself.
  564. *
  565. * @param $file
  566. */
  567. protected function fileDelete($file) {
  568. // Let other modules clean up any references to the deleted file.
  569. module_invoke_all('file_delete', $file);
  570. module_invoke_all('entity_delete', $file, 'file');
  571. db_delete('file_managed')->condition('fid', $file->fid)->execute();
  572. db_delete('file_usage')->condition('fid', $file->fid)->execute();
  573. }
  574. /**
  575. * Import a single file record.
  576. *
  577. * @param $file
  578. * File object to build. Prefilled with any fields mapped in the Migration.
  579. * @param $row
  580. * Raw source data object - passed through to prepare/complete handlers.
  581. * @return array
  582. * Array of key fields (fid only in this case) of the file that was saved if
  583. * successful. FALSE on failure.
  584. */
  585. public function import(stdClass $file, stdClass $row) {
  586. // Updating previously-migrated content?
  587. $migration = Migration::currentMigration();
  588. if (isset($row->migrate_map_destid1)) {
  589. if (isset($file->fid)) {
  590. if ($file->fid != $row->migrate_map_destid1) {
  591. throw new MigrateException(t("Incoming fid !fid and map destination fid !destid1 don't match",
  592. array('!fid' => $file->fid, '!destid1' => $row->migrate_map_destid1)));
  593. }
  594. }
  595. else {
  596. $file->fid = $row->migrate_map_destid1;
  597. }
  598. }
  599. if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
  600. if (!isset($file->fid)) {
  601. throw new MigrateException(t('System-of-record is DESTINATION, but no destination fid provided'));
  602. }
  603. // @todo: Support DESTINATION case
  604. $old_file = file_load($file->fid);
  605. }
  606. // 'type' is the bundle property on file entities. It must be set here for
  607. // the sake of the prepare handlers, although it may be overridden later
  608. // based on the detected mime type.
  609. if (empty($file->type)) {
  610. // If a bundle was specified in the constructor we use it for filetype.
  611. if ($this->bundle != 'file') {
  612. $file->type = $this->bundle;
  613. }
  614. else {
  615. $file->type = 'file';
  616. }
  617. }
  618. // Invoke migration prepare handlers
  619. $this->prepare($file, $row);
  620. if (isset($file->fid)) {
  621. $updating = TRUE;
  622. }
  623. else {
  624. $updating = FALSE;
  625. }
  626. if (!isset($file->uid)) {
  627. $file->uid = 1;
  628. }
  629. // file_save() unconditionally sets timestamp - if we have an explicit
  630. // value we want, we need to set it manually after file_save.
  631. if (isset($file->timestamp)) {
  632. $timestamp = MigrationBase::timestamp($file->timestamp);
  633. }
  634. // Don't pass preserve_files through to the file class, which will add
  635. // file_usage - we will handle it ourselves in rollback().
  636. $file->preserve_files = FALSE;
  637. $file_class = $this->fileClass;
  638. $source = new $file_class((array)$file, $file);
  639. $file = $source->processFile($file->value, $file->uid);
  640. if (is_object($file) && isset($file->fid)) {
  641. $this->complete($file, $row);
  642. if (isset($timestamp)) {
  643. db_update('file_managed')
  644. ->fields(array('timestamp' => $timestamp))
  645. ->condition('fid', $file->fid)
  646. ->execute();
  647. $file->timestamp = $timestamp;
  648. }
  649. $return = array($file->fid);
  650. if ($updating) {
  651. $this->numUpdated++;
  652. }
  653. else {
  654. $this->numCreated++;
  655. }
  656. }
  657. else {
  658. $return = FALSE;
  659. }
  660. return $return;
  661. }
  662. }