destinations.file.inc 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php
  2. /**
  3. * @file
  4. * A destination type for saving locally to the server.
  5. */
  6. /**
  7. * A destination type for saving locally to the server.
  8. *
  9. * @ingroup backup_migrate_destinations
  10. */
  11. class backup_migrate_destination_files extends backup_migrate_destination {
  12. var $supported_ops = array('scheduled backup', 'manual backup', 'restore', 'list files', 'configure', 'delete');
  13. function type_name() {
  14. return t("Server Directory");
  15. }
  16. /**
  17. * Get the file location.
  18. */
  19. function get_realpath() {
  20. return drupal_realpath($this->get_location());
  21. }
  22. /**
  23. * File save destination callback.
  24. */
  25. function _save_file($file, $settings) {
  26. if ($dir = $this->get_location()) {
  27. if ($dir = $this->check_dir($dir)) {
  28. $filepath = rtrim($dir, "/") ."/". $file->filename();
  29. if (file_unmanaged_move($file->filepath(), $filepath)) {
  30. // chmod, chown and chgrp the file if needed.
  31. if ($chmod = $this->settings('chmod')) {
  32. if (!@drupal_chmod($filepath, octdec($chmod))) {
  33. _backup_migrate_message('Unable to set the file mode for: @file', array('@file' => $filepath), 'error');
  34. }
  35. }
  36. if ($chgrp = $this->settings('chgrp')) {
  37. if (!@chgrp($filepath, $chgrp)) {
  38. _backup_migrate_message('Unable to set the file group for: @file', array('@file' => $filepath), 'error');
  39. }
  40. }
  41. return $file;
  42. }
  43. else {
  44. _backup_migrate_message('Unable to save the file to the directory: @dir', array('@dir' => $dir), 'error');
  45. }
  46. }
  47. }
  48. }
  49. /**
  50. * Determine if we can read the given file.
  51. */
  52. function can_read_file($file_id) {
  53. return $this->op('restore') && is_readable($this->get_filepath($file_id));
  54. }
  55. /**
  56. * File load destination callback.
  57. */
  58. function load_file($file_id) {
  59. $filepath = $this->get_filepath($file_id);
  60. if (file_exists($filepath)) {
  61. backup_migrate_include('files');
  62. return new backup_file(array('filepath' => $filepath));
  63. }
  64. }
  65. /**
  66. * File list destination callback.
  67. */
  68. function _list_files() {
  69. $files = array();
  70. if ($dir = $this->get_realpath()) {
  71. if ($handle = @opendir($dir)) {
  72. backup_migrate_include('files');
  73. while (FALSE !== ($file = readdir($handle))) {
  74. $filepath = $dir ."/". $file;
  75. $files[$file] = new backup_file(array('filepath' => $filepath));
  76. }
  77. }
  78. }
  79. return $files;
  80. }
  81. /**
  82. * File delete destination callback.
  83. */
  84. function delete_file($file_id) {
  85. $filepath = $this->get_filepath($file_id);
  86. file_unmanaged_delete($filepath);
  87. }
  88. /**
  89. * Get the filepath from the given file id.
  90. */
  91. function get_filepath($file_id) {
  92. if ($dir = $this->get_realpath()) {
  93. $filepath = rtrim($dir, '/') .'/'. $file_id;
  94. return $filepath;
  95. }
  96. return FALSE;
  97. }
  98. /**
  99. * Get the form for the settings for the files destination.
  100. */
  101. function edit_form() {
  102. $form = parent::edit_form();
  103. $form['location'] = array(
  104. "#type" => "textfield",
  105. "#title" => t("Directory path"),
  106. "#default_value" => $this->get_location(),
  107. "#required" => TRUE,
  108. "#description" => t('Enter the path to the directory to save the backups to. Use a relative path to pick a path relative to your Drupal root directory. The web server must be able to write to this path.'),
  109. );
  110. $form['settings'] = array(
  111. '#type' => 'fieldset',
  112. '#title' => t('Advanced Settings'),
  113. '#tree' => TRUE,
  114. '#collapsible' => TRUE,
  115. '#collapsed' => TRUE,
  116. );
  117. if (function_exists('chmod')) {
  118. $form['settings']['chmod'] = array(
  119. '#type' => 'textfield',
  120. '#title' => t('Change file mode (chmod)'),
  121. '#size' => 5,
  122. '#default_value' => $this->settings('chmod'),
  123. '#description' => t('If you enter a value here, backup files will be chmoded with the mode you specify. Specify the mode in octal form (e.g. 644 or 0644) or leave blank to disable this feature.'),
  124. );
  125. }
  126. if (function_exists('chgrp')) {
  127. $form['settings']['chgrp'] = array(
  128. '#type' => 'textfield',
  129. '#title' => t('Change file group (chgrp)'),
  130. '#size' => 5,
  131. '#default_value' => $this->settings('chgrp'),
  132. '#description' => t('If you enter a value here, backup files will be chgrped to the group you specify. Leave blank to disable this feature.'),
  133. );
  134. }
  135. return $form;
  136. }
  137. /**
  138. * Validate the form for the settings for the files destination.
  139. */
  140. function edit_form_validate($form, &$form_state) {
  141. $values = $form_state['values'];
  142. if (isset($values['settings']['chmod']) && !empty($values['settings']['chmod']) && !preg_match('/0?[0-7]{3}/', $values['settings']['chmod'])) {
  143. form_set_error('chmod', t('You must enter a valid chmod octal value (e.g. 644 or 0644) in the change mode field, or leave it blank.'));
  144. }
  145. parent::edit_form_validate($form, $form_state);
  146. }
  147. /**
  148. * Submit the form for the settings for the files destination.
  149. */
  150. function edit_form_submit($form, &$form_state) {
  151. // Add a 0 to the start of a 3 digit file mode to make it proper PHP encoded octal.
  152. if (strlen($form_state['values']['settings']['chmod']) == 3) {
  153. $form_state['values']['settings']['chmod'] = '0' . $form_state['values']['settings']['chmod'];
  154. }
  155. parent::edit_form_submit($form, $form_state);
  156. }
  157. /**
  158. * Prepare the destination directory for the backups.
  159. */
  160. function check_dir($directory) {
  161. if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) {
  162. // Unable to create destination directory.
  163. _backup_migrate_message("Unable to create or write to the save directory '%directory'. Please check the file permissions that directory and try again.", array('%directory' => $directory), "error");
  164. return FALSE;
  165. }
  166. // If the destination directory is within the webroot, then secure it as best we can.
  167. if ($this->dir_in_webroot($directory)) {
  168. $directory = $this->check_web_dir($directory);
  169. }
  170. return $directory;
  171. }
  172. /**
  173. * Check that a web accessible directory has been properly secured, othewise attempt to secure it.
  174. */
  175. function check_web_dir($directory) {
  176. file_create_htaccess($directory, TRUE);
  177. // Check the user agent to make sure we're not responding to a request from drupal itself.
  178. // That should prevent infinite loops which could be caused by poormanscron in some circumstances.
  179. if (strpos($_SERVER['HTTP_USER_AGENT'], 'Drupal') !== FALSE) {
  180. return FALSE;
  181. }
  182. // Check to see if the destination is publicly accessible
  183. $test_contents = "this file should not be publicly accessible";
  184. // Create the the text.txt file if it's not already there.
  185. if (!is_file($directory .'/test.txt') || file_get_contents($directory .'/test.txt') != $test_contents) {
  186. if ($fp = fopen($directory .'/test.txt', 'w')) {
  187. @fputs($fp, $test_contents);
  188. fclose($fp);
  189. }
  190. else {
  191. $message = t("Security notice: Backup and Migrate was unable to write a test text file to the destination directory %directory, and is therefore unable to check the security of the backup destination. Backups to the server will be disabled until the destination becomes writable and secure.", array('%directory' => $directory));
  192. drupal_set_message($message, "error");
  193. return FALSE;
  194. }
  195. }
  196. // Attempt to read the test file via http. This may fail for other reasons,
  197. // so it's not a bullet-proof check.
  198. if ($this->test_file_readable_remotely($directory .'/test.txt', $test_contents)) {
  199. $message = t("Security notice: Backup and Migrate will not save backup files to the server because the destination directory is publicly accessible. If you want to save files to the server, please secure the '%directory' directory", array('%directory' => $directory));
  200. drupal_set_message($message, "error");
  201. return FALSE;
  202. }
  203. return $directory;
  204. }
  205. /**
  206. * Check if the given directory is within the webroot and is therefore web accessible.
  207. */
  208. function dir_in_webroot($directory) {
  209. if (strpos(drupal_realpath($directory), realpath($_SERVER['DOCUMENT_ROOT'])) !== FALSE) {
  210. return TRUE;
  211. }
  212. return FALSE;
  213. }
  214. /**
  215. * Check if a file can be read remotely via http.
  216. */
  217. function test_file_readable_remotely($path, $contents) {
  218. $url = file_create_url($path);
  219. // TODO: Proper checking for absolute paths.
  220. $result = drupal_http_request($url);
  221. if (!empty($result->data) && strpos($result->data, $contents) !== FALSE) {
  222. return TRUE;
  223. }
  224. return FALSE;
  225. }
  226. }
  227. /**
  228. * The manual files directory.
  229. */
  230. class backup_migrate_destination_files_manual extends backup_migrate_destination_files {
  231. var $supported_ops = array('manual backup', 'restore', 'list files', 'configure', 'delete');
  232. function __construct($params = array()) {
  233. $dir = 'private://backup_migrate/manual';
  234. parent::__construct($params + array('location' => $dir, 'name' => t('Manual Backups Directory')));
  235. }
  236. }
  237. /**
  238. * The scheduled files directory.
  239. */
  240. class backup_migrate_destination_files_scheduled extends backup_migrate_destination_files {
  241. var $supported_ops = array('scheduled backup', 'restore', 'list files', 'configure', 'delete');
  242. function __construct($params = array()) {
  243. $dir = 'private://backup_migrate/scheduled';
  244. parent::__construct($params + array('location' => $dir, 'name' => t('Scheduled Backups Directory')));
  245. }
  246. }