Version 7.6.3.2-android, tag libreoffice-7.6.3.2-android
[LibreOffice.git] / ucb / source / ucp / ftp / ftpurl.cxx
blob67ac4e6564bd23dea5c5a911cb55dc9ed7394937
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 /**************************************************************************
21 TODO
22 **************************************************************************
24 *************************************************************************/
26 #include <sal/config.h>
27 #include <sal/log.hxx>
29 #include <rtl/ustrbuf.hxx>
30 #include <com/sun/star/ucb/OpenMode.hpp>
31 #include <string.h>
32 #include <rtl/uri.hxx>
33 #include <o3tl/safeint.hxx>
35 #include "ftpurl.hxx"
36 #include "ftpcontentprovider.hxx"
37 #include "ftpcfunc.hxx"
38 #include "ftpcontainer.hxx"
39 #include <memory>
41 using namespace ftp;
42 using namespace com::sun::star::ucb;
43 using namespace com::sun::star::uno;
45 namespace {
47 OUString encodePathSegment(OUString const & decoded) {
48 return rtl::Uri::encode(
49 decoded, rtl_UriCharClassPchar, rtl_UriEncodeIgnoreEscapes,
50 RTL_TEXTENCODING_UTF8);
53 OUString decodePathSegment(OUString const & encoded) {
54 return rtl::Uri::decode(
55 encoded, rtl_UriDecodeWithCharset, RTL_TEXTENCODING_UTF8);
60 MemoryContainer::MemoryContainer()
61 : m_nLen(0),
62 m_nWritePos(0),
63 m_pBuffer(nullptr)
67 MemoryContainer::~MemoryContainer()
69 std::free(m_pBuffer);
73 int MemoryContainer::append(
74 const void* pBuffer,
75 size_t size,
76 size_t nmemb
77 ) noexcept
79 sal_uInt32 nLen = size*nmemb;
80 sal_uInt32 tmp(nLen + m_nWritePos);
82 if(m_nLen < tmp) { // enlarge in steps of multiples of 1K
83 do {
84 m_nLen+=1024;
85 } while(m_nLen < tmp);
87 if (auto p = std::realloc(m_pBuffer, m_nLen))
88 m_pBuffer = p;
89 else
90 return 0;
93 memcpy(static_cast<sal_Int8*>(m_pBuffer)+m_nWritePos,
94 pBuffer,nLen);
95 m_nWritePos = tmp;
96 return nLen;
100 extern "C" {
102 int memory_write(void *buffer,size_t size,size_t nmemb,void *stream)
104 MemoryContainer *_stream =
105 static_cast<MemoryContainer*>(stream);
107 if(!_stream)
108 return 0;
110 return _stream->append(buffer,size,nmemb);
116 FTPURL::FTPURL(const FTPURL& r)
117 : m_pFCP(r.m_pFCP),
118 m_aUsername(r.m_aUsername),
119 m_bShowPassword(r.m_bShowPassword),
120 m_aHost(r.m_aHost),
121 m_aPort(r.m_aPort),
122 m_aPathSegmentVec(r.m_aPathSegmentVec)
128 FTPURL::FTPURL(const OUString& url,
129 FTPContentProvider* pFCP)
130 : m_pFCP(pFCP),
131 m_aUsername("anonymous"),
132 m_bShowPassword(false),
133 m_aPort("21")
135 parse(url); // can reset m_bShowPassword
139 FTPURL::~FTPURL()
144 void FTPURL::parse(const OUString& url)
146 OUString aPassword, urlRest;
148 if(url.getLength() < 6 || !url.startsWithIgnoreAsciiCase("ftp://", &urlRest))
149 throw malformed_exception();
151 // determine "username:password@host:port"
152 OUString aExpr;
153 sal_Int32 nIdx = urlRest.indexOf('/');
154 if (nIdx == -1)
156 aExpr = urlRest;
157 urlRest = "";
159 else
161 aExpr = urlRest.copy(0, nIdx);
162 urlRest = urlRest.copy(nIdx + 1);
165 sal_Int32 l = aExpr.indexOf('@');
166 m_aHost = aExpr.copy(1+l);
168 if(l != -1) {
169 // Now username and password.
170 aExpr = aExpr.copy(0,l);
171 l = aExpr.indexOf(':');
172 if(l != -1) {
173 aPassword = aExpr.copy(1+l);
174 if(!aPassword.isEmpty())
175 m_bShowPassword = true;
177 if(l > 0)
178 // Overwritten only if the username is not empty.
179 m_aUsername = aExpr.copy(0,l);
180 else if(!aExpr.isEmpty())
181 m_aUsername = aExpr;
184 l = m_aHost.lastIndexOf(':');
185 sal_Int32 ipv6Back = m_aHost.lastIndexOf(']');
186 if((ipv6Back == -1 && l != -1) // not ipv6, but a port
188 (ipv6Back != -1 && 1+ipv6Back == l) // ipv6, and a port
191 if(1+l<m_aHost.getLength())
192 m_aPort = m_aHost.copy(1+l);
193 m_aHost = m_aHost.copy(0,l);
196 // now determine the pathsegments ...
197 while(!urlRest.isEmpty())
199 nIdx = urlRest.indexOf('/');
200 OUString segment;
201 if(nIdx == -1)
203 segment = urlRest;
204 urlRest = "";
206 else
208 segment = urlRest.copy(0, nIdx);
209 urlRest = urlRest.copy(nIdx + 1);
211 if( segment == ".." && !m_aPathSegmentVec.empty() && m_aPathSegmentVec.back() != ".." )
212 m_aPathSegmentVec.pop_back();
213 else if( segment == "." )
214 ; // Ignore
215 else
216 // This is a legal name.
217 m_aPathSegmentVec.push_back( segment );
220 if(m_bShowPassword)
221 m_pFCP->setHost(m_aHost,
222 m_aPort,
223 m_aUsername,
224 aPassword,
225 ""/*aAccount*/);
227 // now check for something like ";type=i" at end of url
228 if(!m_aPathSegmentVec.empty())
230 l = m_aPathSegmentVec.back().indexOf(';');
231 if (l != -1)
233 m_aType = m_aPathSegmentVec.back().copy(l);
234 m_aPathSegmentVec.back() = m_aPathSegmentVec.back().copy(0,l);
240 OUString FTPURL::ident(bool withslash,bool internal) const
242 // rebuild the url as one without ellipses,
243 // and more important, as one without username and
244 // password. ( These are set together with the command. )
246 OUStringBuffer bff("ftp://");
248 if( m_aUsername != "anonymous" ) {
249 bff.append(m_aUsername);
251 OUString aPassword,aAccount;
252 m_pFCP->forHost(m_aHost,
253 m_aPort,
254 m_aUsername,
255 aPassword,
256 aAccount);
258 if((m_bShowPassword || internal) &&
259 !aPassword.isEmpty() )
260 bff.append(":" + aPassword);
262 bff.append('@');
264 bff.append(m_aHost);
266 if( m_aPort != "21" )
267 bff.append(":" + m_aPort + "/");
268 else
269 bff.append('/');
271 for(size_t i = 0; i < m_aPathSegmentVec.size(); ++i)
272 if(i == 0)
273 bff.append(m_aPathSegmentVec[i]);
274 else
275 bff.append("/" + m_aPathSegmentVec[i]);
276 if(withslash)
277 if(!bff.isEmpty() && bff[bff.getLength()-1] != '/')
278 bff.append('/');
280 bff.append(m_aType);
281 return bff.makeStringAndClear();
285 OUString FTPURL::parent(bool internal) const
287 OUStringBuffer bff("ftp://");
289 if( m_aUsername != "anonymous" ) {
290 bff.append(m_aUsername);
292 OUString aPassword,aAccount;
293 m_pFCP->forHost(m_aHost,
294 m_aPort,
295 m_aUsername,
296 aPassword,
297 aAccount);
299 if((internal || m_bShowPassword) && !aPassword.isEmpty())
300 bff.append(":" + aPassword);
302 bff.append('@');
305 bff.append(m_aHost);
307 if( m_aPort != "21" )
308 bff.append(":" + m_aPort + "/");
309 else
310 bff.append('/');
312 OUString last;
314 for(size_t i = 0; i < m_aPathSegmentVec.size(); ++i)
315 if(1+i == m_aPathSegmentVec.size())
316 last = m_aPathSegmentVec[i];
317 else if(i == 0)
318 bff.append(m_aPathSegmentVec[i]);
319 else
320 bff.append("/" + m_aPathSegmentVec[i]);
322 if(last.isEmpty())
323 bff.append("..");
324 else if ( last == ".." )
325 bff.append(last + "/..");
327 bff.append(m_aType);
328 return bff.makeStringAndClear();
332 void FTPURL::child(const OUString& title)
334 m_aPathSegmentVec.push_back(encodePathSegment(title));
338 OUString FTPURL::child() const
340 return
341 !m_aPathSegmentVec.empty() ?
342 decodePathSegment(m_aPathSegmentVec.back()) : OUString();
346 /** Listing of a directory.
349 namespace ftp {
351 namespace {
353 enum OS {
354 FTP_DOS,FTP_UNIX,FTP_VMS,FTP_UNKNOWN
362 #define SET_CONTROL_CONTAINER \
363 MemoryContainer control; \
364 (void)curl_easy_setopt(curl, \
365 CURLOPT_HEADERFUNCTION, \
366 memory_write); \
367 (void)curl_easy_setopt(curl, \
368 CURLOPT_WRITEHEADER, \
369 &control)
372 static void setCurlUrl(CURL* curl, OUString const & url)
374 OString urlParAscii(url.getStr(),
375 url.getLength(),
376 RTL_TEXTENCODING_UTF8);
377 (void)curl_easy_setopt(curl,
378 CURLOPT_URL,
379 urlParAscii.getStr());
382 oslFileHandle FTPURL::open()
384 if(m_aPathSegmentVec.empty())
385 throw curl_exception(CURLE_FTP_COULDNT_RETR_FILE);
387 CURL *curl = m_pFCP->handle();
389 SET_CONTROL_CONTAINER;
390 OUString url(ident(false,true));
391 setCurlUrl(curl, url);
393 oslFileHandle res( nullptr );
394 if ( osl_createTempFile( nullptr, &res, nullptr ) == osl_File_E_None )
396 (void)curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,file_write);
397 (void)curl_easy_setopt(curl,CURLOPT_WRITEDATA,res);
399 (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,0);
400 CURLcode err = curl_easy_perform(curl);
402 if(err == CURLE_OK)
404 oslFileError rc = osl_setFilePos( res, osl_Pos_Absolut, 0 );
405 SAL_WARN_IF(rc != osl_File_E_None, "ucb.ucp.ftp",
406 "osl_setFilePos failed");
408 else {
409 osl_closeFile(res);
410 res = nullptr;
411 throw curl_exception(err);
415 return res;
419 std::vector<FTPDirentry> FTPURL::list(
420 sal_Int16 nMode
421 ) const
423 CURL *curl = m_pFCP->handle();
425 SET_CONTROL_CONTAINER;
426 (void)curl_easy_setopt(curl,CURLOPT_NOBODY,false);
427 MemoryContainer data;
428 (void)curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,memory_write);
429 (void)curl_easy_setopt(curl,CURLOPT_WRITEDATA,&data);
431 OUString url(ident(true,true));
432 setCurlUrl(curl, url);
433 (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,0);
435 CURLcode err = curl_easy_perform(curl);
436 if(err != CURLE_OK)
437 throw curl_exception(err);
439 // now evaluate the error messages
441 sal_uInt32 len = data.m_nWritePos;
442 char* fwd = static_cast<char*>(data.m_pBuffer);
443 char *p1, *p2;
444 p1 = p2 = fwd;
446 OS osKind(FTP_UNKNOWN);
447 std::vector<FTPDirentry> resvec;
448 FTPDirentry aDirEntry;
449 // ensure slash at the end
450 OUString viewurl(ident(true,false));
452 while(true) {
453 while(o3tl::make_unsigned(p2-fwd) < len && *p2 != '\n') ++p2;
454 if(o3tl::make_unsigned(p2-fwd) == len) break;
456 *p2 = 0;
457 switch(osKind) {
458 // While FTP knows the 'system'-command,
459 // which returns the operating system type,
460 // this is not usable here: There are Windows-server
461 // formatting the output like UNIX-ls command.
462 case FTP_DOS:
463 FTPDirectoryParser::parseDOS(aDirEntry,p1);
464 break;
465 case FTP_UNIX:
466 FTPDirectoryParser::parseUNIX(aDirEntry,p1);
467 break;
468 case FTP_VMS:
469 FTPDirectoryParser::parseVMS(aDirEntry,p1);
470 break;
471 default:
472 if(FTPDirectoryParser::parseUNIX(aDirEntry,p1))
473 osKind = FTP_UNIX;
474 else if(FTPDirectoryParser::parseDOS(aDirEntry,p1))
475 osKind = FTP_DOS;
476 else if(FTPDirectoryParser::parseVMS(aDirEntry,p1))
477 osKind = FTP_VMS;
479 aDirEntry.m_aName = aDirEntry.m_aName.trim();
480 if( osKind != int(FTP_UNKNOWN) && aDirEntry.m_aName != ".." && aDirEntry.m_aName != "." ) {
481 aDirEntry.m_aURL = viewurl + encodePathSegment(aDirEntry.m_aName);
483 bool isDir = (aDirEntry.m_nMode & INETCOREFTP_FILEMODE_ISDIR) == INETCOREFTP_FILEMODE_ISDIR;
484 switch(nMode) {
485 case OpenMode::DOCUMENTS:
486 if(!isDir)
487 resvec.push_back(aDirEntry);
488 break;
489 case OpenMode::FOLDERS:
490 if(isDir)
491 resvec.push_back(aDirEntry);
492 break;
493 default:
494 resvec.push_back(aDirEntry);
497 aDirEntry.clear();
498 p1 = p2 + 1;
501 return resvec;
505 OUString FTPURL::net_title() const
507 CURL *curl = m_pFCP->handle();
509 SET_CONTROL_CONTAINER;
510 (void)curl_easy_setopt(curl,CURLOPT_NOBODY,true); // no data => no transfer
511 struct curl_slist *slist = nullptr;
512 // post request
513 slist = curl_slist_append(slist,"PWD");
514 (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,slist);
516 bool try_more(true);
517 CURLcode err;
518 OUString aNetTitle;
520 while(true) {
521 OUString url(ident(false,true));
523 if(try_more && !url.endsWith("/"))
524 url += "/"; // add end-slash
525 else if(!try_more && url.endsWith("/"))
526 url = url.copy(0,url.getLength()-1); // remove end-slash
528 setCurlUrl(curl, url);
529 err = curl_easy_perform(curl);
531 if(err == CURLE_OK) { // get the title from the server
532 char* fwd = static_cast<char*>(control.m_pBuffer);
533 sal_uInt32 len = control.m_nWritePos;
535 aNetTitle = OUString(fwd,len,RTL_TEXTENCODING_UTF8);
536 // the buffer now contains the name of the file;
537 // analyze the output:
538 // Format of current working directory:
539 // 257 "/bla/bla" is current directory
540 sal_Int32 index1 = aNetTitle.lastIndexOf("257");
541 index1 = aNetTitle.indexOf('"', index1 + std::strlen("257")) + 1;
542 sal_Int32 index2 = aNetTitle.indexOf('"', index1);
543 aNetTitle = index2 > index1
544 ? aNetTitle.copy(index1, index2 - index1) : OUString();
545 if( aNetTitle != "/" ) {
546 index1 = aNetTitle.lastIndexOf('/');
547 aNetTitle = aNetTitle.copy(1+index1);
549 try_more = false;
550 } else if(err == CURLE_BAD_PASSWORD_ENTERED)
551 // the client should retry after getting the correct
552 // username + password
553 throw curl_exception(err);
554 #if LIBCURL_VERSION_NUM>=0x070d01 /* 7.13.1 */
555 else if(err == CURLE_LOGIN_DENIED)
556 // the client should retry after getting the correct
557 // username + password
558 throw curl_exception(err);
559 #endif
560 else if(try_more && err == CURLE_FTP_ACCESS_DENIED) {
561 // We were either denied access when trying to login to
562 // an FTP server or when trying to change working directory
563 // to the one given in the URL.
564 if(!m_aPathSegmentVec.empty())
565 // determine title from URL
566 aNetTitle = decodePathSegment(m_aPathSegmentVec.back());
567 else
568 // must be root
569 aNetTitle = "/";
570 try_more = false;
573 if(try_more)
574 try_more = false;
575 else
576 break;
579 curl_slist_free_all(slist);
580 return aNetTitle;
584 FTPDirentry FTPURL::direntry() const
586 OUString nettitle = net_title();
587 FTPDirentry aDirentry;
589 aDirentry.m_aName = nettitle; // init aDirentry
590 if( nettitle == "/" || nettitle == ".." )
591 aDirentry.m_nMode = INETCOREFTP_FILEMODE_ISDIR;
592 else
593 aDirentry.m_nMode = INETCOREFTP_FILEMODE_UNKNOWN;
595 aDirentry.m_nSize = 0;
597 if( nettitle != "/" ) {
598 // try to open the parent directory
599 FTPURL aURL(parent(),m_pFCP);
601 std::vector<FTPDirentry> aList = aURL.list(OpenMode::ALL);
603 for(const FTPDirentry & d : aList) {
604 if(d.m_aName == nettitle) { // the relevant file is found
605 aDirentry = d;
606 break;
610 return aDirentry;
614 extern "C" {
616 static size_t memory_read(void *ptr,size_t size,size_t nmemb,void *stream)
618 sal_Int32 nRequested = sal_Int32(size*nmemb);
619 CurlInput *curlInput = static_cast<CurlInput*>(stream);
620 if(curlInput)
621 return size_t(curlInput->read(static_cast<sal_Int8*>(ptr),nRequested));
622 else
623 return 0;
629 void FTPURL::insert(bool replaceExisting,void* stream) const
631 if(!replaceExisting) {
632 // FTPDirentry aDirentry(direntry());
633 // if(aDirentry.m_nMode == INETCOREFTP_FILEMODE_UNKNOWN)
634 // throw curl_exception(FILE_EXIST_DURING_INSERT);
635 throw curl_exception(FILE_MIGHT_EXIST_DURING_INSERT);
636 } // else
637 // overwrite is default in libcurl
639 CURL *curl = m_pFCP->handle();
641 SET_CONTROL_CONTAINER;
642 (void)curl_easy_setopt(curl,CURLOPT_NOBODY,false); // no data => no transfer
643 (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,0);
644 (void)curl_easy_setopt(curl,CURLOPT_QUOTE,0);
645 (void)curl_easy_setopt(curl,CURLOPT_READFUNCTION,memory_read);
646 (void)curl_easy_setopt(curl,CURLOPT_READDATA,stream);
647 (void)curl_easy_setopt(curl, CURLOPT_UPLOAD,1);
649 OUString url(ident(false,true));
650 setCurlUrl(curl, url);
652 CURLcode err = curl_easy_perform(curl);
653 (void)curl_easy_setopt(curl, CURLOPT_UPLOAD,false);
655 if(err != CURLE_OK)
656 throw curl_exception(err);
660 void FTPURL::mkdir(bool ReplaceExisting) const
662 OString title;
663 if(!m_aPathSegmentVec.empty()) {
664 OUString titleOU = m_aPathSegmentVec.back();
665 titleOU = decodePathSegment(titleOU);
666 title = OString(titleOU.getStr(),
667 titleOU.getLength(),
668 RTL_TEXTENCODING_UTF8);
670 else
671 // will give an error
672 title = OString("/");
674 OString aDel = "del " + title;
675 OString mkd = "mkd " + title;
677 struct curl_slist *slist = nullptr;
679 FTPDirentry aDirentry(direntry());
680 if(!ReplaceExisting) {
681 // if(aDirentry.m_nMode != INETCOREFTP_FILEMODE_UNKNOWN)
682 // throw curl_exception(FOLDER_EXIST_DURING_INSERT);
683 throw curl_exception(FOLDER_MIGHT_EXIST_DURING_INSERT);
684 } else if(aDirentry.m_nMode != INETCOREFTP_FILEMODE_UNKNOWN)
685 slist = curl_slist_append(slist,aDel.getStr());
687 slist = curl_slist_append(slist,mkd.getStr());
689 CURL *curl = m_pFCP->handle();
690 SET_CONTROL_CONTAINER;
691 (void)curl_easy_setopt(curl,CURLOPT_NOBODY,true); // no data => no transfer
692 (void)curl_easy_setopt(curl,CURLOPT_QUOTE,0);
694 // post request
695 (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,slist);
697 OUString url(parent(true));
698 if(!url.endsWith("/"))
699 url += "/";
700 setCurlUrl(curl, url);
702 CURLcode err = curl_easy_perform(curl);
703 curl_slist_free_all(slist);
704 if(err != CURLE_OK)
705 throw curl_exception(err);
709 OUString FTPURL::ren(const OUString& NewTitle)
711 CURL *curl = m_pFCP->handle();
713 // post request
714 OUString OldTitle = net_title();
715 OString renamefrom = "RNFR " +
716 OUStringToOString(OldTitle,
717 RTL_TEXTENCODING_UTF8);
719 OString renameto = "RNTO " +
720 OUStringToOString(NewTitle,
721 RTL_TEXTENCODING_UTF8);
723 struct curl_slist *slist = nullptr;
724 slist = curl_slist_append(slist,renamefrom.getStr());
725 slist = curl_slist_append(slist,renameto.getStr());
726 (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,slist);
728 SET_CONTROL_CONTAINER;
729 (void)curl_easy_setopt(curl,CURLOPT_NOBODY,true); // no data => no transfer
730 (void)curl_easy_setopt(curl,CURLOPT_QUOTE,0);
732 OUString url(parent(true));
733 if(!url.endsWith("/"))
734 url += "/";
735 setCurlUrl(curl, url);
737 CURLcode err = curl_easy_perform(curl);
738 curl_slist_free_all(slist);
739 if(err != CURLE_OK)
740 throw curl_exception(err);
741 else if( !m_aPathSegmentVec.empty() && m_aPathSegmentVec.back() != ".." )
742 m_aPathSegmentVec.back() = encodePathSegment(NewTitle);
743 return OldTitle;
747 void FTPURL::del() const
749 FTPDirentry aDirentry(direntry());
751 OString dele(aDirentry.m_aName.getStr(),
752 aDirentry.m_aName.getLength(),
753 RTL_TEXTENCODING_UTF8);
755 if(aDirentry.m_nMode & INETCOREFTP_FILEMODE_ISDIR) {
756 std::vector<FTPDirentry> vec = list(sal_Int16(OpenMode::ALL));
757 for(const FTPDirentry & i : vec)
759 try {
760 FTPURL url(i.m_aURL,m_pFCP);
761 url.del();
762 } catch(const curl_exception&) {
765 dele = "RMD " + dele;
767 else if(aDirentry.m_nMode != INETCOREFTP_FILEMODE_UNKNOWN)
768 dele = "DELE " + dele;
769 else
770 return;
772 // post request
773 CURL *curl = m_pFCP->handle();
774 struct curl_slist *slist = nullptr;
775 slist = curl_slist_append(slist,dele.getStr());
776 (void)curl_easy_setopt(curl,CURLOPT_POSTQUOTE,slist);
778 SET_CONTROL_CONTAINER;
779 (void)curl_easy_setopt(curl,CURLOPT_NOBODY,true); // no data => no transfer
780 (void)curl_easy_setopt(curl,CURLOPT_QUOTE,0);
782 OUString url(parent(true));
783 if(!url.endsWith("/"))
784 url += "/";
785 setCurlUrl(curl, url);
787 CURLcode err = curl_easy_perform(curl);
788 curl_slist_free_all(slist);
789 if(err != CURLE_OK)
790 throw curl_exception(err);
793 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */