php-src/ext/standard/ftp_fopen_wrapper.c
2023-07-07 18:02:32 +02:00

1181 lines
31 KiB
C

/*
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| https://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Rasmus Lerdorf <rasmus@php.net> |
| Jim Winstead <jimw@php.net> |
| Hartmut Holzgraefe <hholzgra@php.net> |
| Sara Golemon <pollita@php.net> |
+----------------------------------------------------------------------+
*/
#include "php.h"
#include "php_globals.h"
#include "php_network.h"
#include "php_ini.h"
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifdef PHP_WIN32
#include <winsock2.h>
#define O_RDONLY _O_RDONLY
#include "win32/param.h"
#else
#include <sys/param.h>
#endif
#include "php_standard.h"
#include <sys/types.h>
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#ifdef PHP_WIN32
#include <winsock2.h>
#else
#include <netinet/in.h>
#include <netdb.h>
#ifdef HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif
#endif
#if defined(PHP_WIN32) || defined(__riscos__)
#undef AF_UNIX
#endif
#if defined(AF_UNIX)
#include <sys/un.h>
#endif
#include "php_fopen_wrappers.h"
#define FTPS_ENCRYPT_DATA 1
#define GET_FTP_RESULT(stream) get_ftp_result((stream), tmp_line, sizeof(tmp_line))
typedef struct _php_ftp_dirstream_data {
php_stream *datastream;
php_stream *controlstream;
php_stream *dirstream;
} php_ftp_dirstream_data;
/* {{{ get_ftp_result */
static inline int get_ftp_result(php_stream *stream, char *buffer, size_t buffer_size)
{
buffer[0] = '\0'; /* in case read fails to read anything */
while (php_stream_gets(stream, buffer, buffer_size-1) &&
!(isdigit((int) buffer[0]) && isdigit((int) buffer[1]) &&
isdigit((int) buffer[2]) && buffer[3] == ' '));
return strtol(buffer, NULL, 10);
}
/* }}} */
/* {{{ php_stream_ftp_stream_stat */
static int php_stream_ftp_stream_stat(php_stream_wrapper *wrapper, php_stream *stream, php_stream_statbuf *ssb)
{
/* For now, we return with a failure code to prevent the underlying
* file's details from being used instead. */
return -1;
}
/* }}} */
/* {{{ php_stream_ftp_stream_close */
static int php_stream_ftp_stream_close(php_stream_wrapper *wrapper, php_stream *stream)
{
php_stream *controlstream = stream->wrapperthis;
int ret = 0;
if (controlstream) {
if (strpbrk(stream->mode, "wa+")) {
char tmp_line[512];
int result;
/* For write modes close data stream first to signal EOF to server */
result = GET_FTP_RESULT(controlstream);
if (result != 226 && result != 250) {
php_error_docref(NULL, E_WARNING, "FTP server error %d:%s", result, tmp_line);
ret = EOF;
}
}
php_stream_write_string(controlstream, "QUIT\r\n");
php_stream_close(controlstream);
stream->wrapperthis = NULL;
}
return ret;
}
/* }}} */
/* {{{ php_ftp_fopen_connect */
static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
zend_string **opened_path, php_stream_context *context, php_stream **preuseid,
php_url **presource, int *puse_ssl, int *puse_ssl_on_data)
{
php_stream *stream = NULL, *reuseid = NULL;
php_url *resource = NULL;
int result, use_ssl, use_ssl_on_data = 0;
char tmp_line[512];
char *transport;
int transport_len;
resource = php_url_parse(path);
if (resource == NULL || resource->path == NULL) {
if (resource && presource) {
*presource = resource;
}
return NULL;
}
use_ssl = resource->scheme && (ZSTR_LEN(resource->scheme) > 3) && ZSTR_VAL(resource->scheme)[3] == 's';
/* use port 21 if one wasn't specified */
if (resource->port == 0)
resource->port = 21;
transport_len = (int)spprintf(&transport, 0, "tcp://%s:%d", ZSTR_VAL(resource->host), resource->port);
stream = php_stream_xport_create(transport, transport_len, REPORT_ERRORS, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT, NULL, NULL, context, NULL, NULL);
efree(transport);
if (stream == NULL) {
result = 0; /* silence */
goto connect_errexit;
}
php_stream_context_set(stream, context);
php_stream_notify_info(context, PHP_STREAM_NOTIFY_CONNECT, NULL, 0);
/* Start talking to ftp server */
result = GET_FTP_RESULT(stream);
if (result > 299 || result < 200) {
php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
goto connect_errexit;
}
if (use_ssl) {
/* send the AUTH TLS request name */
php_stream_write_string(stream, "AUTH TLS\r\n");
/* get the response */
result = GET_FTP_RESULT(stream);
if (result != 234) {
/* AUTH TLS not supported try AUTH SSL */
php_stream_write_string(stream, "AUTH SSL\r\n");
/* get the response */
result = GET_FTP_RESULT(stream);
if (result != 334) {
php_stream_wrapper_log_error(wrapper, options, "Server doesn't support FTPS.");
goto connect_errexit;
} else {
/* we must reuse the old SSL session id */
/* if we talk to an old ftpd-ssl */
reuseid = stream;
}
} else {
/* encrypt data etc */
}
}
if (use_ssl) {
if (php_stream_xport_crypto_setup(stream,
STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0
|| php_stream_xport_crypto_enable(stream, 1) < 0) {
php_stream_wrapper_log_error(wrapper, options, "Unable to activate SSL mode");
php_stream_close(stream);
stream = NULL;
goto connect_errexit;
}
/* set PBSZ to 0 */
php_stream_write_string(stream, "PBSZ 0\r\n");
/* ignore the response */
result = GET_FTP_RESULT(stream);
/* set data connection protection level */
#if FTPS_ENCRYPT_DATA
php_stream_write_string(stream, "PROT P\r\n");
/* get the response */
result = GET_FTP_RESULT(stream);
use_ssl_on_data = (result >= 200 && result<=299) || reuseid;
#else
php_stream_write_string(stream, "PROT C\r\n");
/* get the response */
result = GET_FTP_RESULT(stream);
#endif
}
#define PHP_FTP_CNTRL_CHK(val, val_len, err_msg) { \
unsigned char *s = (unsigned char *) val, *e = (unsigned char *) s + val_len; \
while (s < e) { \
if (iscntrl(*s)) { \
php_stream_wrapper_log_error(wrapper, options, err_msg, val); \
goto connect_errexit; \
} \
s++; \
} \
}
/* send the user name */
if (resource->user != NULL) {
ZSTR_LEN(resource->user) = php_raw_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user));
PHP_FTP_CNTRL_CHK(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user), "Invalid login %s")
php_stream_printf(stream, "USER %s\r\n", ZSTR_VAL(resource->user));
} else {
php_stream_write_string(stream, "USER anonymous\r\n");
}
/* get the response */
result = GET_FTP_RESULT(stream);
/* if a password is required, send it */
if (result >= 300 && result <= 399) {
php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_REQUIRED, tmp_line, 0);
if (resource->pass != NULL) {
ZSTR_LEN(resource->pass) = php_raw_url_decode(ZSTR_VAL(resource->pass), ZSTR_LEN(resource->pass));
PHP_FTP_CNTRL_CHK(ZSTR_VAL(resource->pass), ZSTR_LEN(resource->pass), "Invalid password %s")
php_stream_printf(stream, "PASS %s\r\n", ZSTR_VAL(resource->pass));
} else {
/* if the user has configured who they are,
send that as the password */
if (FG(from_address)) {
php_stream_printf(stream, "PASS %s\r\n", FG(from_address));
} else {
php_stream_write_string(stream, "PASS anonymous\r\n");
}
}
/* read the response */
result = GET_FTP_RESULT(stream);
if (result > 299 || result < 200) {
php_stream_notify_error(context, PHP_STREAM_NOTIFY_AUTH_RESULT, tmp_line, result);
} else {
php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_RESULT, tmp_line, result);
}
}
if (result > 299 || result < 200) {
goto connect_errexit;
}
if (puse_ssl) {
*puse_ssl = use_ssl;
}
if (puse_ssl_on_data) {
*puse_ssl_on_data = use_ssl_on_data;
}
if (preuseid) {
*preuseid = reuseid;
}
if (presource) {
*presource = resource;
}
return stream;
connect_errexit:
php_url_free(resource);
if (stream) {
php_stream_close(stream);
}
return NULL;
}
/* }}} */
/* {{{ php_fopen_do_pasv */
static unsigned short php_fopen_do_pasv(php_stream *stream, char *ip, size_t ip_size, char **phoststart)
{
char tmp_line[512];
int result, i;
unsigned short portno;
char *tpath, *ttpath, *hoststart=NULL;
#ifdef HAVE_IPV6
/* We try EPSV first, needed for IPv6 and works on some IPv4 servers */
php_stream_write_string(stream, "EPSV\r\n");
result = GET_FTP_RESULT(stream);
/* check if we got a 229 response */
if (result != 229) {
#endif
/* EPSV failed, let's try PASV */
php_stream_write_string(stream, "PASV\r\n");
result = GET_FTP_RESULT(stream);
/* make sure we got a 227 response */
if (result != 227) {
return 0;
}
/* parse pasv command (129, 80, 95, 25, 13, 221) */
tpath = tmp_line;
/* skip over the "227 Some message " part */
for (tpath += 4; *tpath && !isdigit((int) *tpath); tpath++);
if (!*tpath) {
return 0;
}
/* skip over the host ip, to get the port */
hoststart = tpath;
for (i = 0; i < 4; i++) {
for (; isdigit((int) *tpath); tpath++);
if (*tpath != ',') {
return 0;
}
*tpath='.';
tpath++;
}
tpath[-1] = '\0';
memcpy(ip, hoststart, ip_size);
ip[ip_size-1] = '\0';
hoststart = ip;
/* pull out the MSB of the port */
portno = (unsigned short) strtoul(tpath, &ttpath, 10) * 256;
if (ttpath == NULL) {
/* didn't get correct response from PASV */
return 0;
}
tpath = ttpath;
if (*tpath != ',') {
return 0;
}
tpath++;
/* pull out the LSB of the port */
portno += (unsigned short) strtoul(tpath, &ttpath, 10);
#ifdef HAVE_IPV6
} else {
/* parse epsv command (|||6446|) */
for (i = 0, tpath = tmp_line + 4; *tpath; tpath++) {
if (*tpath == '|') {
i++;
if (i == 3)
break;
}
}
if (i < 3) {
return 0;
}
/* pull out the port */
portno = (unsigned short) strtoul(tpath + 1, &ttpath, 10);
}
#endif
if (ttpath == NULL) {
/* didn't get correct response from EPSV/PASV */
return 0;
}
if (phoststart) {
*phoststart = hoststart;
}
return portno;
}
/* }}} */
/* {{{ php_fopen_url_wrap_ftp */
php_stream * php_stream_url_wrap_ftp(php_stream_wrapper *wrapper, const char *path, const char *mode,
int options, zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
php_stream *stream = NULL, *datastream = NULL;
php_url *resource = NULL;
char tmp_line[512];
char ip[sizeof("123.123.123.123")];
unsigned short portno;
char *hoststart = NULL;
int result = 0, use_ssl, use_ssl_on_data=0;
php_stream *reuseid=NULL;
size_t file_size = 0;
zval *tmpzval;
bool allow_overwrite = 0;
int8_t read_write = 0;
char *transport;
int transport_len;
zend_string *error_message = NULL;
tmp_line[0] = '\0';
if (strpbrk(mode, "r+")) {
read_write = 1; /* Open for reading */
}
if (strpbrk(mode, "wa+")) {
if (read_write) {
php_stream_wrapper_log_error(wrapper, options, "FTP does not support simultaneous read/write connections");
return NULL;
}
if (strchr(mode, 'a')) {
read_write = 3; /* Open for Appending */
} else {
read_write = 2; /* Open for writing */
}
}
if (!read_write) {
/* No mode specified? */
php_stream_wrapper_log_error(wrapper, options, "Unknown file open mode");
return NULL;
}
if (context &&
(tmpzval = php_stream_context_get_option(context, "ftp", "proxy")) != NULL) {
if (read_write == 1) {
/* Use http wrapper to proxy ftp request */
return php_stream_url_wrap_http(wrapper, path, mode, options, opened_path, context STREAMS_CC);
} else {
/* ftp proxy is read-only */
php_stream_wrapper_log_error(wrapper, options, "FTP proxy may only be used in read mode");
return NULL;
}
}
stream = php_ftp_fopen_connect(wrapper, path, mode, options, opened_path, context, &reuseid, &resource, &use_ssl, &use_ssl_on_data);
if (!stream) {
goto errexit;
}
/* set the connection to be binary */
php_stream_write_string(stream, "TYPE I\r\n");
result = GET_FTP_RESULT(stream);
if (result > 299 || result < 200)
goto errexit;
/* find out the size of the file (verifying it exists) */
php_stream_printf(stream, "SIZE %s\r\n", ZSTR_VAL(resource->path));
/* read the response */
result = GET_FTP_RESULT(stream);
if (read_write == 1) {
/* Read Mode */
char *sizestr;
/* when reading file, it must exist */
if (result > 299 || result < 200) {
errno = ENOENT;
goto errexit;
}
sizestr = strchr(tmp_line, ' ');
if (sizestr) {
sizestr++;
file_size = atoi(sizestr);
php_stream_notify_file_size(context, file_size, tmp_line, result);
}
} else if (read_write == 2) {
/* when writing file (but not appending), it must NOT exist, unless a context option exists which allows it */
if (context && (tmpzval = php_stream_context_get_option(context, "ftp", "overwrite")) != NULL) {
allow_overwrite = zend_is_true(tmpzval);
}
if (result <= 299 && result >= 200) {
if (allow_overwrite) {
/* Context permits overwriting file,
so we just delete whatever's there in preparation */
php_stream_printf(stream, "DELE %s\r\n", ZSTR_VAL(resource->path));
result = GET_FTP_RESULT(stream);
if (result >= 300 || result <= 199) {
goto errexit;
}
} else {
php_stream_wrapper_log_error(wrapper, options, "Remote file already exists and overwrite context option not specified");
errno = EEXIST;
goto errexit;
}
}
}
/* set up the passive connection */
portno = php_fopen_do_pasv(stream, ip, sizeof(ip), &hoststart);
if (!portno) {
goto errexit;
}
/* Send RETR/STOR command */
if (read_write == 1) {
/* set resume position if applicable */
if (context &&
(tmpzval = php_stream_context_get_option(context, "ftp", "resume_pos")) != NULL &&
Z_TYPE_P(tmpzval) == IS_LONG &&
Z_LVAL_P(tmpzval) > 0) {
php_stream_printf(stream, "REST " ZEND_LONG_FMT "\r\n", Z_LVAL_P(tmpzval));
result = GET_FTP_RESULT(stream);
if (result < 300 || result > 399) {
php_stream_wrapper_log_error(wrapper, options, "Unable to resume from offset " ZEND_LONG_FMT, Z_LVAL_P(tmpzval));
goto errexit;
}
}
/* retrieve file */
memcpy(tmp_line, "RETR", sizeof("RETR"));
} else if (read_write == 2) {
/* Write new file */
memcpy(tmp_line, "STOR", sizeof("STOR"));
} else {
/* Append */
memcpy(tmp_line, "APPE", sizeof("APPE"));
}
php_stream_printf(stream, "%s %s\r\n", tmp_line, (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
/* open the data channel */
if (hoststart == NULL) {
hoststart = ZSTR_VAL(resource->host);
}
transport_len = (int)spprintf(&transport, 0, "tcp://%s:%d", hoststart, portno);
datastream = php_stream_xport_create(transport, transport_len, REPORT_ERRORS, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT, NULL, NULL, context, &error_message, NULL);
efree(transport);
if (datastream == NULL) {
tmp_line[0]='\0';
goto errexit;
}
result = GET_FTP_RESULT(stream);
if (result != 150 && result != 125) {
/* Could not retrieve or send the file
* this data will only be sent to us after connection on the data port was initiated.
*/
php_stream_close(datastream);
datastream = NULL;
goto errexit;
}
php_stream_context_set(datastream, context);
php_stream_notify_progress_init(context, 0, file_size);
if (use_ssl_on_data && (php_stream_xport_crypto_setup(datastream,
STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0 ||
php_stream_xport_crypto_enable(datastream, 1) < 0)) {
php_stream_wrapper_log_error(wrapper, options, "Unable to activate SSL mode");
php_stream_close(datastream);
datastream = NULL;
tmp_line[0]='\0';
goto errexit;
}
/* remember control stream */
datastream->wrapperthis = stream;
php_url_free(resource);
return datastream;
errexit:
if (resource) {
php_url_free(resource);
}
if (stream) {
php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
php_stream_close(stream);
}
if (tmp_line[0] != '\0')
php_stream_wrapper_log_error(wrapper, options, "FTP server reports %s", tmp_line);
if (error_message) {
php_stream_wrapper_log_error(wrapper, options, "Failed to set up data channel: %s", ZSTR_VAL(error_message));
zend_string_release(error_message);
}
return NULL;
}
/* }}} */
/* {{{ php_ftp_dirsteam_read */
static ssize_t php_ftp_dirstream_read(php_stream *stream, char *buf, size_t count)
{
php_stream_dirent *ent = (php_stream_dirent *)buf;
php_stream *innerstream;
size_t tmp_len;
zend_string *basename;
innerstream = ((php_ftp_dirstream_data *)stream->abstract)->datastream;
if (count != sizeof(php_stream_dirent)) {
return -1;
}
if (php_stream_eof(innerstream)) {
return 0;
}
if (!php_stream_get_line(innerstream, ent->d_name, sizeof(ent->d_name), &tmp_len)) {
return -1;
}
basename = php_basename(ent->d_name, tmp_len, NULL, 0);
tmp_len = MIN(sizeof(ent->d_name), ZSTR_LEN(basename) - 1);
memcpy(ent->d_name, ZSTR_VAL(basename), tmp_len);
ent->d_name[tmp_len - 1] = '\0';
zend_string_release_ex(basename, 0);
ent->d_type = DT_UNKNOWN;
/* Trim off trailing whitespace characters */
while (tmp_len > 0 &&
(ent->d_name[tmp_len - 1] == '\n' || ent->d_name[tmp_len - 1] == '\r' ||
ent->d_name[tmp_len - 1] == '\t' || ent->d_name[tmp_len - 1] == ' ')) {
ent->d_name[--tmp_len] = '\0';
}
return sizeof(php_stream_dirent);
}
/* }}} */
/* {{{ php_ftp_dirstream_close */
static int php_ftp_dirstream_close(php_stream *stream, int close_handle)
{
php_ftp_dirstream_data *data = stream->abstract;
/* close control connection */
if (data->controlstream) {
php_stream_close(data->controlstream);
data->controlstream = NULL;
}
/* close data connection */
php_stream_close(data->datastream);
data->datastream = NULL;
efree(data);
stream->abstract = NULL;
return 0;
}
/* }}} */
/* ftp dirstreams only need to support read and close operations,
They can't be rewound because the underlying ftp stream can't be rewound. */
static const php_stream_ops php_ftp_dirstream_ops = {
NULL, /* write */
php_ftp_dirstream_read, /* read */
php_ftp_dirstream_close, /* close */
NULL, /* flush */
"ftpdir",
NULL, /* rewind */
NULL, /* cast */
NULL, /* stat */
NULL /* set option */
};
/* {{{ php_stream_ftp_opendir */
php_stream * php_stream_ftp_opendir(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
php_stream *stream, *reuseid, *datastream = NULL;
php_ftp_dirstream_data *dirsdata;
php_url *resource = NULL;
int result = 0, use_ssl, use_ssl_on_data = 0;
char *hoststart = NULL, tmp_line[512];
char ip[sizeof("123.123.123.123")];
unsigned short portno;
tmp_line[0] = '\0';
stream = php_ftp_fopen_connect(wrapper, path, mode, options, opened_path, context, &reuseid, &resource, &use_ssl, &use_ssl_on_data);
if (!stream) {
goto opendir_errexit;
}
/* set the connection to be ascii */
php_stream_write_string(stream, "TYPE A\r\n");
result = GET_FTP_RESULT(stream);
if (result > 299 || result < 200)
goto opendir_errexit;
// tmp_line isn't relevant after the php_fopen_do_pasv().
tmp_line[0] = '\0';
/* set up the passive connection */
portno = php_fopen_do_pasv(stream, ip, sizeof(ip), &hoststart);
if (!portno) {
goto opendir_errexit;
}
/* open the data channel */
if (hoststart == NULL) {
hoststart = ZSTR_VAL(resource->host);
}
datastream = php_stream_sock_open_host(hoststart, portno, SOCK_STREAM, 0, 0);
if (datastream == NULL) {
goto opendir_errexit;
}
php_stream_printf(stream, "NLST %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
result = GET_FTP_RESULT(stream);
if (result != 150 && result != 125) {
/* Could not retrieve or send the file
* this data will only be sent to us after connection on the data port was initiated.
*/
php_stream_close(datastream);
datastream = NULL;
goto opendir_errexit;
}
php_stream_context_set(datastream, context);
if (use_ssl_on_data && (php_stream_xport_crypto_setup(datastream,
STREAM_CRYPTO_METHOD_SSLv23_CLIENT, NULL) < 0 ||
php_stream_xport_crypto_enable(datastream, 1) < 0)) {
php_stream_wrapper_log_error(wrapper, options, "Unable to activate SSL mode");
php_stream_close(datastream);
datastream = NULL;
goto opendir_errexit;
}
php_url_free(resource);
dirsdata = emalloc(sizeof *dirsdata);
dirsdata->datastream = datastream;
dirsdata->controlstream = stream;
dirsdata->dirstream = php_stream_alloc(&php_ftp_dirstream_ops, dirsdata, 0, mode);
return dirsdata->dirstream;
opendir_errexit:
if (resource) {
php_url_free(resource);
}
if (stream) {
php_stream_notify_error(context, PHP_STREAM_NOTIFY_FAILURE, tmp_line, result);
php_stream_close(stream);
}
if (tmp_line[0] != '\0') {
php_stream_wrapper_log_error(wrapper, options, "FTP server reports %s", tmp_line);
}
return NULL;
}
/* }}} */
/* {{{ php_stream_ftp_url_stat */
static int php_stream_ftp_url_stat(php_stream_wrapper *wrapper, const char *url, int flags, php_stream_statbuf *ssb, php_stream_context *context)
{
php_stream *stream = NULL;
php_url *resource = NULL;
int result;
char tmp_line[512];
/* If ssb is NULL then someone is misbehaving */
if (!ssb) return -1;
stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
if (!stream) {
goto stat_errexit;
}
ssb->sb.st_mode = 0644; /* FTP won't give us a valid mode, so approximate one based on being readable */
php_stream_printf(stream, "CWD %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/")); /* If we can CWD to it, it's a directory (maybe a link, but we can't tell) */
result = GET_FTP_RESULT(stream);
if (result < 200 || result > 299) {
ssb->sb.st_mode |= S_IFREG;
} else {
ssb->sb.st_mode |= S_IFDIR | S_IXUSR | S_IXGRP | S_IXOTH;
}
php_stream_write_string(stream, "TYPE I\r\n"); /* we need this since some servers refuse to accept SIZE command in ASCII mode */
result = GET_FTP_RESULT(stream);
if(result < 200 || result > 299) {
goto stat_errexit;
}
php_stream_printf(stream, "SIZE %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
result = GET_FTP_RESULT(stream);
if (result < 200 || result > 299) {
/* Failure either means it doesn't exist
or it's a directory and this server
fails on listing directory sizes */
if (ssb->sb.st_mode & S_IFDIR) {
ssb->sb.st_size = 0;
} else {
goto stat_errexit;
}
} else {
ssb->sb.st_size = atoi(tmp_line + 4);
}
php_stream_printf(stream, "MDTM %s\r\n", (resource->path != NULL ? ZSTR_VAL(resource->path) : "/"));
result = GET_FTP_RESULT(stream);
if (result == 213) {
char *p = tmp_line + 4;
int n;
struct tm tm, tmbuf, *gmt;
time_t stamp;
while ((size_t)(p - tmp_line) < sizeof(tmp_line) && !isdigit(*p)) {
p++;
}
if ((size_t)(p - tmp_line) > sizeof(tmp_line)) {
goto mdtm_error;
}
n = sscanf(p, "%4d%2d%2d%2d%2d%2d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
if (n != 6) {
goto mdtm_error;
}
tm.tm_year -= 1900;
tm.tm_mon--;
tm.tm_isdst = -1;
/* figure out the GMT offset */
stamp = time(NULL);
gmt = php_gmtime_r(&stamp, &tmbuf);
if (!gmt) {
goto mdtm_error;
}
gmt->tm_isdst = -1;
/* apply the GMT offset */
tm.tm_sec += (long)(stamp - mktime(gmt));
tm.tm_isdst = gmt->tm_isdst;
ssb->sb.st_mtime = mktime(&tm);
} else {
/* error or unsupported command */
mdtm_error:
ssb->sb.st_mtime = -1;
}
ssb->sb.st_ino = 0; /* Unknown values */
ssb->sb.st_dev = 0;
ssb->sb.st_uid = 0;
ssb->sb.st_gid = 0;
ssb->sb.st_atime = -1;
ssb->sb.st_ctime = -1;
ssb->sb.st_nlink = 1;
ssb->sb.st_rdev = -1;
#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
ssb->sb.st_blksize = 4096; /* Guess since FTP won't expose this information */
#ifdef HAVE_STRUCT_STAT_ST_BLOCKS
ssb->sb.st_blocks = (int)((4095 + ssb->sb.st_size) / ssb->sb.st_blksize); /* emulate ceil */
#endif
#endif
php_stream_close(stream);
php_url_free(resource);
return 0;
stat_errexit:
if (resource) {
php_url_free(resource);
}
if (stream) {
php_stream_close(stream);
}
return -1;
}
/* }}} */
/* {{{ php_stream_ftp_unlink */
static int php_stream_ftp_unlink(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context)
{
php_stream *stream = NULL;
php_url *resource = NULL;
int result;
char tmp_line[512];
stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
if (!stream) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
}
goto unlink_errexit;
}
if (resource->path == NULL) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
}
goto unlink_errexit;
}
/* Attempt to delete the file */
php_stream_printf(stream, "DELE %s\r\n", ZSTR_VAL(resource->path));
result = GET_FTP_RESULT(stream);
if (result < 200 || result > 299) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Error Deleting file: %s", tmp_line);
}
goto unlink_errexit;
}
php_url_free(resource);
php_stream_close(stream);
return 1;
unlink_errexit:
if (resource) {
php_url_free(resource);
}
if (stream) {
php_stream_close(stream);
}
return 0;
}
/* }}} */
/* {{{ php_stream_ftp_rename */
static int php_stream_ftp_rename(php_stream_wrapper *wrapper, const char *url_from, const char *url_to, int options, php_stream_context *context)
{
php_stream *stream = NULL;
php_url *resource_from = NULL, *resource_to = NULL;
int result;
char tmp_line[512];
resource_from = php_url_parse(url_from);
resource_to = php_url_parse(url_to);
/* Must be same scheme (ftp/ftp or ftps/ftps), same host, and same port
(or a 21/0 0/21 combination which is also "same")
Also require paths to/from */
if (!resource_from ||
!resource_to ||
!resource_from->scheme ||
!resource_to->scheme ||
!zend_string_equals(resource_from->scheme, resource_to->scheme) ||
!resource_from->host ||
!resource_to->host ||
!zend_string_equals(resource_from->host, resource_to->host) ||
(resource_from->port != resource_to->port &&
resource_from->port * resource_to->port != 0 &&
resource_from->port + resource_to->port != 21) ||
!resource_from->path ||
!resource_to->path) {
goto rename_errexit;
}
stream = php_ftp_fopen_connect(wrapper, url_from, "r", 0, NULL, context, NULL, NULL, NULL, NULL);
if (!stream) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Unable to connect to %s", ZSTR_VAL(resource_from->host));
}
goto rename_errexit;
}
/* Rename FROM */
php_stream_printf(stream, "RNFR %s\r\n", ZSTR_VAL(resource_from->path));
result = GET_FTP_RESULT(stream);
if (result < 300 || result > 399) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Error Renaming file: %s", tmp_line);
}
goto rename_errexit;
}
/* Rename TO */
php_stream_printf(stream, "RNTO %s\r\n", ZSTR_VAL(resource_to->path));
result = GET_FTP_RESULT(stream);
if (result < 200 || result > 299) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Error Renaming file: %s", tmp_line);
}
goto rename_errexit;
}
php_url_free(resource_from);
php_url_free(resource_to);
php_stream_close(stream);
return 1;
rename_errexit:
if (resource_from) {
php_url_free(resource_from);
}
if (resource_to) {
php_url_free(resource_to);
}
if (stream) {
php_stream_close(stream);
}
return 0;
}
/* }}} */
/* {{{ php_stream_ftp_mkdir */
static int php_stream_ftp_mkdir(php_stream_wrapper *wrapper, const char *url, int mode, int options, php_stream_context *context)
{
php_stream *stream = NULL;
php_url *resource = NULL;
int result, recursive = options & PHP_STREAM_MKDIR_RECURSIVE;
char tmp_line[512];
stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
if (!stream) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
}
goto mkdir_errexit;
}
if (resource->path == NULL) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
}
goto mkdir_errexit;
}
if (!recursive) {
php_stream_printf(stream, "MKD %s\r\n", ZSTR_VAL(resource->path));
result = GET_FTP_RESULT(stream);
} else {
/* we look for directory separator from the end of string, thus hopefully reducing our work load */
char *p, *e, *buf;
buf = estrndup(ZSTR_VAL(resource->path), ZSTR_LEN(resource->path));
e = buf + ZSTR_LEN(resource->path);
/* find a top level directory we need to create */
while ((p = strrchr(buf, '/'))) {
*p = '\0';
php_stream_printf(stream, "CWD %s\r\n", strlen(buf) ? buf : "/");
result = GET_FTP_RESULT(stream);
if (result >= 200 && result <= 299) {
*p = '/';
break;
}
}
php_stream_printf(stream, "MKD %s\r\n", strlen(buf) ? buf : "/");
result = GET_FTP_RESULT(stream);
if (result >= 200 && result <= 299) {
if (!p) {
p = buf;
}
/* create any needed directories if the creation of the 1st directory worked */
while (p != e) {
if (*p == '\0' && *(p + 1) != '\0') {
*p = '/';
php_stream_printf(stream, "MKD %s\r\n", buf);
result = GET_FTP_RESULT(stream);
if (result < 200 || result > 299) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "%s", tmp_line);
}
break;
}
}
++p;
}
}
efree(buf);
}
php_url_free(resource);
php_stream_close(stream);
if (result < 200 || result > 299) {
/* Failure */
return 0;
}
return 1;
mkdir_errexit:
if (resource) {
php_url_free(resource);
}
if (stream) {
php_stream_close(stream);
}
return 0;
}
/* }}} */
/* {{{ php_stream_ftp_rmdir */
static int php_stream_ftp_rmdir(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context)
{
php_stream *stream = NULL;
php_url *resource = NULL;
int result;
char tmp_line[512];
stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL);
if (!stream) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Unable to connect to %s", url);
}
goto rmdir_errexit;
}
if (resource->path == NULL) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "Invalid path provided in %s", url);
}
goto rmdir_errexit;
}
php_stream_printf(stream, "RMD %s\r\n", ZSTR_VAL(resource->path));
result = GET_FTP_RESULT(stream);
if (result < 200 || result > 299) {
if (options & REPORT_ERRORS) {
php_error_docref(NULL, E_WARNING, "%s", tmp_line);
}
goto rmdir_errexit;
}
php_url_free(resource);
php_stream_close(stream);
return 1;
rmdir_errexit:
if (resource) {
php_url_free(resource);
}
if (stream) {
php_stream_close(stream);
}
return 0;
}
/* }}} */
static const php_stream_wrapper_ops ftp_stream_wops = {
php_stream_url_wrap_ftp,
php_stream_ftp_stream_close, /* stream_close */
php_stream_ftp_stream_stat,
php_stream_ftp_url_stat, /* stat_url */
php_stream_ftp_opendir, /* opendir */
"ftp",
php_stream_ftp_unlink, /* unlink */
php_stream_ftp_rename, /* rename */
php_stream_ftp_mkdir, /* mkdir */
php_stream_ftp_rmdir, /* rmdir */
NULL
};
PHPAPI const php_stream_wrapper php_stream_ftp_wrapper = {
&ftp_stream_wops,
NULL,
1 /* is_url */
};