/** * Stream a file to the browser, adding all the headings and fun stuff. * Headers sent include: Content-type, Content-Length, Last-Modified, * and Content-Disposition. * * @param string $fname Full name and path of the file to stream * @param array $headers Any additional headers to send * @param bool $sendErrors Send error messages if errors occur (like 404) * @throws MWException * @return bool Success */ public static function stream($fname, $headers = array(), $sendErrors = true) { wfProfileIn(__METHOD__); if (FileBackend::isStoragePath($fname)) { // sanity wfProfileOut(__METHOD__); throw new MWException(__FUNCTION__ . " given storage path '{$fname}'."); } wfSuppressWarnings(); $stat = stat($fname); wfRestoreWarnings(); $res = self::prepareForStream($fname, $stat, $headers, $sendErrors); if ($res == self::NOT_MODIFIED) { $ok = true; // use client cache } elseif ($res == self::READY_STREAM) { wfProfileIn(__METHOD__ . '-send'); $ok = readfile($fname); wfProfileOut(__METHOD__ . '-send'); } else { $ok = false; // failed } wfProfileOut(__METHOD__); return $ok; }
/** * Sets up the file object * * @param $path string Path to temporary file on local disk * @throws MWException */ public function __construct($path) { if (FileBackend::isStoragePath($path)) { throw new MWException(__METHOD__ . " given storage path `{$path}`."); } $this->path = $path; }
/** * Stream a file to the browser, adding all the headings and fun stuff. * Headers sent include: Content-type, Content-Length, Last-Modified, * and Content-Disposition. * * @param string $fname Full name and path of the file to stream * @param array $headers Any additional headers to send if the file exists * @param bool $sendErrors Send error messages if errors occur (like 404) * @param array $optHeaders HTTP request header map (e.g. "range") (use lowercase keys) * @param integer $flags Bitfield of STREAM_* constants * @throws MWException * @return bool Success */ public static function stream($fname, $headers = [], $sendErrors = true, $optHeaders = [], $flags = 0) { if (FileBackend::isStoragePath($fname)) { // sanity throw new InvalidArgumentException(__FUNCTION__ . " given storage path '{$fname}'."); } $streamer = new HTTPFileStreamer($fname, ['obResetFunc' => 'wfResetOutputBuffers', 'streamMimeFunc' => [__CLASS__, 'contentTypeFromPath']]); return $streamer->stream($headers, $sendErrors, $optHeaders, $flags); }
/** * Stream a file to the browser, adding all the headings and fun stuff. * Headers sent include: Content-type, Content-Length, Last-Modified, * and Content-Disposition. * * @param string $fname Full name and path of the file to stream * @param array $headers Any additional headers to send * @param bool $sendErrors Send error messages if errors occur (like 404) * @throws MWException * @return bool Success */ public static function stream($fname, $headers = [], $sendErrors = true) { if (FileBackend::isStoragePath($fname)) { // sanity throw new MWException(__FUNCTION__ . " given storage path '{$fname}'."); } MediaWiki\suppressWarnings(); $stat = stat($fname); MediaWiki\restoreWarnings(); $res = self::prepareForStream($fname, $stat, $headers, $sendErrors); if ($res == self::NOT_MODIFIED) { $ok = true; // use client cache } elseif ($res == self::READY_STREAM) { $ok = readfile($fname); } else { $ok = false; // failed } return $ok; }
/** * Checks existence of an array of files. * * @param $files Array: Virtual URLs (or storage paths) of files to check * @param $flags Integer: bitwise combination of the following flags: * self::FILES_ONLY Mark file as existing only if it is a file (not directory) * @return array|bool Either array of files and existence flags, or false */ public function fileExistsBatch($files, $flags = 0) { $result = array(); foreach ($files as $key => $file) { if (self::isVirtualUrl($file)) { $file = $this->resolveVirtualUrl($file); } if (FileBackend::isStoragePath($file)) { $result[$key] = $this->backend->fileExists(array('src' => $file)); } else { if ($flags & self::FILES_ONLY) { $result[$key] = is_file($file); // FS only } else { $result[$key] = file_exists($file); // FS only } } } return $result; }
/** * @dataProvider provider_testIsStoragePath */ public function testIsStoragePath($path, $isStorePath) { $this->assertEquals($isStorePath, FileBackend::isStoragePath($path), "FileBackend::isStoragePath on path '{$path}'"); }
/** * Upload a file and record it in the DB * @param string $srcPath Source storage path, virtual URL, or filesystem path * @param string $comment Upload description * @param string $pageText Text to use for the new description page, * if a new description page is created * @param int|bool $flags Flags for publish() * @param array|bool $props File properties, if known. This can be used to * reduce the upload time when uploading virtual URLs for which the file * info is already known * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the * current time * @param User|null $user User object or null to use $wgUser * * @return FileRepoStatus object. On success, the value member contains the * archive name, or an empty string if it was a new file. */ function upload($srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null) { global $wgContLang; if ($this->getRepo()->getReadOnlyReason() !== false) { return $this->readOnlyFatalStatus(); } if (!$props) { wfProfileIn(__METHOD__ . '-getProps'); if ($this->repo->isVirtualUrl($srcPath) || FileBackend::isStoragePath($srcPath)) { $props = $this->repo->getFileProps($srcPath); } else { $props = FSFile::getPropsFromPath($srcPath); } wfProfileOut(__METHOD__ . '-getProps'); } $options = array(); $handler = MediaHandler::getHandler($props['mime']); if ($handler) { $options['headers'] = $handler->getStreamHeaders($props['metadata']); } else { $options['headers'] = array(); } // Trim spaces on user supplied text $comment = trim($comment); // truncate nicely or the DB will do it for us // non-nicely (dangling multi-byte chars, non-truncated version in cache). $comment = $wgContLang->truncate($comment, 255); $this->lock(); // begin $status = $this->publish($srcPath, $flags, $options); if ($status->successCount > 0) { # Essentially we are displacing any existing current file and saving # a new current file at the old location. If just the first succeeded, # we still need to displace the current DB entry and put in a new one. if (!$this->recordUpload2($status->value, $comment, $pageText, $props, $timestamp, $user)) { $status->fatal('filenotfound', $srcPath); } } $this->unlock(); // done return $status; }
/** * Initialize the path information * @param string $name the desired destination name * @param string $tempPath the temporary path * @param int $fileSize the file size * @param bool $removeTempFile (false) remove the temporary file? * @throws MWException */ public function initializePathInfo($name, $tempPath, $fileSize, $removeTempFile = false) { $this->mDesiredDestName = $name; if (FileBackend::isStoragePath($tempPath)) { throw new MWException(__METHOD__ . " given storage path `{$tempPath}`."); } $this->mTempPath = $tempPath; $this->mFileSize = $fileSize; $this->mRemoveTempFile = $removeTempFile; }
/** * @param array $files * @return array */ function fileExistsBatch(array $files) { $results = []; foreach ($files as $k => $f) { if (isset($this->mFileExists[$f])) { $results[$k] = $this->mFileExists[$f]; unset($files[$k]); } elseif (self::isVirtualUrl($f)) { # @todo FIXME: We need to be able to handle virtual # URLs better, at least when we know they refer to the # same repo. $results[$k] = false; unset($files[$k]); } elseif (FileBackend::isStoragePath($f)) { $results[$k] = false; unset($files[$k]); wfWarn("Got mwstore:// path '{$f}'."); } } $data = $this->fetchImageQuery(['titles' => implode($files, '|'), 'prop' => 'imageinfo']); if (isset($data['query']['pages'])) { # First, get results from the query. Note we only care whether the image exists, # not whether it has a description page. foreach ($data['query']['pages'] as $p) { $this->mFileExists[$p['title']] = $p['imagerepository'] !== ''; } # Second, copy the results to any redirects that were queried if (isset($data['query']['redirects'])) { foreach ($data['query']['redirects'] as $r) { $this->mFileExists[$r['from']] = $this->mFileExists[$r['to']]; } } # Third, copy the results to any non-normalized titles that were queried if (isset($data['query']['normalized'])) { foreach ($data['query']['normalized'] as $n) { $this->mFileExists[$n['from']] = $this->mFileExists[$n['to']]; } } # Finally, copy the results to the output foreach ($files as $key => $file) { $results[$key] = $this->mFileExists[$file]; } } return $results; }
/** * Publish a batch of files * * @param $triplets Array: (source, dest, archive) triplets as per publish() * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source files should be deleted if possible * @throws MWException * @return FileRepoStatus */ public function publishBatch(array $triplets, $flags = 0) { $this->assertWritableRepo(); // fail out if read-only $backend = $this->backend; // convenience // Try creating directories $status = $this->initZones('public'); if (!$status->isOK()) { return $status; } $status = $this->newGood(array()); $operations = array(); $sourceFSFilesToDelete = array(); // cleanup for disk source files // Validate each triplet and get the store operation... foreach ($triplets as $i => $triplet) { list($srcPath, $dstRel, $archiveRel) = $triplet; // Resolve source to a storage path if virtual $srcPath = $this->resolveToStoragePath($srcPath); if (!$this->validateFilename($dstRel)) { throw new MWException('Validation error in $dstRel'); } if (!$this->validateFilename($archiveRel)) { throw new MWException('Validation error in $archiveRel'); } $publicRoot = $this->getZonePath('public'); $dstPath = "{$publicRoot}/{$dstRel}"; $archivePath = "{$publicRoot}/{$archiveRel}"; $dstDir = dirname($dstPath); $archiveDir = dirname($archivePath); // Abort immediately on directory creation errors since they're likely to be repetitive if (!$this->initDirectory($dstDir)->isOK()) { return $this->newFatal('directorycreateerror', $dstDir); } if (!$this->initDirectory($archiveDir)->isOK()) { return $this->newFatal('directorycreateerror', $archiveDir); } // Archive destination file if it exists if ($backend->fileExists(array('src' => $dstPath))) { // Check if the archive file exists // This is a sanity check to avoid data loss. In UNIX, the rename primitive // unlinks the destination file if it exists. DB-based synchronisation in // publishBatch's caller should prevent races. In Windows there's no // problem because the rename primitive fails if the destination exists. if ($backend->fileExists(array('src' => $archivePath))) { $operations[] = array('op' => 'null'); continue; } else { $operations[] = array('op' => 'move', 'src' => $dstPath, 'dst' => $archivePath); } $status->value[$i] = 'archived'; } else { $status->value[$i] = 'new'; } // Copy (or move) the source file to the destination if (FileBackend::isStoragePath($srcPath)) { if ($flags & self::DELETE_SOURCE) { $operations[] = array('op' => 'move', 'src' => $srcPath, 'dst' => $dstPath); } else { $operations[] = array('op' => 'copy', 'src' => $srcPath, 'dst' => $dstPath); } } else { // FS source path $operations[] = array('op' => 'store', 'src' => $srcPath, 'dst' => $dstPath); if ($flags & self::DELETE_SOURCE) { $sourceFSFilesToDelete[] = $srcPath; } } } // Execute the operations for each triplet $opts = array('force' => true); $status->merge($backend->doOperations($operations, $opts)); // Cleanup for disk source files... foreach ($sourceFSFilesToDelete as $file) { wfSuppressWarnings(); unlink($file); // FS cleanup wfRestoreWarnings(); } return $status; }
/** * Publish a batch of files * * @param array $ntuples (source, dest, archive) triplets or * (source, dest, archive, options) 4-tuples as per publish(). * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source files should be deleted if possible * @throws MWException * @return FileRepoStatus */ public function publishBatch(array $ntuples, $flags = 0) { $this->assertWritableRepo(); // fail out if read-only $backend = $this->backend; // convenience // Try creating directories $status = $this->initZones('public'); if (!$status->isOK()) { return $status; } $status = $this->newGood(array()); $operations = array(); $sourceFSFilesToDelete = array(); // cleanup for disk source files // Validate each triplet and get the store operation... foreach ($ntuples as $ntuple) { list($srcPath, $dstRel, $archiveRel) = $ntuple; $options = isset($ntuple[3]) ? $ntuple[3] : array(); // Resolve source to a storage path if virtual $srcPath = $this->resolveToStoragePath($srcPath); if (!$this->validateFilename($dstRel)) { throw new MWException('Validation error in $dstRel'); } if (!$this->validateFilename($archiveRel)) { throw new MWException('Validation error in $archiveRel'); } $publicRoot = $this->getZonePath('public'); $dstPath = "{$publicRoot}/{$dstRel}"; $archivePath = "{$publicRoot}/{$archiveRel}"; $dstDir = dirname($dstPath); $archiveDir = dirname($archivePath); // Abort immediately on directory creation errors since they're likely to be repetitive if (!$this->initDirectory($dstDir)->isOK()) { return $this->newFatal('directorycreateerror', $dstDir); } if (!$this->initDirectory($archiveDir)->isOK()) { return $this->newFatal('directorycreateerror', $archiveDir); } // Set any desired headers to be use in GET/HEAD responses $headers = isset($options['headers']) ? $options['headers'] : array(); // Archive destination file if it exists. // This will check if the archive file also exists and fail if does. // This is a sanity check to avoid data loss. On Windows and Linux, // copy() will overwrite, so the existence check is vulnerable to // race conditions unless a functioning LockManager is used. // LocalFile also uses SELECT FOR UPDATE for synchronization. $operations[] = array('op' => 'copy', 'src' => $dstPath, 'dst' => $archivePath, 'ignoreMissingSource' => true); // Copy (or move) the source file to the destination if (FileBackend::isStoragePath($srcPath)) { if ($flags & self::DELETE_SOURCE) { $operations[] = array('op' => 'move', 'src' => $srcPath, 'dst' => $dstPath, 'overwrite' => true, 'headers' => $headers); } else { $operations[] = array('op' => 'copy', 'src' => $srcPath, 'dst' => $dstPath, 'overwrite' => true, 'headers' => $headers); } } else { // FS source path $operations[] = array('op' => 'store', 'src' => $srcPath, 'dst' => $dstPath, 'overwrite' => true, 'headers' => $headers); if ($flags & self::DELETE_SOURCE) { $sourceFSFilesToDelete[] = $srcPath; } } } // Execute the operations for each triplet $status->merge($backend->doOperations($operations)); // Find out which files were archived... foreach ($ntuples as $i => $ntuple) { list(, , $archiveRel) = $ntuple; $archivePath = $this->getZonePath('public') . "/{$archiveRel}"; if ($this->fileExists($archivePath)) { $status->value[$i] = 'archived'; } else { $status->value[$i] = 'new'; } } // Cleanup for disk source files... foreach ($sourceFSFilesToDelete as $file) { wfSuppressWarnings(); unlink($file); // FS cleanup wfRestoreWarnings(); } return $status; }
/** * Stream the file if there were no errors * * @param array $headers Additional HTTP headers to send on success * @return Status * @since 1.27 */ public function streamFileWithStatus($headers = []) { if (!$this->path) { return Status::newFatal('backend-fail-stream', '<no path>'); } elseif (FileBackend::isStoragePath($this->path)) { $be = $this->file->getRepo()->getBackend(); return $be->streamFile(['src' => $this->path, 'headers' => $headers]); } else { // FS-file $success = StreamFile::stream($this->getLocalCopyPath(), $headers); return $success ? Status::newGood() : Status::newFatal('backend-fail-stream', $this->path); } }
/** * Stream the file if there were no errors * * @param array $headers Additional HTTP headers to send on success * @return Bool success */ public function streamFile( $headers = array() ) { if ( !$this->path ) { return false; } elseif ( FileBackend::isStoragePath( $this->path ) ) { $be = $this->file->getRepo()->getBackend(); return $be->streamFile( array( 'src' => $this->path, 'headers' => $headers ) )->isOK(); } else { // FS-file return StreamFile::stream( $this->getLocalCopyPath(), $headers ); } }
/** * Make directory, and make all parent directories if they don't exist * * @param $dir String: full path to directory to create * @param $mode Integer: chmod value to use, default is $wgDirectoryMode * @param $caller String: optional caller param for debugging. * @throws MWException * @return bool */ function wfMkdirParents($dir, $mode = null, $caller = null) { global $wgDirectoryMode; if (FileBackend::isStoragePath($dir)) { // sanity throw new MWException(__FUNCTION__ . " given storage path '{$dir}'."); } if (!is_null($caller)) { wfDebug("{$caller}: called wfMkdirParents({$dir})\n"); } if (strval($dir) === '' || file_exists($dir)) { return true; } $dir = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $dir); if (is_null($mode)) { $mode = $wgDirectoryMode; } // Turn off the normal warning, we're doing our own below wfSuppressWarnings(); $ok = mkdir($dir, $mode, true); // PHP5 <3 wfRestoreWarnings(); if (!$ok) { // PHP doesn't report the path in its warning message, so add our own to aid in diagnosis. trigger_error(sprintf("%s: failed to mkdir \"%s\" mode 0%o", __FUNCTION__, $dir, $mode), E_USER_WARNING); } return $ok; }
/** * @param $files array * @return array */ function fileExistsBatch(array $files) { $results = array(); foreach ($files as $k => $f) { if (isset($this->mFileExists[$k])) { $results[$k] = true; unset($files[$k]); } elseif (self::isVirtualUrl($f)) { # @todo FIXME: We need to be able to handle virtual # URLs better, at least when we know they refer to the # same repo. $results[$k] = false; unset($files[$k]); } elseif (FileBackend::isStoragePath($f)) { $results[$k] = false; unset($files[$k]); wfWarn("Got mwstore:// path '{$f}'."); } } $data = $this->fetchImageQuery(array('titles' => implode($files, '|'), 'prop' => 'imageinfo')); if (isset($data['query']['pages'])) { $i = 0; foreach ($files as $key => $file) { $results[$key] = $this->mFileExists[$key] = !isset($data['query']['pages'][$i]['missing']); $i++; } } return $results; }
/** * Make directory, and make all parent directories if they don't exist * * @param string $dir Full path to directory to create * @param int $mode Chmod value to use, default is $wgDirectoryMode * @param string $caller Optional caller param for debugging. * @throws MWException * @return bool */ function wfMkdirParents($dir, $mode = null, $caller = null) { global $wgDirectoryMode; if (FileBackend::isStoragePath($dir)) { // sanity throw new MWException(__FUNCTION__ . " given storage path '{$dir}'."); } if (!is_null($caller)) { wfDebug("{$caller}: called wfMkdirParents({$dir})\n"); } if (strval($dir) === '' || is_dir($dir)) { return true; } $dir = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $dir); if (is_null($mode)) { $mode = $wgDirectoryMode; } // Turn off the normal warning, we're doing our own below MediaWiki\suppressWarnings(); $ok = mkdir($dir, $mode, true); // PHP5 <3 MediaWiki\restoreWarnings(); if (!$ok) { //directory may have been created on another request since we last checked if (is_dir($dir)) { return true; } // PHP doesn't report the path in its warning message, so add our own to aid in diagnosis. wfLogWarning(sprintf("failed to mkdir \"%s\" mode 0%o", $dir, $mode)); } return $ok; }
/** * Normalize a string if it is a valid storage path * * @param $path string * @return string */ protected static function normalizeIfValidStoragePath($path) { if (FileBackend::isStoragePath($path)) { $res = FileBackend::normalizeStoragePath($path); return $res !== null ? $res : $path; } return $path; }
/** * Upload a file and record it in the DB * @param string $srcPath Source storage path, virtual URL, or filesystem path * @param string $comment Upload description * @param string $pageText Text to use for the new description page, * if a new description page is created * @param int|bool $flags Flags for publish() * @param array|bool $props File properties, if known. This can be used to * reduce the upload time when uploading virtual URLs for which the file * info is already known * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the * current time * @param User|null $user User object or null to use $wgUser * @param string[] $tags Change tags to add to the log entry and page revision. * (This doesn't check $user's permissions.) * @return FileRepoStatus On success, the value member contains the * archive name, or an empty string if it was a new file. */ function upload($srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null, $tags = array()) { global $wgContLang; if ($this->getRepo()->getReadOnlyReason() !== false) { return $this->readOnlyFatalStatus(); } if (!$props) { if ($this->repo->isVirtualUrl($srcPath) || FileBackend::isStoragePath($srcPath)) { $props = $this->repo->getFileProps($srcPath); } else { $props = FSFile::getPropsFromPath($srcPath); } } $options = array(); $handler = MediaHandler::getHandler($props['mime']); if ($handler) { $options['headers'] = $handler->getStreamHeaders($props['metadata']); } else { $options['headers'] = array(); } // Trim spaces on user supplied text $comment = trim($comment); // Truncate nicely or the DB will do it for us // non-nicely (dangling multi-byte chars, non-truncated version in cache). $comment = $wgContLang->truncate($comment, 255); $this->lock(); // begin $status = $this->publish($srcPath, $flags, $options); if ($status->successCount >= 2) { // There will be a copy+(one of move,copy,store). // The first succeeding does not commit us to updating the DB // since it simply copied the current version to a timestamped file name. // It is only *preferable* to avoid leaving such files orphaned. // Once the second operation goes through, then the current version was // updated and we must therefore update the DB too. $oldver = $status->value; if (!$this->recordUpload2($oldver, $comment, $pageText, $props, $timestamp, $user, $tags)) { $status->fatal('filenotfound', $srcPath); } } $this->unlock(); // done return $status; }