您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

filesystem_attachments.php 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. /**
  3. * Filesystem Attachments
  4. *
  5. * This is a core plugin which provides basic, filesystem based
  6. * attachment temporary file handling. This includes storing
  7. * attachments of messages currently being composed, writing attachments
  8. * to disk when drafts with attachments are re-opened and writing
  9. * attachments to disk for inline display in current html compositions.
  10. * It also handles uploaded files for other uses, so not only attachments.
  11. *
  12. * Developers may wish to extend this class when creating attachment
  13. * handler plugins:
  14. * require_once('plugins/filesystem_attachments/filesystem_attachments.php');
  15. * class myCustom_attachments extends filesystem_attachments
  16. *
  17. * Note for developers: It is plugin's responsibility to care about security.
  18. * So, e.g. if the plugin is asked about some file path it should check
  19. * if it's really the storage path of the plugin and not e.g. /etc/passwd.
  20. * It is done by setting 'status' flag on every plugin hook it uses.
  21. * Roundcube core will trust the returned path if status=true.
  22. *
  23. * @license GNU GPLv3+
  24. * @author Ziba Scott <ziba@umich.edu>
  25. * @author Thomas Bruederli <roundcube@gmail.com>
  26. */
  27. class filesystem_attachments extends rcube_plugin
  28. {
  29. public $task = '?(?!login).*';
  30. function init()
  31. {
  32. // Save a newly uploaded attachment
  33. $this->add_hook('attachment_upload', array($this, 'upload'));
  34. // Save an attachment from a non-upload source (draft or forward)
  35. $this->add_hook('attachment_save', array($this, 'save'));
  36. // Remove an attachment from storage
  37. $this->add_hook('attachment_delete', array($this, 'remove'));
  38. // When composing an html message, image attachments may be shown
  39. $this->add_hook('attachment_display', array($this, 'display'));
  40. // Get the attachment from storage and place it on disk to be sent
  41. $this->add_hook('attachment_get', array($this, 'get'));
  42. // Delete all temp files associated with this user
  43. $this->add_hook('attachments_cleanup', array($this, 'cleanup'));
  44. $this->add_hook('session_destroy', array($this, 'cleanup'));
  45. }
  46. /**
  47. * Save a newly uploaded attachment
  48. */
  49. function upload($args)
  50. {
  51. $args['status'] = false;
  52. $group = $args['group'];
  53. $rcmail = rcube::get_instance();
  54. // use common temp dir for file uploads
  55. $temp_dir = $rcmail->config->get('temp_dir');
  56. $tmpfname = tempnam($temp_dir, 'rcmAttmnt');
  57. if (move_uploaded_file($args['path'], $tmpfname) && file_exists($tmpfname)) {
  58. $args['id'] = $this->file_id();
  59. $args['path'] = $tmpfname;
  60. $args['status'] = true;
  61. @chmod($tmpfname, 0600); // set correct permissions (#1488996)
  62. // Note the file for later cleanup
  63. $_SESSION['plugins']['filesystem_attachments'][$group][$args['id']] = $tmpfname;
  64. }
  65. return $args;
  66. }
  67. /**
  68. * Save an attachment from a non-upload source (draft or forward)
  69. */
  70. function save($args)
  71. {
  72. $group = $args['group'];
  73. $args['status'] = false;
  74. if (!$args['path']) {
  75. $rcmail = rcube::get_instance();
  76. $temp_dir = $rcmail->config->get('temp_dir');
  77. $tmp_path = tempnam($temp_dir, 'rcmAttmnt');
  78. if ($fp = fopen($tmp_path, 'w')) {
  79. fwrite($fp, $args['data']);
  80. fclose($fp);
  81. $args['path'] = $tmp_path;
  82. }
  83. else {
  84. return $args;
  85. }
  86. }
  87. $args['id'] = $this->file_id();
  88. $args['status'] = true;
  89. // Note the file for later cleanup
  90. $_SESSION['plugins']['filesystem_attachments'][$group][$args['id']] = $args['path'];
  91. return $args;
  92. }
  93. /**
  94. * Remove an attachment from storage
  95. * This is triggered by the remove attachment button on the compose screen
  96. */
  97. function remove($args)
  98. {
  99. $args['status'] = $this->verify_path($args['path']) && @unlink($args['path']);
  100. return $args;
  101. }
  102. /**
  103. * When composing an html message, image attachments may be shown
  104. * For this plugin, the file is already in place, just check for
  105. * the existence of the proper metadata
  106. */
  107. function display($args)
  108. {
  109. $args['status'] = $this->verify_path($args['path']) && file_exists($args['path']);
  110. return $args;
  111. }
  112. /**
  113. * This attachment plugin doesn't require any steps to put the file
  114. * on disk for use. This stub function is kept here to make this
  115. * class handy as a parent class for other plugins which may need it.
  116. */
  117. function get($args)
  118. {
  119. if (!$this->verify_path($args['path'])) {
  120. $args['path'] = null;
  121. }
  122. return $args;
  123. }
  124. /**
  125. * Delete all temp files associated with this user
  126. */
  127. function cleanup($args)
  128. {
  129. // $_SESSION['compose']['attachments'] is not a complete record of
  130. // temporary files because loading a draft or starting a forward copies
  131. // the file to disk, but does not make an entry in that array
  132. if (is_array($_SESSION['plugins']['filesystem_attachments'])) {
  133. foreach ($_SESSION['plugins']['filesystem_attachments'] as $group => $files) {
  134. if ($args['group'] && $args['group'] != $group) {
  135. continue;
  136. }
  137. foreach ((array)$files as $filename) {
  138. if (file_exists($filename)) {
  139. unlink($filename);
  140. }
  141. }
  142. unset($_SESSION['plugins']['filesystem_attachments'][$group]);
  143. }
  144. }
  145. return $args;
  146. }
  147. function file_id()
  148. {
  149. $userid = rcube::get_instance()->user->ID;
  150. list($usec, $sec) = explode(' ', microtime());
  151. $id = preg_replace('/[^0-9]/', '', $userid . $sec . $usec);
  152. // make sure the ID is really unique (#1489546)
  153. while ($this->find_file_by_id($id)) {
  154. // increment last four characters
  155. $x = substr($id, -4) + 1;
  156. $id = substr($id, 0, -4) . sprintf('%04d', ($x > 9999 ? $x - 9999 : $x));
  157. }
  158. return $id;
  159. }
  160. private function find_file_by_id($id)
  161. {
  162. foreach ((array) $_SESSION['plugins']['filesystem_attachments'] as $group => $files) {
  163. if (isset($files[$id])) {
  164. return true;
  165. }
  166. }
  167. }
  168. /**
  169. * For security we'll always verify the file path stored in session,
  170. * as session entries can be faked in various ways e.g. #6026.
  171. * We allow only files in Roundcube temp dir
  172. */
  173. protected function verify_path($path)
  174. {
  175. if (empty($path)) {
  176. return false;
  177. }
  178. $rcmail = rcube::get_instance();
  179. $temp_dir = $rcmail->config->get('temp_dir');
  180. $file_path = pathinfo($path, PATHINFO_DIRNAME);
  181. if ($temp_dir !== $file_path) {
  182. // When the configured directory is not writable, or out of open_basedir path
  183. // tempnam() fallbacks to system temp without a warning.
  184. // We allow that, but we'll let to know the user about the misconfiguration.
  185. if ($file_path == sys_get_temp_dir()) {
  186. rcube::raise_error(array(
  187. 'file' => __FILE__,
  188. 'line' => __LINE__,
  189. 'message' => "Detected 'temp_dir' change. Access to '$temp_dir' restricted by filesystem permissions or open_basedir",
  190. ), true, false);
  191. return true;
  192. }
  193. rcube::raise_error(array(
  194. 'file' => __FILE__,
  195. 'line' => __LINE__,
  196. 'message' => sprintf("%s can't read %s (not in temp_dir)",
  197. $rcmail->get_user_name(), substr($path, 0, 512))
  198. ), true, false);
  199. return false;
  200. }
  201. return true;
  202. }
  203. }