diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /ucb/source/ucp/ftp/ftpurl.cxx | |
parent | Initial commit. (diff) | |
download | libreoffice-cb75148ebd0135178ff46f89a30139c44f8d2040.tar.xz libreoffice-cb75148ebd0135178ff46f89a30139c44f8d2040.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ucb/source/ucp/ftp/ftpurl.cxx')
-rw-r--r-- | ucb/source/ucp/ftp/ftpurl.cxx | 804 |
1 files changed, 804 insertions, 0 deletions
diff --git a/ucb/source/ucp/ftp/ftpurl.cxx b/ucb/source/ucp/ftp/ftpurl.cxx new file mode 100644 index 000000000..13b7e839c --- /dev/null +++ b/ucb/source/ucp/ftp/ftpurl.cxx @@ -0,0 +1,804 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +/************************************************************************** + TODO + ************************************************************************** + + *************************************************************************/ + +#include <sal/config.h> +#include <sal/log.hxx> + +#include <rtl/ustrbuf.hxx> +#include <com/sun/star/ucb/OpenMode.hpp> +#include <string.h> +#include <rtl/uri.hxx> +#include <o3tl/safeint.hxx> + +#include "ftpurl.hxx" +#include "ftpcontentprovider.hxx" +#include "ftpcfunc.hxx" +#include "ftpcontainer.hxx" +#include <memory> + +using namespace ftp; +using namespace com::sun::star::ucb; +using namespace com::sun::star::uno; + +namespace { + +OUString encodePathSegment(OUString const & decoded) { + return rtl::Uri::encode( + decoded, rtl_UriCharClassPchar, rtl_UriEncodeIgnoreEscapes, + RTL_TEXTENCODING_UTF8); +} + +OUString decodePathSegment(OUString const & encoded) { + return rtl::Uri::decode( + encoded, rtl_UriDecodeWithCharset, RTL_TEXTENCODING_UTF8); +} + +} + +MemoryContainer::MemoryContainer() + : m_nLen(0), + m_nWritePos(0), + m_pBuffer(nullptr) +{ +} + +MemoryContainer::~MemoryContainer() +{ + std::free(m_pBuffer); +} + + +int MemoryContainer::append( + const void* pBuffer, + size_t size, + size_t nmemb +) noexcept +{ + sal_uInt32 nLen = size*nmemb; + sal_uInt32 tmp(nLen + m_nWritePos); + + if(m_nLen < tmp) { // enlarge in steps of multiples of 1K + do { + m_nLen+=1024; + } while(m_nLen < tmp); + + if (auto p = std::realloc(m_pBuffer, m_nLen)) + m_pBuffer = p; + else + return 0; + } + + memcpy(static_cast<sal_Int8*>(m_pBuffer)+m_nWritePos, + pBuffer,nLen); + m_nWritePos = tmp; + return nLen; +} + + +extern "C" { + + int memory_write(void *buffer,size_t size,size_t nmemb,void *stream) + { + MemoryContainer *_stream = + static_cast<MemoryContainer*>(stream); + + if(!_stream) + return 0; + + return _stream->append(buffer,size,nmemb); + } + +} + + +FTPURL::FTPURL(const FTPURL& r) + : m_pFCP(r.m_pFCP), + m_aUsername(r.m_aUsername), + m_bShowPassword(r.m_bShowPassword), + m_aHost(r.m_aHost), + m_aPort(r.m_aPort), + m_aPathSegmentVec(r.m_aPathSegmentVec) + +{ +} + + +FTPURL::FTPURL(const OUString& url, + FTPContentProvider* pFCP) + : m_pFCP(pFCP), + m_aUsername("anonymous"), + m_bShowPassword(false), + m_aPort("21") +{ + parse(url); // can reset m_bShowPassword +} + + +FTPURL::~FTPURL() +{ +} + + +void FTPURL::parse(const OUString& url) +{ + OUString aPassword, urlRest; + + if(url.getLength() < 6 || !url.startsWithIgnoreAsciiCase("ftp://", &urlRest)) + throw malformed_exception(); + + // determine "username:password@host:port" + OUString aExpr; + sal_Int32 nIdx = urlRest.indexOf('/'); + if (nIdx == -1) + { + aExpr = urlRest; + urlRest = ""; + } + else + { + aExpr = urlRest.copy(0, nIdx); + urlRest = urlRest.copy(nIdx + 1); + } + + sal_Int32 l = aExpr.indexOf('@'); + m_aHost = aExpr.copy(1+l); + + if(l != -1) { + // Now username and password. + aExpr = aExpr.copy(0,l); + l = aExpr.indexOf(':'); + if(l != -1) { + aPassword = aExpr.copy(1+l); + if(!aPassword.isEmpty()) + m_bShowPassword = true; + } + if(l > 0) + // Overwritten only if the username is not empty. + m_aUsername = aExpr.copy(0,l); + else if(!aExpr.isEmpty()) + m_aUsername = aExpr; + } + + l = m_aHost.lastIndexOf(':'); + sal_Int32 ipv6Back = m_aHost.lastIndexOf(']'); + if((ipv6Back == -1 && l != -1) // not ipv6, but a port + || + (ipv6Back != -1 && 1+ipv6Back == l) // ipv6, and a port + ) + { + if(1+l<m_aHost.getLength()) + m_aPort = m_aHost.copy(1+l); + m_aHost = m_aHost.copy(0,l); + } + + // now determine the pathsegments ... + while(!urlRest.isEmpty()) + { + nIdx = urlRest.indexOf('/'); + OUString segment; + if(nIdx == -1) + { + segment = urlRest; + urlRest = ""; + } + else + { + segment = urlRest.copy(0, nIdx); + urlRest = urlRest.copy(nIdx + 1); + } + if( segment == ".." && !m_aPathSegmentVec.empty() && m_aPathSegmentVec.back() != ".." ) + m_aPathSegmentVec.pop_back(); + else if( segment == "." ) + ; // Ignore + else + // This is a legal name. + m_aPathSegmentVec.push_back( segment ); + } + + if(m_bShowPassword) + m_pFCP->setHost(m_aHost, + m_aPort, + m_aUsername, + aPassword, + ""/*aAccount*/); + + // now check for something like ";type=i" at end of url + if(!m_aPathSegmentVec.empty()) + { + l = m_aPathSegmentVec.back().indexOf(';'); + if (l != -1) + { + m_aType = m_aPathSegmentVec.back().copy(l); + m_aPathSegmentVec.back() = m_aPathSegmentVec.back().copy(0,l); + } + } +} + + +OUString FTPURL::ident(bool withslash,bool internal) const +{ + // rebuild the url as one without ellipses, + // and more important, as one without username and + // password. ( These are set together with the command. ) + + OUStringBuffer bff; + bff.append("ftp://"); + + if( m_aUsername != "anonymous" ) { + bff.append(m_aUsername); + + OUString aPassword,aAccount; + m_pFCP->forHost(m_aHost, + m_aPort, + m_aUsername, + aPassword, + aAccount); + + if((m_bShowPassword || internal) && + !aPassword.isEmpty() ) + bff.append(':') + .append(aPassword); + + bff.append('@'); + } + bff.append(m_aHost); + + if( m_aPort != "21" ) + bff.append(':') + .append(m_aPort) + .append('/'); + else + bff.append('/'); + + for(size_t i = 0; i < m_aPathSegmentVec.size(); ++i) + if(i == 0) + bff.append(m_aPathSegmentVec[i]); + else + bff.append('/').append(m_aPathSegmentVec[i]); + if(withslash) + if(!bff.isEmpty() && bff[bff.getLength()-1] != '/') + bff.append('/'); + + bff.append(m_aType); + return bff.makeStringAndClear(); +} + + +OUString FTPURL::parent(bool internal) const +{ + OUStringBuffer bff; + + bff.append("ftp://"); + + if( m_aUsername != "anonymous" ) { + bff.append(m_aUsername); + + OUString aPassword,aAccount; + m_pFCP->forHost(m_aHost, + m_aPort, + m_aUsername, + aPassword, + aAccount); + + if((internal || m_bShowPassword) && !aPassword.isEmpty()) + bff.append(':') + .append(aPassword); + + bff.append('@'); + } + + bff.append(m_aHost); + + if( m_aPort != "21" ) + bff.append(':') + .append(m_aPort) + .append('/'); + else + bff.append('/'); + + OUString last; + + for(size_t i = 0; i < m_aPathSegmentVec.size(); ++i) + if(1+i == m_aPathSegmentVec.size()) + last = m_aPathSegmentVec[i]; + else if(i == 0) + bff.append(m_aPathSegmentVec[i]); + else + bff.append('/').append(m_aPathSegmentVec[i]); + + if(last.isEmpty()) + bff.append(".."); + else if ( last == ".." ) + bff.append(last + "/.."); + + bff.append(m_aType); + return bff.makeStringAndClear(); +} + + +void FTPURL::child(const OUString& title) +{ + m_aPathSegmentVec.push_back(encodePathSegment(title)); +} + + +OUString FTPURL::child() const +{ + return + !m_aPathSegmentVec.empty() ? + decodePathSegment(m_aPathSegmentVec.back()) : OUString(); +} + + +/** Listing of a directory. + */ + +namespace ftp { + + namespace { + + enum OS { + FTP_DOS,FTP_UNIX,FTP_VMS,FTP_UNKNOWN + }; + + } + +} + + +#define SET_CONTROL_CONTAINER \ + MemoryContainer control; \ + (void)curl_easy_setopt(curl, \ + CURLOPT_HEADERFUNCTION, \ + memory_write); \ + (void)curl_easy_setopt(curl, \ + CURLOPT_WRITEHEADER, \ + &control) + + +static void setCurlUrl(CURL* curl, OUString const & url) +{ + OString urlParAscii(url.getStr(), + url.getLength(), + RTL_TEXTENCODING_UTF8); + (void)curl_easy_setopt(curl, + CURLOPT_URL, + urlParAscii.getStr()); +}; + +oslFileHandle FTPURL::open() +{ + if(m_aPathSegmentVec.empty()) + throw curl_exception(CURLE_FTP_COULDNT_RETR_FILE); + + CURL *curl = m_pFCP->handle(); + + SET_CONTROL_CONTAINER; + OUString url(ident(false,true)); + setCurlUrl(curl, url); + + oslFileHandle res( nullptr ); + if ( osl_createTempFile( nullptr, &res, nullptr ) == osl_File_E_None ) + { + (void)curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,file_write); + (void)curl_easy_setopt(curl,CURLOPT_WRITEDATA,res); + + (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,0); + CURLcode err = curl_easy_perform(curl); + + if(err == CURLE_OK) + { + oslFileError rc = osl_setFilePos( res, osl_Pos_Absolut, 0 ); + SAL_WARN_IF(rc != osl_File_E_None, "ucb.ucp.ftp", + "osl_setFilePos failed"); + } + else { + osl_closeFile(res); + res = nullptr; + throw curl_exception(err); + } + } + + return res; +} + + +std::vector<FTPDirentry> FTPURL::list( + sal_Int16 nMode +) const +{ + CURL *curl = m_pFCP->handle(); + + SET_CONTROL_CONTAINER; + (void)curl_easy_setopt(curl,CURLOPT_NOBODY,false); + MemoryContainer data; + (void)curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,memory_write); + (void)curl_easy_setopt(curl,CURLOPT_WRITEDATA,&data); + + OUString url(ident(true,true)); + setCurlUrl(curl, url); + (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,0); + + CURLcode err = curl_easy_perform(curl); + if(err != CURLE_OK) + throw curl_exception(err); + + // now evaluate the error messages + + sal_uInt32 len = data.m_nWritePos; + char* fwd = static_cast<char*>(data.m_pBuffer); + char *p1, *p2; + p1 = p2 = fwd; + + OS osKind(FTP_UNKNOWN); + std::vector<FTPDirentry> resvec; + FTPDirentry aDirEntry; + // ensure slash at the end + OUString viewurl(ident(true,false)); + + while(true) { + while(o3tl::make_unsigned(p2-fwd) < len && *p2 != '\n') ++p2; + if(o3tl::make_unsigned(p2-fwd) == len) break; + + *p2 = 0; + switch(osKind) { + // While FTP knows the 'system'-command, + // which returns the operating system type, + // this is not usable here: There are Windows-server + // formatting the output like UNIX-ls command. + case FTP_DOS: + FTPDirectoryParser::parseDOS(aDirEntry,p1); + break; + case FTP_UNIX: + FTPDirectoryParser::parseUNIX(aDirEntry,p1); + break; + case FTP_VMS: + FTPDirectoryParser::parseVMS(aDirEntry,p1); + break; + default: + if(FTPDirectoryParser::parseUNIX(aDirEntry,p1)) + osKind = FTP_UNIX; + else if(FTPDirectoryParser::parseDOS(aDirEntry,p1)) + osKind = FTP_DOS; + else if(FTPDirectoryParser::parseVMS(aDirEntry,p1)) + osKind = FTP_VMS; + } + aDirEntry.m_aName = aDirEntry.m_aName.trim(); + if( osKind != int(FTP_UNKNOWN) && aDirEntry.m_aName != ".." && aDirEntry.m_aName != "." ) { + aDirEntry.m_aURL = viewurl + encodePathSegment(aDirEntry.m_aName); + + bool isDir = (aDirEntry.m_nMode & INETCOREFTP_FILEMODE_ISDIR) == INETCOREFTP_FILEMODE_ISDIR; + switch(nMode) { + case OpenMode::DOCUMENTS: + if(!isDir) + resvec.push_back(aDirEntry); + break; + case OpenMode::FOLDERS: + if(isDir) + resvec.push_back(aDirEntry); + break; + default: + resvec.push_back(aDirEntry); + }; + } + aDirEntry.clear(); + p1 = p2 + 1; + } + + return resvec; +} + + +OUString FTPURL::net_title() const +{ + CURL *curl = m_pFCP->handle(); + + SET_CONTROL_CONTAINER; + (void)curl_easy_setopt(curl,CURLOPT_NOBODY,true); // no data => no transfer + struct curl_slist *slist = nullptr; + // post request + slist = curl_slist_append(slist,"PWD"); + (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,slist); + + bool try_more(true); + CURLcode err; + OUString aNetTitle; + + while(true) { + OUString url(ident(false,true)); + + if(try_more && !url.endsWith("/")) + url += "/"; // add end-slash + else if(!try_more && url.endsWith("/")) + url = url.copy(0,url.getLength()-1); // remove end-slash + + setCurlUrl(curl, url); + err = curl_easy_perform(curl); + + if(err == CURLE_OK) { // get the title from the server + char* fwd = static_cast<char*>(control.m_pBuffer); + sal_uInt32 len = control.m_nWritePos; + + aNetTitle = OUString(fwd,len,RTL_TEXTENCODING_UTF8); + // the buffer now contains the name of the file; + // analyze the output: + // Format of current working directory: + // 257 "/bla/bla" is current directory + sal_Int32 index1 = aNetTitle.lastIndexOf("257"); + index1 = aNetTitle.indexOf('"', index1 + std::strlen("257")) + 1; + sal_Int32 index2 = aNetTitle.indexOf('"', index1); + aNetTitle = index2 > index1 + ? aNetTitle.copy(index1, index2 - index1) : OUString(); + if( aNetTitle != "/" ) { + index1 = aNetTitle.lastIndexOf('/'); + aNetTitle = aNetTitle.copy(1+index1); + } + try_more = false; + } else if(err == CURLE_BAD_PASSWORD_ENTERED) + // the client should retry after getting the correct + // username + password + throw curl_exception(err); +#if LIBCURL_VERSION_NUM>=0x070d01 /* 7.13.1 */ + else if(err == CURLE_LOGIN_DENIED) + // the client should retry after getting the correct + // username + password + throw curl_exception(err); +#endif + else if(try_more && err == CURLE_FTP_ACCESS_DENIED) { + // We were either denied access when trying to login to + // an FTP server or when trying to change working directory + // to the one given in the URL. + if(!m_aPathSegmentVec.empty()) + // determine title from URL + aNetTitle = decodePathSegment(m_aPathSegmentVec.back()); + else + // must be root + aNetTitle = "/"; + try_more = false; + } + + if(try_more) + try_more = false; + else + break; + } + + curl_slist_free_all(slist); + return aNetTitle; +} + + +FTPDirentry FTPURL::direntry() const +{ + OUString nettitle = net_title(); + FTPDirentry aDirentry; + + aDirentry.m_aName = nettitle; // init aDirentry + if( nettitle == "/" || nettitle == ".." ) + aDirentry.m_nMode = INETCOREFTP_FILEMODE_ISDIR; + else + aDirentry.m_nMode = INETCOREFTP_FILEMODE_UNKNOWN; + + aDirentry.m_nSize = 0; + + if( nettitle != "/" ) { + // try to open the parent directory + FTPURL aURL(parent(),m_pFCP); + + std::vector<FTPDirentry> aList = aURL.list(OpenMode::ALL); + + for(const FTPDirentry & d : aList) { + if(d.m_aName == nettitle) { // the relevant file is found + aDirentry = d; + break; + } + } + } + return aDirentry; +} + + +extern "C" { + + static size_t memory_read(void *ptr,size_t size,size_t nmemb,void *stream) + { + sal_Int32 nRequested = sal_Int32(size*nmemb); + CurlInput *curlInput = static_cast<CurlInput*>(stream); + if(curlInput) + return size_t(curlInput->read(static_cast<sal_Int8*>(ptr),nRequested)); + else + return 0; + } + +} + + +void FTPURL::insert(bool replaceExisting,void* stream) const +{ + if(!replaceExisting) { +// FTPDirentry aDirentry(direntry()); +// if(aDirentry.m_nMode == INETCOREFTP_FILEMODE_UNKNOWN) + // throw curl_exception(FILE_EXIST_DURING_INSERT); + throw curl_exception(FILE_MIGHT_EXIST_DURING_INSERT); + } // else + // overwrite is default in libcurl + + CURL *curl = m_pFCP->handle(); + + SET_CONTROL_CONTAINER; + (void)curl_easy_setopt(curl,CURLOPT_NOBODY,false); // no data => no transfer + (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,0); + (void)curl_easy_setopt(curl,CURLOPT_QUOTE,0); + (void)curl_easy_setopt(curl,CURLOPT_READFUNCTION,memory_read); + (void)curl_easy_setopt(curl,CURLOPT_READDATA,stream); + (void)curl_easy_setopt(curl, CURLOPT_UPLOAD,1); + + OUString url(ident(false,true)); + setCurlUrl(curl, url); + + CURLcode err = curl_easy_perform(curl); + (void)curl_easy_setopt(curl, CURLOPT_UPLOAD,false); + + if(err != CURLE_OK) + throw curl_exception(err); +} + + +void FTPURL::mkdir(bool ReplaceExisting) const +{ + OString title; + if(!m_aPathSegmentVec.empty()) { + OUString titleOU = m_aPathSegmentVec.back(); + titleOU = decodePathSegment(titleOU); + title = OString(titleOU.getStr(), + titleOU.getLength(), + RTL_TEXTENCODING_UTF8); + } + else + // will give an error + title = OString("/"); + + OString aDel = "del " + title; + OString mkd = "mkd " + title; + + struct curl_slist *slist = nullptr; + + FTPDirentry aDirentry(direntry()); + if(!ReplaceExisting) { +// if(aDirentry.m_nMode != INETCOREFTP_FILEMODE_UNKNOWN) +// throw curl_exception(FOLDER_EXIST_DURING_INSERT); + throw curl_exception(FOLDER_MIGHT_EXIST_DURING_INSERT); + } else if(aDirentry.m_nMode != INETCOREFTP_FILEMODE_UNKNOWN) + slist = curl_slist_append(slist,aDel.getStr()); + + slist = curl_slist_append(slist,mkd.getStr()); + + CURL *curl = m_pFCP->handle(); + SET_CONTROL_CONTAINER; + (void)curl_easy_setopt(curl,CURLOPT_NOBODY,true); // no data => no transfer + (void)curl_easy_setopt(curl,CURLOPT_QUOTE,0); + + // post request + (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,slist); + + OUString url(parent(true)); + if(!url.endsWith("/")) + url += "/"; + setCurlUrl(curl, url); + + CURLcode err = curl_easy_perform(curl); + curl_slist_free_all(slist); + if(err != CURLE_OK) + throw curl_exception(err); +} + + +OUString FTPURL::ren(const OUString& NewTitle) +{ + CURL *curl = m_pFCP->handle(); + + // post request + OUString OldTitle = net_title(); + OString renamefrom = "RNFR " + + OString(OldTitle.getStr(), + OldTitle.getLength(), + RTL_TEXTENCODING_UTF8); + + OString renameto = "RNTO " + + OString(NewTitle.getStr(), + NewTitle.getLength(), + RTL_TEXTENCODING_UTF8); + + struct curl_slist *slist = nullptr; + slist = curl_slist_append(slist,renamefrom.getStr()); + slist = curl_slist_append(slist,renameto.getStr()); + (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,slist); + + SET_CONTROL_CONTAINER; + (void)curl_easy_setopt(curl,CURLOPT_NOBODY,true); // no data => no transfer + (void)curl_easy_setopt(curl,CURLOPT_QUOTE,0); + + OUString url(parent(true)); + if(!url.endsWith("/")) + url += "/"; + setCurlUrl(curl, url); + + CURLcode err = curl_easy_perform(curl); + curl_slist_free_all(slist); + if(err != CURLE_OK) + throw curl_exception(err); + else if( !m_aPathSegmentVec.empty() && m_aPathSegmentVec.back() != ".." ) + m_aPathSegmentVec.back() = encodePathSegment(NewTitle); + return OldTitle; +} + + +void FTPURL::del() const +{ + FTPDirentry aDirentry(direntry()); + + OString dele(aDirentry.m_aName.getStr(), + aDirentry.m_aName.getLength(), + RTL_TEXTENCODING_UTF8); + + if(aDirentry.m_nMode & INETCOREFTP_FILEMODE_ISDIR) { + std::vector<FTPDirentry> vec = list(sal_Int16(OpenMode::ALL)); + for(const FTPDirentry & i : vec) + { + try { + FTPURL url(i.m_aURL,m_pFCP); + url.del(); + } catch(const curl_exception&) { + } + } + dele = "RMD " + dele; + } + else if(aDirentry.m_nMode != INETCOREFTP_FILEMODE_UNKNOWN) + dele = "DELE " + dele; + else + return; + + // post request + CURL *curl = m_pFCP->handle(); + struct curl_slist *slist = nullptr; + slist = curl_slist_append(slist,dele.getStr()); + (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,slist); + + SET_CONTROL_CONTAINER; + (void)curl_easy_setopt(curl,CURLOPT_NOBODY,true); // no data => no transfer + (void)curl_easy_setopt(curl,CURLOPT_QUOTE,0); + + OUString url(parent(true)); + if(!url.endsWith("/")) + url += "/"; + setCurlUrl(curl, url); + + CURLcode err = curl_easy_perform(curl); + curl_slist_free_all(slist); + if(err != CURLE_OK) + throw curl_exception(err); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |