streams/Code/Storage/File.php

443 lines
12 KiB
PHP
Raw Normal View History

2016-02-19 08:06:10 +00:00
<?php
2022-02-16 04:08:28 +00:00
namespace Code\Storage;
2016-02-19 08:06:10 +00:00
2020-05-06 04:34:31 +00:00
use App;
2016-02-19 08:06:10 +00:00
use Sabre\DAV;
2022-02-16 04:08:28 +00:00
use Code\Lib\Libsync;
use Code\Daemon\Run;
use Code\Lib\Channel;
use Code\Lib\ServiceClass;
2020-05-06 04:34:31 +00:00
require_once('include/photos.php');
2016-02-19 08:06:10 +00:00
/**
* @brief This class represents a file in DAV.
*
2020-07-02 05:21:30 +00:00
* It provides all functions to work with files in the project cloud through DAV protocol.
2016-02-19 08:06:10 +00:00
*
2022-08-27 04:01:22 +00:00
* @extends DAV\\Node
* @implements DAV\\IFile
2016-02-19 08:06:10 +00:00
*
* @license http://opensource.org/licenses/mit-license.php The MIT License (MIT)
*/
2020-07-02 05:21:30 +00:00
2016-02-19 08:06:10 +00:00
class File extends DAV\Node implements DAV\IFile {
/**
* The file from attach table.
*
* @var array $data
* * data
* * flags
* * filename (string)
* * filetype (string)
2016-02-19 08:06:10 +00:00
*/
2020-07-02 05:21:30 +00:00
2020-08-16 23:34:31 +00:00
public $data;
2020-07-02 05:21:30 +00:00
2016-02-19 08:06:10 +00:00
/**
2022-08-14 09:20:43 +00:00
* @see \Sabre\DAV\Auth\Backend\BackendInterface
2022-08-15 11:19:29 +00:00
* @var BasicAuth $auth
2016-02-19 08:06:10 +00:00
*/
2020-07-02 05:21:30 +00:00
2016-02-19 08:06:10 +00:00
private $auth;
2020-07-02 05:21:30 +00:00
2016-02-19 08:06:10 +00:00
/**
* @var string $name
2016-02-19 08:06:10 +00:00
*/
2020-07-02 05:21:30 +00:00
2016-02-19 08:06:10 +00:00
private $name;
/**
* Sets up the node, expects a full path name.
*
* @param string $name
* @param array $data from attach table
2022-11-21 19:15:54 +00:00
* @param $auth
2016-02-19 08:06:10 +00:00
*/
2020-07-02 05:21:30 +00:00
2022-11-21 19:15:54 +00:00
public function __construct($name, $data, $auth) {
2016-02-19 08:06:10 +00:00
$this->name = $name;
$this->data = $data;
$this->auth = $auth;
}
/**
* @brief Returns the name of the file.
*
* @return string
*/
2020-07-02 05:21:30 +00:00
2016-02-19 08:06:10 +00:00
public function getName() {
return basename($this->name);
}
/**
* @brief Renames the file.
*
* @throw "\Sabre\DAV\Exception\Forbidden"
* @param string $newName The new name of the file.
2016-02-19 08:06:10 +00:00
* @return void
*/
2020-07-02 05:21:30 +00:00
2016-02-19 08:06:10 +00:00
public function setName($newName) {
logger('old name ' . basename($this->name) . ' -> ' . $newName, LOGGER_DATA);
if ((! $newName) || (! $this->auth->owner_id) || (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage'))) {
logger('permission denied '. $newName);
throw new DAV\Exception\Forbidden('Permission denied.');
}
2022-11-17 21:05:49 +00:00
// attach_move($channel_id, $resource_id, $new_folder_hash, $newname = '')
2022-11-18 21:44:43 +00:00
2016-02-19 08:06:10 +00:00
$newName = str_replace('/', '%2F', $newName);
$r = q("UPDATE attach SET filename = '%s' WHERE hash = '%s' AND id = %d",
dbesc($newName),
dbesc($this->data['hash']),
intval($this->data['id'])
);
$x = attach_syspaths($this->auth->owner_id,$this->data['hash']);
2022-11-17 21:05:49 +00:00
$y = q("update attach set display_path = '%s' where hash = '%s' and uid = %d",
dbesc($x['path']),
dbesc($this->data['hash']),
intval($this->auth->owner_id)
);
2022-08-14 09:20:43 +00:00
if ($this->data['is_photo']) {
$r = q("update photo set filename = '%s', display_path = '%s' where resource_id = '%s' and uid = %d",
dbesc($newName),
dbesc($x['path']),
dbesc($this->data['hash']),
intval($this->auth->owner_id)
);
}
2022-01-25 01:26:12 +00:00
$ch = Channel::from_id($this->auth->owner_id);
2020-07-02 05:21:30 +00:00
if ($ch) {
$sync = attach_export_data($ch,$this->data['hash']);
2020-07-02 05:21:30 +00:00
if ($sync) {
Libsync::build_sync_packet($ch['channel_id'], [ 'file' => [ $sync ] ]);
}
}
2016-02-19 08:06:10 +00:00
}
/**
* @brief Updates the data of the file.
*
* @param resource $data
* @return void
*/
2020-05-06 04:34:31 +00:00
2016-02-19 08:06:10 +00:00
public function put($data) {
logger('put file: ' . basename($this->name), LOGGER_DEBUG);
$size = 0;
2020-05-06 04:34:31 +00:00
if ((! $this->auth->owner_id) || (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage'))) {
logger('permission denied for put operation');
throw new DAV\Exception\Forbidden('Permission denied.');
}
2022-01-25 01:26:12 +00:00
$channel = Channel::from_id($this->auth->owner_id);
2020-05-06 04:34:31 +00:00
if (! $channel) {
throw new DAV\Exception\Forbidden('Permission denied.');
}
2016-02-19 08:06:10 +00:00
$is_photo = false;
$album = '';
$os_path = '';
2016-02-19 08:06:10 +00:00
// This hidden config allows you to protect your dav contents from cryptolockers by preventing over-write
// and delete from a networked operating system. In this case you are only allowed to over-write the file
// if it is empty. Some DAV clients create the file and then store the contents so these would be allowed.
2020-05-06 04:34:31 +00:00
if (get_pconfig($this->auth->owner_id,'system','os_delete_prohibit') && App::$module == 'dav') {
$r = q("select filesize from attach where hash = '%s' and uid = %d limit 1",
dbesc($this->data['hash']),
2020-05-06 04:34:31 +00:00
intval($channel['channel_id'])
);
2020-05-06 04:34:31 +00:00
if ($r && intval($r[0]['filesize'])) {
throw new DAV\Exception\Forbidden('Permission denied.');
}
}
$r = q("SELECT flags, folder, os_storage, os_path, display_path, filename, is_photo FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1",
2016-02-19 08:06:10 +00:00
dbesc($this->data['hash']),
2020-05-06 04:34:31 +00:00
intval($channel['channel_id'])
2016-02-19 08:06:10 +00:00
);
if ($r) {
$os_path = $r[0]['os_path'];
$display_path = $r[0]['display_path'];
$filename = $r[0]['filename'];
2021-04-01 01:25:14 +00:00
$folder_hash = $r[0]['folder'];
2016-02-19 08:06:10 +00:00
if (intval($r[0]['os_storage'])) {
$d = q("select folder, content from attach where hash = '%s' and uid = %d limit 1",
2016-02-19 08:06:10 +00:00
dbesc($this->data['hash']),
2020-05-06 04:34:31 +00:00
intval($channel['channel_id'])
2016-02-19 08:06:10 +00:00
);
2020-05-06 04:34:31 +00:00
if ($d) {
if ($d[0]['folder']) {
2016-02-19 08:06:10 +00:00
$f1 = q("select * from attach where is_dir = 1 and hash = '%s' and uid = %d limit 1",
dbesc($d[0]['folder']),
2020-05-06 04:34:31 +00:00
intval($channel['channel_id'])
2016-02-19 08:06:10 +00:00
);
2020-05-06 04:34:31 +00:00
if ($f1) {
2016-02-19 08:06:10 +00:00
$album = $f1[0]['filename'];
$direct = $f1[0];
}
}
$f = dbunescbin($d[0]['content']);
2020-05-06 04:34:31 +00:00
if (is_resource($data)) {
$fp = fopen($f,'wb');
if ($fp) {
Stdio::pipe_streams($data,$fp);
2020-05-06 04:34:31 +00:00
fclose($fp);
}
}
else {
file_put_contents($f, $data);
}
2016-02-19 08:06:10 +00:00
$size = @filesize($f);
2020-05-06 04:34:31 +00:00
2016-02-19 08:06:10 +00:00
logger('filename: ' . $f . ' size: ' . $size, LOGGER_DEBUG);
}
$gis = @getimagesize($f);
logger('getimagesize: ' . print_r($gis,true), LOGGER_DATA);
2020-05-06 04:34:31 +00:00
if ($gis && supported_imagetype($gis[2])) {
2016-02-19 08:06:10 +00:00
$is_photo = 1;
}
// If we know it's a photo, over-ride the type in case the source system could not determine what it was
2020-05-06 04:34:31 +00:00
if ($is_photo) {
q("update attach set filetype = '%s' where hash = '%s' and uid = %d",
dbesc($gis['mime']),
dbesc($this->data['hash']),
intval($this->data['uid'])
);
}
}
2016-02-19 08:06:10 +00:00
else {
// this shouldn't happen any more
$r = q("UPDATE attach SET content = '%s' WHERE hash = '%s' AND uid = %d",
2016-02-19 08:06:10 +00:00
dbescbin(stream_get_contents($data)),
dbesc($this->data['hash']),
intval($this->data['uid'])
);
$r = q("SELECT length(content) AS fsize FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1",
2016-02-19 08:06:10 +00:00
dbesc($this->data['hash']),
intval($this->data['uid'])
);
if ($r) {
$size = $r[0]['fsize'];
}
}
}
// returns now()
$edited = datetime_convert();
$d = q("UPDATE attach SET filesize = '%s', is_photo = %d, edited = '%s' WHERE hash = '%s' AND uid = %d",
dbesc($size),
intval($is_photo),
dbesc($edited),
dbesc($this->data['hash']),
2020-05-06 04:34:31 +00:00
intval($channel['channel_id'])
2016-02-19 08:06:10 +00:00
);
2020-05-06 04:34:31 +00:00
if ($is_photo) {
$args = [
'resource_id' => $this->data['hash'],
'album' => $album,
2021-04-01 01:25:14 +00:00
'folder' => $folder_hash,
2020-05-06 04:34:31 +00:00
'os_syspath' => $f,
'os_path' => $os_path,
'display_path' => $display_path,
'filename' => $filename,
'getimagesize' => $gis,
'directory' => $direct
];
$p = photo_upload($channel, App::get_observer(), $args);
logger('photo_upload: ' . print_r($p,true), LOGGER_DATA);
2016-02-19 08:06:10 +00:00
}
// update the folder's lastmodified timestamp
$e = q("UPDATE attach SET edited = '%s' WHERE hash = '%s' AND uid = %d",
dbesc($edited),
dbesc($r[0]['folder']),
2020-05-06 04:34:31 +00:00
intval($channel['channel_id'])
2016-02-19 08:06:10 +00:00
);
// @todo do we really want to remove the whole file if an update fails
// because of maxfilesize or quota?
// There is an Exception "InsufficientStorage" or "PaymentRequired" for
// our service class from SabreDAV we could use.
$maxfilesize = get_config('system', 'maxfilesize');
if (($maxfilesize) && ($size > $maxfilesize)) {
2020-05-06 04:34:31 +00:00
attach_delete($channel['channel_id'], $this->data['hash']);
2016-02-19 08:06:10 +00:00
return;
}
2022-01-25 04:37:14 +00:00
$limit = engr_units_to_bytes(ServiceClass::fetch($channel['channel_id'], 'attach_upload_limit'));
2016-02-19 08:06:10 +00:00
if ($limit !== false) {
$x = q("select sum(filesize) as total from attach where aid = %d ",
2020-05-06 04:34:31 +00:00
intval($channel['channel_account_id'])
2016-02-19 08:06:10 +00:00
);
if (($x) && ($x[0]['total'] + $size > $limit)) {
2020-05-06 04:34:31 +00:00
logger('service class limit exceeded for ' . $channel['channel_name'] . ' total usage is ' . $x[0]['total'] . ' limit is ' . userReadableSize($limit));
attach_delete($channel['channel_id'], $this->data['hash']);
2016-02-19 08:06:10 +00:00
return;
}
}
Run::Summon([ 'Thumbnail' , $this->data['hash'] ]);
2017-11-20 00:56:59 +00:00
2020-05-06 04:34:31 +00:00
$sync = attach_export_data($channel,$this->data['hash']);
2017-11-20 00:56:59 +00:00
2020-05-06 04:34:31 +00:00
if ($sync) {
Libsync::build_sync_packet($channel['channel_id'],array('file' => array($sync)));
}
2016-02-19 08:06:10 +00:00
}
/**
* @brief Returns the raw data.
*
2020-05-06 04:34:31 +00:00
* @return string || resource
2016-02-19 08:06:10 +00:00
*/
2020-05-06 04:34:31 +00:00
2016-02-19 08:06:10 +00:00
public function get() {
logger('get file ' . basename($this->name), LOGGER_DEBUG);
2022-08-14 10:25:56 +00:00
logger('os_path: ' . $this->data['os_path'], LOGGER_DATA);
2016-02-19 08:06:10 +00:00
$r = q("SELECT content, flags, os_storage, filename, filetype FROM attach WHERE hash = '%s' AND uid = %d LIMIT 1",
2016-02-19 08:06:10 +00:00
dbesc($this->data['hash']),
intval($this->data['uid'])
);
if ($r) {
// @todo this should be a global definition
$unsafe_types = array('text/html', 'text/css', 'application/javascript', 'image/svg+xml');
2016-02-19 08:06:10 +00:00
2022-01-25 01:26:12 +00:00
if (in_array($r[0]['filetype'], $unsafe_types) && (!Channel::codeallowed($this->data['uid']))) {
header('Content-Disposition: attachment; filename="' . $r[0]['filename'] . '"');
header('Content-type: ' . $r[0]['filetype']);
2016-02-19 08:06:10 +00:00
}
if (intval($r[0]['os_storage'])) {
$x = dbunescbin($r[0]['content']);
2020-05-06 04:34:31 +00:00
if (strpos($x,'store') === false) {
2022-08-14 10:25:56 +00:00
$f = 'store/' . $this->auth->owner_nick . '/' . (($this->data['os_path']) ? $this->data['os_path'] . '/' : '') . $x;
2020-05-06 04:34:31 +00:00
}
else {
2016-02-19 08:06:10 +00:00
$f = $x;
2020-05-06 04:34:31 +00:00
}
2017-07-24 02:54:32 +00:00
return @fopen($f, 'rb');
2016-02-19 08:06:10 +00:00
}
return dbunescbin($r[0]['content']);
2016-02-19 08:06:10 +00:00
}
2022-08-27 04:01:22 +00:00
return '';
2016-02-19 08:06:10 +00:00
}
/**
* @brief Returns the ETag for a file.
*
* An ETag is a unique identifier representing the current version of the file.
* If the file changes, the ETag MUST change.
* The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
*
* Return null if the ETag can not effectively be determined.
*
* @return null|string
*/
public function getETag() {
$ret = null;
if ($this->data['hash']) {
$ret = '"' . $this->data['hash'] . '"';
}
return $ret;
}
/**
* @brief Returns the mime-type for a file.
*
* If null is returned, we'll assume application/octet-stream
*
* @return mixed
*/
2020-07-02 05:21:30 +00:00
2016-02-19 08:06:10 +00:00
public function getContentType() {
return $this->data['filetype'];
}
/**
* @brief Returns the size of the node, in bytes.
*
* @return int
* filesize in bytes
*/
public function getSize() {
return intval($this->data['filesize']);
2016-02-19 08:06:10 +00:00
}
/**
* @brief Returns the last modification time for the file, as a unix
* timestamp.
*
* @return int last modification time in UNIX timestamp
*/
2020-05-06 04:34:31 +00:00
2016-02-19 08:06:10 +00:00
public function getLastModified() {
return datetime_convert('UTC', 'UTC', $this->data['edited'], 'U');
}
/**
* @brief Delete the file.
*
* This method checks the permissions and then calls attach_delete() function
* to actually remove the file.
*
* @throw "\Sabre\DAV\Exception\Forbidden"
2016-02-19 08:06:10 +00:00
*/
public function delete() {
logger('delete file ' . basename($this->name), LOGGER_DEBUG);
2020-07-02 05:21:30 +00:00
if ((! $this->auth->owner_id) || (! perm_is_allowed($this->auth->owner_id, $this->auth->observer, 'write_storage'))) {
2016-02-19 08:06:10 +00:00
throw new DAV\Exception\Forbidden('Permission denied.');
}
2020-07-02 05:21:30 +00:00
if ($this->auth->owner_id !== $this->auth->channel_id) {
2016-02-19 08:06:10 +00:00
if (($this->auth->observer !== $this->data['creator']) || intval($this->data['is_dir'])) {
throw new DAV\Exception\Forbidden('Permission denied.');
}
}
2020-07-02 05:21:30 +00:00
// This is a subtle solution to crypto-lockers which can wreak havoc on network resources when
// invoked on a dav-mounted filesystem. By setting system.os_delete_prohibit, one can remove files
// via the web interface but from their operating system the filesystem is treated as read-only.
2021-12-02 22:33:36 +00:00
if (get_pconfig($this->auth->owner_id,'system','os_delete_prohibit') && App::$module == 'dav') {
2016-07-16 08:02:41 +00:00
throw new DAV\Exception\Forbidden('Permission denied.');
}
2016-02-19 08:06:10 +00:00
attach_delete($this->auth->owner_id, $this->data['hash']);
2022-01-25 01:26:12 +00:00
$channel = Channel::from_id($this->auth->owner_id);
2020-05-06 04:34:31 +00:00
if ($channel) {
$sync = attach_export_data($channel, $this->data['hash'], true);
if ($sync) {
Libsync::build_sync_packet($channel['channel_id'], [ 'file' => [ $sync ] ]);
}
}
2016-02-19 08:06:10 +00:00
}
}