1<?php
2
3declare(strict_types=1);
4
5/**
6 * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
7 *
8 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
9 * @author Robin Appelman <robin@icewind.nl>
10 *
11 * @license GNU AGPL version 3 or any later version
12 *
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU Affero General Public License as
15 * published by the Free Software Foundation, either version 3 of the
16 * License, or (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU Affero General Public License for more details.
22 *
23 * You should have received a copy of the GNU Affero General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 *
26 */
27namespace OCA\Files_External\Lib\Storage;
28
29use Icewind\Streams\File;
30use phpseclib\Net\SSH2;
31
32class SFTPWriteStream implements File {
33	/** @var resource */
34	public $context;
35
36	/** @var \phpseclib\Net\SFTP */
37	private $sftp;
38
39	/** @var resource */
40	private $handle;
41
42	/** @var int */
43	private $internalPosition = 0;
44
45	/** @var int */
46	private $writePosition = 0;
47
48	/** @var bool */
49	private $eof = false;
50
51	private $buffer = '';
52
53	public static function register($protocol = 'sftpwrite') {
54		if (in_array($protocol, stream_get_wrappers(), true)) {
55			return false;
56		}
57		return stream_wrapper_register($protocol, get_called_class());
58	}
59
60	/**
61	 * Load the source from the stream context and return the context options
62	 *
63	 * @param string $name
64	 * @return array
65	 * @throws \BadMethodCallException
66	 */
67	protected function loadContext($name) {
68		$context = stream_context_get_options($this->context);
69		if (isset($context[$name])) {
70			$context = $context[$name];
71		} else {
72			throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
73		}
74		if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) {
75			$this->sftp = $context['session'];
76		} else {
77			throw new \BadMethodCallException('Invalid context, session not set');
78		}
79		return $context;
80	}
81
82	public function stream_open($path, $mode, $options, &$opened_path) {
83		[, $path] = explode('://', $path);
84		$path = '/' . ltrim($path);
85		$path = str_replace('//', '/', $path);
86
87		$this->loadContext('sftp');
88
89		if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) {
90			return false;
91		}
92
93		$remote_file = $this->sftp->_realpath($path);
94		if ($remote_file === false) {
95			return false;
96		}
97
98		$packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_TRUNCATE, 0);
99		if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
100			return false;
101		}
102
103		$response = $this->sftp->_get_sftp_packet();
104		switch ($this->sftp->packet_type) {
105			case NET_SFTP_HANDLE:
106				$this->handle = substr($response, 4);
107				break;
108			case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
109				$this->sftp->_logError($response);
110				return false;
111			default:
112				user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
113				return false;
114		}
115
116		return true;
117	}
118
119	public function stream_seek($offset, $whence = SEEK_SET) {
120		return false;
121	}
122
123	public function stream_tell() {
124		return $this->writePosition;
125	}
126
127	public function stream_read($count) {
128		return false;
129	}
130
131	public function stream_write($data) {
132		$written = strlen($data);
133		$this->writePosition += $written;
134
135		$this->buffer .= $data;
136
137		if (strlen($this->buffer) > 64 * 1024) {
138			if (!$this->stream_flush()) {
139				return false;
140			}
141		}
142
143		return $written;
144	}
145
146	public function stream_set_option($option, $arg1, $arg2) {
147		return false;
148	}
149
150	public function stream_truncate($size) {
151		return false;
152	}
153
154	public function stream_stat() {
155		return false;
156	}
157
158	public function stream_lock($operation) {
159		return false;
160	}
161
162	public function stream_flush() {
163		$size = strlen($this->buffer);
164		$packet = pack('Na*N3a*', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size, $this->buffer);
165		if (!$this->sftp->_send_sftp_packet(NET_SFTP_WRITE, $packet)) {
166			return false;
167		}
168		$this->internalPosition += $size;
169		$this->buffer = '';
170
171		return $this->sftp->_read_put_responses(1);
172	}
173
174	public function stream_eof() {
175		return $this->eof;
176	}
177
178	public function stream_close() {
179		$this->stream_flush();
180		if (!$this->sftp->_close_handle($this->handle)) {
181			return false;
182		}
183	}
184}
185