| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 3 | //
 | 
        
           |  |  | 4 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 5 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 6 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 7 | // (at your option) any later version.
 | 
        
           |  |  | 8 | //
 | 
        
           |  |  | 9 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 12 | // GNU General Public License for more details.
 | 
        
           |  |  | 13 | //
 | 
        
           |  |  | 14 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 15 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 16 |   | 
        
           |  |  | 17 | /**
 | 
        
           |  |  | 18 |  * Core file system class definition.
 | 
        
           |  |  | 19 |  *
 | 
        
           |  |  | 20 |  * @package   core_files
 | 
        
           |  |  | 21 |  * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
 | 
        
           |  |  | 22 |  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 23 |  */
 | 
        
           |  |  | 24 |   | 
        
           |  |  | 25 | defined('MOODLE_INTERNAL') || die();
 | 
        
           |  |  | 26 |   | 
        
           |  |  | 27 | /**
 | 
        
           |  |  | 28 |  * File system class used for low level access to real files in filedir.
 | 
        
           |  |  | 29 |  *
 | 
        
           |  |  | 30 |  * @package   core_files
 | 
        
           |  |  | 31 |  * @category  files
 | 
        
           |  |  | 32 |  * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
 | 
        
           |  |  | 33 |  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 34 |  */
 | 
        
           |  |  | 35 | class file_system_filedir extends file_system {
 | 
        
           |  |  | 36 |   | 
        
           |  |  | 37 |     /**
 | 
        
           |  |  | 38 |      * @var string The path to the local copy of the filedir.
 | 
        
           |  |  | 39 |      */
 | 
        
           |  |  | 40 |     protected $filedir = null;
 | 
        
           |  |  | 41 |   | 
        
           |  |  | 42 |     /**
 | 
        
           |  |  | 43 |      * @var string The path to the trashdir.
 | 
        
           |  |  | 44 |      */
 | 
        
           |  |  | 45 |     protected $trashdir = null;
 | 
        
           |  |  | 46 |   | 
        
           |  |  | 47 |     /**
 | 
        
           |  |  | 48 |      * @var string Default directory permissions for new dirs.
 | 
        
           |  |  | 49 |      */
 | 
        
           |  |  | 50 |     protected $dirpermissions = null;
 | 
        
           |  |  | 51 |   | 
        
           |  |  | 52 |     /**
 | 
        
           |  |  | 53 |      * @var string Default file permissions for new files.
 | 
        
           |  |  | 54 |      */
 | 
        
           |  |  | 55 |     protected $filepermissions = null;
 | 
        
           |  |  | 56 |   | 
        
           |  |  | 57 |   | 
        
           |  |  | 58 |     /**
 | 
        
           |  |  | 59 |      * Perform any custom setup for this type of file_system.
 | 
        
           |  |  | 60 |      */
 | 
        
           |  |  | 61 |     public function __construct() {
 | 
        
           |  |  | 62 |         global $CFG;
 | 
        
           |  |  | 63 |   | 
        
           |  |  | 64 |         if (isset($CFG->filedir)) {
 | 
        
           |  |  | 65 |             $this->filedir = $CFG->filedir;
 | 
        
           |  |  | 66 |         } else {
 | 
        
           |  |  | 67 |             $this->filedir = $CFG->dataroot.'/filedir';
 | 
        
           |  |  | 68 |         }
 | 
        
           |  |  | 69 |   | 
        
           |  |  | 70 |         if (isset($CFG->trashdir)) {
 | 
        
           |  |  | 71 |             $this->trashdir = $CFG->trashdir;
 | 
        
           |  |  | 72 |         } else {
 | 
        
           |  |  | 73 |             $this->trashdir = $CFG->dataroot.'/trashdir';
 | 
        
           |  |  | 74 |         }
 | 
        
           |  |  | 75 |   | 
        
           |  |  | 76 |         $this->dirpermissions = $CFG->directorypermissions;
 | 
        
           |  |  | 77 |         $this->filepermissions = $CFG->filepermissions;
 | 
        
           |  |  | 78 |   | 
        
           |  |  | 79 |         // Make sure the file pool directory exists.
 | 
        
           |  |  | 80 |         if (!is_dir($this->filedir)) {
 | 
        
           |  |  | 81 |             if (!mkdir($this->filedir, $this->dirpermissions, true)) {
 | 
        
           |  |  | 82 |                 // Permission trouble.
 | 
        
           |  |  | 83 |                 throw new file_exception('storedfilecannotcreatefiledirs');
 | 
        
           |  |  | 84 |             }
 | 
        
           |  |  | 85 |   | 
        
           |  |  | 86 |             // Place warning file in file pool root.
 | 
        
           |  |  | 87 |             if (!file_exists($this->filedir.'/warning.txt')) {
 | 
        
           |  |  | 88 |                 file_put_contents($this->filedir.'/warning.txt',
 | 
        
           |  |  | 89 |                         'This directory contains the content of uploaded files and is controlled by Moodle code. ' .
 | 
        
           |  |  | 90 |                         'Do not manually move, change or rename any of the files and subdirectories here.');
 | 
        
           |  |  | 91 |                 chmod($this->filedir . '/warning.txt', $this->filepermissions);
 | 
        
           |  |  | 92 |             }
 | 
        
           |  |  | 93 |         }
 | 
        
           |  |  | 94 |   | 
        
           |  |  | 95 |         // Make sure the trashdir directory exists too.
 | 
        
           |  |  | 96 |         if (!is_dir($this->trashdir)) {
 | 
        
           |  |  | 97 |             if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
 | 
        
           |  |  | 98 |                 // Permission trouble.
 | 
        
           |  |  | 99 |                 throw new file_exception('storedfilecannotcreatefiledirs');
 | 
        
           |  |  | 100 |             }
 | 
        
           |  |  | 101 |         }
 | 
        
           |  |  | 102 |     }
 | 
        
           |  |  | 103 |   | 
        
           |  |  | 104 |     /**
 | 
        
           |  |  | 105 |      * Get the full path for the specified hash, including the path to the filedir.
 | 
        
           |  |  | 106 |      *
 | 
        
           |  |  | 107 |      * @param string $contenthash The content hash
 | 
        
           |  |  | 108 |      * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 | 
        
           |  |  | 109 |      * @return string The full path to the content file
 | 
        
           |  |  | 110 |      */
 | 
        
           |  |  | 111 |     protected function get_local_path_from_hash($contenthash, $fetchifnotfound = false) {
 | 
        
           |  |  | 112 |         return $this->get_fulldir_from_hash($contenthash) . '/' .$contenthash;
 | 
        
           |  |  | 113 |     }
 | 
        
           |  |  | 114 |   | 
        
           |  |  | 115 |     /**
 | 
        
           |  |  | 116 |      * Get a remote filepath for the specified stored file.
 | 
        
           |  |  | 117 |      *
 | 
        
           |  |  | 118 |      * @param stored_file $file The file to fetch the path for
 | 
        
           |  |  | 119 |      * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 | 
        
           |  |  | 120 |      * @return string The full path to the content file
 | 
        
           |  |  | 121 |      */
 | 
        
           |  |  | 122 |     public function get_local_path_from_storedfile(stored_file $file, $fetchifnotfound = false) {
 | 
        
           |  |  | 123 |         $filepath = $this->get_local_path_from_hash($file->get_contenthash(), $fetchifnotfound);
 | 
        
           |  |  | 124 |   | 
        
           |  |  | 125 |         // Try content recovery.
 | 
        
           |  |  | 126 |         if ($fetchifnotfound && !is_readable($filepath)) {
 | 
        
           |  |  | 127 |             $this->recover_file($file);
 | 
        
           |  |  | 128 |         }
 | 
        
           |  |  | 129 |   | 
        
           |  |  | 130 |         return $filepath;
 | 
        
           |  |  | 131 |     }
 | 
        
           |  |  | 132 |   | 
        
           |  |  | 133 |     /**
 | 
        
           |  |  | 134 |      * Get a remote filepath for the specified stored file.
 | 
        
           |  |  | 135 |      *
 | 
        
           |  |  | 136 |      * @param stored_file $file The file to serve.
 | 
        
           |  |  | 137 |      * @return string full path to pool file with file content
 | 
        
           |  |  | 138 |      */
 | 
        
           |  |  | 139 |     public function get_remote_path_from_storedfile(stored_file $file) {
 | 
        
           |  |  | 140 |         return $this->get_local_path_from_storedfile($file, false);
 | 
        
           |  |  | 141 |     }
 | 
        
           |  |  | 142 |   | 
        
           |  |  | 143 |     /**
 | 
        
           |  |  | 144 |      * Get the full path for the specified hash, including the path to the filedir.
 | 
        
           |  |  | 145 |      *
 | 
        
           |  |  | 146 |      * @param string $contenthash The content hash
 | 
        
           |  |  | 147 |      * @return string The full path to the content file
 | 
        
           |  |  | 148 |      */
 | 
        
           |  |  | 149 |     protected function get_remote_path_from_hash($contenthash) {
 | 
        
           |  |  | 150 |         return $this->get_local_path_from_hash($contenthash, false);
 | 
        
           |  |  | 151 |     }
 | 
        
           |  |  | 152 |   | 
        
           |  |  | 153 |     /**
 | 
        
           |  |  | 154 |      * Get the full directory to the stored file, including the path to the
 | 
        
           |  |  | 155 |      * filedir, and the directory which the file is actually in.
 | 
        
           |  |  | 156 |      *
 | 
        
           |  |  | 157 |      * Note: This function does not ensure that the file is present on disk.
 | 
        
           |  |  | 158 |      *
 | 
        
           |  |  | 159 |      * @param stored_file $file The file to fetch details for.
 | 
        
           |  |  | 160 |      * @return string The full path to the content directory
 | 
        
           |  |  | 161 |      */
 | 
        
           |  |  | 162 |     protected function get_fulldir_from_storedfile(stored_file $file) {
 | 
        
           |  |  | 163 |         return $this->get_fulldir_from_hash($file->get_contenthash());
 | 
        
           |  |  | 164 |     }
 | 
        
           |  |  | 165 |   | 
        
           |  |  | 166 |     /**
 | 
        
           |  |  | 167 |      * Get the full directory to the stored file, including the path to the
 | 
        
           |  |  | 168 |      * filedir, and the directory which the file is actually in.
 | 
        
           |  |  | 169 |      *
 | 
        
           |  |  | 170 |      * @param string $contenthash The content hash
 | 
        
           |  |  | 171 |      * @return string The full path to the content directory
 | 
        
           |  |  | 172 |      */
 | 
        
           |  |  | 173 |     protected function get_fulldir_from_hash($contenthash) {
 | 
        
           |  |  | 174 |         return $this->filedir . '/' . $this->get_contentdir_from_hash($contenthash);
 | 
        
           |  |  | 175 |     }
 | 
        
           |  |  | 176 |   | 
        
           |  |  | 177 |     /**
 | 
        
           |  |  | 178 |      * Get the content directory for the specified content hash.
 | 
        
           |  |  | 179 |      * This is the directory that the file will be in, but without the
 | 
        
           |  |  | 180 |      * fulldir.
 | 
        
           |  |  | 181 |      *
 | 
        
           |  |  | 182 |      * @param string $contenthash The content hash
 | 
        
           |  |  | 183 |      * @return string The directory within filedir
 | 
        
           |  |  | 184 |      */
 | 
        
           |  |  | 185 |     protected function get_contentdir_from_hash($contenthash) {
 | 
        
           |  |  | 186 |         $l1 = $contenthash[0] . $contenthash[1];
 | 
        
           |  |  | 187 |         $l2 = $contenthash[2] . $contenthash[3];
 | 
        
           |  |  | 188 |         return "$l1/$l2";
 | 
        
           |  |  | 189 |     }
 | 
        
           |  |  | 190 |   | 
        
           |  |  | 191 |     /**
 | 
        
           |  |  | 192 |      * Get the content path for the specified content hash within filedir.
 | 
        
           |  |  | 193 |      *
 | 
        
           |  |  | 194 |      * This does not include the filedir, and is often used by file systems
 | 
        
           |  |  | 195 |      * as the object key for storage and retrieval.
 | 
        
           |  |  | 196 |      *
 | 
        
           |  |  | 197 |      * @param string $contenthash The content hash
 | 
        
           |  |  | 198 |      * @return string The filepath within filedir
 | 
        
           |  |  | 199 |      */
 | 
        
           |  |  | 200 |     protected function get_contentpath_from_hash($contenthash) {
 | 
        
           |  |  | 201 |         return $this->get_contentdir_from_hash($contenthash) . '/' . $contenthash;
 | 
        
           |  |  | 202 |     }
 | 
        
           |  |  | 203 |   | 
        
           |  |  | 204 |     /**
 | 
        
           |  |  | 205 |      * Get the full directory for the specified hash in the trash, including the path to the
 | 
        
           |  |  | 206 |      * trashdir, and the directory which the file is actually in.
 | 
        
           |  |  | 207 |      *
 | 
        
           |  |  | 208 |      * @param string $contenthash The content hash
 | 
        
           |  |  | 209 |      * @return string The full path to the trash directory
 | 
        
           |  |  | 210 |      */
 | 
        
           |  |  | 211 |     protected function get_trash_fulldir_from_hash($contenthash) {
 | 
        
           |  |  | 212 |         return $this->trashdir . '/' . $this->get_contentdir_from_hash($contenthash);
 | 
        
           |  |  | 213 |     }
 | 
        
           |  |  | 214 |   | 
        
           |  |  | 215 |     /**
 | 
        
           |  |  | 216 |      * Get the full path for the specified hash in the trash, including the path to the trashdir.
 | 
        
           |  |  | 217 |      *
 | 
        
           |  |  | 218 |      * @param string $contenthash The content hash
 | 
        
           |  |  | 219 |      * @return string The full path to the trash file
 | 
        
           |  |  | 220 |      */
 | 
        
           |  |  | 221 |     protected function get_trash_fullpath_from_hash($contenthash) {
 | 
        
           |  |  | 222 |         return $this->trashdir . '/' . $this->get_contentpath_from_hash($contenthash);
 | 
        
           |  |  | 223 |     }
 | 
        
           |  |  | 224 |   | 
        
           |  |  | 225 |     /**
 | 
        
           |  |  | 226 |      * Copy content of file to given pathname.
 | 
        
           |  |  | 227 |      *
 | 
        
           |  |  | 228 |      * @param stored_file $file The file to be copied
 | 
        
           |  |  | 229 |      * @param string $target real path to the new file
 | 
        
           |  |  | 230 |      * @return bool success
 | 
        
           |  |  | 231 |      */
 | 
        
           |  |  | 232 |     public function copy_content_from_storedfile(stored_file $file, $target) {
 | 
        
           |  |  | 233 |         $source = $this->get_local_path_from_storedfile($file, true);
 | 
        
           |  |  | 234 |         return copy($source, $target);
 | 
        
           |  |  | 235 |     }
 | 
        
           |  |  | 236 |   | 
        
           |  |  | 237 |     /**
 | 
        
           |  |  | 238 |      * Tries to recover missing content of file from trash.
 | 
        
           |  |  | 239 |      *
 | 
        
           |  |  | 240 |      * @param stored_file $file stored_file instance
 | 
        
           |  |  | 241 |      * @return bool success
 | 
        
           |  |  | 242 |      */
 | 
        
           |  |  | 243 |     protected function recover_file(stored_file $file) {
 | 
        
           |  |  | 244 |         $contentfile = $this->get_local_path_from_storedfile($file, false);
 | 
        
           |  |  | 245 |   | 
        
           |  |  | 246 |         if (file_exists($contentfile)) {
 | 
        
           |  |  | 247 |             // The file already exists on the file system. No need to recover.
 | 
        
           |  |  | 248 |             return true;
 | 
        
           |  |  | 249 |         }
 | 
        
           |  |  | 250 |   | 
        
           |  |  | 251 |         $contenthash = $file->get_contenthash();
 | 
        
           |  |  | 252 |         $contentdir = $this->get_fulldir_from_storedfile($file);
 | 
        
           |  |  | 253 |         $trashfile = $this->get_trash_fullpath_from_hash($contenthash);
 | 
        
           |  |  | 254 |         $alttrashfile = "{$this->trashdir}/{$contenthash}";
 | 
        
           |  |  | 255 |   | 
        
           |  |  | 256 |         if (!is_readable($trashfile)) {
 | 
        
           |  |  | 257 |             // The trash file was not found. Check the alternative trash file too just in case.
 | 
        
           |  |  | 258 |             if (!is_readable($alttrashfile)) {
 | 
        
           |  |  | 259 |                 return false;
 | 
        
           |  |  | 260 |             }
 | 
        
           |  |  | 261 |             // The alternative trash file in trash root exists.
 | 
        
           |  |  | 262 |             $trashfile = $alttrashfile;
 | 
        
           |  |  | 263 |         }
 | 
        
           |  |  | 264 |   | 
        
           |  |  | 265 |         if (filesize($trashfile) != $file->get_filesize() or file_storage::hash_from_path($trashfile) != $contenthash) {
 | 
        
           |  |  | 266 |             // The files are different. Leave this one in trash - something seems to be wrong with it.
 | 
        
           |  |  | 267 |             return false;
 | 
        
           |  |  | 268 |         }
 | 
        
           |  |  | 269 |   | 
        
           |  |  | 270 |         if (!is_dir($contentdir)) {
 | 
        
           |  |  | 271 |             if (!mkdir($contentdir, $this->dirpermissions, true)) {
 | 
        
           |  |  | 272 |                 // Unable to create the target directory.
 | 
        
           |  |  | 273 |                 return false;
 | 
        
           |  |  | 274 |             }
 | 
        
           |  |  | 275 |         }
 | 
        
           |  |  | 276 |   | 
        
           |  |  | 277 |         // Perform a rename - these are generally atomic which gives us big
 | 
        
           |  |  | 278 |         // performance wins, especially for large files.
 | 
        
           |  |  | 279 |         return rename($trashfile, $contentfile);
 | 
        
           |  |  | 280 |     }
 | 
        
           |  |  | 281 |   | 
        
           |  |  | 282 |     /**
 | 
        
           |  |  | 283 |      * Marks pool file as candidate for deleting.
 | 
        
           |  |  | 284 |      *
 | 
        
           |  |  | 285 |      * @param string $contenthash
 | 
        
           |  |  | 286 |      */
 | 
        
           |  |  | 287 |     public function remove_file($contenthash) {
 | 
        
           |  |  | 288 |         if (!self::is_file_removable($contenthash)) {
 | 
        
           |  |  | 289 |             // Don't remove the file - it's still in use.
 | 
        
           |  |  | 290 |             return;
 | 
        
           |  |  | 291 |         }
 | 
        
           |  |  | 292 |   | 
        
           |  |  | 293 |         if (!$this->is_file_readable_remotely_by_hash($contenthash)) {
 | 
        
           |  |  | 294 |             // The file wasn't found in the first place. Just ignore it.
 | 
        
           |  |  | 295 |             return;
 | 
        
           |  |  | 296 |         }
 | 
        
           |  |  | 297 |   | 
        
           |  |  | 298 |         $trashpath  = $this->get_trash_fulldir_from_hash($contenthash);
 | 
        
           |  |  | 299 |         $trashfile  = $this->get_trash_fullpath_from_hash($contenthash);
 | 
        
           |  |  | 300 |         $contentfile = $this->get_local_path_from_hash($contenthash, true);
 | 
        
           |  |  | 301 |   | 
        
           |  |  | 302 |         if (!is_dir($trashpath)) {
 | 
        
           |  |  | 303 |             mkdir($trashpath, $this->dirpermissions, true);
 | 
        
           |  |  | 304 |         }
 | 
        
           |  |  | 305 |   | 
        
           |  |  | 306 |         if (file_exists($trashfile)) {
 | 
        
           |  |  | 307 |             // A copy of this file is already in the trash.
 | 
        
           |  |  | 308 |             // Remove the old version.
 | 
        
           |  |  | 309 |             unlink($contentfile);
 | 
        
           |  |  | 310 |             return;
 | 
        
           |  |  | 311 |         }
 | 
        
           |  |  | 312 |   | 
        
           |  |  | 313 |         // Move the contentfile to the trash, and fix permissions as required.
 | 
        
           |  |  | 314 |         rename($contentfile, $trashfile);
 | 
        
           |  |  | 315 |   | 
        
           |  |  | 316 |         // Fix permissions, only if needed.
 | 
        
           |  |  | 317 |         $currentperms = octdec(substr(decoct(fileperms($trashfile)), -4));
 | 
        
           |  |  | 318 |         if ((int)$this->filepermissions !== $currentperms) {
 | 
        
           |  |  | 319 |             chmod($trashfile, $this->filepermissions);
 | 
        
           |  |  | 320 |         }
 | 
        
           |  |  | 321 |     }
 | 
        
           |  |  | 322 |   | 
        
           |  |  | 323 |     /**
 | 
        
           |  |  | 324 |      * Cleanup the trash directory.
 | 
        
           |  |  | 325 |      */
 | 
        
           |  |  | 326 |     public function cron() {
 | 
        
           |  |  | 327 |         $this->empty_trash();
 | 
        
           |  |  | 328 |     }
 | 
        
           |  |  | 329 |   | 
        
           |  |  | 330 |     protected function empty_trash() {
 | 
        
           |  |  | 331 |         fulldelete($this->trashdir);
 | 
        
           |  |  | 332 |         set_config('fileslastcleanup', time());
 | 
        
           |  |  | 333 |     }
 | 
        
           |  |  | 334 |   | 
        
           |  |  | 335 |     /**
 | 
        
           |  |  | 336 |      * Add the supplied file to the file system.
 | 
        
           |  |  | 337 |      *
 | 
        
           |  |  | 338 |      * Note: If overriding this function, it is advisable to store the file
 | 
        
           |  |  | 339 |      * in the path returned by get_local_path_from_hash as there may be
 | 
        
           |  |  | 340 |      * subsequent uses of the file in the same request.
 | 
        
           |  |  | 341 |      *
 | 
        
           |  |  | 342 |      * @param string $pathname Path to file currently on disk
 | 
        
           |  |  | 343 |      * @param string $contenthash SHA1 hash of content if known (performance only)
 | 
        
           |  |  | 344 |      * @return array (contenthash, filesize, newfile)
 | 
        
           |  |  | 345 |      */
 | 
        
           |  |  | 346 |     public function add_file_from_path($pathname, $contenthash = null) {
 | 
        
           |  |  | 347 |   | 
        
           |  |  | 348 |         list($contenthash, $filesize) = $this->validate_hash_and_file_size($contenthash, $pathname);
 | 
        
           |  |  | 349 |   | 
        
           |  |  | 350 |         $hashpath = $this->get_fulldir_from_hash($contenthash);
 | 
        
           |  |  | 351 |         $hashfile = $this->get_local_path_from_hash($contenthash, false);
 | 
        
           |  |  | 352 |   | 
        
           |  |  | 353 |         $newfile = true;
 | 
        
           |  |  | 354 |   | 
        
           |  |  | 355 |         $hashsize = self::check_file_exists_and_get_size($hashfile);
 | 
        
           |  |  | 356 |         if ($hashsize !== null) {
 | 
        
           |  |  | 357 |             if ($hashsize === $filesize) {
 | 
        
           |  |  | 358 |                 return array($contenthash, $filesize, false);
 | 
        
           |  |  | 359 |             }
 | 
        
           |  |  | 360 |             if (file_storage::hash_from_path($hashfile) === $contenthash) {
 | 
        
           |  |  | 361 |                 // Jackpot! We have a hash collision.
 | 
        
           |  |  | 362 |                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
 | 
        
           |  |  | 363 |                 copy($pathname, "$this->filedir/jackpot/{$contenthash}_1");
 | 
        
           |  |  | 364 |                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_2");
 | 
        
           |  |  | 365 |                 throw new file_pool_content_exception($contenthash);
 | 
        
           |  |  | 366 |             }
 | 
        
           |  |  | 367 |             debugging("Replacing invalid content file $contenthash");
 | 
        
           |  |  | 368 |             unlink($hashfile);
 | 
        
           |  |  | 369 |             $newfile = false;
 | 
        
           |  |  | 370 |         }
 | 
        
           |  |  | 371 |   | 
        
           |  |  | 372 |         if (!is_dir($hashpath)) {
 | 
        
           |  |  | 373 |             if (!mkdir($hashpath, $this->dirpermissions, true)) {
 | 
        
           |  |  | 374 |                 // Permission trouble.
 | 
        
           |  |  | 375 |                 throw new file_exception('storedfilecannotcreatefiledirs');
 | 
        
           |  |  | 376 |             }
 | 
        
           |  |  | 377 |         }
 | 
        
           |  |  | 378 |   | 
        
           |  |  | 379 |         // Let's try to prevent some race conditions.
 | 
        
           |  |  | 380 |   | 
        
           |  |  | 381 |         $prev = ignore_user_abort(true);
 | 
        
           |  |  | 382 |         if (file_exists($hashfile.'.tmp')) {
 | 
        
           |  |  | 383 |             @unlink($hashfile.'.tmp');
 | 
        
           |  |  | 384 |         }
 | 
        
           |  |  | 385 |         if (!copy($pathname, $hashfile.'.tmp')) {
 | 
        
           |  |  | 386 |             // Borked permissions or out of disk space.
 | 
        
           |  |  | 387 |             @unlink($hashfile.'.tmp');
 | 
        
           |  |  | 388 |             ignore_user_abort($prev);
 | 
        
           |  |  | 389 |             throw new file_exception('storedfilecannotcreatefile');
 | 
        
           |  |  | 390 |         }
 | 
        
           |  |  | 391 |         if (file_storage::hash_from_path($hashfile.'.tmp') !== $contenthash) {
 | 
        
           |  |  | 392 |             // Highly unlikely edge case, but this can happen on an NFS volume with no space remaining.
 | 
        
           |  |  | 393 |             @unlink($hashfile.'.tmp');
 | 
        
           |  |  | 394 |             ignore_user_abort($prev);
 | 
        
           |  |  | 395 |             throw new file_exception('storedfilecannotcreatefile');
 | 
        
           |  |  | 396 |         }
 | 
        
           |  |  | 397 |         if (!rename($hashfile.'.tmp', $hashfile)) {
 | 
        
           |  |  | 398 |             // Something very strange went wrong.
 | 
        
           |  |  | 399 |             @unlink($hashfile . '.tmp');
 | 
        
           |  |  | 400 |             // Note, we don't try to clean up $hashfile. Almost certainly, if it exists
 | 
        
           |  |  | 401 |             // (e.g. written by another process?) it will be right, so don't wipe it.
 | 
        
           |  |  | 402 |             ignore_user_abort($prev);
 | 
        
           |  |  | 403 |             throw new file_exception('storedfilecannotcreatefile');
 | 
        
           |  |  | 404 |         }
 | 
        
           |  |  | 405 |         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
 | 
        
           |  |  | 406 |         if (file_exists($hashfile.'.tmp')) {
 | 
        
           |  |  | 407 |             // Just in case anything fails in a weird way.
 | 
        
           |  |  | 408 |             @unlink($hashfile.'.tmp');
 | 
        
           |  |  | 409 |         }
 | 
        
           |  |  | 410 |         ignore_user_abort($prev);
 | 
        
           |  |  | 411 |   | 
        
           |  |  | 412 |         return array($contenthash, $filesize, $newfile);
 | 
        
           |  |  | 413 |     }
 | 
        
           |  |  | 414 |   | 
        
           |  |  | 415 |     /**
 | 
        
           |  |  | 416 |      * Checks if the file exists and gets its size. This function avoids a specific issue with
 | 
        
           |  |  | 417 |      * networked file systems if they incorrectly report the file exists, but then decide it doesn't
 | 
        
           |  |  | 418 |      * as soon as you try to get the file size.
 | 
        
           |  |  | 419 |      *
 | 
        
           |  |  | 420 |      * @param string $hashfile File to check
 | 
        
           |  |  | 421 |      * @return int|null Null if the file does not exist, or the result of filesize(), or -1 if error
 | 
        
           |  |  | 422 |      */
 | 
        
           |  |  | 423 |     protected static function check_file_exists_and_get_size(string $hashfile): ?int {
 | 
        
           |  |  | 424 |         if (!file_exists($hashfile)) {
 | 
        
           |  |  | 425 |             // The file does not exist, return null.
 | 
        
           |  |  | 426 |             return null;
 | 
        
           |  |  | 427 |         }
 | 
        
           |  |  | 428 |   | 
        
           |  |  | 429 |         // In some networked file systems, it's possible that file_exists will return true when
 | 
        
           |  |  | 430 |         // the file doesn't exist (due to caching), but filesize will then return false because
 | 
        
           |  |  | 431 |         // it doesn't exist.
 | 
        
           |  |  | 432 |         $hashsize = @filesize($hashfile);
 | 
        
           |  |  | 433 |         if ($hashsize !== false) {
 | 
        
           |  |  | 434 |             // We successfully got a file size. Return it.
 | 
        
           |  |  | 435 |             return $hashsize;
 | 
        
           |  |  | 436 |         }
 | 
        
           |  |  | 437 |   | 
        
           |  |  | 438 |         // If we can't get the filesize, let's check existence again to see if we really
 | 
        
           |  |  | 439 |         // for sure think it exists.
 | 
        
           |  |  | 440 |         clearstatcache();
 | 
        
           |  |  | 441 |         if (!file_exists($hashfile)) {
 | 
        
           |  |  | 442 |             // The file doesn't exist any more, so return null.
 | 
        
           |  |  | 443 |             return null;
 | 
        
           |  |  | 444 |         }
 | 
        
           |  |  | 445 |   | 
        
           |  |  | 446 |         // It still thinks the file exists, but filesize failed, so we had better return an invalid
 | 
        
           |  |  | 447 |         // value for filesize.
 | 
        
           |  |  | 448 |         return -1;
 | 
        
           |  |  | 449 |     }
 | 
        
           |  |  | 450 |   | 
        
           |  |  | 451 |     /**
 | 
        
           |  |  | 452 |      * Add a file with the supplied content to the file system.
 | 
        
           |  |  | 453 |      *
 | 
        
           |  |  | 454 |      * Note: If overriding this function, it is advisable to store the file
 | 
        
           |  |  | 455 |      * in the path returned by get_local_path_from_hash as there may be
 | 
        
           |  |  | 456 |      * subsequent uses of the file in the same request.
 | 
        
           |  |  | 457 |      *
 | 
        
           |  |  | 458 |      * @param string $content file content - binary string
 | 
        
           |  |  | 459 |      * @return array (contenthash, filesize, newfile)
 | 
        
           |  |  | 460 |      */
 | 
        
           |  |  | 461 |     public function add_file_from_string($content) {
 | 
        
           |  |  | 462 |         global $CFG;
 | 
        
           |  |  | 463 |   | 
        
           |  |  | 464 |         $contenthash = file_storage::hash_from_string($content);
 | 
        
           |  |  | 465 |         // Binary length.
 | 
        
           |  |  | 466 |         $filesize = strlen($content ?? '');
 | 
        
           |  |  | 467 |   | 
        
           |  |  | 468 |         $hashpath = $this->get_fulldir_from_hash($contenthash);
 | 
        
           |  |  | 469 |         $hashfile = $this->get_local_path_from_hash($contenthash, false);
 | 
        
           |  |  | 470 |   | 
        
           |  |  | 471 |         $newfile = true;
 | 
        
           |  |  | 472 |   | 
        
           |  |  | 473 |         $hashsize = self::check_file_exists_and_get_size($hashfile);
 | 
        
           |  |  | 474 |         if ($hashsize !== null) {
 | 
        
           |  |  | 475 |             if ($hashsize === $filesize) {
 | 
        
           |  |  | 476 |                 return array($contenthash, $filesize, false);
 | 
        
           |  |  | 477 |             }
 | 
        
           |  |  | 478 |             if (file_storage::hash_from_path($hashfile) === $contenthash) {
 | 
        
           |  |  | 479 |                 // Jackpot! We have a hash collision.
 | 
        
           |  |  | 480 |                 mkdir("$this->filedir/jackpot/", $this->dirpermissions, true);
 | 
        
           |  |  | 481 |                 copy($hashfile, "$this->filedir/jackpot/{$contenthash}_1");
 | 
        
           |  |  | 482 |                 file_put_contents("$this->filedir/jackpot/{$contenthash}_2", $content);
 | 
        
           |  |  | 483 |                 throw new file_pool_content_exception($contenthash);
 | 
        
           |  |  | 484 |             }
 | 
        
           |  |  | 485 |             debugging("Replacing invalid content file $contenthash");
 | 
        
           |  |  | 486 |             unlink($hashfile);
 | 
        
           |  |  | 487 |             $newfile = false;
 | 
        
           |  |  | 488 |         }
 | 
        
           |  |  | 489 |   | 
        
           |  |  | 490 |         if (!is_dir($hashpath)) {
 | 
        
           |  |  | 491 |             if (!mkdir($hashpath, $this->dirpermissions, true)) {
 | 
        
           |  |  | 492 |                 // Permission trouble.
 | 
        
           |  |  | 493 |                 throw new file_exception('storedfilecannotcreatefiledirs');
 | 
        
           |  |  | 494 |             }
 | 
        
           |  |  | 495 |         }
 | 
        
           |  |  | 496 |   | 
        
           |  |  | 497 |         // Hopefully this works around most potential race conditions.
 | 
        
           |  |  | 498 |   | 
        
           |  |  | 499 |         $prev = ignore_user_abort(true);
 | 
        
           |  |  | 500 |   | 
        
           |  |  | 501 |         if (!empty($CFG->preventfilelocking)) {
 | 
        
           |  |  | 502 |             $newsize = file_put_contents($hashfile.'.tmp', $content);
 | 
        
           |  |  | 503 |         } else {
 | 
        
           |  |  | 504 |             $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
 | 
        
           |  |  | 505 |         }
 | 
        
           |  |  | 506 |   | 
        
           |  |  | 507 |         if ($newsize === false) {
 | 
        
           |  |  | 508 |             // Borked permissions most likely.
 | 
        
           |  |  | 509 |             ignore_user_abort($prev);
 | 
        
           |  |  | 510 |             throw new file_exception('storedfilecannotcreatefile');
 | 
        
           |  |  | 511 |         }
 | 
        
           |  |  | 512 |         if (filesize($hashfile.'.tmp') !== $filesize) {
 | 
        
           |  |  | 513 |             // Out of disk space?
 | 
        
           |  |  | 514 |             unlink($hashfile.'.tmp');
 | 
        
           |  |  | 515 |             ignore_user_abort($prev);
 | 
        
           |  |  | 516 |             throw new file_exception('storedfilecannotcreatefile');
 | 
        
           |  |  | 517 |         }
 | 
        
           |  |  | 518 |         if (!rename($hashfile.'.tmp', $hashfile)) {
 | 
        
           |  |  | 519 |             // Something very strange went wrong.
 | 
        
           |  |  | 520 |             @unlink($hashfile . '.tmp');
 | 
        
           |  |  | 521 |             // Note, we don't try to clean up $hashfile. Almost certainly, if it exists
 | 
        
           |  |  | 522 |             // (e.g. written by another process?) it will be right, so don't wipe it.
 | 
        
           |  |  | 523 |             ignore_user_abort($prev);
 | 
        
           |  |  | 524 |             throw new file_exception('storedfilecannotcreatefile');
 | 
        
           |  |  | 525 |         }
 | 
        
           |  |  | 526 |         chmod($hashfile, $this->filepermissions); // Fix permissions if needed.
 | 
        
           |  |  | 527 |         if (file_exists($hashfile.'.tmp')) {
 | 
        
           |  |  | 528 |             // Just in case anything fails in a weird way.
 | 
        
           |  |  | 529 |             @unlink($hashfile.'.tmp');
 | 
        
           |  |  | 530 |         }
 | 
        
           |  |  | 531 |         ignore_user_abort($prev);
 | 
        
           |  |  | 532 |   | 
        
           |  |  | 533 |         return array($contenthash, $filesize, $newfile);
 | 
        
           |  |  | 534 |     }
 | 
        
           |  |  | 535 |   | 
        
           |  |  | 536 | }
 |