1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
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 .
21 #include <com/sun/star/task/XInteractionAbort.hpp>
22 #include <com/sun/star/ucb/XWebDAVCommandEnvironment.hpp>
24 #include <ucbhelper/simpleauthenticationrequest.hxx>
25 #include <comphelper/seekableinput.hxx>
27 #include "DAVAuthListenerImpl.hxx"
28 #include "DAVResourceAccess.hxx"
30 #include <com/sun/star/lang/IllegalArgumentException.hpp>
31 #include <com/sun/star/io/IOException.hpp>
34 using namespace http_dav_ucp
;
35 using namespace com::sun::star
;
38 // DAVAuthListener_Impl Implementation.
42 int DAVAuthListener_Impl::authenticate(
43 const OUString
& inRealm
,
44 const OUString
& inHostName
,
45 OUString
& inoutUserName
,
46 OUString
& outPassWord
,
47 bool bCanUseSystemCredentials
,
48 bool bUsePreviousCredentials
)
52 uno::Reference
< task::XInteractionHandler
> xIH
53 = m_xEnv
->getInteractionHandler();
57 // Providing previously retrieved credentials will cause the password
58 // container to reject these. Thus, the credential input dialog will be shown again.
59 // #102871# - Supply username and password from previous try.
60 // Password container service depends on this!
61 if ( inoutUserName
.isEmpty() && bUsePreviousCredentials
)
62 inoutUserName
= m_aPrevUsername
;
64 if ( outPassWord
.isEmpty() && bUsePreviousCredentials
)
65 outPassWord
= m_aPrevPassword
;
67 rtl::Reference
< ucbhelper::SimpleAuthenticationRequest
> xRequest
68 = new ucbhelper::SimpleAuthenticationRequest(
69 m_aURL
, inHostName
, inRealm
, inoutUserName
,
71 bCanUseSystemCredentials
);
72 xIH
->handle( xRequest
);
74 rtl::Reference
< ucbhelper::InteractionContinuation
> xSelection
75 = xRequest
->getSelection();
77 if ( xSelection
.is() )
79 // Handler handled the request.
80 uno::Reference
< task::XInteractionAbort
> xAbort(
81 xSelection
.get(), uno::UNO_QUERY
);
85 ucbhelper::InteractionSupplyAuthentication
> & xSupp
86 = xRequest
->getAuthenticationSupplier();
88 bool bUseSystemCredentials
= false;
90 if ( bCanUseSystemCredentials
)
92 = xSupp
->getUseSystemCredentials();
94 if ( bUseSystemCredentials
)
96 // This is the (strange) way to tell neon to use
97 // system credentials.
98 inoutUserName
.clear();
103 inoutUserName
= xSupp
->getUserName();
104 outPassWord
= xSupp
->getPassword();
107 // #102871# - Remember username and password.
108 m_aPrevUsername
= inoutUserName
;
109 m_aPrevPassword
= outPassWord
;
122 // DAVResourceAccess Implementation.
124 constexpr size_t g_nRedirectLimit
= 5;
126 DAVResourceAccess::DAVResourceAccess(
127 uno::Reference
< uno::XComponentContext
> xContext
,
128 rtl::Reference
< DAVSessionFactory
> xSessionFactory
,
130 : m_aURL(std::move( aURL
)),
131 m_xSessionFactory(std::move( xSessionFactory
)),
132 m_xContext(std::move( xContext
))
137 DAVResourceAccess::DAVResourceAccess( const DAVResourceAccess
& rOther
)
138 : m_aURL( rOther
.m_aURL
),
139 m_aPath( rOther
.m_aPath
),
140 m_aFlags( rOther
.m_aFlags
),
141 m_xSession( rOther
.m_xSession
),
142 m_xSessionFactory( rOther
.m_xSessionFactory
),
143 m_xContext( rOther
.m_xContext
),
144 m_aRedirectURIs( rOther
.m_aRedirectURIs
)
149 DAVResourceAccess
& DAVResourceAccess::operator=(
150 const DAVResourceAccess
& rOther
)
152 m_aURL
= rOther
.m_aURL
;
153 m_aPath
= rOther
.m_aPath
;
154 m_aFlags
= rOther
.m_aFlags
;
155 m_xSession
= rOther
.m_xSession
;
156 m_xSessionFactory
= rOther
.m_xSessionFactory
;
157 m_xContext
= rOther
.m_xContext
;
158 m_aRedirectURIs
= rOther
.m_aRedirectURIs
;
163 void DAVResourceAccess::OPTIONS(
164 DAVOptions
& rOptions
,
165 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
176 DAVRequestHeaders aHeaders
;
178 getUserRequestHeaders( xEnv
,
180 ucb::WebDAVHTTPMethod_OPTIONS
,
183 m_xSession
->OPTIONS( getRequestURI(),
185 DAVRequestEnvironment(
186 new DAVAuthListener_Impl( xEnv
, m_aURL
),
189 catch (DAVException
const& e
)
192 bRetry
= handleException( e
, errorCount
);
200 void DAVResourceAccess::PROPFIND(
202 const std::vector
< OUString
> & rPropertyNames
,
203 std::vector
< DAVResource
> & rResources
,
204 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
215 DAVRequestHeaders aHeaders
;
217 getUserRequestHeaders( xEnv
,
219 ucb::WebDAVHTTPMethod_PROPFIND
,
222 m_xSession
->PROPFIND( getRequestURI(),
226 DAVRequestEnvironment(
227 new DAVAuthListener_Impl( xEnv
, m_aURL
),
230 catch (DAVException
const& e
)
233 bRetry
= handleException( e
, errorCount
);
242 void DAVResourceAccess::PROPFIND(
244 std::vector
< DAVResourceInfo
> & rResInfo
,
245 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
256 DAVRequestHeaders aHeaders
;
257 getUserRequestHeaders( xEnv
,
259 ucb::WebDAVHTTPMethod_PROPFIND
,
262 m_xSession
->PROPFIND( getRequestURI(),
265 DAVRequestEnvironment(
266 new DAVAuthListener_Impl( xEnv
, m_aURL
),
269 catch (DAVException
const& e
)
272 bRetry
= handleException( e
, errorCount
);
281 void DAVResourceAccess::PROPPATCH(
282 const std::vector
< ProppatchValue
>& rValues
,
283 const uno::Reference
< ucb::XCommandEnvironment
>& xEnv
)
294 DAVRequestHeaders aHeaders
;
295 getUserRequestHeaders( xEnv
,
297 ucb::WebDAVHTTPMethod_PROPPATCH
,
300 m_xSession
->PROPPATCH( getRequestURI(),
302 DAVRequestEnvironment(
303 new DAVAuthListener_Impl( xEnv
, m_aURL
),
306 catch (DAVException
const& e
)
309 bRetry
= handleException( e
, errorCount
);
318 void DAVResourceAccess::HEAD(
319 const std::vector
< OUString
> & rHeaderNames
,
320 DAVResource
& rResource
,
321 const uno::Reference
< ucb::XCommandEnvironment
>& xEnv
)
332 DAVRequestHeaders aHeaders
;
333 getUserRequestHeaders( xEnv
,
335 ucb::WebDAVHTTPMethod_HEAD
,
338 m_xSession
->HEAD( getRequestURI(),
341 DAVRequestEnvironment(
342 new DAVAuthListener_Impl( xEnv
, m_aURL
),
345 catch (DAVException
const& e
)
348 bRetry
= handleException( e
, errorCount
);
357 uno::Reference
< io::XInputStream
> DAVResourceAccess::GET(
358 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
362 uno::Reference
< io::XInputStream
> xStream
;
370 DAVRequestHeaders aHeaders
;
371 getUserRequestHeaders( xEnv
,
373 ucb::WebDAVHTTPMethod_GET
,
376 xStream
= m_xSession
->GET( getRequestURI(),
377 DAVRequestEnvironment(
378 new DAVAuthListener_Impl(
382 catch (DAVException
const& e
)
385 bRetry
= handleException( e
, errorCount
);
396 void DAVResourceAccess::GET(
397 uno::Reference
< io::XOutputStream
> & rStream
,
398 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
409 DAVRequestHeaders aHeaders
;
410 getUserRequestHeaders( xEnv
,
412 ucb::WebDAVHTTPMethod_GET
,
415 m_xSession
->GET( getRequestURI(),
417 DAVRequestEnvironment(
418 new DAVAuthListener_Impl( xEnv
, m_aURL
),
421 catch (DAVException
const& e
)
424 bRetry
= handleException( e
, errorCount
);
433 uno::Reference
< io::XInputStream
> DAVResourceAccess::GET(
434 const std::vector
< OUString
> & rHeaderNames
,
435 DAVResource
& rResource
,
436 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
440 uno::Reference
< io::XInputStream
> xStream
;
448 DAVRequestHeaders aHeaders
;
449 getUserRequestHeaders( xEnv
,
451 ucb::WebDAVHTTPMethod_GET
,
454 xStream
= m_xSession
->GET( getRequestURI(),
457 DAVRequestEnvironment(
458 new DAVAuthListener_Impl(
462 catch (DAVException
const& e
)
465 bRetry
= handleException( e
, errorCount
);
476 // used as HEAD substitute when HEAD is not implemented on server
477 void DAVResourceAccess::GET0(
478 DAVRequestHeaders
&rRequestHeaders
,
479 const std::vector
< OUString
> & rHeaderNames
,
480 DAVResource
& rResource
,
481 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
492 getUserRequestHeaders( xEnv
,
494 ucb::WebDAVHTTPMethod_GET
,
497 m_xSession
->GET( getRequestURI(),
500 DAVRequestEnvironment(
501 new DAVAuthListener_Impl(
505 catch (DAVException
const& e
)
508 bRetry
= handleException( e
, errorCount
);
517 void DAVResourceAccess::GET(
518 uno::Reference
< io::XOutputStream
> & rStream
,
519 const std::vector
< OUString
> & rHeaderNames
,
520 DAVResource
& rResource
,
521 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
532 DAVRequestHeaders aHeaders
;
533 getUserRequestHeaders( xEnv
,
535 ucb::WebDAVHTTPMethod_GET
,
538 m_xSession
->GET( getRequestURI(),
542 DAVRequestEnvironment(
543 new DAVAuthListener_Impl( xEnv
, m_aURL
),
546 catch (DAVException
const& e
)
549 bRetry
= handleException( e
, errorCount
);
558 void DAVResourceAccess::abort()
560 // seems pointless to call initialize() here, but prepare for nullptr
561 decltype(m_xSession
) xSession
;
563 osl::Guard
<osl::Mutex
> const g(m_aMutex
);
564 xSession
= m_xSession
;
575 /// @throws DAVException
576 void resetInputStream( const uno::Reference
< io::XInputStream
> & rStream
)
580 uno::Reference
< io::XSeekable
> xSeekable(
581 rStream
, uno::UNO_QUERY
);
582 if ( xSeekable
.is() )
584 xSeekable
->seek( 0 );
588 catch ( lang::IllegalArgumentException
const & )
591 catch ( io::IOException
const & )
595 throw DAVException( DAVException::DAV_INVALID_ARG
);
601 void DAVResourceAccess::PUT(
602 const uno::Reference
< io::XInputStream
> & rStream
,
603 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
607 // Make stream seekable, if it not. Needed, if request must be retried.
608 uno::Reference
< io::XInputStream
> xSeekableStream
609 = comphelper::OSeekableInputWrapper::CheckSeekableCanWrap(
610 rStream
, m_xContext
);
617 resetInputStream( xSeekableStream
);
622 DAVRequestHeaders aHeaders
;
623 getUserRequestHeaders( xEnv
,
625 ucb::WebDAVHTTPMethod_PUT
,
628 m_xSession
->PUT( getRequestURI(),
630 DAVRequestEnvironment(
631 new DAVAuthListener_Impl( xEnv
, m_aURL
),
634 catch (DAVException
const& e
)
637 bRetry
= handleException( e
, errorCount
);
646 uno::Reference
< io::XInputStream
> DAVResourceAccess::POST(
647 const OUString
& rContentType
,
648 const OUString
& rReferer
,
649 const uno::Reference
< io::XInputStream
> & rInputStream
,
650 const uno::Reference
< ucb::XCommandEnvironment
>& xEnv
)
654 // Make stream seekable, if it not. Needed, if request must be retried.
655 uno::Reference
< io::XInputStream
> xSeekableStream
656 = comphelper::OSeekableInputWrapper::CheckSeekableCanWrap(
657 rInputStream
, m_xContext
);
659 uno::Reference
< io::XInputStream
> xStream
;
666 resetInputStream( xSeekableStream
);
672 DAVRequestHeaders aHeaders
;
673 getUserRequestHeaders( xEnv
,
675 ucb::WebDAVHTTPMethod_POST
,
678 xStream
= m_xSession
->POST( getRequestURI(),
682 DAVRequestEnvironment(
683 new DAVAuthListener_Impl(
687 catch (DAVException
const& e
)
690 bRetry
= handleException( e
, errorCount
);
694 if ( e
.getError() == DAVException::DAV_HTTP_REDIRECT
)
696 // #i74980# - Upon POST redirect, do a GET.
707 void DAVResourceAccess::POST(
708 const OUString
& rContentType
,
709 const OUString
& rReferer
,
710 const uno::Reference
< io::XInputStream
> & rInputStream
,
711 uno::Reference
< io::XOutputStream
> & rOutputStream
,
712 const uno::Reference
< ucb::XCommandEnvironment
>& xEnv
)
716 // Make stream seekable, if it not. Needed, if request must be retried.
717 uno::Reference
< io::XInputStream
> xSeekableStream
718 = comphelper::OSeekableInputWrapper::CheckSeekableCanWrap(
719 rInputStream
, m_xContext
);
727 resetInputStream( xSeekableStream
);
733 DAVRequestHeaders aHeaders
;
734 getUserRequestHeaders( xEnv
,
736 ucb::WebDAVHTTPMethod_POST
,
739 m_xSession
->POST( getRequestURI(),
744 DAVRequestEnvironment(
745 new DAVAuthListener_Impl( xEnv
, m_aURL
),
748 catch (DAVException
const& e
)
751 bRetry
= handleException( e
, errorCount
);
755 if ( e
.getError() == DAVException::DAV_HTTP_REDIRECT
)
757 // #i74980# - Upon POST redirect, do a GET.
758 GET( rOutputStream
, xEnv
);
767 void DAVResourceAccess::MKCOL(
768 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
779 DAVRequestHeaders aHeaders
;
780 getUserRequestHeaders( xEnv
,
782 ucb::WebDAVHTTPMethod_MKCOL
,
785 m_xSession
->MKCOL( getRequestURI(),
786 DAVRequestEnvironment(
787 new DAVAuthListener_Impl( xEnv
, m_aURL
),
790 catch (DAVException
const& e
)
793 bRetry
= handleException( e
, errorCount
);
802 void DAVResourceAccess::COPY(
803 const OUString
& rSourcePath
,
804 const OUString
& rDestinationURI
,
806 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
817 DAVRequestHeaders aHeaders
;
818 getUserRequestHeaders( xEnv
,
820 ucb::WebDAVHTTPMethod_COPY
,
823 m_xSession
->COPY( rSourcePath
,
825 DAVRequestEnvironment(
826 new DAVAuthListener_Impl( xEnv
, m_aURL
),
830 catch (DAVException
const& e
)
833 bRetry
= handleException( e
, errorCount
);
842 void DAVResourceAccess::MOVE(
843 const OUString
& rSourcePath
,
844 const OUString
& rDestinationURI
,
846 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
857 DAVRequestHeaders aHeaders
;
858 getUserRequestHeaders( xEnv
,
860 ucb::WebDAVHTTPMethod_MOVE
,
863 m_xSession
->MOVE( rSourcePath
,
865 DAVRequestEnvironment(
866 new DAVAuthListener_Impl( xEnv
, m_aURL
),
870 catch (DAVException
const& e
)
873 bRetry
= handleException( e
, errorCount
);
882 void DAVResourceAccess::DESTROY(
883 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
894 DAVRequestHeaders aHeaders
;
895 getUserRequestHeaders( xEnv
,
897 ucb::WebDAVHTTPMethod_DELETE
,
900 m_xSession
->DESTROY( getRequestURI(),
901 DAVRequestEnvironment(
902 new DAVAuthListener_Impl( xEnv
, m_aURL
),
905 catch (DAVException
const& e
)
908 bRetry
= handleException( e
, errorCount
);
918 void DAVResourceAccess::LOCK(
920 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
931 DAVRequestHeaders aHeaders
;
932 getUserRequestHeaders( xEnv
,
934 ucb::WebDAVHTTPMethod_LOCK
,
937 m_xSession
->LOCK( getRequestURI(),
939 DAVRequestEnvironment(
940 new DAVAuthListener_Impl( xEnv
, m_aURL
),
943 catch (DAVException
const& e
)
946 bRetry
= handleException( e
, errorCount
);
954 void DAVResourceAccess::UNLOCK(
955 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
)
966 DAVRequestHeaders aHeaders
;
967 getUserRequestHeaders( xEnv
,
969 ucb::WebDAVHTTPMethod_UNLOCK
,
972 m_xSession
->UNLOCK( getRequestURI(),
973 DAVRequestEnvironment(
974 new DAVAuthListener_Impl( xEnv
, m_aURL
),
977 catch (DAVException
const& e
)
980 bRetry
= handleException( e
, errorCount
);
988 void DAVResourceAccess::setFlags( const uno::Sequence
< beans::NamedValue
>& rFlags
)
990 osl::Guard
< osl::Mutex
> aGuard( m_aMutex
);
994 void DAVResourceAccess::setURL( const OUString
& rNewURL
)
996 osl::Guard
< osl::Mutex
> aGuard( m_aMutex
);
998 m_aPath
.clear(); // Next initialize() will create new session.
1002 // init dav session and path
1003 void DAVResourceAccess::initialize()
1005 osl::Guard
< osl::Mutex
> aGuard( m_aMutex
);
1006 if ( m_aPath
.isEmpty() )
1008 CurlUri
const aURI( m_aURL
);
1009 OUString
aPath( aURI
.GetRelativeReference() );
1011 /* #134089# - Check URI */
1012 if ( aPath
.isEmpty() )
1013 throw DAVException( DAVException::DAV_INVALID_ARG
);
1015 /* #134089# - Check URI */
1016 if ( aURI
.GetHost().isEmpty() )
1017 throw DAVException( DAVException::DAV_INVALID_ARG
);
1019 if ( !m_xSession
.is() || !m_xSession
->CanUse( m_aURL
, m_aFlags
) )
1023 // create new webdav session
1025 = m_xSessionFactory
->createDAVSession( m_aURL
, m_aFlags
, m_xContext
);
1027 if ( !m_xSession
.is() )
1031 // Own URI is needed to redirect cycle detection.
1032 m_aRedirectURIs
.push_back( aURI
);
1037 // Not only the path has to be encoded
1038 m_aURL
= aURI
.GetURI();
1043 const OUString
& DAVResourceAccess::getRequestURI() const
1045 assert(m_xSession
.is() &&
1046 "DAVResourceAccess::getRequestURI - Not initialized!");
1048 // In case a proxy is used we have to use the absolute URI for a request.
1049 if ( m_xSession
->UsesProxy() )
1057 void DAVResourceAccess::getUserRequestHeaders(
1058 const uno::Reference
< ucb::XCommandEnvironment
> & xEnv
,
1059 const OUString
& rURI
,
1060 ucb::WebDAVHTTPMethod eMethod
,
1061 DAVRequestHeaders
& rRequestHeaders
)
1066 uno::Reference
< ucb::XWebDAVCommandEnvironment
> xDAVEnv(
1067 xEnv
, uno::UNO_QUERY
);
1069 if ( !xDAVEnv
.is() )
1072 uno::Sequence
< beans::StringPair
> aRequestHeaders
1073 = xDAVEnv
->getUserRequestHeaders( rURI
, eMethod
);
1075 for ( sal_Int32 n
= 0; n
< aRequestHeaders
.getLength(); ++n
)
1077 rRequestHeaders
.push_back(
1078 DAVRequestHeader( aRequestHeaders
[ n
].First
,
1079 aRequestHeaders
[ n
].Second
) );
1083 // This function member implements the control on cyclical redirections
1084 bool DAVResourceAccess::detectRedirectCycle(
1085 ::std::u16string_view
const rRedirectURL
)
1087 osl::Guard
< osl::Mutex
> aGuard( m_aMutex
);
1089 CurlUri
const aUri( rRedirectURL
);
1091 // Check for maximum number of redirections
1092 // according to <https://tools.ietf.org/html/rfc7231#section-6.4>.
1093 // A practical limit may be 5, due to earlier specifications:
1094 // <https://tools.ietf.org/html/rfc2068#section-10.3>
1095 // it can be raised keeping in mind the added net activity.
1096 if( g_nRedirectLimit
<= m_aRedirectURIs
.size() )
1099 // try to detect a cyclical redirection
1100 return std::any_of(m_aRedirectURIs
.begin(), m_aRedirectURIs
.end(),
1101 [&aUri
](const CurlUri
& rUri
) { return aUri
== rUri
; });
1105 void DAVResourceAccess::resetUri()
1107 osl::Guard
< osl::Mutex
> aGuard( m_aMutex
);
1108 if ( ! m_aRedirectURIs
.empty() )
1110 auto const it
= m_aRedirectURIs
.begin();
1112 CurlUri
const aUri( *it
);
1113 m_aRedirectURIs
.clear();
1114 setURL ( aUri
.GetURI() );
1120 bool DAVResourceAccess::handleException(DAVException
const& e
, int const errorCount
)
1122 switch ( e
.getError() )
1124 case DAVException::DAV_HTTP_REDIRECT
:
1125 if ( !detectRedirectCycle( e
.getData() ) )
1127 // set new URL and path.
1128 setURL( e
.getData() );
1133 // i#67048 copy & paste images doesn't display. This bug refers
1134 // to an old OOo problem about getting resources from sites with a bad connection.
1135 // If we have a bad connection try again. Up to three times.
1136 case DAVException::DAV_HTTP_ERROR
:
1137 // retry up to three times, if not a client-side error (4xx error codes)
1138 if ( e
.getStatus() < SC_BAD_REQUEST
&& errorCount
< 3 )
1140 // check the server side errors
1141 switch( e
.getStatus() )
1143 // the HTTP server side response status codes that can be retried
1144 // [Serf TODO? i#119036] case SC_REQUEST_ENTITY_TOO_LARGE:
1145 case SC_BAD_GATEWAY
: // retry, can be an excessive load
1146 case SC_GATEWAY_TIMEOUT
: // retry, may be we get lucky
1147 case SC_SERVICE_UNAVAILABLE
: // retry, the service may become available
1148 case SC_INSUFFICIENT_STORAGE
: // space may be freed, retry
1150 if ( errorCount
< 3 )
1156 // all the other HTTP server response status codes are NOT retry
1161 // if connection has said retry then retry!
1162 case DAVException::DAV_HTTP_RETRY
:
1166 return false; // Abort
1170 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */