class TarArchiveReader in Backup and Migrate 8.4
Class TarArchiveReader.
@package BackupMigrate\Core\Service
Hierarchy
- class \BackupMigrate\Core\Service\TarArchiveReader implements ArchiveReaderInterface
Expanded class hierarchy of TarArchiveReader
File
- lib/
backup_migrate_core/ src/ Service/ TarArchiveReader.php, line 13
Namespace
BackupMigrate\Core\ServiceView source
class TarArchiveReader implements ArchiveReaderInterface {
/**
* @var BackupFileReadableInterface
*/
protected $archive;
/**
* Get the file extension for this archiver. For a tarball writer this would
* be 'tar'. For a Zip file writer this would be 'zip'.
*
* @return string
*/
public function getFileExt() {
return 'tar';
}
/**
* {@inheritdoc}
*/
public function setArchive(BackupFileReadableInterface $out) {
$this->archive = $out;
}
/**
* Extract all files to the given directory.
*
* @param $directory
*
* @return mixed
*/
public function extractTo($directory) {
$this->archive
->openForRead(TRUE);
$result = $this
->extractAllToDirectory($directory);
$this->archive
->close();
return $result;
}
/**
* @param $directory
* The directory to extract the files to.
* @return bool
* @throws \BackupMigrate\Core\Exception\BackupMigrateException
*/
private function extractAllToDirectory($directory) {
clearstatcache();
// Read a header block.
while (strlen($block = $this->archive
->readBytes(512)) != 0) {
$header = $this
->readHeader($block);
if (!$header) {
return FALSE;
}
if ($header['filename'] == '') {
continue;
}
// Check for potentially malicious files (containing '..' etc.).
if ($this
->maliciousFilename($header['filename'])) {
throw new BackupMigrateException('Malicious .tar detected, file %filename. Will not install in desired directory tree', [
'%filename' => $header['filename'],
]);
}
// ignore extended / pax headers.
if ($header['typeflag'] == 'x' || $header['typeflag'] == 'g') {
$this->archive
->seekBytes(ceil($header['size'] / 512));
continue;
}
// Add the destination directory to the path.
if (substr($header['filename'], 0, 1) == '/') {
$header['filename'] = $directory . $header['filename'];
}
else {
$header['filename'] = $directory . '/' . $header['filename'];
}
// If the file already exists, make sure we can overwrite it.
if (file_exists($header['filename'])) {
// Cannot overwrite a directory with a file.
if (@is_dir($header['filename']) && $header['typeflag'] == '') {
throw new BackupMigrateException('File %filename already exists as a directory', [
'%filename' => $header['filename'],
]);
}
// Cannot overwrite a file with a directory.
if (@is_file($header['filename']) && !@is_link($header['filename']) && $header['typeflag'] == "5") {
throw new BackupMigrateException('Directory %filename already exists as file', [
'%filename' => $header['filename'],
]);
}
// Cannot overwrite a read-only file.
if (!is_writeable($header['filename'])) {
throw new BackupMigrateException('File %filename already exists and is write protected', [
'%filename' => $header['filename'],
]);
}
}
// Extract a directory.
if ($header['typeflag'] == "5") {
if (!$this
->createDir($header['filename'])) {
throw new BackupMigrateException('Unable to create directory %filename', [
'%filename' => $header['filename'],
]);
}
}
else {
if (!$this
->createDir(dirname($header['filename']))) {
throw new BackupMigrateException('Unable to create directory for %filename', [
'%filename' => $header['filename'],
]);
}
// Symlink.
if ($header['typeflag'] == "2") {
if (@file_exists($header['filename'])) {
@unlink($header['filename']);
}
if (!@symlink($header['link'], $header['filename'])) {
throw new BackupMigrateException('Unable to extract symbolic link: %filename', [
'%filename' => $header['filename'],
]);
}
}
else {
// Open the file for writing.
if (($dest_file = @fopen($header['filename'], "wb")) == 0) {
throw new BackupMigrateException('Error while opening %filename in write binary mode', [
'%filename' => $header['filename'],
]);
}
// Write the file.
$n = floor($header['size'] / 512);
for ($i = 0; $i < $n; $i++) {
$content = $this->archive
->readBytes(512);
fwrite($dest_file, $content, 512);
}
if ($header['size'] % 512 != 0) {
$content = $this->archive
->readBytes(512);
fwrite($dest_file, $content, $header['size'] % 512);
}
@fclose($dest_file);
// Change the file mode, mtime.
@touch($header['filename'], $header['mtime']);
if ($header['mode'] & 0111) {
// make file executable, obey umask.
$mode = fileperms($header['filename']) | ~umask() & 0111;
@chmod($header['filename'], $mode);
}
clearstatcache();
// Check if the file exists.
if (!is_file($header['filename'])) {
throw new BackupMigrateException('Extracted file %filename does not exist. Archive may be corrupted.', [
'%filename' => $header['filename'],
]);
}
// Check the file size.
$file_size = filesize($header['filename']);
if ($file_size != $header['size']) {
throw new BackupMigrateException('Extracted file %filename does not have the correct file size. File is %actual bytes (%expected bytes expected). Archive may be corrupted', [
'%filename' => $header['filename'],
'%expected' => (int) $header['size'],
(int) '%actual' => $file_size,
]);
}
}
}
}
return TRUE;
}
/**
* Create a directory or return true if it already exists.
*
* @param $directory
*
* @return boolean
*/
private function createDir($directory) {
if (@is_dir($directory) || $directory == '') {
return TRUE;
}
$parent = dirname($directory);
if ($parent != $directory && $parent != '' && !$this
->createDir($parent)) {
return FALSE;
}
if (@(!mkdir($directory, 0777))) {
return FALSE;
}
return TRUE;
}
/**
* Read a tar file header block.
*
* @param $block
* @param array $header
*
* @return array
*
* @throws \BackupMigrate\Core\Exception\BackupMigrateException
*/
private function readHeader($block, $header = []) {
if (strlen($block) == 0) {
$header['filename'] = '';
return TRUE;
}
if (strlen($block) != 512) {
$header['filename'] = '';
throw new BackupMigrateException('Invalid block size: %size bytes', [
'%size' => strlen($block),
]);
}
if (!is_array($header)) {
$header = [];
}
// Calculate the checksum.
$checksum = 0;
// First part of the header.
for ($i = 0; $i < 148; $i++) {
$checksum += ord(substr($block, $i, 1));
}
// Ignore the checksum value and replace it by ' ' (space).
for ($i = 148; $i < 156; $i++) {
$checksum += ord(' ');
}
// Last part of the header.
for ($i = 156; $i < 512; $i++) {
$checksum += ord(substr($block, $i, 1));
}
if (version_compare(PHP_VERSION, "5.5.0-dev") < 0) {
$fmt = "a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" . "a8checksum/a1typeflag/a100link/a6magic/a2version/" . "a32uname/a32gname/a8devmajor/a8devminor/a131prefix";
}
else {
$fmt = "Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/" . "Z8checksum/Z1typeflag/Z100link/Z6magic/Z2version/" . "Z32uname/Z32gname/Z8devmajor/Z8devminor/Z131prefix";
}
$data = unpack($fmt, $block);
if (strlen($data["prefix"]) > 0) {
$data["filename"] = "{$data['prefix']}/{$data['filename']}";
}
// Extract the checksum.
$header['checksum'] = octdec(trim($data['checksum']));
if ($header['checksum'] != $checksum) {
$header['filename'] = '';
// Look for last block (empty block).
if ($checksum == 256 && $header['checksum'] == 0) {
return $header;
}
throw new BackupMigrateException('Invalid checksum for file %filename', [
'%filename' => $data['filename'],
]);
}
// Extract the properties.
$header['filename'] = rtrim($data['filename'], "\0");
$header['mode'] = octdec(trim($data['mode']));
$header['uid'] = octdec(trim($data['uid']));
$header['gid'] = octdec(trim($data['gid']));
$header['size'] = octdec(trim($data['size']));
$header['mtime'] = octdec(trim($data['mtime']));
if (($header['typeflag'] = $data['typeflag']) == "5") {
$header['size'] = 0;
}
$header['link'] = trim($data['link']);
// Look for long filename.
if ($header['typeflag'] == 'L') {
$header = $this
->readLongHeader($header);
}
return $header;
}
/**
* Read a tar file header block for files with long names.
*
* @param $header
*
* @return array
*
* @throws \BackupMigrate\Core\Exception\BackupMigrateException
*/
private function readLongHeader($header) {
$filename = '';
$filesize = $header['size'];
$n = floor($header['size'] / 512);
for ($i = 0; $i < $n; $i++) {
$content = $this->archive
->readBytes(512);
$filename .= $content;
}
if ($header['size'] % 512 != 0) {
$content = $this->archive
->readBytes(512);
$filename .= $content;
}
$filename = rtrim(substr($filename, 0, $filesize), "\0");
// Read the next header.
$data = $this->archive
->readBytes(512);
$header = $this
->readHeader($data, $header);
$header['filename'] = $filename;
return $header;
}
/**
* Detect and report a malicious file name.
*
* @param string $file
*
* @return bool
*/
private function maliciousFilename($file) {
if (strpos($file, '/../') !== FALSE) {
return TRUE;
}
if (strpos($file, '../') === 0) {
return TRUE;
}
return FALSE;
}
/**
* This will be called when all files have been added. It gives the implementation
* a chance to clean up and commit the changes if needed.
*
* @return mixed
*/
public function closeArchive() {
if ($this->archive) {
$this->archive
->close();
}
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
TarArchiveReader:: |
protected | property | ||
TarArchiveReader:: |
public | function |
This will be called when all files have been added. It gives the implementation
a chance to clean up and commit the changes if needed. Overrides ArchiveReaderInterface:: |
|
TarArchiveReader:: |
private | function | Create a directory or return true if it already exists. | |
TarArchiveReader:: |
private | function | ||
TarArchiveReader:: |
public | function |
Extract all files to the given directory. Overrides ArchiveReaderInterface:: |
|
TarArchiveReader:: |
public | function |
Get the file extension for this archiver. For a tarball writer this would
be 'tar'. For a Zip file writer this would be 'zip'. Overrides ArchiveReaderInterface:: |
|
TarArchiveReader:: |
private | function | Detect and report a malicious file name. | |
TarArchiveReader:: |
private | function | Read a tar file header block. | |
TarArchiveReader:: |
private | function | Read a tar file header block for files with long names. | |
TarArchiveReader:: |
public | function |
Overrides ArchiveReaderInterface:: |