]> git.saurik.com Git - wxWidgets.git/blob - src/common/ftp.cpp
Beginings of wxX11 support. Much more to do but I have no more time
[wxWidgets.git] / src / common / ftp.cpp
1 /////////////////////////////////////////////////////////////////////////////
2 // Name: ftp.cpp
3 // Purpose: FTP protocol
4 // Author: Guilhem Lavaux
5 // Modified by: Mark Johnson, wxWindows@mj10777.de
6 // 20000917 : RmDir, GetLastResult, GetList
7 // Vadim Zeitlin (numerous fixes and rewrites to all part of the
8 // code, support ASCII/Binary modes, better error reporting, more
9 // robust Abort(), support for arbitrary FTP commands, ...)
10 // Created: 07/07/1997
11 // RCS-ID: $Id$
12 // Copyright: (c) 1997, 1998 Guilhem Lavaux
13 // Licence: wxWindows licence
14 /////////////////////////////////////////////////////////////////////////////
15
16 // ============================================================================
17 // declarations
18 // ============================================================================
19
20 #if defined(__GNUG__) && !defined(NO_GCC_PRAGMA)
21 #pragma implementation "ftp.h"
22 #endif
23
24 // ----------------------------------------------------------------------------
25 // headers
26 // ----------------------------------------------------------------------------
27
28 // For compilers that support precompilation, includes "wx.h".
29 #include "wx/wxprec.h"
30
31 #ifdef __BORLANDC__
32 #pragma hdrstop
33 #endif
34
35 #if wxUSE_PROTOCOL_FTP
36
37 #ifndef WX_PRECOMP
38 #include <stdlib.h>
39 #include "wx/string.h"
40 #include "wx/utils.h"
41 #include "wx/log.h"
42 #include "wx/intl.h"
43 #endif // WX_PRECOMP
44
45 #include "wx/sckaddr.h"
46 #include "wx/socket.h"
47 #include "wx/url.h"
48 #include "wx/sckstrm.h"
49 #include "wx/protocol/protocol.h"
50 #include "wx/protocol/ftp.h"
51
52 #if defined(__WXMAC__)
53 #include "wx/mac/macsock.h"
54 #endif
55
56 #ifndef __MWERKS__
57 #include <memory.h>
58 #endif
59
60 // ----------------------------------------------------------------------------
61 // constants
62 // ----------------------------------------------------------------------------
63
64 // the length of FTP status code (3 digits)
65 static const size_t LEN_CODE = 3;
66
67 // ----------------------------------------------------------------------------
68 // macros
69 // ----------------------------------------------------------------------------
70
71 IMPLEMENT_DYNAMIC_CLASS(wxFTP, wxProtocol)
72 IMPLEMENT_PROTOCOL(wxFTP, wxT("ftp"), wxT("ftp"), TRUE)
73
74 // ============================================================================
75 // implementation
76 // ============================================================================
77
78 // ----------------------------------------------------------------------------
79 // wxFTP constructor and destructor
80 // ----------------------------------------------------------------------------
81
82 wxFTP::wxFTP()
83 {
84 m_lastError = wxPROTO_NOERR;
85 m_streaming = FALSE;
86 m_currentTransfermode = NONE;
87
88 m_user = wxT("anonymous");
89 m_passwd << wxGetUserId() << wxT('@') << wxGetFullHostName();
90
91 SetNotify(0);
92 SetFlags(wxSOCKET_NONE);
93 }
94
95 wxFTP::~wxFTP()
96 {
97 if ( m_streaming )
98 {
99 (void)Abort();
100 }
101
102 Close();
103 }
104
105 // ----------------------------------------------------------------------------
106 // wxFTP connect and login methods
107 // ----------------------------------------------------------------------------
108
109 bool wxFTP::Connect(wxSockAddress& addr, bool WXUNUSED(wait))
110 {
111 if ( !wxProtocol::Connect(addr) )
112 {
113 m_lastError = wxPROTO_NETERR;
114 return FALSE;
115 }
116
117 if ( !m_user )
118 {
119 m_lastError = wxPROTO_CONNERR;
120 return FALSE;
121 }
122
123 // we should have 220 welcome message
124 if ( !CheckResult('2') )
125 {
126 Close();
127 return FALSE;
128 }
129
130 wxString command;
131 command.Printf(wxT("USER %s"), m_user.c_str());
132 char rc = SendCommand(command);
133 if ( rc == '2' )
134 {
135 // 230 return: user accepted without password
136 return TRUE;
137 }
138
139 if ( rc != '3' )
140 {
141 Close();
142 return FALSE;
143 }
144
145 command.Printf(wxT("PASS %s"), m_passwd.c_str());
146 if ( !CheckCommand(command, '2') )
147 {
148 Close();
149 return FALSE;
150 }
151
152 return TRUE;
153 }
154
155 bool wxFTP::Connect(const wxString& host)
156 {
157 wxIPV4address addr;
158 addr.Hostname(host);
159 addr.Service(wxT("ftp"));
160
161 return Connect(addr);
162 }
163
164 bool wxFTP::Close()
165 {
166 if ( m_streaming )
167 {
168 m_lastError = wxPROTO_STREAMING;
169 return FALSE;
170 }
171
172 if ( IsConnected() )
173 {
174 if ( !CheckCommand(wxT("QUIT"), '2') )
175 {
176 wxLogDebug(_T("Failed to close connection gracefully."));
177 }
178 }
179
180 return wxSocketClient::Close();
181 }
182
183 // ============================================================================
184 // low level methods
185 // ============================================================================
186
187 // ----------------------------------------------------------------------------
188 // Send command to FTP server
189 // ----------------------------------------------------------------------------
190
191 char wxFTP::SendCommand(const wxString& command)
192 {
193 if ( m_streaming )
194 {
195 m_lastError = wxPROTO_STREAMING;
196 return 0;
197 }
198
199 wxString tmp_str = command + wxT("\r\n");
200 const wxWX2MBbuf tmp_buf = tmp_str.mb_str();
201 if ( Write(wxMBSTRINGCAST tmp_buf, strlen(tmp_buf)).Error())
202 {
203 m_lastError = wxPROTO_NETERR;
204 return 0;
205 }
206
207 #ifdef __WXDEBUG__
208 // don't show the passwords in the logs (even in debug ones)
209 wxString cmd, password;
210 if ( command.Upper().StartsWith(_T("PASS "), &password) )
211 {
212 cmd << _T("PASS ") << wxString(_T('*'), password.length());
213 }
214 else
215 {
216 cmd = command;
217 }
218
219 wxLogTrace(FTP_TRACE_MASK, _T("==> %s"), cmd.c_str());
220 #endif // __WXDEBUG__
221
222 return GetResult();
223 }
224
225 // ----------------------------------------------------------------------------
226 // Recieve servers reply
227 // ----------------------------------------------------------------------------
228
229 char wxFTP::GetResult()
230 {
231 wxString code;
232
233 // m_lastResult will contain the entire server response, possibly on
234 // multiple lines
235 m_lastResult.clear();
236
237 // we handle multiline replies here according to RFC 959: it says that a
238 // reply may either be on 1 line of the form "xyz ..." or on several lines
239 // in whuch case it looks like
240 // xyz-...
241 // ...
242 // xyz ...
243 // and the intermeidate lines may start with xyz or not
244 bool badReply = FALSE;
245 bool firstLine = TRUE;
246 bool endOfReply = FALSE;
247 while ( !endOfReply && !badReply )
248 {
249 wxString line;
250 m_lastError = ReadLine(line);
251 if ( m_lastError )
252 return 0;
253
254 if ( !m_lastResult.empty() )
255 {
256 // separate from last line
257 m_lastResult += _T('\n');
258 }
259
260 m_lastResult += line;
261
262 // unless this is an intermediate line of a multiline reply, it must
263 // contain the code in the beginning and '-' or ' ' following it
264 if ( line.Len() < LEN_CODE + 1 )
265 {
266 if ( firstLine )
267 {
268 badReply = TRUE;
269 }
270 else
271 {
272 wxLogTrace(FTP_TRACE_MASK, _T("<== %s %s"),
273 code.c_str(), line.c_str());
274 }
275 }
276 else // line has at least 4 chars
277 {
278 // this is the char which tells us what we're dealing with
279 wxChar chMarker = line.GetChar(LEN_CODE);
280
281 if ( firstLine )
282 {
283 code = wxString(line, LEN_CODE);
284 wxLogTrace(FTP_TRACE_MASK, _T("<== %s %s"),
285 code.c_str(), line.c_str() + LEN_CODE + 1);
286
287 switch ( chMarker )
288 {
289 case _T(' '):
290 endOfReply = TRUE;
291 break;
292
293 case _T('-'):
294 firstLine = FALSE;
295 break;
296
297 default:
298 // unexpected
299 badReply = TRUE;
300 }
301 }
302 else // subsequent line of multiline reply
303 {
304 if ( wxStrncmp(line, code, LEN_CODE) == 0 )
305 {
306 if ( chMarker == _T(' ') )
307 {
308 endOfReply = TRUE;
309 }
310
311 wxLogTrace(FTP_TRACE_MASK, _T("<== %s %s"),
312 code.c_str(), line.c_str() + LEN_CODE + 1);
313 }
314 else
315 {
316 // just part of reply
317 wxLogTrace(FTP_TRACE_MASK, _T("<== %s %s"),
318 code.c_str(), line.c_str());
319 }
320 }
321 }
322 }
323
324 if ( badReply )
325 {
326 wxLogDebug(_T("Broken FTP server: '%s' is not a valid reply."),
327 m_lastResult.c_str());
328
329 m_lastError = wxPROTO_PROTERR;
330
331 return 0;
332 }
333
334 // if we got here we must have a non empty code string
335 return code[0u];
336 }
337
338 // ----------------------------------------------------------------------------
339 // wxFTP simple commands
340 // ----------------------------------------------------------------------------
341
342 bool wxFTP::SetTransferMode(TransferMode transferMode)
343 {
344 if ( transferMode == m_currentTransfermode )
345 {
346 // nothing to do
347 return TRUE;
348 }
349
350 wxString mode;
351 switch ( transferMode )
352 {
353 default:
354 wxFAIL_MSG(_T("unknown FTP transfer mode"));
355 // fall through
356
357 case BINARY:
358 mode = _T('I');
359 break;
360
361 case ASCII:
362 mode = _T('A');
363 break;
364 }
365
366 if ( !DoSimpleCommand(_T("TYPE"), mode) )
367 {
368 wxLogError(_("Failed to set FTP transfer mode to %s."), (const wxChar*)
369 (transferMode == ASCII ? _("ASCII") : _("binary")));
370
371 return FALSE;
372 }
373
374 // If we get here the operation has been succesfully completed
375 // Set the status-member
376 m_currentTransfermode = transferMode;
377
378 return TRUE;
379 }
380
381 bool wxFTP::DoSimpleCommand(const wxChar *command, const wxString& arg)
382 {
383 wxString fullcmd = command;
384 if ( !arg.empty() )
385 {
386 fullcmd << _T(' ') << arg;
387 }
388
389 if ( !CheckCommand(fullcmd, '2') )
390 {
391 wxLogDebug(_T("FTP command '%s' failed."), fullcmd.c_str());
392
393 return FALSE;
394 }
395
396 return TRUE;
397 }
398
399 bool wxFTP::ChDir(const wxString& dir)
400 {
401 // some servers might not understand ".." if they use different directory
402 // tree conventions, but they always understand CDUP - should we use it if
403 // dir == ".."? OTOH, do such servers (still) exist?
404
405 return DoSimpleCommand(_T("CWD"), dir);
406 }
407
408 bool wxFTP::MkDir(const wxString& dir)
409 {
410 return DoSimpleCommand(_T("MKD"), dir);
411 }
412
413 bool wxFTP::RmDir(const wxString& dir)
414 {
415 return DoSimpleCommand(_T("RMD"), dir);
416 }
417
418 wxString wxFTP::Pwd()
419 {
420 wxString path;
421
422 if ( CheckCommand(wxT("PWD"), '2') )
423 {
424 // the result is at least that long if CheckCommand() succeeded
425 const wxChar *p = m_lastResult.c_str() + LEN_CODE + 1;
426 if ( *p != _T('"') )
427 {
428 wxLogDebug(_T("Missing starting quote in reply for PWD: %s"), p);
429 }
430 else
431 {
432 for ( p++; *p; p++ )
433 {
434 if ( *p == _T('"') )
435 {
436 // check if the quote is doubled
437 p++;
438 if ( !*p || *p != _T('"') )
439 {
440 // no, this is the end
441 break;
442 }
443 //else: yes, it is: this is an embedded quote in the
444 // filename, treat as normal char
445 }
446
447 path += *p;
448 }
449
450 if ( !*p )
451 {
452 wxLogDebug(_T("Missing ending quote in reply for PWD: %s"),
453 m_lastResult.c_str() + LEN_CODE + 1);
454 }
455 }
456 }
457 else
458 {
459 wxLogDebug(_T("FTP PWD command failed."));
460 }
461
462 return path;
463 }
464
465 bool wxFTP::Rename(const wxString& src, const wxString& dst)
466 {
467 wxString str;
468
469 str = wxT("RNFR ") + src;
470 if ( !CheckCommand(str, '3') )
471 return FALSE;
472
473 str = wxT("RNTO ") + dst;
474
475 return CheckCommand(str, '2');
476 }
477
478 bool wxFTP::RmFile(const wxString& path)
479 {
480 wxString str;
481 str = wxT("DELE ") + path;
482
483 return CheckCommand(str, '2');
484 }
485
486 // ----------------------------------------------------------------------------
487 // wxFTP download and upload
488 // ----------------------------------------------------------------------------
489
490 class wxInputFTPStream : public wxSocketInputStream
491 {
492 public:
493 wxInputFTPStream(wxFTP *ftp, wxSocketBase *sock)
494 : wxSocketInputStream(*sock)
495 {
496 m_ftp = ftp;
497
498 // FIXME make the timeout configurable
499
500 // set a shorter than default timeout
501 m_i_socket->SetTimeout(60); // 1 minute
502 }
503
504 size_t GetSize() const { return m_ftpsize; }
505
506 virtual ~wxInputFTPStream()
507 {
508 delete m_i_socket;
509
510 if ( IsOk() )
511 {
512 // wait for "226 transfer completed"
513 m_ftp->CheckResult('2');
514
515 m_ftp->m_streaming = FALSE;
516 }
517 else
518 {
519 m_ftp->Abort();
520 }
521
522 // delete m_i_socket; // moved to top of destructor to accomodate wu-FTPd >= 2.6.0
523 }
524
525 wxFTP *m_ftp;
526 size_t m_ftpsize;
527
528 DECLARE_NO_COPY_CLASS(wxInputFTPStream)
529 };
530
531 class wxOutputFTPStream : public wxSocketOutputStream
532 {
533 public:
534 wxOutputFTPStream(wxFTP *ftp_clt, wxSocketBase *sock)
535 : wxSocketOutputStream(*sock), m_ftp(ftp_clt)
536 {
537 }
538
539 virtual ~wxOutputFTPStream(void)
540 {
541 if ( IsOk() )
542 {
543 // close data connection first, this will generate "transfer
544 // completed" reply
545 delete m_o_socket;
546
547 // read this reply
548 m_ftp->CheckResult('2');
549
550 m_ftp->m_streaming = FALSE;
551 }
552 else
553 {
554 // abort data connection first
555 m_ftp->Abort();
556
557 // and close it after
558 delete m_o_socket;
559 }
560 }
561
562 wxFTP *m_ftp;
563
564 DECLARE_NO_COPY_CLASS(wxOutputFTPStream)
565 };
566
567 wxSocketClient *wxFTP::GetPort()
568 {
569 int a[6];
570
571 if ( !DoSimpleCommand(_T("PASV")) )
572 {
573 wxLogError(_("The FTP server doesn't support passive mode."));
574
575 return NULL;
576 }
577
578 const wxChar *addrStart = wxStrchr(m_lastResult, _T('('));
579 if ( !addrStart )
580 {
581 m_lastError = wxPROTO_PROTERR;
582
583 return NULL;
584 }
585
586 const wxChar *addrEnd = wxStrchr(addrStart, _T(')'));
587 if ( !addrEnd )
588 {
589 m_lastError = wxPROTO_PROTERR;
590
591 return NULL;
592 }
593
594 wxString straddr(addrStart + 1, addrEnd);
595
596 wxSscanf(straddr, wxT("%d,%d,%d,%d,%d,%d"),
597 &a[2],&a[3],&a[4],&a[5],&a[0],&a[1]);
598
599 wxUint32 hostaddr = (wxUint16)a[5] << 24 |
600 (wxUint16)a[4] << 16 |
601 (wxUint16)a[3] << 8 |
602 a[2];
603 wxUint16 port = (wxUint16)a[0] << 8 | a[1];
604
605 wxIPV4address addr;
606 addr.Hostname(hostaddr);
607 addr.Service(port);
608
609 wxSocketClient *client = new wxSocketClient();
610 if ( !client->Connect(addr) )
611 {
612 delete client;
613 return NULL;
614 }
615
616 client->Notify(FALSE);
617
618 return client;
619 }
620
621 bool wxFTP::Abort()
622 {
623 if ( !m_streaming )
624 return TRUE;
625
626 m_streaming = FALSE;
627 if ( !CheckCommand(wxT("ABOR"), '4') )
628 return FALSE;
629
630 return CheckResult('2');
631 }
632
633 wxInputStream *wxFTP::GetInputStream(const wxString& path)
634 {
635 int pos_size;
636 wxInputFTPStream *in_stream;
637
638 if ( ( m_currentTransfermode == NONE ) && !SetTransferMode(BINARY) )
639 return NULL;
640
641 wxSocketClient *sock = GetPort();
642
643 if ( !sock )
644 {
645 m_lastError = wxPROTO_NETERR;
646 return NULL;
647 }
648
649 wxString tmp_str = wxT("RETR ") + wxURL::ConvertFromURI(path);
650 if ( !CheckCommand(tmp_str, '1') )
651 return NULL;
652
653 m_streaming = TRUE;
654
655 in_stream = new wxInputFTPStream(this, sock);
656
657 pos_size = m_lastResult.Index(wxT('('));
658 if ( pos_size != wxNOT_FOUND )
659 {
660 wxString str_size = m_lastResult(pos_size+1, m_lastResult.Index(wxT(')'))-1);
661
662 in_stream->m_ftpsize = wxAtoi(WXSTRINGCAST str_size);
663 }
664
665 sock->SetFlags(wxSOCKET_WAITALL);
666
667 return in_stream;
668 }
669
670 wxOutputStream *wxFTP::GetOutputStream(const wxString& path)
671 {
672 if ( ( m_currentTransfermode == NONE ) && !SetTransferMode(BINARY) )
673 return NULL;
674
675 wxSocketClient *sock = GetPort();
676
677 wxString tmp_str = wxT("STOR ") + path;
678 if ( !CheckCommand(tmp_str, '1') )
679 return NULL;
680
681 m_streaming = TRUE;
682
683 return new wxOutputFTPStream(this, sock);
684 }
685
686 // ----------------------------------------------------------------------------
687 // FTP directory listing
688 // ----------------------------------------------------------------------------
689
690 bool wxFTP::GetList(wxArrayString& files,
691 const wxString& wildcard,
692 bool details)
693 {
694 wxSocketBase *sock = GetPort();
695 if (!sock)
696 return FALSE;
697
698 // NLST : List of Filenames (including Directory's !)
699 // LIST : depending on BS of FTP-Server
700 // - Unix : result like "ls" command
701 // - Windows : like "dir" command
702 // - others : ?
703 wxString line(details ? _T("LIST") : _T("NLST"));
704 if ( !!wildcard )
705 {
706 line << _T(' ') << wildcard;
707 }
708
709 if (!CheckCommand(line, '1'))
710 {
711 return FALSE;
712 }
713 files.Empty();
714 while ( ReadLine(sock, line) == wxPROTO_NOERR )
715 {
716 files.Add(line);
717 }
718 delete sock;
719
720 // the file list should be terminated by "226 Transfer complete""
721 if ( !CheckResult('2') )
722 return FALSE;
723
724 return TRUE;
725 }
726
727 bool wxFTP::FileExists(const wxString& fileName)
728 {
729 // This function checks if the file specified in fileName exists in the
730 // current dir. It does so by simply doing an NLST (via GetList).
731 // If this succeeds (and the list is not empty) the file exists.
732
733 bool retval = FALSE;
734 wxArrayString fileList;
735
736 if ( GetList(fileList, fileName, FALSE) )
737 {
738 // Some ftp-servers (Ipswitch WS_FTP Server 1.0.5 does this)
739 // displays this behaviour when queried on a non-existing file:
740 // NLST this_file_does_not_exist
741 // 150 Opening ASCII data connection for directory listing
742 // (no data transferred)
743 // 226 Transfer complete
744 // Here wxFTP::GetList(...) will succeed but it will return an empty
745 // list.
746 retval = !fileList.IsEmpty();
747 }
748
749 return retval;
750 }
751
752 // ----------------------------------------------------------------------------
753 // FTP GetSize
754 // ----------------------------------------------------------------------------
755
756 int wxFTP::GetFileSize(const wxString& fileName)
757 {
758 // return the filesize of the given file if possible
759 // return -1 otherwise (predominantly if file doesn't exist
760 // in current dir)
761
762 int filesize = -1;
763
764 // Check for existance of file via wxFTP::FileExists(...)
765 if ( FileExists(fileName) )
766 {
767 wxString command;
768
769 // First try "SIZE" command using BINARY(IMAGE) transfermode
770 // Especially UNIX ftp-servers distinguish between the different
771 // transfermodes and reports different filesizes accordingly.
772 // The BINARY size is the interesting one: How much memory
773 // will we need to hold this file?
774 TransferMode oldTransfermode = m_currentTransfermode;
775 SetTransferMode(BINARY);
776 command << _T("SIZE ") << fileName;
777
778 bool ok = CheckCommand(command, '2');
779
780 if ( ok )
781 {
782 // The answer should be one line: "213 <filesize>\n"
783 // 213 is File Status (STD9)
784 // "SIZE" is not described anywhere..? It works on most servers
785 int statuscode;
786 if ( wxSscanf(GetLastResult().c_str(), _T("%i %i"),
787 &statuscode, &filesize) == 2 )
788 {
789 // We've gotten a good reply.
790 ok = TRUE;
791 }
792 else
793 {
794 // Something bad happened.. A "2yz" reply with no size
795 // Fallback
796 ok = FALSE;
797 }
798 }
799
800 // Set transfermode back to the original. Only the "SIZE"-command
801 // is dependant on transfermode
802 if ( oldTransfermode != NONE )
803 {
804 SetTransferMode(oldTransfermode);
805 }
806
807 // this is not a direct else clause.. The size command might return an
808 // invalid "2yz" reply
809 if ( !ok )
810 {
811 // The server didn't understand the "SIZE"-command or it
812 // returned an invalid reply.
813 // We now try to get details for the file with a "LIST"-command
814 // and then parse the output from there..
815 wxArrayString fileList;
816 if ( GetList(fileList, fileName, TRUE) )
817 {
818 if ( !fileList.IsEmpty() )
819 {
820 // We _should_ only get one line in return, but just to be
821 // safe we run through the line(s) returned and look for a
822 // substring containing the name we are looking for. We
823 // stop the iteration at the first occurrence of the
824 // filename. The search is not case-sensitive.
825 bool foundIt = FALSE;
826
827 size_t i;
828 for ( i = 0; !foundIt && i < fileList.Count(); i++ )
829 {
830 foundIt = fileList[i].Upper().Contains(fileName.Upper());
831 }
832
833 if ( foundIt )
834 {
835 // The index i points to the first occurrence of
836 // fileName in the array Now we have to find out what
837 // format the LIST has returned. There are two
838 // "schools": Unix-like
839 //
840 // '-rw-rw-rw- owner group size month day time filename'
841 //
842 // or Windows-like
843 //
844 // 'date size filename'
845
846 // check if the first character is '-'. This would
847 // indicate Unix-style (this also limits this function
848 // to searching for files, not directories)
849 if ( fileList[i].Mid(0, 1) == _T("-") )
850 {
851
852 if ( wxSscanf(fileList[i].c_str(),
853 _T("%*s %*s %*s %*s %i %*s %*s %*s %*s"),
854 &filesize) != 9 )
855 {
856 // Hmm... Invalid response
857 wxLogTrace(FTP_TRACE_MASK,
858 _T("Invalid LIST response"));
859 }
860 }
861 else // Windows-style response (?)
862 {
863 if ( wxSscanf(fileList[i].c_str(),
864 _T("%*s %*s %i %*s"),
865 &filesize) != 4 )
866 {
867 // something bad happened..?
868 wxLogTrace(FTP_TRACE_MASK,
869 _T("Invalid or unknown LIST response"));
870 }
871 }
872 }
873 }
874 }
875 }
876 }
877
878 // filesize might still be -1 when exiting
879 return filesize;
880 }
881
882 #endif // wxUSE_PROTOCOL_FTP
883