pax_global_header00006660000000000000000000000064147511206330014514gustar00rootroot0000000000000052 comment=5d7c5f8d7b254c643bed77372d68646fffb0ba88 buteo-sync-plugin-carddav-0.1.12/000077500000000000000000000000001475112063300165635ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/.gitignore000066400000000000000000000001151475112063300205500ustar00rootroot00000000000000*.o *.so Makefile moc_* *.moc .qmake.stash tests/replyparser/tst_replyparser buteo-sync-plugin-carddav-0.1.12/LICENSE000066400000000000000000000643061475112063300176010ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE buteo-sync-plugin-carddav Copyright (C) 2014 Jolla Ltd. Contact: info@jolla.com You may use, distribute and copy this library/plugin under the terms of GNU Lesser General Public License version 2.1. ------------------------------------------------------------------------- GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation and appearing in the file license.lgpl included in the packaging of this file. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! buteo-sync-plugin-carddav-0.1.12/README000066400000000000000000000006531475112063300174470ustar00rootroot00000000000000This package provides a "client" synchronization plugin to the Buteo sync framework. It connects to CardDAV servers using a valid account (accounts&sso framework) and synchronizes contact data from the remote CardDAV service to the device and vice versa. It is a two-way sync plugin. Huge thanks to SabreDAV for the in-depth information available on their site about CardDAV (http://sabre.io/dav/building-a-carddav-client/) buteo-sync-plugin-carddav-0.1.12/buteo-sync-plugin-carddav.pro000066400000000000000000000002521475112063300242720ustar00rootroot00000000000000TEMPLATE=subdirs SUBDIRS=src tests CONFIG(build-tools) { SUBDIRS += tools tools.depends=src } tests.depends=src OTHER_FILES+=rpm/buteo-sync-plugin-carddav.spec buteo-sync-plugin-carddav-0.1.12/rpm/000077500000000000000000000000001475112063300173615ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/rpm/buteo-sync-plugin-carddav.spec000066400000000000000000000105351475112063300252270ustar00rootroot00000000000000Name: buteo-sync-plugin-carddav Summary: Syncs contact data from CardDAV services Version: 0.1.12 Release: 1 License: LGPLv2 URL: https://github.com/sailfishos/buteo-sync-plugin-carddav Source0: %{name}-%{version}.tar.bz2 BuildRequires: pkgconfig(Qt5Core) BuildRequires: pkgconfig(Qt5Gui) BuildRequires: pkgconfig(Qt5DBus) BuildRequires: pkgconfig(Qt5Sql) BuildRequires: pkgconfig(Qt5Network) BuildRequires: pkgconfig(Qt5Contacts) BuildRequires: pkgconfig(Qt5Versit) BuildRequires: pkgconfig(mlite5) BuildRequires: pkgconfig(buteosyncfw5) >= 0.10.0 BuildRequires: pkgconfig(accounts-qt5) >= 1.13 BuildRequires: pkgconfig(libsignon-qt5) BuildRequires: pkgconfig(libsailfishkeyprovider) BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions) >= 0.3.0 BuildRequires: pkgconfig(contactcache-qt5) >= 0.3.0 Requires: buteo-syncfw-qt5-msyncd %description A Buteo plugin which syncs contact data from CardDAV services %package tests Summary: Unit tests for buteo-sync-plugin-carddav BuildRequires: pkgconfig(Qt5Test) Requires: %{name} = %{version}-%{release} %description tests This package contains unit tests for the CardDAV Buteo sync plugin. %prep %autosetup -n %{name}-%{version} %build %qmake5 "CONFIG+=build-tools" %make_build %install %qmake5_install %files %license LICENSE %{_libdir}/buteo-plugins-qt5/oopp/libcarddav-client.so %config %{_sysconfdir}/buteo/profiles/client/carddav.xml %config %{_sysconfdir}/buteo/profiles/sync/carddav.Contacts.xml %files tests /opt/tests/buteo/plugins/carddav/cdavtool /opt/tests/buteo/plugins/carddav/tests.xml /opt/tests/buteo/plugins/carddav/tst_replyparser /opt/tests/buteo/plugins/carddav/data/replyparser_userprincipal_empty.xml /opt/tests/buteo/plugins/carddav/data/replyparser_userprincipal_single-well-formed.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookhome_empty.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookhome_single-well-formed.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookinformation_empty.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookinformation_single-well-formed.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookinformation_two-with-privileges.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookinformation_addressbook-plus-contact.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookinformation_addressbook-calendar-principal.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookinformation_addressbook-principal-proxy.xml /opt/tests/buteo/plugins/carddav/data/replyparser_addressbookinformation_addressbook-plus-collection-resource.xml /opt/tests/buteo/plugins/carddav/data/replyparser_synctokendelta_empty.xml /opt/tests/buteo/plugins/carddav/data/replyparser_synctokendelta_single-well-formed-add-mod-rem.xml /opt/tests/buteo/plugins/carddav/data/replyparser_synctokendelta_single-well-formed-addition.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactmetadata_empty.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactmetadata_single-well-formed-add-mod-rem-unch.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactmetadata_single-vcf-and-non-vcf.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_empty.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-well-formed.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-hs-utc-iso8601-bday.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-ns-utc-iso8601-bday.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-hs-notz-iso8601-bday.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-ns-notz-iso8601-bday.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-ns-do-iso8601-bday.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-ns-do-iso8601-bday-multiple.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-contact-multiple-formattedname.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-contact-multiple-name.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-contact-multiple-rev.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-contact-multiple-uid.xml /opt/tests/buteo/plugins/carddav/data/replyparser_contactdata_single-contact-multiple-xgender.xml buteo-sync-plugin-carddav-0.1.12/src/000077500000000000000000000000001475112063300173525ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/src/auth.cpp000066400000000000000000000205671475112063300210310ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "auth_p.h" #include "logging.h" #include #include #ifdef USE_SAILFISHKEYPROVIDER #include namespace { QString skp_storedKey(const QString &provider, const QString &service, const QString &key) { QString retn; char *value = NULL; int success = SailfishKeyProvider_storedKey(provider.toLatin1(), service.toLatin1(), key.toLatin1(), &value); if (value) { if (success == 0) { retn = QString::fromLatin1(value); } free(value); } return retn; } } #endif // USE_SAILFISHKEYPROVIDER Auth::Auth(QObject *parent) : QObject(parent) , m_account(0) , m_ident(0) , m_session(0) , m_ignoreSslErrors(false) { } Auth::~Auth() { delete m_account; if (m_ident && m_session) { m_ident->destroySession(m_session); } delete m_ident; } void Auth::signIn(int accountId) { m_account = Accounts::Account::fromId(&m_manager, accountId, this); if (!m_account) { qCWarning(lcCardDav) << Q_FUNC_INFO << "unable to load account" << accountId; emit signInError(); return; } // determine which service to sign in with. Accounts::Service srv; Accounts::ServiceList services = m_account->services(); Q_FOREACH (const Accounts::Service &s, services) { if (s.serviceType().toLower() == QStringLiteral("carddav")) { srv = s; break; } } if (!srv.isValid()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "unable to find carddav service for account" << accountId; emit signInError(); return; } // determine the remote URL from the account settings, and then sign in. Accounts::AccountService globalSrv(m_account, Accounts::Service()); Accounts::AccountService accSrv(m_account, srv); if (!accSrv.isEnabled()) { qCWarning(lcCardDav) << "Service:" << srv.name() << "is not enabled for account:" << m_account->id(); emit signInError(); return; } m_ignoreSslErrors = accSrv.value("ignore_ssl_errors").toBool(); m_serverUrl = accSrv.value("server_address").toString(); if (m_serverUrl.isEmpty()) { QUrl host(globalSrv.value("host").toString()); QString path = accSrv.value("server_path").toString(); if (!path.isEmpty()) { if (path[0] != '/') { /* If "server_path" holds a relative path, then let's assume * it's relative to the host path. This nicely handles the case * of NextCloud/OwnCloud installations: "host" defines the base * URL, whereas "server_path" would be the subdirectory where * the DAV stuff is located. */ path = QDir::cleanPath(host.path() + '/' + path); } host.setPath(path); } m_serverUrl = host.toString(); } m_addressbookPath = accSrv.value("addressbook_path").toString(); // optional, may be empty. if (m_serverUrl.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "no valid server url setting in account" << accountId; emit signInError(); return; } m_ident = accSrv.authData().credentialsId() > 0 ? SignOn::Identity::existingIdentity(accSrv.authData().credentialsId()) : 0; if (!m_ident) { qCWarning(lcCardDav) << Q_FUNC_INFO << "no valid credentials for account" << accountId; emit signInError(); return; } QString method = accSrv.authData().method(); QString mechanism = accSrv.authData().mechanism(); SignOn::AuthSession *session = m_ident->createSession(method); if (!session) { qCWarning(lcCardDav) << Q_FUNC_INFO << "unable to create authentication session with account" << accountId; emit signInError(); return; } QString providerName = m_account->providerName(); QString clientId; QString clientSecret; QString consumerKey; QString consumerSecret; #ifdef USE_SAILFISHKEYPROVIDER clientId = skp_storedKey(providerName, QString(), QStringLiteral("client_id")); clientSecret = skp_storedKey(providerName, QString(), QStringLiteral("client_secret")); consumerKey = skp_storedKey(providerName, QString(), QStringLiteral("consumer_key")); consumerSecret = skp_storedKey(providerName, QString(), QStringLiteral("consumer_secret")); #endif QVariantMap signonSessionData = accSrv.authData().parameters(); signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); if (!clientId.isEmpty()) signonSessionData.insert("ClientId", clientId); if (!clientSecret.isEmpty()) signonSessionData.insert("ClientSecret", clientSecret); if (!consumerKey.isEmpty()) signonSessionData.insert("ConsumerKey", consumerKey); if (!consumerSecret.isEmpty()) signonSessionData.insert("ConsumerSecret", consumerSecret); connect(session, SIGNAL(response(SignOn::SessionData)), this, SLOT(signOnResponse(SignOn::SessionData)), Qt::UniqueConnection); connect(session, SIGNAL(error(SignOn::Error)), this, SLOT(signOnError(SignOn::Error)), Qt::UniqueConnection); session->setProperty("accountId", accountId); session->setProperty("mechanism", mechanism); session->setProperty("signonSessionData", signonSessionData); session->process(SignOn::SessionData(signonSessionData), mechanism); } void Auth::signOnResponse(const SignOn::SessionData &response) { QString username, password, accessToken; Q_FOREACH (const QString &key, response.propertyNames()) { if (key.toLower() == QStringLiteral("username")) { username = response.getProperty(key).toString(); } else if (key.toLower() == QStringLiteral("secret")) { password = response.getProperty(key).toString(); } else if (key.toLower() == QStringLiteral("password")) { password = response.getProperty(key).toString(); } else if (key.toLower() == QStringLiteral("accesstoken")) { accessToken = response.getProperty(key).toString(); } } // we need both username+password, OR accessToken. if (!accessToken.isEmpty()) { emit signInCompleted(m_serverUrl, m_addressbookPath, QString(), QString(), accessToken, m_ignoreSslErrors); } else if (!username.isEmpty() && !password.isEmpty()) { emit signInCompleted(m_serverUrl, m_addressbookPath, username, password, QString(), m_ignoreSslErrors); } else { qCWarning(lcCardDav) << Q_FUNC_INFO << "authentication succeeded, but couldn't find valid credentials"; emit signInError(); } } void Auth::signOnError(const SignOn::Error &error) { qCWarning(lcCardDav) << Q_FUNC_INFO << "authentication error:" << error.type() << ":" << error.message(); emit signInError(); return; } void Auth::setCredentialsNeedUpdate(int accountId) { Accounts::Account *account = m_manager.account(accountId); if (account) { Accounts::ServiceList services = account->services(); Q_FOREACH (const Accounts::Service &s, services) { if (s.serviceType().toLower() == QStringLiteral("carddav")) { account->setValue(QStringLiteral("CredentialsNeedUpdate"), QVariant::fromValue(true)); account->setValue(QStringLiteral("CredentialsNeedUpdateFrom"), QVariant::fromValue(QString::fromLatin1("carddav-sync"))); account->selectService(Accounts::Service()); account->syncAndBlock(); break; } } } } buteo-sync-plugin-carddav-0.1.12/src/auth_p.h000066400000000000000000000035471475112063300210140ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include #include #include #include #include #include #include #include #include class Auth : public QObject { Q_OBJECT public: Auth(QObject *parent); ~Auth(); void signIn(int accountId); void setCredentialsNeedUpdate(int accountId); Q_SIGNALS: void signInCompleted(const QString &serverUrl, const QString &addressbookPath, const QString &username, const QString &password, const QString &accessToken, bool ignoreSslErrors); void signInError(); private Q_SLOTS: void signOnResponse(const SignOn::SessionData &response); void signOnError(const SignOn::Error &error); private: Accounts::Manager m_manager; Accounts::Account *m_account; SignOn::Identity *m_ident; SignOn::AuthSession *m_session; QString m_serverUrl; QString m_addressbookPath; bool m_ignoreSslErrors; }; buteo-sync-plugin-carddav-0.1.12/src/carddav.Contacts.xml000066400000000000000000000012071475112063300232550ustar00rootroot00000000000000 buteo-sync-plugin-carddav-0.1.12/src/carddav.cpp000066400000000000000000001564201475112063300214720ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "carddav_p.h" #include "syncer_p.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_LIBCONTACTS #include #include #endif #include namespace { void debugDumpData(const QString &data) { if (!lcCardDavProtocol().isDebugEnabled()) { return; } QString dbgout; Q_FOREACH (const QChar &c, data) { if (c == '\r' || c == '\n') { if (!dbgout.isEmpty()) { qCDebug(lcCardDavProtocol) << dbgout; dbgout.clear(); } } else { dbgout += c; } } if (!dbgout.isEmpty()) { qCDebug(lcCardDavProtocol) << dbgout; } } QContactId matchingContactFromList(const QContact &c, const QList &contacts) { const QString uri = c.detail().syncTarget(); for (const QContact &other : contacts) { if (!uri.isEmpty() && uri == other.detail().syncTarget()) { return other.id(); } } return QContactId(); } } CardDavVCardConverter::CardDavVCardConverter() { } CardDavVCardConverter::~CardDavVCardConverter() { } QStringList CardDavVCardConverter::supportedPropertyNames() { // We only support a small number of (core) vCard properties // in this sync adapter. The rest of the properties will // be cached so that we can stitch them back into the vCard // we upload on modification. QStringList supportedProperties; supportedProperties << "VERSION" << "PRODID" << "REV" << "N" << "FN" << "NICKNAME" << "BDAY" << "X-GENDER" << "EMAIL" << "TEL" << "ADR" << "URL" << "PHOTO" << "ORG" << "TITLE" << "ROLE" << "NOTE" << "UID"; return supportedProperties; } QPair CardDavVCardConverter::convertVCardToContact(const QString &vcard, bool *ok) { m_unsupportedProperties.clear(); QVersitReader reader(vcard.toUtf8()); reader.startReading(); reader.waitForFinished(); QList vdocs = reader.results(); if (vdocs.size() != 1) { qCWarning(lcCardDav) << Q_FUNC_INFO << "invalid results during vcard import, got" << vdocs.size() << "output from input:\n" << vcard; *ok = false; return QPair(); } // convert the vCard into a QContact QVersitContactImporter importer; importer.setPropertyHandler(this); importer.importDocuments(vdocs); QList importedContacts = importer.contacts(); if (importedContacts.size() != 1) { qCWarning(lcCardDav) << Q_FUNC_INFO << "invalid results during vcard conversion, got" << importedContacts.size() << "output from input:\n" << vcard; *ok = false; return QPair(); } QContact importedContact = importedContacts.first(); QStringList unsupportedProperties = m_unsupportedProperties.value(importedContact.detail().guid()); m_unsupportedProperties.clear(); // If the contact has no structured name data, create a best-guess name for it. // This may be the case if the server provides an FN property but no N property. // Also, some detail types should be unique, so remove duplicates if present. QString displaylabelField, nicknameField; QContactName nameDetail; QSet seenUniqueDetailTypes; QList importedContactDetails = importedContact.details(); Q_FOREACH (const QContactDetail &d, importedContactDetails) { if (d.type() == QContactDetail::TypeName) { nameDetail = d; } else if (d.type() == QContactDetail::TypeDisplayLabel) { displaylabelField = d.value(QContactDisplayLabel::FieldLabel).toString().trimmed(); } else if (d.type() == QContactDetail::TypeNickname) { nicknameField = d.value(QContactNickname::FieldNickname).toString().trimmed(); } else if (d.type() == QContactDetail::TypeBirthday) { if (seenUniqueDetailTypes.contains(QContactDetail::TypeBirthday)) { // duplicated BDAY field seen from vCard. // remove this duplicate, else save will fail. QContactBirthday dupBday(d); importedContact.removeDetail(&dupBday); qCDebug(lcCardDav) << "Removed duplicate BDAY detail:" << dupBday; } else { seenUniqueDetailTypes.insert(QContactDetail::TypeBirthday); } } else if (d.type() == QContactDetail::TypeTimestamp) { if (seenUniqueDetailTypes.contains(QContactDetail::TypeTimestamp)) { // duplicated REV field seen from vCard. // remove this duplicate, else save will fail. QContactTimestamp dupRev(d); importedContact.removeDetail(&dupRev, QContact::IgnoreAccessConstraints); qCDebug(lcCardDav) << "Removed duplicate REV detail:" << dupRev; QContactTimestamp firstRev = importedContact.detail(); if (dupRev.lastModified().isValid() && (!firstRev.lastModified().isValid() || dupRev.lastModified() > firstRev.lastModified())) { firstRev.setLastModified(dupRev.lastModified()); importedContact.saveDetail(&firstRev, QContact::IgnoreAccessConstraints); } } else { seenUniqueDetailTypes.insert(QContactDetail::TypeTimestamp); } } else if (d.type() == QContactDetail::TypeGuid) { if (seenUniqueDetailTypes.contains(QContactDetail::TypeGuid)) { // duplicated UID field seen from vCard. // remove this duplicate, else save will fail. QContactGuid dupUid(d); importedContact.removeDetail(&dupUid); qCDebug(lcCardDav) << "Removed duplicate UID detail:" << dupUid; } else { seenUniqueDetailTypes.insert(QContactDetail::TypeGuid); } } else if (d.type() == QContactDetail::TypeGender) { if (seenUniqueDetailTypes.contains(QContactDetail::TypeGender)) { // duplicated X-GENDER field seen from vCard. // remove this duplicate, else save will fail. QContactGender dupGender(d); importedContact.removeDetail(&dupGender); qCDebug(lcCardDav) << "Removed duplicate X-GENDER detail:" << dupGender; } else { seenUniqueDetailTypes.insert(QContactDetail::TypeGender); } } } if (nameDetail.isEmpty() || (nameDetail.firstName().isEmpty() && nameDetail.lastName().isEmpty())) { // we have no valid name data but we may have display label or nickname data which we can decompose. #ifdef USE_LIBCONTACTS if (!displaylabelField.isEmpty()) { SeasideCache::decomposeDisplayLabel(displaylabelField, &nameDetail); if (nameDetail.isEmpty()) { nameDetail.setCustomLabel(displaylabelField); } importedContact.saveDetail(&nameDetail, QContact::IgnoreAccessConstraints); qCDebug(lcCardDav) << "Decomposed vCard display name into structured name:" << nameDetail; } else if (!nicknameField.isEmpty()) { SeasideCache::decomposeDisplayLabel(nicknameField, &nameDetail); importedContact.saveDetail(&nameDetail, QContact::IgnoreAccessConstraints); qCDebug(lcCardDav) << "Decomposed vCard nickname into structured name:" << nameDetail; } else { qCWarning(lcCardDav) << "No structured name data exists in the vCard, contact will be unnamed!"; } #else qCWarning(lcCardDav) << "No structured name data exists in the vCard, contact will be unnamed!"; #endif } // mark each detail of the contact as modifiable Q_FOREACH (QContactDetail det, importedContact.details()) { det.setValue(QContactDetail__FieldModifiable, true); importedContact.saveDetail(&det, QContact::IgnoreAccessConstraints); } *ok = true; return qMakePair(importedContact, unsupportedProperties); } QString CardDavVCardConverter::convertContactToVCard(const QContact &c, const QStringList &unsupportedProperties) { QList exportList; exportList << c; QVersitContactExporter e; e.setDetailHandler(this); e.exportContacts(exportList); QByteArray output; QBuffer vCardBuffer(&output); vCardBuffer.open(QBuffer::WriteOnly); QVersitWriter writer(&vCardBuffer); writer.startWriting(e.documents()); writer.waitForFinished(); QString retn = QString::fromUtf8(output); // now add back the unsupported properties. Q_FOREACH (const QString &propStr, unsupportedProperties) { int endIdx = retn.lastIndexOf(QStringLiteral("END:VCARD")); if (endIdx > 0) { QString ecrlf = propStr + '\r' + '\n'; retn.insert(endIdx, ecrlf); } } qCDebug(lcCardDav) << "generated vcard:"; debugDumpData(retn); return retn; } QString CardDavVCardConverter::convertPropertyToString(const QVersitProperty &p) const { QVersitDocument d(QVersitDocument::VCard30Type); d.addProperty(p); QByteArray out; QBuffer bout(&out); bout.open(QBuffer::WriteOnly); QVersitWriter w(&bout); w.startWriting(d); w.waitForFinished(); QString retn = QString::fromLatin1(out); // strip out the BEGIN:VCARD\r\nVERSION:3.0\r\n and END:VCARD\r\n\r\n bits. int headerIdx = retn.indexOf(QStringLiteral("VERSION:3.0")) + 11; int footerIdx = retn.indexOf(QStringLiteral("END:VCARD")); if (headerIdx > 11 && footerIdx > 0 && footerIdx > headerIdx) { retn = retn.mid(headerIdx, footerIdx - headerIdx).trimmed(); return retn; } qCWarning(lcCardDav) << Q_FUNC_INFO << "no string conversion possible for versit property:" << p.name(); return QString(); } void CardDavVCardConverter::propertyProcessed(const QVersitDocument &, const QVersitProperty &property, const QContact &, bool *alreadyProcessed, QList *updatedDetails) { static QStringList supportedProperties(supportedPropertyNames()); const QString propertyName(property.name().toUpper()); if (propertyName == QLatin1String("PHOTO")) { #ifdef USE_LIBCONTACTS // use the standard PHOTO handler from Seaside libcontacts QContactAvatar newAvatar = SeasidePropertyHandler::avatarFromPhotoProperty(property); #else QContactAvatar newAvatar; QUrl url(property.variantValue().toString()); if (url.isValid() && !url.isLocalFile()) { newAvatar.setImageUrl(url); } #endif if (!newAvatar.isEmpty()) { updatedDetails->append(newAvatar); } // don't let the default PHOTO handler import it, even if we failed above. *alreadyProcessed = true; return; } else if (supportedProperties.contains(propertyName)) { // do nothing, let the default handler import them. *alreadyProcessed = true; return; } // cache the unsupported property string, and remove any detail // which was added by the default handler for this property. *alreadyProcessed = true; QString unsupportedProperty = convertPropertyToString(property); m_tempUnsupportedProperties.append(unsupportedProperty); updatedDetails->clear(); } void CardDavVCardConverter::documentProcessed(const QVersitDocument &, QContact *c) { // the UID of the contact will be contained in the QContactGuid detail. QString uid = c->detail().guid(); if (uid.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "imported contact has no UID, discarding unsupported properties!"; } else { m_unsupportedProperties.insert(uid, m_tempUnsupportedProperties); } // get ready for the next import. m_tempUnsupportedProperties.clear(); } void CardDavVCardConverter::contactProcessed(const QContact &c, QVersitDocument *d) { // FN is a required field in vCard 3.0 and 4.0. Add it if it does not exist. bool foundFN = false; Q_FOREACH (const QVersitProperty &p, d->properties()) { if (p.name() == QStringLiteral("FN")) { foundFN = true; break; } } // N is also a required field in vCard 3.0. Add it if it does not exist. bool foundN = false; Q_FOREACH (const QVersitProperty &p, d->properties()) { if (p.name() == QStringLiteral("N")) { foundN = true; break; } } if (!foundFN || !foundN) { #ifdef USE_LIBCONTACTS QString displaylabel = SeasideCache::generateDisplayLabel(c); #else QContactName name = c.detail(); QString displaylabel = QStringList { name.firstName(), name.middleName(), name.lastName(), }.join(' '); #endif if (!foundFN) { QVersitProperty fnProp; fnProp.setName("FN"); fnProp.setValue(displaylabel); d->addProperty(fnProp); } if (!foundN) { QContactName name = c.detail(); #ifdef USE_LIBCONTACTS SeasideCache::decomposeDisplayLabel(displaylabel, &name); #endif if (name.firstName().isEmpty()) { // If we could not decompose the display label (e.g., only one token) // then just assume that the display label is a useful first name. name.setFirstName(displaylabel); } static const QStringList nvalue = { "", "", "", "", "" }; QVersitProperty nProp; nProp.setName("N"); nProp.setValueType(QVersitProperty::CompoundType); nProp.setValue(nvalue); d->addProperty(nProp); } } } void CardDavVCardConverter::detailProcessed(const QContact &, const QContactDetail &, const QVersitDocument &, QSet *, QList *, QList *toBeAdded) { static QStringList supportedProperties(supportedPropertyNames()); for (int i = toBeAdded->size() - 1; i >= 0; --i) { const QString propName = toBeAdded->at(i).name().toUpper(); if (!supportedProperties.contains(propName)) { // we don't support importing these properties, so we shouldn't // attempt to export them. toBeAdded->removeAt(i); } else if (propName == QStringLiteral("X-GENDER") && toBeAdded->at(i).value().toUpper() == QStringLiteral("UNSPECIFIED")) { // this is probably added "by default" since qtcontacts-sqlite always stores a gender. toBeAdded->removeAt(i); } } } CardDav::CardDav(Syncer *parent, const QString &serverUrl, const QString &addressbookPath, const QString &username, const QString &password) : QObject(parent) , q(parent) , m_converter(new CardDavVCardConverter) , m_request(new RequestGenerator(q, username, password)) , m_parser(new ReplyParser(q, m_converter)) , m_serverUrl(serverUrl) , m_addressbookPath(addressbookPath) , m_discoveryStage(CardDav::DiscoveryStarted) , m_addressbooksListOnly(false) , m_triedAddressbookPathAsHomeSetUrl(false) { } CardDav::CardDav(Syncer *parent, const QString &serverUrl, const QString &addressbookPath, const QString &accessToken) : QObject(parent) , q(parent) , m_converter(new CardDavVCardConverter) , m_request(new RequestGenerator(q, accessToken)) , m_parser(new ReplyParser(q, m_converter)) , m_serverUrl(serverUrl) , m_addressbookPath(addressbookPath) , m_discoveryStage(CardDav::DiscoveryStarted) , m_addressbooksListOnly(false) { } CardDav::~CardDav() { delete m_converter; delete m_parser; delete m_request; } void CardDav::errorOccurred(int httpError) { emit error(httpError); } void CardDav::determineAddressbooksList() { m_addressbooksListOnly = true; determineRemoteAMR(); } void CardDav::determineRemoteAMR() { if (m_addressbookPath.isEmpty()) { // The CardDAV sequence for determining the A/M/R delta is: // a) fetch user information from the principal URL // b) fetch addressbooks home url // c) fetch addressbook information // d) for each addressbook, either: // i) perform immediate delta sync (if webdav-sync enabled) OR // ii) fetch etags, manually calculate delta // e) fetch full contacts for delta. // We start by fetching user information. fetchUserInformation(); } else { // we can skip to step (c) of the discovery. fetchAddressbooksInformation(m_addressbookPath); } } void CardDav::fetchUserInformation() { qCDebug(lcCardDav) << Q_FUNC_INFO << "requesting principal urls for user"; // we need to specify the .well-known/carddav endpoint if it's the first // request (so we have not yet been redirected to the correct endpoint) // and if the path is empty/unknown. /* RFC 6764 section 6.5: * The client does a "PROPFIND" [RFC4918] request with the request URI set to the initial "context path". The body of the request SHOULD include the DAV:current-user-principal [RFC5397] property as one of the properties to return. Note that clients MUST properly handle HTTP redirect responses for the request. The server will use the HTTP authentication procedure outlined in [RFC2617] or use some other appropriate authentication schemes to authenticate the user. * When an initial "context path" has not been determined from a TXT record, the initial "context path" is taken to be "/.well-known/caldav" (for CalDAV) or "/.well-known/carddav" (for CardDAV). * If the server returns a 404 ("Not Found") HTTP status response to the request on the initial "context path", clients MAY try repeating the request on the "root" URI "/" or prompt the user for a suitable path. */ QUrl serverUrl(m_serverUrl); if (serverUrl.scheme().isEmpty() && (serverUrl.host().isEmpty() || serverUrl.path().isEmpty())) { // assume the supplied server url is like: "carddav.server.tld" m_serverUrl = QStringLiteral("https://%1/").arg(m_serverUrl); serverUrl = QUrl(m_serverUrl); } const QString wellKnownUrl = serverUrl.port() == -1 ? QStringLiteral("%1://%2/.well-known/carddav").arg(serverUrl.scheme()).arg(serverUrl.host()) : QStringLiteral("%1://%2:%3/.well-known/carddav").arg(serverUrl.scheme()).arg(serverUrl.host()).arg(serverUrl.port()); bool firstRequest = m_discoveryStage == CardDav::DiscoveryStarted; m_serverUrl = firstRequest && (serverUrl.path().isEmpty() || serverUrl.path() == QStringLiteral("/")) ? wellKnownUrl : m_serverUrl; QNetworkReply *reply = m_request->currentUserInformation(m_serverUrl); if (!reply) { emit error(); return; } connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(userInformationResponse())); } void CardDav::sslErrorsOccurred(const QList &errors) { QNetworkReply *reply = qobject_cast(sender()); if (q->m_ignoreSslErrors) { qCDebug(lcCardDav) << Q_FUNC_INFO << "ignoring SSL errors due to account policy:" << errors; reply->ignoreSslErrors(errors); } else { qCWarning(lcCardDav) << Q_FUNC_INFO << "SSL errors occurred, aborting:" << errors; errorOccurred(401); } } void CardDav::userInformationResponse() { QNetworkReply *reply = qobject_cast(sender()); const QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { int httpError = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCWarning(lcCardDav) << Q_FUNC_INFO << "error:" << reply->error() << "(" << httpError << ") to request" << m_serverUrl; debugDumpData(QString::fromUtf8(data)); QUrl oldServerUrl(m_serverUrl); if (m_discoveryStage == CardDav::DiscoveryStarted && (httpError == 404 || httpError == 405)) { if (!oldServerUrl.path().endsWith(QStringLiteral(".well-known/carddav"))) { // From RFC 6764: If the initial "context path" derived from a TXT record // generates HTTP errors when targeted by requests, the client // SHOULD repeat its "bootstrapping" procedure using the // appropriate ".well-known" URI instead. qCDebug(lcCardDav) << Q_FUNC_INFO << "got HTTP response" << httpError << "to initial discovery request; trying well-known URI"; m_serverUrl = oldServerUrl.port() == -1 ? QStringLiteral("%1://%2/.well-known/carddav").arg(oldServerUrl.scheme()).arg(oldServerUrl.host()) : QStringLiteral("%1://%2:%3/.well-known/carddav").arg(oldServerUrl.scheme()).arg(oldServerUrl.host()).arg(oldServerUrl.port()); fetchUserInformation(); // set initial context path to well-known URI. } else { // From RFC 6764: if the server returns a 404 HTTP status response to the // request on the initial context path, clients may try repeating the request // on the root URI. // We also do this on HTTP 405 in case some implementation is non-spec-conformant. qCDebug(lcCardDav) << Q_FUNC_INFO << "got HTTP response" << httpError << "to well-known request; trying root URI"; m_discoveryStage = CardDav::DiscoveryTryRoot; m_serverUrl = oldServerUrl.port() == -1 ? QStringLiteral("%1://%2/").arg(oldServerUrl.scheme()).arg(oldServerUrl.host()) : QStringLiteral("%1://%2:%3/").arg(oldServerUrl.scheme()).arg(oldServerUrl.host()).arg(oldServerUrl.port()); fetchUserInformation(); } return; } errorOccurred(httpError); return; } // if the request was to the /.well-known/carddav path, then we need to redirect QUrl redir = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); if (!redir.isEmpty()) { QUrl orig = reply->url(); // In case of a relative redirect, resolve it, so the code below does not have to take relative redirects into account redir = orig.resolved(redir); qCDebug(lcCardDav) << Q_FUNC_INFO << "server requested redirect from:" << orig.toString() << "to:" << redir.toString(); const bool hostChanged = orig.host() != redir.host(); const bool pathChanged = orig.path() != redir.path(); const bool schemeChanged = orig.scheme() != redir.scheme(); const bool portChanged = orig.port() != redir.port(); const bool validPathRedirect = orig.path().endsWith(QStringLiteral(".well-known/carddav")) || orig.path() == redir.path(); // e.g. scheme change. if (!hostChanged && !pathChanged && !schemeChanged && !portChanged) { // circular redirect, avoid the endless loop by aborting sync. qCWarning(lcCardDav) << Q_FUNC_INFO << "redirect specified is circular:" << redir.toString(); errorOccurred(301); } else if (hostChanged || !validPathRedirect) { // possibly unsafe redirect. for security, assume it's malicious and abort sync. qCWarning(lcCardDav) << Q_FUNC_INFO << "unexpected redirect from:" << orig.toString() << "to:" << redir.toString(); errorOccurred(301); } else { // redirect as required, and change our server URL to point to the redirect URL. qCDebug(lcCardDav) << Q_FUNC_INFO << "redirecting from:" << orig.toString() << "to:" << redir.toString(); m_serverUrl = redir.url(); m_discoveryStage = CardDav::DiscoveryRedirected; fetchUserInformation(); } return; } ReplyParser::ResponseType responseType = ReplyParser::UserPrincipalResponse; const QString userPath = m_parser->parseUserPrincipal(data, &responseType); if (responseType == ReplyParser::UserPrincipalResponse) { // the server responded with the expected user principal information. if (userPath.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "unable to parse user principal from response"; emit error(); return; } fetchAddressbookUrls(userPath); } else if (responseType == ReplyParser::AddressbookInformationResponse) { // the server responded with addressbook information instead // of user principal information. Skip the next discovery step. QList infos = m_parser->parseAddressbookInformation(data, QString()); if (infos.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "unable to parse addressbook info from user principal response"; emit error(); return; } emit addressbooksList(infos); } else { qCWarning(lcCardDav) << Q_FUNC_INFO << "unknown response from user principal request"; emit error(); } } void CardDav::fetchAddressbookUrls(const QString &userPath) { qCDebug(lcCardDav) << Q_FUNC_INFO << "requesting addressbook urls for user"; QNetworkReply *reply = m_request->addressbookUrls(m_serverUrl, userPath); if (!reply) { emit error(); return; } connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(addressbookUrlsResponse())); } void CardDav::addressbookUrlsResponse() { QNetworkReply *reply = qobject_cast(sender()); const QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { int httpError = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCWarning(lcCardDav) << Q_FUNC_INFO << "error:" << reply->error() << "(" << httpError << ")"; debugDumpData(QString::fromUtf8(data)); errorOccurred(httpError); return; } const QString addressbooksHomePath = m_parser->parseAddressbookHome(data); if (addressbooksHomePath.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "unable to parse addressbook home from response"; emit error(); return; } fetchAddressbooksInformation(addressbooksHomePath); } void CardDav::fetchAddressbooksInformation(const QString &addressbooksHomePath) { qCDebug(lcCardDav) << Q_FUNC_INFO << "requesting addressbook sync information from" << addressbooksHomePath; QNetworkReply *reply = m_request->addressbooksInformation(m_serverUrl, addressbooksHomePath); reply->setProperty("addressbooksHomePath", addressbooksHomePath); if (!reply) { emit error(); return; } connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(addressbooksInformationResponse())); } void CardDav::addressbooksInformationResponse() { QNetworkReply *reply = qobject_cast(sender()); QString addressbooksHomePath = reply->property("addressbooksHomePath").toString(); const QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { int httpError = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCWarning(lcCardDav) << Q_FUNC_INFO << "error:" << reply->error() << "(" << httpError << ")"; debugDumpData(QString::fromUtf8(data)); errorOccurred(httpError); return; } // if we didn't parse the addressbooks home path via discovery, but instead were provided it by the user, // then don't pass the path to the parser, as it uses it for cycle detection. if (m_addressbookPath == addressbooksHomePath) { addressbooksHomePath = QString(); } QList infos = m_parser->parseAddressbookInformation(data, addressbooksHomePath); if (infos.isEmpty()) { if (!m_addressbookPath.isEmpty() && !m_triedAddressbookPathAsHomeSetUrl) { // the user provided an addressbook path during account creation, which didn't work. // it may not be an addressbook path but instead the home set url; try that. qCDebug(lcCardDav) << Q_FUNC_INFO << "Given path is not addressbook path; trying as home set url"; m_triedAddressbookPathAsHomeSetUrl = true; fetchAddressbookUrls(m_addressbookPath); } else { qCWarning(lcCardDav) << Q_FUNC_INFO << "unable to parse addressbook info from response"; emit error(); } } else { emit addressbooksList(infos); } } bool CardDav::downsyncAddressbookContent( const QString &addressbookUrl, const QString &newSyncToken, const QString &newCtag, const QString &oldSyncToken, const QString &oldCtag) { if (newSyncToken.isEmpty() && newCtag.isEmpty()) { // we cannot use either sync-token or ctag for this addressbook. // we need to manually calculate the complete delta. qCDebug(lcCardDav) << "No sync-token or ctag given for addressbook:" << addressbookUrl << ", manual delta detection required"; return fetchContactMetadata(addressbookUrl); } else if (newSyncToken.isEmpty()) { // we cannot use sync-token for this addressbook, but instead ctag. if (oldCtag.isEmpty()) { // first time sync // do etag request, the delta will be all remote additions return fetchContactMetadata(addressbookUrl); } else if (oldCtag != newCtag) { // changes have occurred since last sync // perform etag request and then manually calculate deltas. return fetchContactMetadata(addressbookUrl); } else { // no changes have occurred in this addressbook since last sync qCDebug(lcCardDav) << Q_FUNC_INFO << "no changes since last sync for" << addressbookUrl << "from account" << q->m_accountId; QTimer::singleShot(0, this, [this, addressbookUrl] () { calculateContactChanges(addressbookUrl, QList(), QList()); }); return true; } } else { // the server supports webdav-sync for this addressbook. // attempt to perform synctoken sync if (oldSyncToken.isEmpty()) { // first time sync // perform slow sync / full report return fetchContactMetadata(addressbookUrl); } else if (oldSyncToken != newSyncToken) { // changes have occurred since last sync. // perform immediate delta sync, by passing the old sync token to the server. return fetchImmediateDelta(addressbookUrl, oldSyncToken); } else { // no changes have occurred in this addressbook since last sync qCDebug(lcCardDav) << Q_FUNC_INFO << "no changes since last sync for" << addressbookUrl << "from account" << q->m_accountId; QTimer::singleShot(0, this, [this, addressbookUrl] () { calculateContactChanges(addressbookUrl, QList(), QList()); }); return true; } } } bool CardDav::fetchImmediateDelta(const QString &addressbookUrl, const QString &syncToken) { qCDebug(lcCardDav) << Q_FUNC_INFO << "requesting immediate delta for addressbook" << addressbookUrl << "with sync token" << syncToken; QNetworkReply *reply = m_request->syncTokenDelta(m_serverUrl, addressbookUrl, syncToken); if (!reply) { return false; } reply->setProperty("addressbookUrl", addressbookUrl); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(immediateDeltaResponse())); return true; } void CardDav::immediateDeltaResponse() { QNetworkReply *reply = qobject_cast(sender()); const QString addressbookUrl = reply->property("addressbookUrl").toString(); const QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { qCWarning(lcCardDav) << Q_FUNC_INFO << "error:" << reply->error() << "(" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ")"; debugDumpData(QString::fromUtf8(data)); // The server is allowed to forget the syncToken by the // carddav protocol. Try a full report sync just in case. fetchContactMetadata(addressbookUrl); return; } QString newSyncToken; QList infos = m_parser->parseSyncTokenDelta(data, addressbookUrl, &newSyncToken); QContactCollection addressbook = q->m_currentCollections[addressbookUrl]; addressbook.setExtendedMetaData(KEY_SYNCTOKEN, newSyncToken); q->m_currentCollections.insert(addressbookUrl, addressbook); fetchContacts(addressbookUrl, infos); } bool CardDav::fetchContactMetadata(const QString &addressbookUrl) { qCDebug(lcCardDav) << Q_FUNC_INFO << "requesting contact metadata for addressbook" << addressbookUrl; QNetworkReply *reply = m_request->contactEtags(m_serverUrl, addressbookUrl); if (!reply) { return false; } reply->setProperty("addressbookUrl", addressbookUrl); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(contactMetadataResponse())); return true; } void CardDav::contactMetadataResponse() { QNetworkReply *reply = qobject_cast(sender()); const QString addressbookUrl = reply->property("addressbookUrl").toString(); const QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { int httpError = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCWarning(lcCardDav) << Q_FUNC_INFO << "error:" << reply->error() << "(" << httpError << ")"; debugDumpData(QString::fromUtf8(data)); errorOccurred(httpError); return; } // if we are determining contact changes (i.e. delta) then we will // have local contact AMRU information cached for this addressbook. // build a cache list of the old etags of the still-existent contacts. QHash uriToEtag; if (q->m_collectionAMRU.contains(addressbookUrl)) { auto createHash = [&uriToEtag] (const QList &contacts) { for (const QContact &c : contacts) { const QString uri = c.detail().syncTarget(); if (uri.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << ": carddav contact has empty sync target (uri): " << QString::fromLatin1(c.id().localId()); } else { const QList dets = c.details(); for (const QContactExtendedDetail &d : dets) { if (d.name() == KEY_ETAG) { uriToEtag.insert(uri, d.data().toString()); break; } } } } }; createHash(q->m_collectionAMRU[addressbookUrl].modified); createHash(q->m_collectionAMRU[addressbookUrl].unmodified); } QList infos = m_parser->parseContactMetadata(data, addressbookUrl, uriToEtag); fetchContacts(addressbookUrl, infos); } void CardDav::fetchContacts(const QString &addressbookUrl, const QList &amrInfo) { qCDebug(lcCardDav) << Q_FUNC_INFO << "requesting full contact information from addressbook" << addressbookUrl; // split into A/M/R/U request sets QStringList contactUris; Q_FOREACH (const ReplyParser::ContactInformation &info, amrInfo) { if (info.modType == ReplyParser::ContactInformation::Addition) { q->m_remoteAdditions[addressbookUrl].insert(info.uri, info); contactUris.append(info.uri); } else if (info.modType == ReplyParser::ContactInformation::Modification) { q->m_remoteModifications[addressbookUrl].insert(info.uri, info); contactUris.append(info.uri); } else if (info.modType == ReplyParser::ContactInformation::Deletion) { q->m_remoteRemovals[addressbookUrl].insert(info.uri, info); } else if (info.modType == ReplyParser::ContactInformation::Unmodified) { q->m_remoteUnmodified[addressbookUrl].insert(info.uri, info); } else { qCWarning(lcCardDav) << Q_FUNC_INFO << "no modification type in info for:" << info.uri; } } qCDebug(lcCardDav) << Q_FUNC_INFO << "Have calculated A/M/R/U:" << q->m_remoteAdditions[addressbookUrl].size() << "/" << q->m_remoteModifications[addressbookUrl].size() << "/" << q->m_remoteRemovals[addressbookUrl].size() << "/" << q->m_remoteUnmodified[addressbookUrl].size() << "for addressbook:" << addressbookUrl; if (contactUris.isEmpty()) { // no additions or modifications to fetch. qCDebug(lcCardDav) << Q_FUNC_INFO << "no further data to fetch"; calculateContactChanges(addressbookUrl, QList(), QList()); } else { // fetch the full contact data for additions/modifications. qCDebug(lcCardDav) << Q_FUNC_INFO << "fetching vcard data for" << contactUris.size() << "contacts"; QNetworkReply *reply = m_request->contactMultiget(m_serverUrl, addressbookUrl, contactUris); if (!reply) { emit error(); return; } reply->setProperty("addressbookUrl", addressbookUrl); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(contactsResponse())); } } void CardDav::contactsResponse() { QNetworkReply *reply = qobject_cast(sender()); const QString addressbookUrl = reply->property("addressbookUrl").toString(); const QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { int httpError = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCWarning(lcCardDav) << Q_FUNC_INFO << "error:" << reply->error() << "(" << httpError << ")"; debugDumpData(QString::fromUtf8(data)); errorOccurred(httpError); return; } QList added; QList modified; const QHash addMods = m_parser->parseContactData(data, addressbookUrl); QHash::const_iterator it = addMods.constBegin(), end = addMods.constEnd(); for ( ; it != end; ++it) { const QString contactUri = it.key(); if (q->m_remoteAdditions[addressbookUrl].contains(contactUri)) { added.append(it.value()); } else if (q->m_remoteModifications[addressbookUrl].contains(contactUri)) { modified.append(it.value()); } else { qCWarning(lcCardDav) << Q_FUNC_INFO << "ignoring unknown addition/modification:" << contactUri; } } calculateContactChanges(addressbookUrl, added, modified); } void CardDav::calculateContactChanges(const QString &addressbookUrl, const QList &added, const QList &modified) { // at this point, we have already retrieved the added+modified contacts from the server. // we need to populate the removed contacts list, by inspecting the local data. if (!q->m_collectionAMRU.contains(addressbookUrl)) { Q_ASSERT(modified.isEmpty()); q->remoteContactsDetermined(q->m_currentCollections[addressbookUrl], added); } else { QList removed; const Syncer::AMRU amru = q->m_collectionAMRU.take(addressbookUrl); auto appendMatches = [] (const QList &contacts, const QHash > &hash, QList *list) { for (const QContact &c : contacts) { const QString uri = c.detail().syncTarget(); if (!uri.isEmpty() && hash.contains(uri)) { list->append(c); } } }; appendMatches(amru.added, q->m_remoteRemovals, &removed); appendMatches(amru.modified, q->m_remoteRemovals, &removed); appendMatches(amru.removed, q->m_remoteRemovals, &removed); appendMatches(amru.unmodified, q->m_remoteRemovals, &removed); // we also need to find the local ids associated with the modified contacts. QList modifiedWithIds = modified; for (int i = 0; i < modifiedWithIds.size(); ++i) { QContact &c(modifiedWithIds[i]); QContactId matchingId = matchingContactFromList(c, amru.added); if (matchingId.isNull()) matchingId = matchingContactFromList(c, amru.modified); if (matchingId.isNull()) matchingId = matchingContactFromList(c, amru.removed); if (matchingId.isNull()) matchingId = matchingContactFromList(c, amru.unmodified); if (!matchingId.isNull()) { c.setId(matchingId); } } // TODO: also match remotely added to locally added, to find partial upsync artifacts. q->remoteContactChangesDetermined(q->m_currentCollections[addressbookUrl], added, modifiedWithIds, removed); } } static void setContactGuid(QContact *c, const QString &uid) { QContactGuid newGuid = c->detail(); newGuid.setGuid(uid); c->saveDetail(&newGuid, QContact::IgnoreAccessConstraints); } bool CardDav::upsyncUpdates(const QString &addressbookUrl, const QList &added, const QList &modified, const QList &removed) { qCDebug(lcCardDav) << Q_FUNC_INFO << "upsyncing updates to addressbook:" << addressbookUrl << ":" << added.count() << modified.count() << removed.count(); bool hadNonSpuriousChanges = false; int spuriousModifications = 0; m_upsyncRequests.insert(addressbookUrl, 0); if (added.size() || modified.size()) { m_upsyncedChanges.insert(addressbookUrl, UpsyncedContacts()); } // put local additions for (int i = 0; i < added.size(); ++i) { QContact c = added.at(i); // generate a server-side uid. this does NOT contain addressbook prefix etc. const QString uid = QUuid::createUuid().toString().replace(QRegularExpression(QStringLiteral("[\\-{}]")), QString()); // set the uid so that the VCF UID is generated. setContactGuid(&c, uid); // generate a valid uri const QString uri = addressbookUrl + (addressbookUrl.endsWith('/') ? QString() : QStringLiteral("/")) + uid + QStringLiteral(".vcf"); QContactSyncTarget st = c.detail(); st.setSyncTarget(uri); c.saveDetail(&st, QContact::IgnoreAccessConstraints); // ensure that we haven't already upsynced this one previously, i.e. partial upsync artifact if (q->m_remoteAdditions[addressbookUrl].contains(uri) || q->m_remoteModifications[addressbookUrl].contains(uri) || q->m_remoteRemovals[addressbookUrl].contains(uri) || q->m_remoteUnmodified[addressbookUrl].contains(uri)) { // this contact was previously upsynced already. continue; } // generate a vcard const QString vcard = m_converter->convertContactToVCard(c, QStringList()); // upload QNetworkReply *reply = m_request->upsyncAddMod(m_serverUrl, uri, QString(), vcard); if (!reply) { return false; } // set the addressbook-prefixed guid into the contact. const QString guid = QStringLiteral("%1:AB:%2:%3").arg(QString::number(q->m_accountId), addressbookUrl, uid); setContactGuid(&c, guid); // cached the updated contact, as it will eventually be written back to the local database with updated guid + etag. m_upsyncedChanges[addressbookUrl].additions.append(c); m_upsyncRequests[addressbookUrl] += 1; hadNonSpuriousChanges = true; reply->setProperty("addressbookUrl", addressbookUrl); reply->setProperty("contactGuid", guid); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(upsyncResponse())); } // put local modifications for (int i = 0; i < modified.size(); ++i) { QContact c = modified.at(i); // reinstate the server-side UID into the guid detail for upsync const QString guidstr = c.detail().guid(); const QString uidPrefix = QStringLiteral("%1:AB:%2:").arg(QString::number(q->m_accountId), addressbookUrl); if (guidstr.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "modified contact has no guid:" << c.id().toString(); continue; // TODO: this is actually an error. } else if (!guidstr.startsWith(uidPrefix)) { qCWarning(lcCardDav) << Q_FUNC_INFO << "modified contact: " << QString::fromLatin1(c.id().localId()) << "has guid with invalid form: " << guidstr; continue; // TODO: this is actually an error. } else { const QString uidstr = guidstr.mid(uidPrefix.size()); setContactGuid(&c, uidstr); } QString etag; for (const QContactExtendedDetail &ed : c.details()) { if (ed.name() == KEY_ETAG) { etag = ed.data().toString(); break; } } QStringList unsupportedProperties; for (const QContactExtendedDetail &ed : c.details()) { if (ed.name() == KEY_UNSUPPORTEDPROPERTIES) { unsupportedProperties = ed.data().toStringList(); break; } } // convert to vcard and upsync to remote server. const QString uri = c.detail().syncTarget(); const QString vcard = m_converter->convertContactToVCard(c, unsupportedProperties); // upload QNetworkReply *reply = m_request->upsyncAddMod(m_serverUrl, uri, etag, vcard); if (!reply) { return false; } // cached the updated contact, as it will eventually be written back to the local database with updated guid + etag. setContactGuid(&c, guidstr); m_upsyncedChanges[addressbookUrl].modifications.append(c); m_upsyncRequests[addressbookUrl] += 1; hadNonSpuriousChanges = true; reply->setProperty("addressbookUrl", addressbookUrl); reply->setProperty("contactGuid", guidstr); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(upsyncResponse())); } // delete local removals for (int i = 0; i < removed.size(); ++i) { QContact c = removed[i]; const QString guidstr = c.detail().guid(); const QString uri = c.detail().syncTarget(); if (uri.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "deleted contact server uri unknown:" << QString::fromLatin1(c.id().localId()) << " - " << guidstr; continue; // TODO: this is actually an error. } QString etag; for (const QContactExtendedDetail &ed : c.details()) { if (ed.name() == KEY_ETAG) { etag = ed.data().toString(); break; } } QNetworkReply *reply = m_request->upsyncDeletion(m_serverUrl, uri, etag); if (!reply) { return false; } m_upsyncRequests[addressbookUrl] += 1; hadNonSpuriousChanges = true; reply->setProperty("addressbookUrl", addressbookUrl); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(sslErrorsOccurred(QList))); connect(reply, SIGNAL(finished()), this, SLOT(upsyncResponse())); } if (!hadNonSpuriousChanges || (added.size() == 0 && modified.size() == 0 && removed.size() == 0)) { // nothing to upsync. Use a singleshot to avoid synchronously // decrementing the m_upsyncRequests count to zero if there // happens to be nothing to upsync to the first addressbook. m_upsyncRequests[addressbookUrl] += 1; QMetaObject::invokeMethod(this, "upsyncComplete", Qt::QueuedConnection, Q_ARG(QString, addressbookUrl)); } // clear our caches of info for this addressbook, no longer required. q->m_remoteAdditions.remove(addressbookUrl); q->m_remoteModifications.remove(addressbookUrl); q->m_remoteRemovals.remove(addressbookUrl); q->m_remoteUnmodified.remove(addressbookUrl); qCDebug(lcCardDav) << Q_FUNC_INFO << "ignored" << spuriousModifications << "spurious updates to addressbook:" << addressbookUrl; return true; } void CardDav::upsyncResponse() { QNetworkReply *reply = qobject_cast(sender()); const QString addressbookUrl = reply->property("addressbookUrl").toString(); const QString guid = reply->property("contactGuid").toString(); const QByteArray data = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { int httpError = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCWarning(lcCardDav) << Q_FUNC_INFO << "error:" << reply->error() << "(" << httpError << ")"; debugDumpData(QString::fromUtf8(data)); if (httpError == 405) { // MethodNotAllowed error. Most likely the server has restricted // new writes to the collection (e.g., read-only or update-only). // We should not abort the sync if we receive this error. qCWarning(lcCardDav) << Q_FUNC_INFO << "405 MethodNotAllowed - is the collection read-only?"; qCWarning(lcCardDav) << Q_FUNC_INFO << "continuing sync despite this error - upsync will have failed!"; } else { errorOccurred(httpError); return; } } if (!guid.isEmpty()) { // this is an addition or modification. // get the new etag value reported by the server. QString etag; Q_FOREACH(const QByteArray &header, reply->rawHeaderList()) { if (QString::fromUtf8(header).contains(QLatin1String("etag"), Qt::CaseInsensitive)) { etag = reply->rawHeader(header); break; } } if (!etag.isEmpty()) { qCDebug(lcCardDav) << "Got updated etag for" << guid << ":" << etag; // store the updated etag into the upsynced contact auto updateEtag = [this, &guid, etag] (QList &upsynced) { for (int i = upsynced.size() - 1; i >= 0; --i) { if (upsynced[i].detail().guid() == guid) { QContactExtendedDetail etagDetail; for (const QContactExtendedDetail &ed : upsynced[i].details()) { if (ed.name() == KEY_ETAG) { etagDetail = ed; break; } } etagDetail.setName(KEY_ETAG); etagDetail.setData(etag); upsynced[i].saveDetail(&etagDetail, QContact::IgnoreAccessConstraints); break; } } }; updateEtag(m_upsyncedChanges[addressbookUrl].additions); updateEtag(m_upsyncedChanges[addressbookUrl].modifications); } else { // If we don't perform an additional request, the etag server-side will be different to the etag // we have locally, and thus on next sync we would spuriously detect a server-side modification. // That's ok, we'll just detect that it's spurious via data inspection during the next sync. qCWarning(lcCardDav) << "No updated etag provided for" << guid << ": will be reported as spurious remote modification next sync"; } } upsyncComplete(addressbookUrl); } void CardDav::upsyncComplete(const QString &addressbookUrl) { m_upsyncRequests[addressbookUrl] -= 1; if (m_upsyncRequests[addressbookUrl] == 0) { // finished upsyncing all data for the addressbook. qCDebug(lcCardDav) << Q_FUNC_INFO << "upsync complete for addressbook: " << addressbookUrl; // TODO: perform another request to get the ctag/synctoken after updates have been upsynced? q->localChangesStoredRemotely( q->m_currentCollections[addressbookUrl], m_upsyncedChanges[addressbookUrl].additions, m_upsyncedChanges[addressbookUrl].modifications); m_upsyncedChanges.remove(addressbookUrl); q->m_previousCtagSyncToken.remove(addressbookUrl); q->m_currentCollections.remove(addressbookUrl); q->m_localContactUrisEtags.remove(addressbookUrl); } } buteo-sync-plugin-carddav-0.1.12/src/carddav.xml000066400000000000000000000003521475112063300215000ustar00rootroot00000000000000 buteo-sync-plugin-carddav-0.1.12/src/carddav_p.h000066400000000000000000000130211475112063300214430ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CARDDAV_P_H #define CARDDAV_P_H #include "requestgenerator_p.h" #include "replyparser_p.h" #include #include #include #include #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE QTVERSIT_USE_NAMESPACE class Syncer; class CardDavVCardConverter; class CardDav : public QObject { Q_OBJECT public: CardDav(Syncer *parent, const QString &serverUrl, const QString &addressbookPath, const QString &username, const QString &password); CardDav(Syncer *parent, const QString &serverUrl, const QString &addressbookPath, const QString &accessToken); ~CardDav(); void determineAddressbooksList(); bool downsyncAddressbookContent( const QString &addressbookUrl, const QString &newSyncToken, const QString &newCtag, const QString &oldSyncToken, const QString &oldCtag); bool upsyncUpdates(const QString &addressbookUrl, const QList &added, const QList &modified, const QList &removed); Q_SIGNALS: void error(int errorCode = 0); void remoteChanges(const QList &added, const QList &modified, const QList &removed); void upsyncCompleted(); void addressbooksList(const QList &paths); private: void determineRemoteAMR(); void fetchUserInformation(); void fetchAddressbookUrls(const QString &userPath); void fetchAddressbooksInformation(const QString &addressbooksHomePath); bool fetchImmediateDelta(const QString &addressbookUrl, const QString &syncToken); bool fetchContactMetadata(const QString &addressbookUrl); void fetchContacts(const QString &addressbookUrl, const QList &amrInfo); private Q_SLOTS: void sslErrorsOccurred(const QList &errors); void userInformationResponse(); void addressbookUrlsResponse(); void addressbooksInformationResponse(); void immediateDeltaResponse(); void contactMetadataResponse(); void contactsResponse(); void upsyncResponse(); void upsyncComplete(const QString &addressbookUrl); void errorOccurred(int httpError); private: void calculateContactChanges(const QString &addressbookUrl, const QList &added, const QList &modified); enum DiscoveryStage { DiscoveryStarted = 0, DiscoveryRedirected, DiscoveryTryRoot }; Syncer *q; CardDavVCardConverter *m_converter; RequestGenerator *m_request; ReplyParser *m_parser; QString m_serverUrl; QString m_addressbookPath; DiscoveryStage m_discoveryStage; bool m_addressbooksListOnly; bool m_triedAddressbookPathAsHomeSetUrl; struct UpsyncedContacts { QList additions; QList modifications; }; QHash m_upsyncedChanges; QHash m_upsyncRequests; }; class CardDavVCardConverter : public QVersitContactImporterPropertyHandlerV2, public QVersitContactExporterDetailHandlerV2 { public: CardDavVCardConverter(); ~CardDavVCardConverter(); // QVersitContactImporterPropertyHandlerV2 void propertyProcessed(const QVersitDocument &d, const QVersitProperty &property, const QContact &c, bool *alreadyProcessed, QList *updatedDetails); void documentProcessed(const QVersitDocument &d, QContact *c); // QVersitContactExporterDetailHandlerV2 void contactProcessed(const QContact &c, QVersitDocument *d); void detailProcessed(const QContact &c, const QContactDetail &detail, const QVersitDocument &d, QSet *processedFields, QList *toBeRemoved, QList *toBeAdded); // API exposed to clients QPair convertVCardToContact(const QString &vcard, bool *ok); QString convertContactToVCard(const QContact &c, const QStringList &unsupportedProperties); private: static QStringList supportedPropertyNames(); QString convertPropertyToString(const QVersitProperty &p) const; QMap m_unsupportedProperties; // uid -> unsupported properties QStringList m_tempUnsupportedProperties; }; #endif // CARDDAV_P_H buteo-sync-plugin-carddav-0.1.12/src/carddavclient.cpp000066400000000000000000000121271475112063300226640ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 - 2021 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "carddavclient.h" #include "syncer_p.h" #include #include "logging.h" #include #include Buteo::ClientPlugin* CardDavClientLoader::createClientPlugin( const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) { return new CardDavClient(pluginName, profile, cbInterface); } CardDavClient::CardDavClient(const QString& aPluginName, const Buteo::SyncProfile& aProfile, Buteo::PluginCbInterface *aCbInterface) : ClientPlugin(aPluginName, aProfile, aCbInterface) , m_syncer(0) , m_accountId(0) { FUNCTION_CALL_TRACE(lcCardDavTrace); } CardDavClient::~CardDavClient() { FUNCTION_CALL_TRACE(lcCardDavTrace); } void CardDavClient::connectivityStateChanged(Sync::ConnectivityType aType, bool aState) { FUNCTION_CALL_TRACE(lcCardDavTrace); qCDebug(lcCardDav) << "Received connectivity change event:" << aType << " changed to " << aState; if (aType == Sync::CONNECTIVITY_INTERNET && !aState) { // we lost connectivity during sync. abortSync(Buteo::SyncResults::CONNECTION_ERROR); } } bool CardDavClient::init() { FUNCTION_CALL_TRACE(lcCardDavTrace); QString accountIdString = iProfile.key(Buteo::KEY_ACCOUNT_ID); m_accountId = accountIdString.toInt(); if (m_accountId == 0) { qCCritical(lcCardDav) << "profile does not specify" << Buteo::KEY_ACCOUNT_ID; return false; } m_syncDirection = iProfile.syncDirection(); m_conflictResPolicy = iProfile.conflictResolutionPolicy(); if (!m_syncer) { m_syncer = new Syncer(this, &iProfile, m_accountId); connect(m_syncer, SIGNAL(syncSucceeded()), this, SLOT(syncSucceeded())); connect(m_syncer, SIGNAL(syncFailed()), this, SLOT(syncFailed())); } return true; } bool CardDavClient::uninit() { FUNCTION_CALL_TRACE(lcCardDavTrace); delete m_syncer; m_syncer = 0; return true; } bool CardDavClient::startSync() { FUNCTION_CALL_TRACE(lcCardDavTrace); if (m_accountId == 0) return false; m_syncer->startSync(m_accountId); return true; } void CardDavClient::syncSucceeded() { syncFinished(Buteo::SyncResults::NO_ERROR, QString()); } void CardDavClient::syncFailed() { syncFinished(Buteo::SyncResults::INTERNAL_ERROR, QString()); } void CardDavClient::abortSync(Buteo::SyncResults::MinorCode code) { FUNCTION_CALL_TRACE(lcCardDavTrace); m_syncer->abortSync(); syncFinished(code, QStringLiteral("Sync aborted")); } void CardDavClient::syncFinished(Buteo::SyncResults::MinorCode minorErrorCode, const QString &message) { FUNCTION_CALL_TRACE(lcCardDavTrace); if (minorErrorCode == Buteo::SyncResults::NO_ERROR) { qCDebug(lcCardDav) << "CardDAV sync succeeded!" << message; m_results = Buteo::SyncResults(QDateTime::currentDateTimeUtc(), Buteo::SyncResults::SYNC_RESULT_SUCCESS, Buteo::SyncResults::NO_ERROR); emit success(getProfileName(), message); } else { qCCritical(lcCardDav) << "CardDAV sync failed:" << minorErrorCode << message; m_results = Buteo::SyncResults(iProfile.lastSuccessfulSyncTime(), // don't change the last sync time Buteo::SyncResults::SYNC_RESULT_FAILED, minorErrorCode); emit error(getProfileName(), message, minorErrorCode); } } Buteo::SyncResults CardDavClient::getSyncResults() const { FUNCTION_CALL_TRACE(lcCardDavTrace); return m_results; } bool CardDavClient::cleanUp() { FUNCTION_CALL_TRACE(lcCardDavTrace); // This function is called after the account has been deleted. QString accountIdString = iProfile.key(Buteo::KEY_ACCOUNT_ID); m_accountId = accountIdString.toInt(); if (m_accountId == 0) { qCCritical(lcCardDav) << "profile does not specify" << Buteo::KEY_ACCOUNT_ID; return false; } if (!m_syncer) m_syncer = new Syncer(this, &iProfile, m_accountId); m_syncer->purgeAccount(m_accountId); delete m_syncer; m_syncer = 0; return true; } buteo-sync-plugin-carddav-0.1.12/src/carddavclient.h000066400000000000000000000057231475112063300223350ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 - 2021 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef CARDDAVCLIENT_H #define CARDDAVCLIENT_H #include #include #include #include #include #include #include class Syncer; class Q_DECL_EXPORT CardDavClient : public Buteo::ClientPlugin { Q_OBJECT public: CardDavClient(const QString &aPluginName, const Buteo::SyncProfile &aProfile, Buteo::PluginCbInterface *aCbInterface); ~CardDavClient(); bool init(); bool uninit(); bool startSync(); Buteo::SyncResults getSyncResults() const; bool cleanUp(); public Q_SLOTS: void connectivityStateChanged(Sync::ConnectivityType aType, bool aState); private Q_SLOTS: void syncSucceeded(); void syncFailed(); private: void abortSync(Buteo::SyncResults::MinorCode code); void syncFinished(Buteo::SyncResults::MinorCode minorErrorCode, const QString &message); Buteo::SyncProfile::SyncDirection syncDirection(); Buteo::SyncProfile::ConflictResolutionPolicy conflictResolutionPolicy(); Sync::SyncStatus m_syncStatus; Buteo::SyncResults m_results; Buteo::SyncProfile::SyncDirection m_syncDirection; Buteo::SyncProfile::ConflictResolutionPolicy m_conflictResPolicy; Syncer* m_syncer; int m_accountId; }; class CardDavClientLoader : public Buteo::SyncPluginLoader { Q_OBJECT Q_PLUGIN_METADATA(IID "org.sailfishos.plugins.sync.CardDavClientLoader") Q_INTERFACES(Buteo::SyncPluginLoader) public: /*! \brief Creates CardDav client plugin * * @param aPluginName Name of this client plugin * @param aProfile Profile to use * @param aCbInterface Pointer to the callback interface * @return Client plugin on success, otherwise NULL */ Buteo::ClientPlugin* createClientPlugin(const QString& pluginName, const Buteo::SyncProfile& profile, Buteo::PluginCbInterface* cbInterface) override; }; #endif // CARDDAVCLIENT_H buteo-sync-plugin-carddav-0.1.12/src/logging.cpp000066400000000000000000000020131475112063300215000ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2021 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #include "logging.h" Q_LOGGING_CATEGORY(lcCardDav, "buteo.plugin.carddav", QtWarningMsg) Q_LOGGING_CATEGORY(lcCardDavProtocol, "buteo.plugin.carddav.protocol", QtWarningMsg) Q_LOGGING_CATEGORY(lcCardDavTrace, "buteo.plugin.carddav.trace", QtWarningMsg) buteo-sync-plugin-carddav-0.1.12/src/logging.h000066400000000000000000000021571475112063300211560ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2021 Jolla Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * */ #ifndef CARDDAV_LOGGING_H #define CARDDAV_LOGGING_H #include #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) #include #else #include #endif Q_DECLARE_LOGGING_CATEGORY(lcCardDav) Q_DECLARE_LOGGING_CATEGORY(lcCardDavProtocol) Q_DECLARE_LOGGING_CATEGORY(lcCardDavTrace) #endif buteo-sync-plugin-carddav-0.1.12/src/replyparser.cpp000066400000000000000000001143471475112063300224400ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "replyparser_p.h" #include "syncer_p.h" #include "carddav_p.h" #include "logging.h" #include #include #include #include #include #include #include #include namespace { void debugDumpData(const QString &data) { if (!lcCardDavProtocol().isDebugEnabled()) { return; } QString dbgout; Q_FOREACH (const QChar &c, data) { if (c == '\r' || c == '\n') { if (!dbgout.isEmpty()) { qCDebug(lcCardDavProtocol) << dbgout; dbgout.clear(); } } else { dbgout += c; } } if (!dbgout.isEmpty()) { qCDebug(lcCardDavProtocol) << dbgout; } } QVariantMap elementToVMap(QXmlStreamReader &reader) { QVariantMap element; // store the attributes of the element QXmlStreamAttributes attrs = reader.attributes(); while (attrs.size()) { QXmlStreamAttribute attr = attrs.takeFirst(); element.insert(attr.name().toString(), attr.value().toString()); } while (reader.readNext() != QXmlStreamReader::EndElement) { if (reader.isCharacters()) { // store the text of the element, if any QString elementText = reader.text().toString(); if (!elementText.isEmpty()) { element.insert(QLatin1String("@text"), elementText); } } else if (reader.isStartElement()) { // recurse if necessary. QString subElementName = reader.name().toString(); QVariantMap subElement = elementToVMap(reader); if (element.contains(subElementName)) { // already have an element with this name. // create a variantlist and append. QVariant existing = element.value(subElementName); QVariantList subElementList; if (existing.type() == QVariant::Map) { // we need to convert the value into a QVariantList subElementList << existing.toMap(); } else if (existing.type() == QVariant::List) { subElementList = existing.toList(); } subElementList << subElement; element.insert(subElementName, subElementList); } else { // first element with this name. insert as a map. element.insert(subElementName, subElement); } } } return element; } QVariantMap xmlToVMap(QXmlStreamReader &reader) { QVariantMap retn; while (!reader.atEnd() && !reader.hasError() && reader.readNextStartElement()) { QString elementName = reader.name().toString(); QVariantMap element = elementToVMap(reader); retn.insert(elementName, element); } return retn; } } ReplyParser::ReplyParser(Syncer *parent, CardDavVCardConverter *converter) : q(parent), m_converter(converter) { } ReplyParser::~ReplyParser() { } QString ReplyParser::parseUserPrincipal(const QByteArray &userInformationResponse, ReplyParser::ResponseType *responseType) const { /* We expect a response of the form: HTTP/1.1 207 Multi-status Content-Type: application/xml; charset=utf-8 / /principals/users/johndoe/ HTTP/1.1 200 OK Note however that some CardDAV servers return addressbook information instead of user principal information. */ debugDumpData(QString::fromUtf8(userInformationResponse)); QXmlStreamReader reader(userInformationResponse); QVariantMap vmap = xmlToVMap(reader); QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { // This should not be the case for a UserPrincipal response. *responseType = ReplyParser::AddressbookInformationResponse; return QString(); } // Only one response - this could be either a UserPrincipal response // or an AddressbookInformation response. QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); QString statusText = response.value("propstat").toMap().value("status").toMap().value("@text").toString(); QString userPrincipal = response.value("propstat").toMap().value("prop").toMap() .value("current-user-principal").toMap().value("href").toMap().value("@text").toString(); QString ctag = response.value("propstat").toMap().value("prop").toMap().value("getctag").toMap().value("@text").toString(); if (!statusText.contains(QLatin1String("200 OK"))) { qCWarning(lcCardDav) << Q_FUNC_INFO << "invalid status response to current user information request:" << statusText; } else if (userPrincipal.isEmpty() && !ctag.isEmpty()) { // this server has responded with an addressbook information response. qCDebug(lcCardDav) << Q_FUNC_INFO << "addressbook information response to current user information request:" << statusText; *responseType = ReplyParser::AddressbookInformationResponse; return QString(); } *responseType = ReplyParser::UserPrincipalResponse; return userPrincipal; } QString ReplyParser::parseAddressbookHome(const QByteArray &addressbookUrlsResponse) const { /* We expect a response of the form: HTTP/1.1 207 Multi-status Content-Type: application/xml; charset=utf-8 / /addressbooks/johndoe/ HTTP/1.1 200 OK */ debugDumpData(QString::fromUtf8(addressbookUrlsResponse)); QXmlStreamReader reader(addressbookUrlsResponse); QString statusText; QString addressbookHome; while (!reader.atEnd() && !reader.hasError()) { QXmlStreamReader::TokenType token = reader.readNext(); if (token == QXmlStreamReader::StartElement) { if (reader.name().toString() == QLatin1String("addressbook-home-set")) { if (reader.readNextStartElement() && reader.name().toString() == QLatin1String("href")) { addressbookHome = reader.readElementText(); } } else if (reader.name().toString() == QLatin1String("status")) { statusText = reader.readElementText(); } } } if (reader.hasError()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "error parsing response to addressbook home request:" << reader.errorString(); } if (!statusText.contains(QLatin1String("200 OK"))) { qCWarning(lcCardDav) << Q_FUNC_INFO << "invalid status response to addressbook home request:" << statusText; } return addressbookHome; } QList ReplyParser::parseAddressbookInformation(const QByteArray &addressbookInformationResponse, const QString &addressbooksHomePath) const { /* We expect a response of the form: /addressbooks/johndoe/contacts/ My Address Book 3145 http://sabredav.org/ns/sync-token/3145 HTTP/1.1 200 OK */ debugDumpData(QString::fromUtf8(addressbookInformationResponse)); QXmlStreamReader reader(addressbookInformationResponse); QList infos; QList possibleAddressbookInfos; QList unlikelyAddressbookInfos; QVariantMap vmap = xmlToVMap(reader); QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); QVariantList responses; if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { // multiple addressbooks. responses = multistatusMap[QLatin1String("response")].toList(); } else { // only one addressbook. QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); responses << response; } // parse the information about each addressbook (response element) Q_FOREACH (const QVariant &rv, responses) { QVariantMap rmap = rv.toMap(); ReplyParser::AddressBookInformation currInfo; currInfo.url = QUrl::fromPercentEncoding(rmap.value("href").toMap().value("@text").toString().toUtf8()); if (!addressbooksHomePath.isEmpty() && (currInfo.url == addressbooksHomePath || currInfo.url == QStringLiteral("%1/").arg(addressbooksHomePath) || (!currInfo.url.endsWith('/') && addressbooksHomePath.endsWith('/') && currInfo.url == addressbooksHomePath.mid(0, addressbooksHomePath.size()-1)))) { qCDebug(lcCardDav) << "ignoring addressbook-home-set response returned for addressbook information request:" << currInfo.url; continue; } // some services (e.g. Cozy) return multiple propstat elements in each response QVariantList propstats; if (rmap.value("propstat").type() == QVariant::List) { propstats = rmap.value("propstat").toList(); } else { QVariantMap propstat = rmap.value("propstat").toMap(); propstats << propstat; } // examine the propstat elements to find the features we're interested in enum ResourceStatus { StatusUnknown = 0, StatusExplicitly2xxOk = 1, StatusExplicitlyTrue = 1, StatusExplicitlyNotOk = 2, StatusExplicitlyFalse = 2 }; ResourceStatus addressbookResourceSpecified = StatusUnknown; // valid values are Unknown/True/False ResourceStatus resourcetypeStatus = StatusUnknown; // valid values are Unknown/2xxOk/NotOk ResourceStatus otherPropertyStatus = StatusUnknown; // valid values are Unknown/2xxOk/NotOk Q_FOREACH (const QVariant &vpropstat, propstats) { QVariantMap propstat = vpropstat.toMap(); const QVariantMap &prop(propstat.value("prop").toMap()); if (prop.contains("getctag")) { currInfo.ctag = prop.value("getctag").toMap().value("@text").toString(); } if (prop.contains("sync-token")) { currInfo.syncToken = prop.value("sync-token").toMap().value("@text").toString(); } if (prop.contains("displayname")) { currInfo.displayName = prop.value("displayname").toMap().value("@text").toString(); } if (prop.contains("current-user-privilege-set")) { bool foundWrite = false; const QVariantList privileges = prop.value("current-user-privilege-set").toMap().value("privilege").toList(); for (const QVariant &pv : privileges) { const QVariantMap pvm = pv.toMap(); if (pvm.contains("write")) { foundWrite = true; } } currInfo.readOnly = !foundWrite; } bool thisPropstatIsForResourceType = false; if (prop.contains("resourcetype")) { thisPropstatIsForResourceType = true; const QStringList resourceTypeKeys = prop.value("resourcetype").toMap().keys(); const bool resourcetypeText = resourceTypeKeys.contains(QStringLiteral("@text")); // non-empty element. const bool resourcetypePrincipal = resourceTypeKeys.contains(QStringLiteral("principal"), Qt::CaseInsensitive); const bool resourcetypeAddressbook = resourceTypeKeys.contains(QStringLiteral("addressbook"), Qt::CaseInsensitive); const bool resourcetypeCollection = resourceTypeKeys.contains(QStringLiteral("collection"), Qt::CaseInsensitive); const bool resourcetypeCalendar = resourceTypeKeys.contains(QStringLiteral("calendar"), Qt::CaseInsensitive); const bool resourcetypeWriteProxy = resourceTypeKeys.contains(QStringLiteral("calendar-proxy-write"), Qt::CaseInsensitive); const bool resourcetypeReadProxy = resourceTypeKeys.contains(QStringLiteral("calendar-proxy-read"), Qt::CaseInsensitive); if (resourcetypeCalendar) { // the resource is explicitly described as a calendar resource, not an addressbook. addressbookResourceSpecified = StatusExplicitlyFalse; qCDebug(lcCardDav) << Q_FUNC_INFO << "have calendar resource:" << currInfo.url << ", ignoring"; } else if (resourcetypeWriteProxy || resourcetypeReadProxy) { // the resource is a proxy resource, we don't support these resources. addressbookResourceSpecified = StatusExplicitlyFalse; qCDebug(lcCardDav) << Q_FUNC_INFO << "have" << (resourcetypeWriteProxy ? "write" : "read") << "proxy resource:" << currInfo.url << ", ignoring"; } else if (resourcetypeAddressbook) { // the resource is explicitly described as an addressbook resource. addressbookResourceSpecified = StatusExplicitlyTrue; qCDebug(lcCardDav) << Q_FUNC_INFO << "have addressbook resource:" << currInfo.url; } else if (resourcetypeCollection) { if (resourceTypeKeys.size() == 1 || (resourceTypeKeys.size() == 2 && resourcetypeText) || (resourceTypeKeys.size() == 3 && resourcetypeText && resourcetypePrincipal)) { // This is probably a carddav addressbook collection. // Despite section 5.2 of RFC6352 stating that a CardDAV // server MUST return the 'addressbook' value in the resource types // property, some CardDAV implementations (eg, Memotoo, Kerio) do not. addressbookResourceSpecified = StatusUnknown; qCDebug(lcCardDav) << Q_FUNC_INFO << "have probable addressbook resource:" << currInfo.url; } else { // we don't know how to handle this resource type. addressbookResourceSpecified = StatusExplicitlyFalse; qCDebug(lcCardDav) << Q_FUNC_INFO << "have unknown" << (resourcetypePrincipal ? "principal" : "") << "non-addressbook collection resource:" << currInfo.url; } } else { // we don't know how to handle this resource type. addressbookResourceSpecified = StatusExplicitlyFalse; qCDebug(lcCardDav) << Q_FUNC_INFO << "have unknown" << (resourcetypePrincipal ? "principal" : "") << "non-collection resource:" << currInfo.url; } } // Some services (e.g. Cozy) return multiple propstats // where only one will refer to the resourcetype property itself; // others will refer to incidental properties like displayname etc. // Each propstat will (should) contain a status code, which applies // only to the properties referred to within the propstat. // Thus, a 404 code may only apply to a displayname, etc. if (propstat.contains("status")) { static const QRegularExpression Http2xxOk("2[0-9][0-9]"); QString status = propstat.value("status").toMap().value("@text").toString(); bool statusOk = status.contains(Http2xxOk); // any HTTP 2xx OK response if (thisPropstatIsForResourceType) { // This status applies to the resourcetype property. if (statusOk) { resourcetypeStatus = StatusExplicitly2xxOk; // explicitly ok } else { resourcetypeStatus = StatusExplicitlyNotOk; // explicitly not ok qCDebug(lcCardDav) << Q_FUNC_INFO << "response has non-OK status:" << status << "for properties:" << prop.keys() << "for url:" << currInfo.url; } } else { // This status applies to some other property. // In some cases (e.g. Memotoo) we may need // to infer that this status refers to the // entire response. if (statusOk) { otherPropertyStatus = StatusExplicitly2xxOk; // explicitly ok } else { otherPropertyStatus = StatusExplicitlyNotOk; // explicitly not ok qCDebug(lcCardDav) << Q_FUNC_INFO << "response has non-OK status:" << status << "for non-resourcetype properties:" << prop.keys() << "for url:" << currInfo.url; } } } } // now check to see if we have all of the required information if (addressbookResourceSpecified == StatusExplicitlyTrue && resourcetypeStatus == StatusExplicitly2xxOk) { // we definitely had a well-specified resourcetype response, with 200 OK status. qCDebug(lcCardDav) << Q_FUNC_INFO << "have addressbook resource with status OK:" << currInfo.url; } else if (propstats.count() == 1 // only one response element && addressbookResourceSpecified == StatusUnknown // resource type unknown && otherPropertyStatus == StatusExplicitly2xxOk) { // status was explicitly ok // we assume that this was an implicit Addressbook Collection resourcetype response. // append it to our list of possible addressbook infos, to be added if we have no "certain" addressbooks. qCDebug(lcCardDav) << Q_FUNC_INFO << "have possible addressbook resource with status OK:" << currInfo.url; possibleAddressbookInfos.append(currInfo); continue; } else if (addressbookResourceSpecified == StatusUnknown && resourcetypeStatus == StatusExplicitly2xxOk) { // workaround for Kerio servers. The "principal" may be used as // the carddav addressbook url if no other urls are valid. qCDebug(lcCardDav) << Q_FUNC_INFO << "have unlikely addressbook resource with status OK:" << currInfo.url; unlikelyAddressbookInfos.append(currInfo); continue; } else { // we either cannot infer that this was an Addressbook Collection // or we were told explicitly that the collection status was NOT OK. qCDebug(lcCardDav) << Q_FUNC_INFO << "ignoring resource:" << currInfo.url << "due to type or status:" << addressbookResourceSpecified << resourcetypeStatus << otherPropertyStatus; continue; } // add the addressbook to our return list. If we have no sync-token or c-tag, we do manual delta detection. if (currInfo.ctag.isEmpty() && currInfo.syncToken.isEmpty()) { qCDebug(lcCardDav) << Q_FUNC_INFO << "addressbook:" << currInfo.url << "has no sync-token or c-tag"; } else { qCDebug(lcCardDav) << Q_FUNC_INFO << "found valid addressbook:" << currInfo.url << "with sync-token or c-tag"; } infos.append(currInfo); } // if the server was returning malformed response (without 'addressbook' resource type) // we can still use the response path as an addressbook url in some cases (e.g. Memotoo). if (infos.isEmpty()) { qCDebug(lcCardDav) << Q_FUNC_INFO << "Have no certain addressbook resources; assuming possible resources are addressbooks!"; infos = possibleAddressbookInfos; if (infos.isEmpty()) { qCDebug(lcCardDav) << Q_FUNC_INFO << "Have no possible addressbook resources; assuming unlikely resources are addressbooks!"; infos = unlikelyAddressbookInfos; } } return infos; } QList ReplyParser::parseSyncTokenDelta( const QByteArray &syncTokenDeltaResponse, const QString &addressbookUrl, QString *newSyncToken) const { /* We expect a response of the form: /addressbooks/johndoe/contacts/newcard.vcf "33441-34321" HTTP/1.1 200 OK /addressbooks/johndoe/contacts/updatedcard.vcf "33541-34696" HTTP/1.1 200 OK /addressbooks/johndoe/contacts/deletedcard.vcf HTTP/1.1 404 Not Found http://sabredav.org/ns/sync/5001 */ debugDumpData(QString::fromUtf8(syncTokenDeltaResponse)); QList info; QXmlStreamReader reader(syncTokenDeltaResponse); QVariantMap vmap = xmlToVMap(reader); QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); if (newSyncToken) { *newSyncToken = multistatusMap.value("sync-token").toMap().value("@text").toString(); } QVariantList responses; if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { // multiple updates in the delta. responses = multistatusMap[QLatin1String("response")].toList(); } else { // only one update in the delta. QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); responses << response; } Q_FOREACH (const QVariant &rv, responses) { QVariantMap rmap = rv.toMap(); ReplyParser::ContactInformation currInfo; currInfo.uri = QUrl::fromPercentEncoding(rmap.value("href").toMap().value("@text").toString().toUtf8()); currInfo.etag = rmap.value("propstat").toMap().value("prop").toMap().value("getetag").toMap().value("@text").toString(); QString status = rmap.value("status").toMap().value("@text").toString(); if (status.isEmpty()) { status = rmap.value("propstat").toMap().value("status").toMap().value("@text").toString(); } if (status.contains(QLatin1String("200 OK"))) { if (currInfo.uri.endsWith(QChar('/'))) { // this is probably a response for the addressbook resource, // rather than for a contact resource within the addressbook. qCDebug(lcCardDav) << Q_FUNC_INFO << "ignoring non-contact (addressbook?) resource:" << currInfo.uri << currInfo.etag << status; continue; } else if (currInfo.uri.length() > 5 && (currInfo.uri.at(currInfo.uri.length()-4) == QChar('.') || currInfo.uri.at(currInfo.uri.length()-3) == QChar('.')) && !currInfo.uri.endsWith(QStringLiteral(".vcf"), Qt::CaseInsensitive)) { // the uri has a file suffix like .ics or .eml rather than .vcf. // this is probably not a contact resource, but instead some other file reported erroneously. qCDebug(lcCardDav) << Q_FUNC_INFO << "ignoring non-contact resource:" << currInfo.uri << currInfo.etag << status; continue; } const QString oldEtag = q->m_localContactUrisEtags[addressbookUrl].value(currInfo.uri); currInfo.modType = oldEtag.isEmpty() ? ReplyParser::ContactInformation::Addition : (currInfo.etag != oldEtag) ? ReplyParser::ContactInformation::Modification : ReplyParser::ContactInformation::Unmodified; } else if (status.contains(QLatin1String("404 Not Found"))) { currInfo.modType = ReplyParser::ContactInformation::Deletion; } else { qCWarning(lcCardDav) << Q_FUNC_INFO << "unknown response:" << currInfo.uri << currInfo.etag << status; } // only append the info if some valid info was contained in the response. if (!(currInfo.uri.isEmpty() && currInfo.etag.isEmpty() && status.isEmpty())) { info.append(currInfo); } } return info; } QList ReplyParser::parseContactMetadata( const QByteArray &contactMetadataResponse, const QString &addressbookUrl, const QHash &contactUriToEtag) const { /* We expect a response of the form: HTTP/1.1 207 Multi-status Content-Type: application/xml; charset=utf-8 /addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf "2134-888" HTTP/1.1 200 OK /addressbooks/johndoe/contacts/acme-12345.vcf "9999-2344"" HTTP/1.1 200 OK */ debugDumpData(QString::fromUtf8(contactMetadataResponse)); QList info; QXmlStreamReader reader(contactMetadataResponse); QVariantMap vmap = xmlToVMap(reader); QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); QVariantList responses; if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { // multiple updates in the delta. responses = multistatusMap[QLatin1String("response")].toList(); } else { // only one update in the delta. QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); responses << response; } QSet seenUris; Q_FOREACH (const QVariant &rv, responses) { QVariantMap rmap = rv.toMap(); ReplyParser::ContactInformation currInfo; currInfo.uri = QUrl::fromPercentEncoding(rmap.value("href").toMap().value("@text").toString().toUtf8()); currInfo.etag = rmap.value("propstat").toMap().value("prop").toMap().value("getetag").toMap().value("@text").toString(); QString status = rmap.value("propstat").toMap().value("status").toMap().value("@text").toString(); if (status.isEmpty()) { status = rmap.value("status").toMap().value("@text").toString(); } if (currInfo.uri.endsWith(QChar('/'))) { // this is probably a response for the addressbook resource, // rather than for a contact resource within the addressbook. qCDebug(lcCardDav) << Q_FUNC_INFO << "ignoring non-contact (addressbook?) resource:" << currInfo.uri << currInfo.etag << status; continue; } else if (currInfo.uri.length() > 5 && (currInfo.uri.at(currInfo.uri.length()-4) == QChar('.') || currInfo.uri.at(currInfo.uri.length()-3) == QChar('.')) && !currInfo.uri.endsWith(QStringLiteral(".vcf"), Qt::CaseInsensitive)) { // the uri has a file suffix like .ics or .eml rather than .vcf. // this is probably not a contact resource, but instead some other file reported erroneously. qCDebug(lcCardDav) << Q_FUNC_INFO << "ignoring non-contact resource:" << currInfo.uri << currInfo.etag << status; continue; } if (status.contains(QLatin1String("200 OK"))) { seenUris.insert(currInfo.uri); // only append if it's an addition or an actual modification // the etag will have changed since the last time we saw it, // if the contact has been modified server-side since last sync. if (!contactUriToEtag.contains(currInfo.uri)) { qCDebug(lcCardDavTrace) << "Resource" << currInfo.uri << "was added on server with etag" << currInfo.etag << "to addressbook:" << addressbookUrl; currInfo.modType = ReplyParser::ContactInformation::Addition; info.append(currInfo); } else if (contactUriToEtag[currInfo.uri] != currInfo.etag) { qCDebug(lcCardDavTrace) << "Resource" << currInfo.uri << "was modified on server in addressbook:" << addressbookUrl; qCDebug(lcCardDavTrace) << "Old etag:" << contactUriToEtag[currInfo.uri] << "New etag:" << currInfo.etag; currInfo.modType = ReplyParser::ContactInformation::Modification; info.append(currInfo); } else { qCDebug(lcCardDavTrace) << "Resource" << currInfo.uri << "is unchanged since last sync with etag" << currInfo.etag << "in addressbook:" << addressbookUrl; currInfo.modType = ReplyParser::ContactInformation::Unmodified; info.append(currInfo); } } else { qCWarning(lcCardDav) << Q_FUNC_INFO << "unknown response:" << currInfo.uri << currInfo.etag << status; } } // we now need to determine deletions. for (const QString &uri : contactUriToEtag.keys()) { if (!seenUris.contains(uri)) { // this uri wasn't listed in the report, so this contact must have been deleted. qCDebug(lcCardDavTrace) << "Resource" << uri << "was deleted on server in addressbook:" << addressbookUrl; ReplyParser::ContactInformation currInfo; currInfo.etag = contactUriToEtag.value(uri); currInfo.uri = uri; currInfo.modType = ReplyParser::ContactInformation::Deletion; info.append(currInfo); } } return info; } QHash ReplyParser::parseContactData(const QByteArray &contactData, const QString &addressbookUrl) const { /* We expect a response of the form: HTTP/1.1 207 Multi-status Content-Type: application/xml; charset=utf-8 /addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf "2134-314" BEGIN:VCARD VERSION:3.0 FN:My Mother UID:abc-def-fez-1234546578 END:VCARD HTTP/1.1 200 OK /addressbooks/johndoe/contacts/someapplication-12345678.vcf "5467-323" BEGIN:VCARD VERSION:3.0 FN:Your Mother UID:foo-bar-zim-gir-1234567 END:VCARD HTTP/1.1 200 OK */ debugDumpData(QString::fromUtf8(contactData)); QXmlStreamReader reader(contactData); const QVariantMap vmap = xmlToVMap(reader); const QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); const QVariantList responses = (multistatusMap[QLatin1String("response")].type() == QVariant::List) ? multistatusMap[QLatin1String("response")].toList() : (QVariantList() << multistatusMap[QLatin1String("response")].toMap()); QHash uriToContactData; for (const QVariant &rv : responses) { const QVariantMap rmap = rv.toMap(); const QString uri = QUrl::fromPercentEncoding(rmap.value("href").toMap().value("@text").toString().toUtf8()); const QString etag = rmap.value("propstat").toMap().value("prop").toMap().value("getetag").toMap().value("@text").toString(); const QString vcard = rmap.value("propstat").toMap().value("prop").toMap().value("address-data").toMap().value("@text").toString(); // import the data as a vCard bool ok = true; QPair result = m_converter->convertVCardToContact(vcard, &ok); if (!ok) { continue; } // fix up the GUID of the contact if required. QContact importedContact = result.first; QContactGuid guid = importedContact.detail(); const QString uid = guid.guid(); if (uid.isEmpty()) { qCWarning(lcCardDav) << Q_FUNC_INFO << "contact import from vcard has no UID:\n" << vcard; continue; } if (!uid.startsWith(QStringLiteral("%1:AB:%2:").arg(QString::number(q->m_accountId), addressbookUrl))) { // prefix the UID with accountId and addressbook URI to avoid duplicated GUID issue. // RFC6352 only requires that the UID be unique within a single collection (addressbook). // So, we set the guid to be a compound of the accountId, addressbook URI and the UID. guid.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(q->m_accountId), addressbookUrl, uid)); importedContact.saveDetail(&guid, QContact::IgnoreAccessConstraints); } // store the sync target of the contact QContactSyncTarget syncTarget = importedContact.detail(); syncTarget.setSyncTarget(uri); importedContact.saveDetail(&syncTarget, QContact::IgnoreAccessConstraints); // store the etag into the contact QContactExtendedDetail etagDetail; for (const QContactExtendedDetail &ed : importedContact.details()) { if (ed.name() == KEY_ETAG) { etagDetail = ed; break; } } etagDetail.setName(KEY_ETAG); etagDetail.setData(etag); importedContact.saveDetail(&etagDetail, QContact::IgnoreAccessConstraints); // store unsupported properties into the contact. QContactExtendedDetail unsupportedPropertiesDetail; for (const QContactExtendedDetail &ed : importedContact.details()) { if (ed.name() == KEY_UNSUPPORTEDPROPERTIES) { unsupportedPropertiesDetail = ed; break; } } unsupportedPropertiesDetail.setName(KEY_UNSUPPORTEDPROPERTIES); unsupportedPropertiesDetail.setData(result.second); importedContact.saveDetail(&unsupportedPropertiesDetail, QContact::IgnoreAccessConstraints); // and insert into the return map. uriToContactData.insert(uri, importedContact); } return uriToContactData; } buteo-sync-plugin-carddav-0.1.12/src/replyparser_p.h000066400000000000000000000065311475112063300224170ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef REPLYPARSER_P_H #define REPLYPARSER_P_H #include #include #include #include #include static const QString KEY_CTAG = QStringLiteral("ctag"); static const QString KEY_SYNCTOKEN = QStringLiteral("syncToken"); static const QString KEY_ETAG = QStringLiteral("etag"); static const QString KEY_UNSUPPORTEDPROPERTIES = QStringLiteral("unsupportedProperties"); QTCONTACTS_USE_NAMESPACE class CardDavVCardConverter; class Syncer; class ReplyParser { public: class AddressBookInformation { public: QString url; QString displayName; QString ctag; QString syncToken; bool readOnly = false; }; class ContactInformation { public: enum ModificationType { Uninitialized = 0, Addition, Modification, Deletion, Unmodified }; ContactInformation() : modType(Uninitialized) {} ModificationType modType; QString uri; QString etag; }; class FullContactInformation { public: QContact contact; QStringList unsupportedProperties; QString etag; }; enum ResponseType { UserPrincipalResponse = 0, AddressbookHomeResponse, AddressbookInformationResponse, ContactDataResponse }; ReplyParser(Syncer *parent, CardDavVCardConverter *converter); ~ReplyParser(); QString parseUserPrincipal(const QByteArray &userInformationResponse, ResponseType *responseType) const; QString parseAddressbookHome(const QByteArray &addressbookUrlsResponse) const; QList parseAddressbookInformation(const QByteArray &addressbookInformationResponse, const QString &addressbooksHomePath) const; QList parseSyncTokenDelta(const QByteArray &syncTokenDeltaResponse, const QString &addressbookUrl, QString *newSyncToken) const; QList parseContactMetadata(const QByteArray &contactMetadataResponse, const QString &addressbookUrl, const QHash &contactUriToEtag) const; QHash parseContactData(const QByteArray &contactData, const QString &addressbookUrl) const; private: Syncer *q; mutable CardDavVCardConverter *m_converter; }; Q_DECLARE_METATYPE(ReplyParser::AddressBookInformation) Q_DECLARE_METATYPE(ReplyParser::ContactInformation) Q_DECLARE_METATYPE(ReplyParser::FullContactInformation) #endif // REPLYPARSER_P_H buteo-sync-plugin-carddav-0.1.12/src/requestgenerator.cpp000066400000000000000000000373461475112063300234720ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "requestgenerator_p.h" #include "syncer_p.h" #include "logging.h" #include #include #include #include #include #include #include #include namespace { QUrl setRequestUrl(const QString &url, const QString &path, const QString &username, const QString &password) { QUrl ret(url); QString modifiedPath(path); if (!path.isEmpty()) { // common case: the path may contain %40 instead of @ symbol, // if the server returns paths in percent-encoded form. // QUrl::setPath() will automatically percent-encode the input, // so if we have received percent-encoded path, we need to undo // the percent encoding first. This is suboptimal but works // at least for the common case. if (path.contains(QStringLiteral("%40"))) { modifiedPath = QUrl::fromPercentEncoding(path.toUtf8()); } // override the path from the given url with the path argument. // this is because the initial URL may be a user-principals URL // but subsequent paths are not relative to that one, but instead // are relative to the root path / if (path.startsWith('/')) { ret.setPath(modifiedPath); } else { ret.setPath('/' + modifiedPath); } } if (!username.isEmpty() && !password.isEmpty()) { ret.setUserName(username); ret.setPassword(password); } return ret; } QNetworkRequest setRequestData(const QUrl &url, const QByteArray &requestData, const QString &depth, const QString &ifMatch, const QString &contentType, const QString &accessToken) { QNetworkRequest ret(url); if (!contentType.isEmpty()) { ret.setHeader(QNetworkRequest::ContentTypeHeader, contentType.toUtf8()); } ret.setHeader(QNetworkRequest::ContentLengthHeader, requestData.length()); if (!depth.isEmpty()) { ret.setRawHeader("Depth", depth.toUtf8()); } if (!ifMatch.isEmpty()) { ret.setRawHeader("If-Match", ifMatch.toUtf8()); } if (!accessToken.isEmpty()) { ret.setRawHeader("Authorization", QString(QLatin1String("Bearer ") + accessToken).toUtf8()); } return ret; } } RequestGenerator::RequestGenerator(Syncer *parent, const QString &username, const QString &password) : q(parent) , m_username(username) , m_password(password) { } RequestGenerator::RequestGenerator(Syncer *parent, const QString &accessToken) : q(parent) , m_accessToken(accessToken) { } QNetworkReply *RequestGenerator::generateRequest(const QString &url, const QString &path, const QString &depth, const QString &requestType, const QString &request) const { const QByteArray contentType("application/xml; charset=utf-8"); QByteArray requestData(request.toUtf8()); QUrl reqUrl(setRequestUrl(url, path, m_username, m_password)); QNetworkRequest req(setRequestData(reqUrl, requestData, depth, QString(), contentType, m_accessToken)); QBuffer *requestDataBuffer = new QBuffer(q); requestDataBuffer->setData(requestData); qCDebug(lcCardDav) << "generateRequest():" << m_accessToken << reqUrl << depth << requestType << QString::fromUtf8(requestData); return q->m_qnam.sendCustomRequest(req, requestType.toLatin1(), requestDataBuffer); } QNetworkReply *RequestGenerator::generateUpsyncRequest(const QString &url, const QString &path, const QString &ifMatch, const QString &contentType, const QString &requestType, const QString &request) const { QByteArray requestData(request.toUtf8()); QUrl reqUrl(setRequestUrl(url, path, m_username, m_password)); QNetworkRequest req(setRequestData(reqUrl, requestData, QString(), ifMatch, contentType, m_accessToken)); qCDebug(lcCardDav) << "generateUpsyncRequest():" << m_accessToken << reqUrl << requestType << ":" << requestData.length() << "bytes"; Q_FOREACH (const QByteArray &headerName, req.rawHeaderList()) { qCDebug(lcCardDav) << " " << headerName << "=" << req.rawHeader(headerName); } if (!request.isEmpty()) { QBuffer *requestDataBuffer = new QBuffer(q); requestDataBuffer->setData(requestData); return q->m_qnam.sendCustomRequest(req, requestType.toLatin1(), requestDataBuffer); } return q->m_qnam.sendCustomRequest(req, requestType.toLatin1()); } QNetworkReply *RequestGenerator::currentUserInformation(const QString &serverUrl) { if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } QString requestStr = QStringLiteral( "" "" "" "" ""); return generateRequest(serverUrl, QString(), QLatin1String("0"), QLatin1String("PROPFIND"), requestStr); } QNetworkReply *RequestGenerator::addressbookUrls(const QString &serverUrl, const QString &userPath) { if (Q_UNLIKELY(userPath.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "user path empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } QString requestStr = QStringLiteral( "" "" "" "" ""); return generateRequest(serverUrl, userPath, QLatin1String("0"), QLatin1String("PROPFIND"), requestStr); } QNetworkReply *RequestGenerator::addressbooksInformation(const QString &serverUrl, const QString &userAddressbooksPath) { if (Q_UNLIKELY(userAddressbooksPath.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "addressbooks path empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } QString requestStr = QStringLiteral( "" "" "" "" "" "" "" "" ""); return generateRequest(serverUrl, userAddressbooksPath, QLatin1String("1"), QLatin1String("PROPFIND"), requestStr); } QNetworkReply *RequestGenerator::addressbookInformation(const QString &serverUrl, const QString &addressbookPath) { if (Q_UNLIKELY(addressbookPath.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "addressbook path empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } QString requestStr = QStringLiteral( "" "" "" "" "" "" "" ""); return generateRequest(serverUrl, addressbookPath, QLatin1String("0"), QLatin1String("PROPFIND"), requestStr); } QNetworkReply *RequestGenerator::syncTokenDelta(const QString &serverUrl, const QString &addressbookUrl, const QString &syncToken) { if (Q_UNLIKELY(syncToken.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "sync token empty, aborting"; return 0; } if (Q_UNLIKELY(addressbookUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "addressbook url empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } QString requestStr = QStringLiteral( "" "" "%1" "1" "" "" "" "").arg(syncToken.toHtmlEscaped()); return generateRequest(serverUrl, addressbookUrl, QString(), QLatin1String("REPORT"), requestStr); } QNetworkReply *RequestGenerator::contactEtags(const QString &serverUrl, const QString &addressbookPath) { if (Q_UNLIKELY(addressbookPath.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "addressbook path empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } QString requestStr = QStringLiteral( "" "" "" "" ""); return generateRequest(serverUrl, addressbookPath, QLatin1String("1"), QLatin1String("PROPFIND"), requestStr); } QNetworkReply *RequestGenerator::contactData(const QString &serverUrl, const QString &addressbookPath, const QStringList &contactEtags) { if (Q_UNLIKELY(contactEtags.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "etag list empty, aborting"; return 0; } if (Q_UNLIKELY(addressbookPath.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "addressbook path empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } // Note: this may not work with all cardDav servers, since according to the RFC: // "The filter component is not optional, but required." Thus, may need to use the // PROPFIND query to get etags, then perform a filter with those etags. Q_UNUSED(contactEtags); // TODO QString requestStr = QStringLiteral( "" "" "" "" "" ""); return generateRequest(serverUrl, addressbookPath, QLatin1String("1"), QLatin1String("REPORT"), requestStr); } QNetworkReply *RequestGenerator::contactMultiget(const QString &serverUrl, const QString &addressbookPath, const QStringList &contactUris) { if (Q_UNLIKELY(contactUris.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "etag list empty, aborting"; return 0; } if (Q_UNLIKELY(addressbookPath.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "addressbook path empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } QString uriHrefs; Q_FOREACH (const QString &uri, contactUris) { // note: uriHref is of form: /addressbooks/johndoe/contacts/acme-12345.vcf etc. QString href = uri.toHtmlEscaped(); int lastPathMarker = href.lastIndexOf('/'); if (lastPathMarker > 0) { // percent-encode the filename QString vcfName = QUrl::toPercentEncoding(href.mid(lastPathMarker + 1)); href = href.mid(0, lastPathMarker+1) + vcfName; } if (uri.endsWith(QStringLiteral(".vcf")) && uri.startsWith(addressbookPath)) { uriHrefs.append(QStringLiteral("%1").arg(href)); } else if (uri.startsWith(addressbookPath)) { // contact resource which doesn't end in .vcf but is otherwise well-formed / fully specified. uriHrefs.append(QStringLiteral("%1").arg(href)); } else { uriHrefs.append(QStringLiteral("%1/%2.vcf").arg(addressbookPath).arg(href)); } } QString requestStr = QStringLiteral( "" "" "" "" "" "%1" "").arg(uriHrefs); return generateRequest(serverUrl, addressbookPath, QLatin1String("1"), QLatin1String("REPORT"), requestStr); } QNetworkReply *RequestGenerator::upsyncAddMod(const QString &serverUrl, const QString &contactPath, const QString &etag, const QString &vcard) { if (Q_UNLIKELY(vcard.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "vcard empty, aborting"; return 0; } // the etag can be empty if it's an addition if (Q_UNLIKELY(contactPath.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "contact uri empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } return generateUpsyncRequest(serverUrl, contactPath, etag, QStringLiteral("text/vcard; charset=utf-8"), QStringLiteral("PUT"), vcard); } QNetworkReply *RequestGenerator::upsyncDeletion(const QString &serverUrl, const QString &contactPath, const QString &etag) { if (Q_UNLIKELY(etag.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "etag empty, aborting"; return 0; } if (Q_UNLIKELY(contactPath.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "contact uri empty, aborting"; return 0; } if (Q_UNLIKELY(serverUrl.isEmpty())) { qCWarning(lcCardDav) << Q_FUNC_INFO << "server url empty, aborting"; return 0; } return generateUpsyncRequest(serverUrl, contactPath, etag, QString(), QStringLiteral("DELETE"), QString()); } buteo-sync-plugin-carddav-0.1.12/src/requestgenerator_p.h000066400000000000000000000061551475112063300234500ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef REQUESTGENERATOR_P_H #define REQUESTGENERATOR_P_H #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class Syncer; class RequestGenerator { public: RequestGenerator(Syncer *parent, const QString &username, const QString &password); RequestGenerator(Syncer *parent, const QString &accessToken); QNetworkReply *currentUserInformation(const QString &serverUrl); QNetworkReply *addressbookUrls(const QString &serverUrl, const QString &userPath); QNetworkReply *addressbooksInformation(const QString &serverUrl, const QString &userAddressbooksPath); QNetworkReply *addressbookInformation(const QString &serverUrl, const QString &addressbookPath); QNetworkReply *syncTokenDelta(const QString &serverUrl, const QString &addressbookUrl, const QString &syncToken); QNetworkReply *contactEtags(const QString &serverUrl, const QString &addressbookPath); QNetworkReply *contactData(const QString &serverUrl, const QString &addressbookPath, const QStringList &contactEtags); QNetworkReply *contactMultiget(const QString &serverUrl, const QString &addressbookPath, const QStringList &contactUris); QNetworkReply *upsyncAddMod(const QString &serverUrl, const QString &contactPath, const QString &etag, const QString &vcard); QNetworkReply *upsyncDeletion(const QString &serverUrl, const QString &contactPath, const QString &etag); private: QNetworkReply *generateRequest(const QString &url, const QString &path, const QString &depth, const QString &requestType, const QString &request) const; QNetworkReply *generateUpsyncRequest(const QString &url, const QString &path, const QString &ifMatch, const QString &contentType, const QString &requestType, const QString &request) const; Syncer *q; QString m_username; QString m_password; QString m_accessToken; }; #endif // REQUESTGENERATOR_P_H buteo-sync-plugin-carddav-0.1.12/src/src.pri000066400000000000000000000025451475112063300206630ustar00rootroot00000000000000QT -= gui QT += network dbus CONFIG += link_pkgconfig console c++11 PKGCONFIG += buteosyncfw$${QT_MAJOR_VERSION} libsignon-qt$${QT_MAJOR_VERSION} accounts-qt$${QT_MAJOR_VERSION} PKGCONFIG += Qt$${QT_MAJOR_VERSION}Versit Qt$${QT_MAJOR_VERSION}Contacts qtcontacts-sqlite-qt$${QT_MAJOR_VERSION}-extensions packagesExist(libsailfishkeyprovider) { PKGCONFIG += libsailfishkeyprovider DEFINES += USE_SAILFISHKEYPROVIDER } packagesExist(contactcache-qt$${QT_MAJOR_VERSION}) { PKGCONFIG += contactcache-qt$${QT_MAJOR_VERSION} DEFINES += USE_LIBCONTACTS } # We need the moc output for the headers from sqlite-extensions extensionsIncludePath = $$system(pkg-config --cflags-only-I qtcontacts-sqlite-qt$${QT_MAJOR_VERSION}-extensions) VPATH += $$replace(extensionsIncludePath, -I, ) HEADERS += qcontactclearchangeflagsrequest.h contactmanagerengine.h INCLUDEPATH += $$PWD SOURCES += \ $$PWD/carddavclient.cpp \ $$PWD/syncer.cpp \ $$PWD/auth.cpp \ $$PWD/carddav.cpp \ $$PWD/requestgenerator.cpp \ $$PWD/replyparser.cpp \ $$PWD/logging.cpp HEADERS += \ $$EXTENSION_HEADERS \ $$PWD/carddavclient.h \ $$PWD/syncer_p.h \ $$PWD/auth_p.h \ $$PWD/carddav_p.h \ $$PWD/requestgenerator_p.h \ $$PWD/replyparser_p.h \ $$PWD/logging.h \ OTHER_FILES += \ $$PWD/carddav.xml \ $$PWD/carddav.Contacts.xml buteo-sync-plugin-carddav-0.1.12/src/src.pro000066400000000000000000000006331475112063300206650ustar00rootroot00000000000000TARGET = carddav-client include(src.pri) QMAKE_CXXFLAGS = -Wall \ -g \ -Wno-cast-align \ -O2 -finline-functions TEMPLATE = lib CONFIG += plugin target.path = $$[QT_INSTALL_LIBS]/buteo-plugins-qt$${QT_MAJOR_VERSION}/oopp sync.path = /etc/buteo/profiles/sync sync.files = carddav.Contacts.xml client.path = /etc/buteo/profiles/client client.files = carddav.xml INSTALLS += target sync client buteo-sync-plugin-carddav-0.1.12/src/syncer.cpp000066400000000000000000000416001475112063300213620ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "syncer_p.h" #include "carddav_p.h" #include "auth_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logging.h" #define CARDDAV_CONTACTS_APPLICATION QLatin1String("carddav") static const int HTTP_UNAUTHORIZED_ACCESS = 401; Syncer::Syncer(QObject *parent, Buteo::SyncProfile *syncProfile, int accountId) : QObject(parent), QtContactsSqliteExtensions::TwoWayContactSyncAdaptor( accountId, CARDDAV_CONTACTS_APPLICATION) , m_syncProfile(syncProfile) , m_cardDav(0) , m_auth(0) , m_contactManager(QStringLiteral("org.nemomobile.contacts.sqlite")) , m_syncAborted(false) , m_syncError(false) , m_accountId(accountId) , m_ignoreSslErrors(false) { TwoWayContactSyncAdaptor::setManager(m_contactManager); } Syncer::~Syncer() { delete m_auth; delete m_cardDav; } void Syncer::abortSync() { m_syncAborted = true; } void Syncer::startSync(int accountId) { Q_ASSERT(accountId != 0); m_accountId = accountId; m_auth = new Auth(this); connect(m_auth, SIGNAL(signInCompleted(QString,QString,QString,QString,QString,bool)), this, SLOT(sync(QString,QString,QString,QString,QString,bool))); connect(m_auth, SIGNAL(signInError()), this, SLOT(signInError())); qCDebug(lcCardDav) << Q_FUNC_INFO << "starting carddav sync with account" << m_accountId; m_auth->signIn(accountId); } void Syncer::signInError() { emit syncFailed(); } void Syncer::sync(const QString &serverUrl, const QString &addressbookPath, const QString &username, const QString &password, const QString &accessToken, bool ignoreSslErrors) { m_serverUrl = serverUrl; m_addressbookPath = addressbookPath; m_username = username; m_password = password; m_accessToken = accessToken; m_ignoreSslErrors = ignoreSslErrors; m_cardDav = m_username.isEmpty() ? new CardDav(this, m_serverUrl, m_addressbookPath, m_accessToken) : new CardDav(this, m_serverUrl, m_addressbookPath, m_username, m_password); connect(m_cardDav, &CardDav::error, this, &Syncer::cardDavError); qCDebug(lcCardDav) << "CardDAV Sync adapter initialised for account" << m_accountId << ", starting sync..."; if (!TwoWayContactSyncAdaptor::startSync(TwoWayContactSyncAdaptor::ContinueAfterError)) { qCDebug(lcCardDav) << "Unable to start CardDAV sync!"; } } bool Syncer::determineRemoteCollections() { m_cardDav->determineAddressbooksList(); connect(m_cardDav, &CardDav::addressbooksList, this, [this] (const QList &infos) { QStringList paths; QList addressbooks; for (QList::const_iterator it = infos.constBegin(); it != infos.constEnd(); ++it) { if (!paths.contains(it->url)) { paths.append(it->url); QContactCollection addressbook; addressbook.setMetaData(QContactCollection::KeyName, it->displayName); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, CARDDAV_CONTACTS_APPLICATION); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, m_accountId); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, it->url); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_READONLY, it->readOnly); addressbook.setExtendedMetaData(KEY_CTAG, it->ctag); addressbook.setExtendedMetaData(KEY_SYNCTOKEN, it->syncToken); addressbooks.append(addressbook); } } remoteCollectionsDetermined(addressbooks); }, Qt::UniqueConnection); return true; } bool Syncer::determineRemoteCollectionChanges( const QList &locallyAddedCollections, const QList &locallyModifiedCollections, const QList &locallyRemovedCollections, const QList &locallyUnmodifiedCollections, QContactManager::Error *) { m_cardDav->determineAddressbooksList(); connect(m_cardDav, &CardDav::addressbooksList, this, [this, locallyAddedCollections, locallyModifiedCollections, locallyRemovedCollections, locallyUnmodifiedCollections] (const QList &infos) { // create a list of collections from the addressbooks information QHash remoteCollections; for (QList::const_iterator it = infos.constBegin(); it != infos.constEnd(); ++it) { const QString path = it->url; if (!remoteCollections.contains(path)) { QContactCollection addressbook; addressbook.setMetaData(QContactCollection::KeyName, it->displayName); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_AGGREGABLE, true); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, CARDDAV_CONTACTS_APPLICATION); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, m_accountId); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH, path); addressbook.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_READONLY, it->readOnly); addressbook.setExtendedMetaData(KEY_CTAG, it->ctag); addressbook.setExtendedMetaData(KEY_SYNCTOKEN, it->syncToken); remoteCollections.insert(path, addressbook); } } // determine which locally-present collections are not present remotely, // these will be remote deletions. // determine which locally-present collections are present remotely, // these will be either remote modifications, or unmodified. QList remotelyAddedCollections; QList remotelyModifiedCollections; QList remotelyRemovedCollections; QList remotelyUnmodifiedCollections; auto comparisonMethod = [this, &remoteCollections, &remotelyAddedCollections, &remotelyModifiedCollections, &remotelyRemovedCollections, &remotelyUnmodifiedCollections] (const QList &localCollections) { for (const QContactCollection &local : localCollections) { const QString path = local.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString(); if (!path.isEmpty()) { if (!remoteCollections.contains(path)) { // remote deletion remotelyRemovedCollections.append(local); } else { // cache the previously stored ctag and synctoken value. // this will be needed during the sync contacts step. const QString prevCtag = local.extendedMetaData(KEY_CTAG).toString(); const QString prevSyncToken = local.extendedMetaData(KEY_SYNCTOKEN).toString(); m_previousCtagSyncToken.insert(path, qMakePair(prevCtag, prevSyncToken)); const QString remoteCtag = remoteCollections.value(path).extendedMetaData(KEY_CTAG).toString(); const QString remoteSyncToken = remoteCollections.value(path).extendedMetaData(KEY_SYNCTOKEN).toString(); if (prevCtag != remoteCtag || prevSyncToken != remoteSyncToken) { // we assume that the only remote modification is the ctag/synctoken values. // in future: sync more information (color etc) and detect changes. remoteCollections.remove(path); QContactCollection remoteMod = local; remoteMod.setExtendedMetaData(KEY_CTAG, remoteCtag); remoteMod.setExtendedMetaData(KEY_SYNCTOKEN, remoteSyncToken); remotelyModifiedCollections.append(remoteMod); } else { // we assume that the remote collection is unmodified. remoteCollections.remove(path); QContactCollection remoteUnmod = local; // need id etc. remotelyUnmodifiedCollections.append(remoteUnmod); } } } } }; comparisonMethod(locallyAddedCollections); // partial upsync artifact detection? XXXXXX TODO: shouldn't hit this in the "normal" case... comparisonMethod(locallyModifiedCollections); comparisonMethod(locallyUnmodifiedCollections); // TODO: look at the collections which have been marked as locally added, // and try to match those to remote collections according to the display name. // these may be artifacts a previous failed sync cycle. // any collections left in the remoteCollections hash must be new/added remotely. remotelyAddedCollections.append(remoteCollections.values()); // finished determining remote collection changes. remoteCollectionChangesDetermined(remotelyAddedCollections, remotelyModifiedCollections, remotelyRemovedCollections, remotelyUnmodifiedCollections); }, Qt::UniqueConnection); return true; } bool Syncer::determineRemoteContacts(const QContactCollection &collection) { // don't attempt any delta detection, so pass in null ctag/syncToken values. const QString remotePath = collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString(); m_currentCollections.insert(remotePath, collection); // will call remoteContactsDetermined() when complete. return m_cardDav->downsyncAddressbookContent(remotePath, QString(), QString(), QString(), QString()); } bool Syncer::determineRemoteContactChanges( const QContactCollection &collection, const QList &localAddedContacts, const QList &localModifiedContacts, const QList &localDeletedContacts, const QList &localUnmodifiedContacts, QContactManager::Error *error) { const QString remotePath = collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString(); const QString newSyncToken = collection.extendedMetaData(KEY_SYNCTOKEN).toString(); const QString newCtag = collection.extendedMetaData(KEY_CTAG).toString(); const QString oldSyncToken = m_previousCtagSyncToken.value(remotePath).second; const QString oldCtag = m_previousCtagSyncToken.value(remotePath).first; // build a set of known contact uris/etags for use by the parser to determine delta. QHash contactUrisEtags; auto builder = [&contactUrisEtags] (const QList &contacts) { for (const QContact &c : contacts) { const QString uri = c.detail().syncTarget(); if (!uri.isEmpty()) { const QList dets = c.details(); for (const QContactExtendedDetail &d : dets) { if (d.name() == KEY_ETAG) { contactUrisEtags.insert(uri, d.data().toString()); break; } } } } }; builder(localModifiedContacts); builder(localDeletedContacts); builder(localUnmodifiedContacts); m_localContactUrisEtags.insert(remotePath, contactUrisEtags); m_currentCollections.insert(remotePath, collection); // will call remoteContactChangesDetermined() when complete. bool ret = m_cardDav->downsyncAddressbookContent( remotePath, newSyncToken, newCtag, oldSyncToken, oldCtag); if (ret) { m_collectionAMRU.insert(remotePath, { localAddedContacts, localModifiedContacts, localDeletedContacts, localUnmodifiedContacts }); *error = QContactManager::NoError; } else { *error = QContactManager::UnspecifiedError; } return ret; } bool Syncer::deleteRemoteCollection(const QContactCollection &) { // TODO: implement this. qCWarning(lcCardDav) << Q_FUNC_INFO << "delete remote collection operation not supported for carddav!"; return true; } bool Syncer::storeLocalChangesRemotely( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts) { const QString remotePath = collection.extendedMetaData( COLLECTION_EXTENDEDMETADATA_KEY_REMOTEPATH).toString(); // will call localChangesStoredRemotely() when complete. return m_cardDav->upsyncUpdates(remotePath, addedContacts, modifiedContacts, deletedContacts); } void Syncer::syncFinishedSuccessfully() { qCDebug(lcCardDav) << Q_FUNC_INFO << "CardDAV sync with account" << m_accountId << "finished successfully!"; emit syncSucceeded(); } void Syncer::syncFinishedWithError() { emit syncFailed(); } void Syncer::cardDavError(int errorCode) { qCWarning(lcCardDav) << "CardDAV sync for account: " << m_accountId << " finished with error:" << errorCode; m_syncError = true; if (errorCode == HTTP_UNAUTHORIZED_ACCESS) { m_auth->setCredentialsNeedUpdate(m_accountId); } QMetaObject::invokeMethod(this, "syncFailed", Qt::QueuedConnection); } void Syncer::purgeAccount(int accountId) { QContactManager::Error err = QContactManager::NoError; QtContactsSqliteExtensions::ContactManagerEngine *cme = QtContactsSqliteExtensions::contactManagerEngine(m_contactManager); QList added, modified, deleted, unmodified; if (!cme->fetchCollectionChanges(accountId, QString(), &added, &modified, &deleted, &unmodified, &err)) { qCWarning(lcCardDav) << "Unable to retrieve CardDAV collections for purged account: " << m_accountId; return; } const QList all = added + modified + deleted + unmodified; QList purge; for (const QContactCollection &col : all) { purge.append(col.id()); } if (purge.size() && !cme->storeChanges(nullptr, nullptr, purge, QtContactsSqliteExtensions::ContactManagerEngine::PreserveLocalChanges, true, &err)) { qCWarning(lcCardDav) << "Unable to delete CardDAV collections for purged account: " << m_accountId; return; } qCDebug(lcCardDav) << Q_FUNC_INFO << "Purged contacts for account: " << accountId; } buteo-sync-plugin-carddav-0.1.12/src/syncer_p.h000066400000000000000000000112741475112063300213520ustar00rootroot00000000000000/* * This file is part of buteo-sync-plugin-carddav package * * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). * * Contributors: Chris Adams * * This program/library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1 as published by the Free Software Foundation. * * This program/library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program/library; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef SYNCER_P_H #define SYNCER_P_H #include "replyparser_p.h" #include #include #include #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE class tst_replyparser; class Auth; class CardDav; class RequestGenerator; namespace Buteo { class SyncProfile; } class Syncer : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdaptor { Q_OBJECT public: Syncer(QObject *parent, Buteo::SyncProfile *profile, int accountId); ~Syncer(); void startSync(int accountId); void purgeAccount(int accountId); void abortSync(); Q_SIGNALS: void syncSucceeded(); void syncFailed(); protected: // implementing the TWCSA interface bool determineRemoteCollections(); bool determineRemoteCollectionChanges( const QList &locallyAddedCollections, const QList &locallyModifiedCollections, const QList &locallyRemovedCollections, const QList &locallyUnmodifiedCollections, QContactManager::Error *error); bool determineRemoteContacts(const QContactCollection &collection); bool determineRemoteContactChanges( const QContactCollection &collection, const QList &localAddedContacts, const QList &localModifiedContacts, const QList &localDeletedContacts, const QList &localUnmodifiedContacts, QContactManager::Error *error); bool deleteRemoteCollection(const QContactCollection &collection); bool storeLocalChangesRemotely( const QContactCollection &collection, const QList &addedContacts, const QList &modifiedContacts, const QList &deletedContacts); void syncFinishedSuccessfully(); void syncFinishedWithError(); private Q_SLOTS: void sync(const QString &serverUrl, const QString &addressbookPath, const QString &username, const QString &password, const QString &accessToken, bool ignoreSslErrors); void signInError(); void cardDavError(int errorCode = 0); private: friend class CardDav; friend class RequestGenerator; friend class ReplyParser; friend class tst_replyparser; Buteo::SyncProfile *m_syncProfile; CardDav *m_cardDav; Auth *m_auth; QContactManager m_contactManager; QNetworkAccessManager m_qnam; bool m_syncAborted; bool m_syncError; // auth related int m_accountId; QString m_serverUrl; QString m_addressbookPath; QString m_username; QString m_password; QString m_accessToken; bool m_ignoreSslErrors; // the ctag and sync token for each particular addressbook, as stored during the previous sync cycle. QHash > m_previousCtagSyncToken; // uri to ctag+synctoken. QHash m_currentCollections; QHash > m_localContactUrisEtags; // colletion uri to contact uri (sync target) to contact info QHash > m_remoteAdditions; QHash > m_remoteModifications; QHash > m_remoteRemovals; QHash > m_remoteUnmodified; // for change detection struct AMRU { QList added; QList modified; QList removed; QList unmodified; }; QHash m_collectionAMRU; // collection uri to AMRU }; #endif // SYNCER_P_H buteo-sync-plugin-carddav-0.1.12/tests/000077500000000000000000000000001475112063300177255ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/000077500000000000000000000000001475112063300222755ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data/000077500000000000000000000000001475112063300232065ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data/replyparser_addressbookhome_empty.xml000066400000000000000000000000001475112063300327350ustar00rootroot00000000000000replyparser_addressbookhome_single-well-formed.xml000066400000000000000000000006661475112063300352360ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data / /addressbooks/johndoe/ HTTP/1.1 200 OK replyparser_addressbookinformation_addressbook-calendar-principal.xml000066400000000000000000000027101475112063300411550ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /dav/johndoe/ Principal 11111 HTTP/1.1 200 OK /dav/johndoe/contacts.vcf/ Contacts 22222 HTTP/1.1 200 OK /dav/johndoe/calendar.ics/ Calendar 33333 HTTP/1.1 200 OK replyparser_addressbookinformation_addressbook-plus-collection-resource.xml000066400000000000000000000027511475112063300423730ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /carddav/accountname%40server.tld/ Display Name HTTP/1.1 200 OK HTTP/1.1 404 Not Found /carddav/accountname%40server.tld/addressbook/ Display Name 123456789 HTTP/1.1 200 OK HTTP/1.1 404 Not Found replyparser_addressbookinformation_addressbook-plus-contact.xml000066400000000000000000000022441475112063300400430ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/ HTTP/1.1 200 OK 12345 Contacts /addressbooks/johndoe/contacts/testcontact.vcf HTTP/1.1 404 Not Found HTTP/1.1 200 OK Test Contact replyparser_addressbookinformation_addressbook-principal-proxy.xml000066400000000000000000000041611475112063300405670ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /carddav HTTP/1.1 200 OK Display Name HTTP/1.1 403 Forbidden /carddav/calendar-proxy-write/ HTTP/1.1 200 OK Display Name Write Proxy HTTP/1.1 403 Forbidden /carddav/calendar-proxy-read/ HTTP/1.1 200 OK Display Name Read Proxy HTTP/1.1 403 Forbidden buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data/replyparser_addressbookinformation_empty.xml000066400000000000000000000000001475112063300343320ustar00rootroot00000000000000replyparser_addressbookinformation_single-well-formed.xml000066400000000000000000000007751475112063300366340ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/ My Address Book 3145 http://sabredav.org/ns/sync-token/3145 HTTP/1.1 200 OK replyparser_addressbookinformation_two-with-privileges.xml000066400000000000000000000024351475112063300370640ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/ My Address Book 3145 http://sabredav.org/ns/sync-token/3145 HTTP/1.1 200 OK /addressbooks/johndoe/readonly-contacts/ ReadOnly Address Book 3149 http://sabredav.org/ns/sync-token/3149 HTTP/1.1 200 OK buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data/replyparser_contactdata_empty.xml000066400000000000000000000000001475112063300320510ustar00rootroot00000000000000replyparser_contactdata_single-contact-multiple-formattedname.xml000066400000000000000000000011341475112063300402340ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson8.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Other Testname FN:Testy Testperson UID:testy-testperson-uid-8 TEL;TYPE=HOME,CELL:555333111 BDAY:19901231 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-contact-multiple-name.xml000066400000000000000000000011641475112063300363310ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson9.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson N:Testperson;Testy;;; N:Testname;Other;;; UID:testy-testperson-uid-9 TEL;TYPE=HOME,CELL:555333111 BDAY:19901231 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-contact-multiple-rev.xml000066400000000000000000000011661475112063300362070ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson11.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson REV:19951031T222710Z REV:19961031T222710Z UID:testy-testperson-uid-11 TEL;TYPE=HOME,CELL:555333111 BDAY:19901231 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-contact-multiple-uid.xml000066400000000000000000000012041475112063300361650ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson10.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson REV:19951031T222710Z UID:testy-testperson-uid-10 UID:testy-testperson-uid-duplicate TEL;TYPE=HOME,CELL:555333111 BDAY:19901231 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-contact-multiple-xgender.xml000066400000000000000000000011521475112063300370420ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson12.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson X-GENDER:male X-GENDER:female UID:testy-testperson-uid-12 TEL;TYPE=HOME,CELL:555333111 BDAY:19901231 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-hs-notz-iso8601-bday.xml000066400000000000000000000011251475112063300355520ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson4.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson UID:testy-testperson-uid-4 TEL;TYPE=HOME,CELL:555333111 BDAY:1990-12-31T02:00:00 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-hs-utc-iso8601-bday.xml000066400000000000000000000011261475112063300353540ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson2.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson UID:testy-testperson-uid-2 TEL;TYPE=HOME,CELL:555333111 BDAY:1990-12-31T02:00:00Z END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-ns-do-iso8601-bday-multiple.xml000066400000000000000000000011301475112063300370150ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson7.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson UID:testy-testperson-uid-7 TEL;TYPE=HOME,CELL:555333111 BDAY:19901231 BDAY:19901229 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-ns-do-iso8601-bday.xml000066400000000000000000000011121475112063300351640ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson6.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson UID:testy-testperson-uid-6 TEL;TYPE=HOME,CELL:555333111 BDAY:19901231 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-ns-notz-iso8601-bday.xml000066400000000000000000000011211475112063300355540ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson5.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson UID:testy-testperson-uid-5 TEL;TYPE=HOME,CELL:555333111 BDAY:19901231T020000 END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-ns-utc-iso8601-bday.xml000066400000000000000000000011221475112063300353560ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson3.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson UID:testy-testperson-uid-3 TEL;TYPE=HOME,CELL:555333111 BDAY:19901231T020000Z END:VCARD HTTP/1.1 200 OK replyparser_contactdata_single-well-formed.xml000066400000000000000000000011321475112063300343370ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/testytestperson.vcf "0001-0001" BEGIN:VCARD VERSION:3.0 FN:Testy Testperson UID:testy-testperson-uid TEL;TYPE=HOME,CELL:555333111 X-UNSUPPORTED-TEST-PROPERTY:7357 END:VCARD HTTP/1.1 200 OK buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data/replyparser_contactmetadata_empty.xml000066400000000000000000000000001475112063300327200ustar00rootroot00000000000000replyparser_contactmetadata_single-vcf-and-non-vcf.xml000066400000000000000000000013241475112063300356560ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/new.vcf "0021-0021" HTTP/1.1 200 OK /addressbooks/johndoe/contacts/alsonew "0022-0022" HTTP/1.1 200 OK replyparser_contactmetadata_single-well-formed-add-mod-rem-unch.xml000066400000000000000000000026621475112063300402360ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/newcard.vcf "0001-0001" HTTP/1.1 200 OK /addressbooks/johndoe/contacts/updatedcard.vcf "0002-0002" HTTP/1.1 200 OK /addressbooks/johndoe/contacts/unchangedcard.vcf "0004-0001" HTTP/1.1 200 OK buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data/replyparser_synctokendelta_empty.xml000066400000000000000000000000001475112063300326130ustar00rootroot00000000000000replyparser_synctokendelta_single-well-formed-add-mod-rem.xml000066400000000000000000000016041475112063300371710ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/newcard.vcf "33441-34321" HTTP/1.1 200 OK /addressbooks/johndoe/contacts/updatedcard.vcf "33541-34696" HTTP/1.1 200 OK /addressbooks/johndoe/contacts/deletedcard.vcf HTTP/1.1 404 Not Found http://sabredav.org/ns/sync/5001 replyparser_synctokendelta_single-well-formed-addition.xml000066400000000000000000000005741475112063300367030ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data /addressbooks/johndoe/contacts/newcard.vcf "33441-34321" HTTP/1.1 200 OK buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data/replyparser_userprincipal_empty.xml000066400000000000000000000000001475112063300324440ustar00rootroot00000000000000replyparser_userprincipal_single-well-formed.xml000066400000000000000000000006141475112063300347360ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tests/replyparser/data / /principals/users/johndoe/ HTTP/1.1 200 OK buteo-sync-plugin-carddav-0.1.12/tests/replyparser/replyparser.pro000066400000000000000000000004611475112063300253700ustar00rootroot00000000000000TEMPLATE = app TARGET = tst_replyparser include($$PWD/../../src/src.pri) QT += testlib SOURCES += tst_replyparser.cpp OTHER_FILES += data/*xml datafiles.files += data/*xml datafiles.path = /opt/tests/buteo/plugins/carddav/data/ target.path = /opt/tests/buteo/plugins/carddav/ INSTALLS += target datafiles buteo-sync-plugin-carddav-0.1.12/tests/replyparser/tst_replyparser.cpp000066400000000000000000001007541475112063300262520ustar00rootroot00000000000000#include #include #include #include #include "replyparser_p.h" #include "syncer_p.h" #include "carddav_p.h" #include #include #include #include #include #include #include #include #include #include #include QTCONTACTS_USE_NAMESPACE typedef QSet QSetString; typedef QHash QHashStringString; typedef QHash QHashStringContact; Q_DECLARE_METATYPE(QHashStringContact); namespace { void dumpContactDetail(const QContactDetail &d) { qWarning() << "++ ---------" << d.type(); QMap values = d.values(); foreach (int key, values.keys()) { qWarning() << " " << key << "=" << values.value(key); } } void dumpContact(const QContact &c) { qWarning() << "++++ ---- Contact:" << c.id(); QList cdets = c.details(); foreach (const QContactDetail &det, cdets) { dumpContactDetail(det); } } QContact removeIgnorableFields(const QContact &c) { QContact ret; ret.setId(c.id()); QList cdets = c.details(); foreach (const QContactDetail &det, cdets) { QContactDetail d = det; d.removeValue(QContactDetail::FieldProvenance); d.removeValue(QContactDetail__FieldModifiable); d.removeValue(QContactDetail__FieldNonexportable); ret.saveDetail(&d); } return ret; } } class tst_replyparser : public QObject { Q_OBJECT public: tst_replyparser() : m_s(Q_NULLPTR, Q_NULLPTR, 7357) , m_rp(&m_s, &m_vcc) {} public slots: void initTestCase(); void cleanupTestCase(); private slots: void parseUserPrincipal_data(); void parseUserPrincipal(); void parseAddressbookHome_data(); void parseAddressbookHome(); void parseAddressbookInformation_data(); void parseAddressbookInformation(); void parseSyncTokenDelta_data(); void parseSyncTokenDelta(); void parseContactMetadata_data(); void parseContactMetadata(); void parseContactData_data(); void parseContactData(); private: CardDavVCardConverter m_vcc; Syncer m_s; ReplyParser m_rp; }; void tst_replyparser::initTestCase() { } void tst_replyparser::cleanupTestCase() { } void tst_replyparser::parseUserPrincipal_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("expectedUserPrincipal"); QTest::addColumn("expectedResponseType"); QTest::newRow("empty user information response") << QStringLiteral("data/replyparser_userprincipal_empty.xml") << QString() << static_cast(ReplyParser::UserPrincipalResponse); QTest::newRow("single user principal in well-formed response") << QStringLiteral("data/replyparser_userprincipal_single-well-formed.xml") << QStringLiteral("/principals/users/johndoe/") << static_cast(ReplyParser::UserPrincipalResponse); } void tst_replyparser::parseUserPrincipal() { QFETCH(QString, xmlFilename); QFETCH(QString, expectedUserPrincipal); QFETCH(int, expectedResponseType); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } QByteArray userInformationResponse = f.readAll(); ReplyParser::ResponseType responseType = ReplyParser::UserPrincipalResponse; QString userPrincipal = m_rp.parseUserPrincipal(userInformationResponse, &responseType); QCOMPARE(userPrincipal, expectedUserPrincipal); QCOMPARE(responseType, static_cast(expectedResponseType)); } void tst_replyparser::parseAddressbookHome_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("expectedAddressbooksHomePath"); QTest::newRow("empty addressbook urls response") << QStringLiteral("data/replyparser_addressbookhome_empty.xml") << QString(); QTest::newRow("single well-formed addressbook urls set response") << QStringLiteral("data/replyparser_addressbookhome_single-well-formed.xml") << QStringLiteral("/addressbooks/johndoe/"); } void tst_replyparser::parseAddressbookHome() { QFETCH(QString, xmlFilename); QFETCH(QString, expectedAddressbooksHomePath); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } QByteArray addressbookHomeSetResponse = f.readAll(); QString addressbooksHomePath = m_rp.parseAddressbookHome(addressbookHomeSetResponse); QCOMPARE(addressbooksHomePath, expectedAddressbooksHomePath); } void tst_replyparser::parseAddressbookInformation_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("addressbooksHomePath"); QTest::addColumn >("expectedAddressbookInformation"); QTest::newRow("empty addressbook information response") << QStringLiteral("data/replyparser_addressbookinformation_empty.xml") << QString() << QList(); QList infos; ReplyParser::AddressBookInformation a; a.url = QStringLiteral("/addressbooks/johndoe/contacts/"); a.displayName = QStringLiteral("My Address Book"); a.ctag = QStringLiteral("3145"); a.syncToken = QStringLiteral("http://sabredav.org/ns/sync-token/3145"); infos << a; QTest::newRow("single addressbook information in well-formed response") << QStringLiteral("data/replyparser_addressbookinformation_single-well-formed.xml") << QStringLiteral("/addressbooks/johndoe/") << infos; infos.clear(); ReplyParser::AddressBookInformation a2; a2.url = QStringLiteral("/addressbooks/johndoe/contacts/"); a2.displayName = QStringLiteral("Contacts"); a2.ctag = QStringLiteral("12345"); a2.syncToken = QString(); infos << a2; QTest::newRow("addressbook information in response including non-collection resources") << QStringLiteral("data/replyparser_addressbookinformation_addressbook-plus-contact.xml") << QStringLiteral("/addressbooks/johndoe/") << infos; infos.clear(); ReplyParser::AddressBookInformation a3; a3.url = QStringLiteral("/dav/johndoe/contacts.vcf/"); a3.displayName = QStringLiteral("Contacts"); a3.ctag = QStringLiteral("22222"); a3.syncToken = QString(); infos << a3; QTest::newRow("addressbook information in response including principal and calendar collection") << QStringLiteral("data/replyparser_addressbookinformation_addressbook-calendar-principal.xml") << QStringLiteral("/dav/johndoe/") << infos; infos.clear(); // all of the contents should be ignored, since the addressbook-home-set path matches the addressbook href url. QTest::newRow("addressbook information in response including principal and calendar collection, discovery-case") << QStringLiteral("data/replyparser_addressbookinformation_addressbook-principal-proxy.xml") << QStringLiteral("/carddav") << infos; infos.clear(); ReplyParser::AddressBookInformation a5; a5.url = QStringLiteral("/carddav"); a5.displayName = QStringLiteral("Display Name"); a5.ctag = QString(); a5.syncToken = QString(); infos << a5; QTest::newRow("addressbook information in response including principal and calendar collection, non-discovery-case") << QStringLiteral("data/replyparser_addressbookinformation_addressbook-principal-proxy.xml") << QString() // in the non-discovery case, the user provides the addressbook-home-set path directly. << infos; // we then don't pass that into the parseAddressbookInformation() function, to avoid incorrect cycle detection. infos.clear(); ReplyParser::AddressBookInformation a6; a6.url = QStringLiteral("/carddav/accountname@server.tld/addressbook/"); a6.displayName = QStringLiteral("Display Name"); a6.ctag = QStringLiteral("123456789"); a6.syncToken = QString(); infos << a6; QTest::newRow("addressbook information in response including home-set collections resource") << QStringLiteral("data/replyparser_addressbookinformation_addressbook-plus-collection-resource.xml") << QStringLiteral("/carddav/accountname%40server.tld/addressbook/") << infos; infos.clear(); ReplyParser::AddressBookInformation a7; a7.url = QStringLiteral("/addressbooks/johndoe/contacts/"); a7.displayName = QStringLiteral("My Address Book"); a7.ctag = QStringLiteral("3145"); a7.syncToken = QStringLiteral("http://sabredav.org/ns/sync-token/3145"); a7.readOnly = false; ReplyParser::AddressBookInformation a8; a8.url = QStringLiteral("/addressbooks/johndoe/readonly-contacts/"); a8.displayName = QStringLiteral("ReadOnly Address Book"); a8.ctag = QStringLiteral("3149"); a8.syncToken = QStringLiteral("http://sabredav.org/ns/sync-token/3149"); a8.readOnly = true; infos << a7 << a8; QTest::newRow("two addressbooks information in response with privileges specified") << QStringLiteral("data/replyparser_addressbookinformation_two-with-privileges.xml") << QStringLiteral("/addressbooks/johndoe/") << infos; } bool operator==(const ReplyParser::AddressBookInformation& first, const ReplyParser::AddressBookInformation& second) { return first.url == second.url && first.displayName == second.displayName && first.ctag == second.ctag && first.syncToken == second.syncToken && first.readOnly == second.readOnly; } void tst_replyparser::parseAddressbookInformation() { QFETCH(QString, xmlFilename); QFETCH(QString, addressbooksHomePath); QFETCH(QList, expectedAddressbookInformation); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } QByteArray addressbookInformationResponse = f.readAll(); QList addressbookInfo = m_rp.parseAddressbookInformation(addressbookInformationResponse, addressbooksHomePath); QCOMPARE(addressbookInfo.size(), expectedAddressbookInformation.size()); QCOMPARE(addressbookInfo, expectedAddressbookInformation); } void tst_replyparser::parseSyncTokenDelta_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("injectContactUrisEtags"); QTest::addColumn("expectedNewSyncToken"); QTest::addColumn >("expectedContactInformation"); QList infos; QTest::newRow("empty sync token delta response") << QStringLiteral("data/replyparser_synctokendelta_empty.xml") << QHash() << QString() << infos; infos.clear(); ReplyParser::ContactInformation c1; c1.modType = ReplyParser::ContactInformation::Addition; c1.uri = QStringLiteral("/addressbooks/johndoe/contacts/newcard.vcf"); c1.etag = QStringLiteral("\"33441-34321\""); infos << c1; QTest::newRow("single contact addition in well-formed sync token delta response") << QStringLiteral("data/replyparser_synctokendelta_single-well-formed-addition.xml") << QHash() << QString() << infos; infos.clear(); ReplyParser::ContactInformation c2; c2.modType = ReplyParser::ContactInformation::Modification; c2.uri = QStringLiteral("/addressbooks/johndoe/contacts/updatedcard.vcf"); c2.etag = QStringLiteral("\"33541-34696\""); ReplyParser::ContactInformation c3; c3.modType = ReplyParser::ContactInformation::Deletion; c3.uri = QStringLiteral("/addressbooks/johndoe/contacts/deletedcard.vcf"); c3.etag = QString(); infos << c1 << c2 << c3; QHash mContactUrisEtags; mContactUrisEtags.insert(c2.uri, QStringLiteral("\"0001-0001\"")); // some previous etag. mContactUrisEtags.insert(c3.uri, c3.etag); QTest::newRow("single contact addition + modification + removal in well-formed sync token delta response") << QStringLiteral("data/replyparser_synctokendelta_single-well-formed-add-mod-rem.xml") << mContactUrisEtags << QStringLiteral("http://sabredav.org/ns/sync/5001") << infos; } bool operator==(const ReplyParser::ContactInformation& first, const ReplyParser::ContactInformation& second) { return first.modType == second.modType && first.uri == second.uri && first.etag == second.etag; } void tst_replyparser::parseSyncTokenDelta() { QFETCH(QString, xmlFilename); QFETCH(QHashStringString, injectContactUrisEtags); QFETCH(QString, expectedNewSyncToken); QFETCH(QList, expectedContactInformation); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } const QString addressbookUrl = QStringLiteral("test/addressbook/path"); m_s.m_localContactUrisEtags.insert(addressbookUrl, injectContactUrisEtags); QString newSyncToken; QByteArray syncTokenDeltaResponse = f.readAll(); QList contactInfo = m_rp.parseSyncTokenDelta(syncTokenDeltaResponse, addressbookUrl, &newSyncToken); QCOMPARE(newSyncToken, expectedNewSyncToken); QCOMPARE(contactInfo.size(), expectedContactInformation.size()); if (contactInfo != expectedContactInformation) { for (int i = 0; i < contactInfo.size(); ++i) { if (!(contactInfo[i] == expectedContactInformation[i])) { qWarning() << " actual:" << contactInfo[i].modType << contactInfo[i].uri << contactInfo[i].etag; qWarning() << "expected:" << expectedContactInformation[i].modType << expectedContactInformation[i].uri << expectedContactInformation[i].etag; } } QFAIL("contact information different"); } m_s.m_localContactUrisEtags.clear(); } void tst_replyparser::parseContactMetadata_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("addressbookUrl"); QTest::addColumn("injectContactEtags"); QTest::addColumn >("expectedContactInformation"); QList infos; QTest::newRow("empty contact metadata response") << QStringLiteral("data/replyparser_contactmetadata_empty.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << QHash() << infos; infos.clear(); ReplyParser::ContactInformation c1; c1.modType = ReplyParser::ContactInformation::Addition; c1.uri = QStringLiteral("/addressbooks/johndoe/contacts/newcard.vcf"); c1.etag = QStringLiteral("\"0001-0001\""); ReplyParser::ContactInformation c2; c2.modType = ReplyParser::ContactInformation::Modification; c2.uri = QStringLiteral("/addressbooks/johndoe/contacts/updatedcard.vcf"); c2.etag = QStringLiteral("\"0002-0002\""); ReplyParser::ContactInformation c3; c3.modType = ReplyParser::ContactInformation::Deletion; c3.uri = QStringLiteral("/addressbooks/johndoe/contacts/deletedcard.vcf"); c3.etag = QStringLiteral("\"0003-0001\""); ReplyParser::ContactInformation c4; c4.modType = ReplyParser::ContactInformation::Unmodified; c4.uri = QStringLiteral("/addressbooks/johndoe/contacts/unchangedcard.vcf"); c4.etag = QStringLiteral("\"0004-0001\""); infos << c1 << c2 << c4 << c3; QHash mContactEtags; mContactEtags.insert(c2.uri, QStringLiteral("\"0002-0001\"")); // changed to 0002-0002 mContactEtags.insert(c3.uri, QStringLiteral("\"0003-0001\"")); // unchanged but deleted mContactEtags.insert(c4.uri, QStringLiteral("\"0004-0001\"")); // unchanged. QTest::newRow("single contact addition + modification + removal + unchanged in well-formed sync token delta response") << QStringLiteral("data/replyparser_contactmetadata_single-well-formed-add-mod-rem-unch.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << mContactEtags << infos; infos.clear(); mContactEtags.clear(); ReplyParser::ContactInformation c5; c5.modType = ReplyParser::ContactInformation::Addition; c5.uri = QStringLiteral("/addressbooks/johndoe/contacts/new.vcf"); c5.etag = QStringLiteral("\"0021-0021\""); ReplyParser::ContactInformation c6; c6.modType = ReplyParser::ContactInformation::Addition; c6.uri = QStringLiteral("/addressbooks/johndoe/contacts/alsonew"); c6.etag = QStringLiteral("\"0022-0022\""); infos << c5 << c6; QTest::newRow("two contact additions with vcf and non-vcf extenions in well-formed sync token delta response") << QStringLiteral("data/replyparser_contactmetadata_single-vcf-and-non-vcf.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << mContactEtags << infos; } void tst_replyparser::parseContactMetadata() { QFETCH(QString, xmlFilename); QFETCH(QString, addressbookUrl); QFETCH(QHashStringString, injectContactEtags); QFETCH(QList, expectedContactInformation); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } m_s.m_localContactUrisEtags.insert(addressbookUrl, injectContactEtags); QByteArray contactMetadataResponse = f.readAll(); QList contactInfo = m_rp.parseContactMetadata(contactMetadataResponse, addressbookUrl, injectContactEtags); QCOMPARE(contactInfo, expectedContactInformation); m_s.m_localContactUrisEtags.clear(); } void tst_replyparser::parseContactData_data() { QTest::addColumn("xmlFilename"); QTest::addColumn("addressbookUrl"); QTest::addColumn("expectedContactInformation"); QHash infos; QTest::newRow("empty contact data response") << QStringLiteral("data/replyparser_contactdata_empty.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; infos.clear(); QContact contact; QContactDisplayLabel cd; cd.setLabel(QStringLiteral("Testy Testperson")); QContactName cn; cn.setFirstName(QStringLiteral("Testy")); cn.setLastName(QStringLiteral("Testperson")); QContactPhoneNumber cp; cp.setNumber(QStringLiteral("555333111")); cp.setContexts(QList() << QContactDetail::ContextHome); cp.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); QContactGuid cg; cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid"))); QContactSyncTarget cs; cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson.vcf")); QContactExtendedDetail ce; ce.setName(KEY_ETAG); ce.setData(QStringLiteral("\"0001-0001\"")); QContactExtendedDetail cu; cu.setName(KEY_UNSUPPORTEDPROPERTIES); cu.setData(QStringList() << QStringLiteral("X-UNSUPPORTED-TEST-PROPERTY:7357")); contact.saveDetail(&cd); contact.saveDetail(&cn); contact.saveDetail(&cp); contact.saveDetail(&cg); contact.saveDetail(&cs); contact.saveDetail(&ce); contact.saveDetail(&cu); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson.vcf"), contact); QTest::newRow("single contact in well-formed contact data response") << QStringLiteral("data/replyparser_contactdata_single-well-formed.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; cu.setData(QStringList()); contact.saveDetail(&cu); QContactBirthday cb; cb.setDateTime(QDateTime(QDate(1990, 12, 31), QTime(2, 0, 0), Qt::UTC)); cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-2"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson2.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); contact.saveDetail(&cb); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson2.vcf"), contact); QTest::newRow("single contact with fully-specified, hyphen-separated UTC ISO8601 BDAY") << QStringLiteral("data/replyparser_contactdata_single-hs-utc-iso8601-bday.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-3"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson3.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson3.vcf"), contact); QTest::newRow("single contact with fully-specified, non-separated UTC ISO8601 BDAY") << QStringLiteral("data/replyparser_contactdata_single-ns-utc-iso8601-bday.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; cb.setDateTime(QDateTime(QDate(1990, 12, 31), QTime(2, 0, 0), Qt::LocalTime)); cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-4"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson4.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); contact.saveDetail(&cb); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson4.vcf"), contact); QTest::newRow("single contact with fully-specified, hyphen-separated no-tz ISO8601 BDAY") << QStringLiteral("data/replyparser_contactdata_single-hs-notz-iso8601-bday.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-5"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson5.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson5.vcf"), contact); QTest::newRow("single contact with fully-specified, non-separated no-tz ISO8601 BDAY") << QStringLiteral("data/replyparser_contactdata_single-ns-notz-iso8601-bday.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; cb.setDate(QDate(1990, 12, 31)); cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-6"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson6.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); contact.saveDetail(&cb); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson6.vcf"), contact); QTest::newRow("single contact with non-separated, date-only ISO8601 BDAY") << QStringLiteral("data/replyparser_contactdata_single-ns-do-iso8601-bday.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-7"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson7.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson7.vcf"), contact); QTest::newRow("single contact with multiple non-separated, date-only ISO8601 BDAY fields") << QStringLiteral("data/replyparser_contactdata_single-ns-do-iso8601-bday-multiple.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-8"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson8.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson8.vcf"), contact); QTest::newRow("single contact with multiple FN fields") << QStringLiteral("data/replyparser_contactdata_single-contact-multiple-formattedname.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-9"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson9.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson9.vcf"), contact); QTest::newRow("single contact with multiple N fields") << QStringLiteral("data/replyparser_contactdata_single-contact-multiple-name.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; QContactGender cgender; cgender.setGender(QContactGender::GenderFemale); cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-12"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson12.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); contact.saveDetail(&cgender); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson12.vcf"), contact); QTest::newRow("single contact with multiple X-GENDER fields") << QStringLiteral("data/replyparser_contactdata_single-contact-multiple-xgender.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; QContactTimestamp ct; ct.setLastModified(QDateTime::fromString(QStringLiteral("1996-10-31T22:27:10Z"), Qt::ISODate)); cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-11"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson11.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); contact.saveDetail(&ct); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson11.vcf"), contact); QTest::newRow("single contact with multiple REV fields") << QStringLiteral("data/replyparser_contactdata_single-contact-multiple-rev.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; ct.setLastModified(QDateTime::fromString(QStringLiteral("1995-10-31T22:27:10Z"), Qt::ISODate)); cg.setGuid(QStringLiteral("%1:AB:%2:%3").arg(QString::number(7357), QStringLiteral("/addressbooks/johndoe/contacts/"), QStringLiteral("testy-testperson-uid-10"))); cs.setSyncTarget(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson10.vcf")); contact.saveDetail(&cs); contact.saveDetail(&cg); contact.saveDetail(&ct); infos.clear(); infos.insert(QStringLiteral("/addressbooks/johndoe/contacts/testytestperson10.vcf"), contact); QTest::newRow("single contact with multiple UID fields") << QStringLiteral("data/replyparser_contactdata_single-contact-multiple-uid.xml") << QStringLiteral("/addressbooks/johndoe/contacts/") << infos; } bool operator==(const ReplyParser::FullContactInformation& first, const ReplyParser::FullContactInformation& second) { return first.unsupportedProperties == second.unsupportedProperties && first.etag == second.etag && first.contact == second.contact; } QContactExtendedDetail unsupportedPropertiesDetail(const QContact &contact) { for (const QContactExtendedDetail &d : contact.details()) { if (d.name() == KEY_UNSUPPORTEDPROPERTIES) { return d; } } return QContactExtendedDetail(); } QContactExtendedDetail etagDetail(const QContact &contact) { for (const QContactExtendedDetail &d : contact.details()) { if (d.name() == KEY_ETAG) { return d; } } return QContactExtendedDetail(); } void tst_replyparser::parseContactData() { QFETCH(QString, xmlFilename); QFETCH(QString, addressbookUrl); QFETCH(QHashStringContact, expectedContactInformation); QFile f(QStringLiteral("%1/%2").arg(QCoreApplication::applicationDirPath(), xmlFilename)); if (!f.exists() || !f.open(QIODevice::ReadOnly)) { QFAIL("Data file does not exist or cannot be opened for reading!"); } QByteArray contactDataResponse = f.readAll(); QHash contactInfo = m_rp.parseContactData(contactDataResponse, addressbookUrl); QCOMPARE(contactInfo.size(), expectedContactInformation.size()); QCOMPARE(contactInfo.keys(), expectedContactInformation.keys()); Q_FOREACH (const QString &contactUri, contactInfo.keys()) { QCOMPARE(unsupportedPropertiesDetail(contactInfo[contactUri]).data(), unsupportedPropertiesDetail(expectedContactInformation[contactUri]).data()); QCOMPARE(etagDetail(contactInfo[contactUri]).data(), etagDetail(expectedContactInformation[contactUri]).data()); bool identical = false; QContact actualContact = removeIgnorableFields(contactInfo[contactUri]); QContact expectedContact = removeIgnorableFields(expectedContactInformation[contactUri]); QContact resolved = m_s.resolveConflictingChanges(actualContact, expectedContact, &identical); if (!identical) { qWarning() << " actual:"; dumpContact(actualContact); qWarning() << " resolved:"; dumpContact(resolved); qWarning() << " expected:"; dumpContact(expectedContact); } QVERIFY(identical); // explicitly test for multiples of unique details QVERIFY(contactInfo[contactUri].details().size() <= 1); QVERIFY(contactInfo[contactUri].details().size() <= 1); QVERIFY(contactInfo[contactUri].details().size() <= 1); QVERIFY(contactInfo[contactUri].details().size() <= 1); QVERIFY(contactInfo[contactUri].details().size() <= 1); QVERIFY(contactInfo[contactUri].details().size() <= 1); } } #include "tst_replyparser.moc" QTEST_MAIN(tst_replyparser) buteo-sync-plugin-carddav-0.1.12/tests/tests.pro000066400000000000000000000002351475112063300216110ustar00rootroot00000000000000TEMPLATE=subdirs SUBDIRS+=replyparser OTHER_FILES+=tests.xml tests_xml.path=/opt/tests/buteo/plugins/carddav/ tests_xml.files=tests.xml INSTALLS+=tests_xml buteo-sync-plugin-carddav-0.1.12/tests/tests.xml000066400000000000000000000011221475112063300216050ustar00rootroot00000000000000 CardDAV sync plugin automatic tests Reply parser correctness automatic tests /usr/sbin/run-blts-root /bin/su -g privileged -c '/opt/tests/buteo/plugins/carddav/tst_replyparser' nemo buteo-sync-plugin-carddav-0.1.12/tools/000077500000000000000000000000001475112063300177235ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tools/cdavtool/000077500000000000000000000000001475112063300215365ustar00rootroot00000000000000buteo-sync-plugin-carddav-0.1.12/tools/cdavtool/cdavtool.pro000066400000000000000000000011441475112063300240730ustar00rootroot00000000000000TEMPLATE=app TARGET=cdavtool QT-=gui QT+=network CONFIG += link_pkgconfig console PKGCONFIG += buteosyncfw$${QT_MAJOR_VERSION} PKGCONFIG += libsignon-qt$${QT_MAJOR_VERSION} accounts-qt$${QT_MAJOR_VERSION} PKGCONFIG += Qt$${QT_MAJOR_VERSION}Contacts Qt$${QT_MAJOR_VERSION}Versit contactcache-qt$${QT_MAJOR_VERSION} QMAKE_CXXFLAGS += -fPIE -fvisibility=hidden -fvisibility-inlines-hidden HEADERS+=worker.h helpers.h SOURCES+=worker.cpp helpers.cpp main.cpp # included from the main carddav plugin include($$PWD/../../src/src.pri) target.path = $$INSTALL_ROOT/opt/tests/buteo/plugins/carddav/ INSTALLS+=target buteo-sync-plugin-carddav-0.1.12/tools/cdavtool/helpers.cpp000066400000000000000000000647411475112063300237200ustar00rootroot00000000000000/* * Copyright (C) 2016 Jolla Ltd. * Contact: Chris Adams * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #include "helpers.h" static const QString ServiceSettingCalendars = QStringLiteral("calendars"); static const QString ServiceSettingEnabledCalendars = QStringLiteral("enabled_calendars"); static const QString ServiceSettingCalendarDisplayNames = QStringLiteral("calendar_display_names"); static const QString ServiceSettingCalendarColors = QStringLiteral("calendar_colors"); static const QByteArray PropFindRequest = "PROPFIND"; static const QString XmlElementResponse = QStringLiteral("response"); static const QString XmlElementHref = QStringLiteral("href"); static const QString XmlElementComp = QStringLiteral("comp"); static const QString XmlElementResourceType = QStringLiteral("resourcetype"); static const QString XmlElementCalendar = QStringLiteral("calendar"); static const QString XmlElementPrincipal = QStringLiteral("principal"); static const QString XmlElementCalendarColor = QStringLiteral("calendar-color"); static const QString XmlElementDisplayName = QStringLiteral("displayname"); static void DebugRequest(const QNetworkRequest &request, const QByteArray &data = QByteArray()) { qDebug() << "------------------- Dumping request data:"; const QList& rawHeaderList(request.rawHeaderList()); Q_FOREACH (const QByteArray &rawHeader, rawHeaderList) { qDebug() << rawHeader << " : " << request.rawHeader(rawHeader); } qDebug() << "URL = " << request.url(); qDebug() << "Request:"; QString allData = QString::fromUtf8(data); Q_FOREACH (const QString &line, allData.split('\n')) { qDebug() << line; } qDebug() << "---------------------------------------------------------------------\n"; } static void DebugReply(const QNetworkReply &reply, const QByteArray &data = QByteArray()) { qDebug() << "------------------- Dumping reply data:"; qDebug() << "response status code:" << reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); QList headers = reply.rawHeaderPairs(); qDebug() << "response headers:"; for (int i=0; i and that becomes "tag". Print indent, print tag, print newline. If tag didn't contain / then indent += " " else deindent. // see anything else, then read until < and that becomes "text". Print indent, print text, print newline. QString indent; QString formatted; QString allData = QString::fromUtf8(xml); for (QString::const_iterator it = allData.constBegin(); it != allData.constEnd(); ) { QString text; QString tag; bool withinString = false, seenSlash = false, needDeindent = false; if (*it == QChar('\n') || *it == QChar('\r')) { it++; continue; } if (*it == QChar('<')) { while (it != allData.constEnd() && *it != QChar('>')) { tag += *it; if (*it == '\"') { withinString = !withinString; } if (*it == '/' && !withinString) { seenSlash = true; if (tag == QStringLiteral("= 4) indent.chop(4); formatted += indent + tag + '\n'; if (!seenSlash) indent += " "; } else { while (it != allData.constEnd() && *it != QChar('<')) { text += *it; it++; } formatted += indent + text + '\n'; } } qDebug() << "------------------- Dumping XML data:"; Q_FOREACH (const QString &line, formatted.split('\n')) { qDebug() << line; } qDebug() << "---------------------------------------------------------------------\n"; } CalDAVDiscovery::CalDAVDiscovery(const QString &serviceName, const QString &username, const QString &password, Accounts::Account *account, Accounts::Manager *accountManager, QNetworkAccessManager *networkManager, QObject *parent) : QObject(parent) , m_account(account) , m_accountManager(accountManager) , m_networkAccessManager(networkManager) , m_status(UnknownStatus) , m_serviceName(serviceName) , m_username(username) , m_password(password) , m_verbose(false) { } CalDAVDiscovery::~CalDAVDiscovery() { } void CalDAVDiscovery::start(const QString &serverAddress, const QString &calendarHomePath) { if (m_status != UnknownStatus) { qWarning() << "Already started!"; emitError(InternalError); return; } if (!m_account || m_serviceName.isEmpty()) { qWarning() << "account or service not provided!"; emitError(InternalError); return; } m_serverAddress = serverAddress.endsWith('/') ? serverAddress.mid(0, serverAddress.length()-1) : serverAddress; // path must start and end with '/' m_calendarHomePath = calendarHomePath; if (!m_calendarHomePath.isEmpty()) { if (!m_calendarHomePath.startsWith('/')) m_calendarHomePath = '/' + m_calendarHomePath; if (!m_calendarHomePath.endsWith('/')) m_calendarHomePath += '/'; } QUrl testUrl(m_serverAddress); testUrl.setPath(m_calendarHomePath); if (!testUrl.isValid()) { qWarning() << "Supplied server address + path produced bad URL. serverAddress =" << serverAddress << "serverPath = " << calendarHomePath; emitError(InvalidUrlError); return; } startRequests(); } void CalDAVDiscovery::writeCalendars(Accounts::Account *account, const Accounts::Service &srv, const QList &calendars) { if (!account || !srv.isValid()) { qWarning() << "account is null or service is invalid"; return; } QStringList serverPaths; QStringList enabled; QStringList displayNames; QStringList colors; for (int i=0; iselectService(srv); account->setEnabled(true); account->setValue(ServiceSettingCalendars, serverPaths); account->setValue(ServiceSettingEnabledCalendars, enabled); account->setValue(ServiceSettingCalendarDisplayNames, displayNames); account->setValue(ServiceSettingCalendarColors, colors); account->selectService(Accounts::Service()); } QNetworkRequest CalDAVDiscovery::templateRequest(const QString &destUrlString) const { QUrl url; if (destUrlString.isEmpty()) { url.setUrl(m_serverAddress); } else { if (destUrlString.startsWith('/')) { // this is a path, so use the default server address url.setUrl(m_serverAddress); url.setPath(destUrlString); if (!url.isValid()) { qWarning() << "Cannot read URL:" << url << "with address:" << m_serverAddress << "and path:" << destUrlString; return QNetworkRequest(); } } else { url.setUrl(destUrlString); if (!url.isValid()) { qWarning() << "Cannot read URL:" << destUrlString; return QNetworkRequest(); } } } QNetworkRequest req; url.setUserName(m_username); url.setPassword(m_password); req.setUrl(url); req.setRawHeader("Prefer", "return-minimal"); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8"); return req; } void CalDAVDiscovery::startRequests() { if (m_calendarHomePath.isEmpty()) { qDebug() << "calendar home path is empty, requesting user principal url"; requestUserPrincipalUrl(QString()); } else { qDebug() << "calendar home path given, requesting calendar list from:" << m_calendarHomePath; requestCalendarList(m_calendarHomePath); } } void CalDAVDiscovery::requestUserPrincipalUrl(const QString &discoveryPath) { QNetworkRequest request = templateRequest(discoveryPath); request.setRawHeader("Depth", "0"); QBuffer *buffer = new QBuffer(this); buffer->setData("" \ "" \ "" \ "" \ ""); if (m_verbose) { DebugRequest(request, buffer->data()); } QNetworkReply *reply = m_networkAccessManager->sendCustomRequest(request, PropFindRequest, buffer); reply->setProperty("discoveryPath", discoveryPath); connect(reply, SIGNAL(finished()), this, SLOT(requestUserPrincipalUrlFinished())); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(handleSslErrors(QList))); m_pendingReplies.insert(reply, buffer); setStatus(RequestingUserPrincipalUrl); } void CalDAVDiscovery::requestUserPrincipalUrlFinished() { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); QString discoveryPath = reply->property("discoveryPath").toString(); if (reply->error() != QNetworkReply::NoError) { // perform discovery as per RFC 6764 if (discoveryPath.isEmpty()) { // try the well-known path instead. requestUserPrincipalUrl(QStringLiteral("/.well-known/caldav")); } else if (discoveryPath == QStringLiteral("/.well-known/caldav")) { // try the root URI instead. requestUserPrincipalUrl(QStringLiteral("/")); } else { // abort. if (m_verbose) { DebugReply(*reply, replyData); } emitNetworkReplyError(*reply); } } else { // handle redirects if required as per RFC 6764 QUrl redirectUrl(reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl()); if (!redirectUrl.isEmpty()) { QUrl originalUrl(m_serverAddress); if (!discoveryPath.isEmpty()) { originalUrl.setPath(discoveryPath); } QUrl sanitizedRedirectUrl = redirectUrl; sanitizedRedirectUrl.setUserName(QString()); sanitizedRedirectUrl.setPassword(QString()); if (originalUrl.path().endsWith(QStringLiteral(".well-known/caldav"))) { qDebug() << "being redirected from" << originalUrl << "to" << sanitizedRedirectUrl; requestUserPrincipalUrl(redirectUrl.toString()); } else { qWarning() << "ignoring possibly malicious redirect from" << originalUrl << "to" << sanitizedRedirectUrl; emitError(CurrentUserPrincipalNotFoundError); } } else { QXmlStreamReader reader(replyData); reader.setNamespaceProcessing(true); QString userPrincipalPath; while (!reader.atEnd() && reader.name() != "current-user-principal") { reader.readNext(); } while (reader.readNextStartElement()) { if (reader.name() == XmlElementHref) { userPrincipalPath = reader.readElementText(); break; } } if (reader.hasError()) { qWarning() << QString("XML parse error: %1: %2").arg(reader.error()).arg(reader.errorString()); if (m_verbose) { DebugReply(*reply, replyData); } emitError(InvalidServerResponseError); } else if (userPrincipalPath.isEmpty()) { qWarning() << "Request for user calendar path failed, response is missing current-user-principal href"; if (m_verbose) { dumpXml(replyData); } emitError(CurrentUserPrincipalNotFoundError); } else { if (m_verbose) { dumpXml(replyData); } m_userPrincipalPaths.insert(userPrincipalPath); requestCalendarHomeUrl(userPrincipalPath); } } } QIODevice *device = m_pendingReplies.take(reply); if (device) { device->deleteLater(); } reply->deleteLater(); } void CalDAVDiscovery::requestCalendarHomeUrl(const QString &userPrincipalPath) { QNetworkRequest request = templateRequest(userPrincipalPath); request.setRawHeader("Depth", "0"); QBuffer *buffer = new QBuffer(this); buffer->setData("" \ "" \ "" \ "" \ ""); if (m_verbose) { DebugRequest(request, buffer->data()); } QNetworkReply *reply = m_networkAccessManager->sendCustomRequest(request, PropFindRequest, buffer); connect(reply, SIGNAL(finished()), this, SLOT(requestCalendarHomeUrlFinished())); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(handleSslErrors(QList))); m_pendingReplies.insert(reply, buffer); setStatus(RequestingCalendarHomeUrl); } void CalDAVDiscovery::requestCalendarHomeUrlFinished() { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { if (m_verbose) { DebugReply(*reply, replyData); } emitNetworkReplyError(*reply); } else { QXmlStreamReader reader(replyData); reader.setNamespaceProcessing(true); QString calendarHome; while (!reader.atEnd() && reader.name() != "calendar-home-set") { reader.readNext(); } while (reader.readNextStartElement()) { if (reader.name() == XmlElementHref) { calendarHome = reader.readElementText(); break; } } if (reader.hasError()) { qWarning() << QString("XML parse error: %1: %2").arg(reader.error()).arg(reader.errorString()); if (m_verbose) { DebugReply(*reply, replyData); } emitError(InvalidServerResponseError); } else if (calendarHome.isEmpty()) { qWarning() << "Request for user calendar home failed, response is missing calendar-home-set href"; if (m_verbose) { dumpXml(replyData); } emitError(CalendarHomeNotFoundError); } else { if (m_verbose) { dumpXml(replyData); } requestCalendarList(calendarHome); } } QIODevice *device = m_pendingReplies.take(reply); if (device) { device->deleteLater(); } reply->deleteLater(); } void CalDAVDiscovery::requestCalendarList(const QString &calendarHomePath) { QNetworkRequest request = templateRequest(calendarHomePath); request.setRawHeader("Depth", "1"); QBuffer *buffer = new QBuffer(this); buffer->setData("" \ "" \ "" \ "" \ "" \ "" \ "" \ "" \ ""); if (m_verbose) { DebugRequest(request, buffer->data()); } QNetworkReply *reply = m_networkAccessManager->sendCustomRequest(request, PropFindRequest, buffer); connect(reply, SIGNAL(finished()), this, SLOT(requestCalendarListFinished())); connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(handleSslErrors(QList))); m_pendingReplies.insert(reply, buffer); setStatus(RequestingCalendarListing); } void CalDAVDiscovery::requestCalendarListFinished() { QNetworkReply *reply = qobject_cast(sender()); QByteArray replyData = reply->readAll(); if (reply->error() != QNetworkReply::NoError) { if (m_verbose) { DebugReply(*reply, replyData); } emitNetworkReplyError(*reply); } else { // if the server returns the user principal path instead of the calendar set // we may need to then request the calendar home set url from there. QString userPrincipalPath; bool foundCalendars = false; QXmlStreamReader reader(replyData); reader.setNamespaceProcessing(true); while (!reader.atEnd()) { reader.readNext(); if (reader.name() == XmlElementResponse && reader.isStartElement()) { bool responseContainsCalendar = addNextCalendar(&reader, &userPrincipalPath); foundCalendars |= responseContainsCalendar; } } if (reader.hasError()) { qWarning() << QString("XML parse error: %1: %2").arg(reader.error()).arg(reader.errorString()); if (m_verbose) { DebugReply(*reply, replyData); } m_userPrincipalPaths.clear(); // reset state emitError(InvalidServerResponseError); } else if (!foundCalendars && !userPrincipalPath.isEmpty()) { if (!m_userPrincipalPaths.contains(userPrincipalPath)) { qDebug() << "calendar list response returned (different) user principal; performing calendar home url request."; if (m_verbose) { dumpXml(replyData); } m_userPrincipalPaths.insert(userPrincipalPath); requestCalendarHomeUrl(userPrincipalPath); } else { qDebug() << "calendar list response is returning (identical) user principal; aborting"; if (m_verbose) { dumpXml(replyData); } m_userPrincipalPaths.clear(); emitError(InvalidServerResponseError); } } else { // write the calendars to the service settings and sync the changes if (m_verbose) { dumpXml(replyData); } m_userPrincipalPaths.clear(); // reset state Accounts::Service srv = m_accountManager->service(m_serviceName); writeCalendars(m_account, srv, m_calendars); m_account->syncAndBlock(); setStatus(Finished); } } QIODevice *device = m_pendingReplies.take(reply); if (device) { device->deleteLater(); } reply->deleteLater(); } bool CalDAVDiscovery::addNextCalendar(QXmlStreamReader *reader, QString *parsedUserPrincipalPath) { QString calendarPath; bool isCalendar = false, isPrincipal = false; QString calendarDisplayName; QString colorCode; if (!(reader->name() == XmlElementResponse && reader->isStartElement())) { qWarning() << "Parse error: expected to be reading a "; return false; } while (!reader->atEnd() && !(reader->name() == XmlElementResponse && reader->isEndElement())) { reader->readNext(); if (reader->name() == XmlElementHref && calendarPath.isEmpty()) { calendarPath = reader->readElementText(); } else if (reader->name() == XmlElementResourceType) { reader->readNext(); while (!reader->atEnd() && reader->name() != XmlElementResourceType) { reader->readNext(); if (reader->name() == XmlElementCalendar) { isCalendar = true; } else if (reader->name() == XmlElementPrincipal) { *parsedUserPrincipalPath = calendarPath; isPrincipal = true; } } } else if (reader->name() == XmlElementDisplayName) { calendarDisplayName = reader->readElementText(); } else if (reader->name() == XmlElementCalendarColor) { QString colorName = reader->readElementText(); if (colorName.length() == 9 && colorName.startsWith(QStringLiteral("#"))) { // color is in "#RRGGBBAA" format colorName = colorName.mid(0, 7); } colorCode = colorName; } } if (isCalendar) { OnlineCalendar calendar; QString removeCalendarPathSuffix; if (m_serverAddress.contains(QStringLiteral("memotoo.com"))) { removeCalendarPathSuffix = QStringLiteral("category0/"); } QString fixedCalendarPath = calendarPath; if (!removeCalendarPathSuffix.isEmpty() && calendarPath.endsWith(removeCalendarPathSuffix)) { // some providers (e.g. Memotoo) need special treatment... fixedCalendarPath.chop(removeCalendarPathSuffix.size()); } calendar.serverPath = fixedCalendarPath; calendar.enabled = true; calendar.displayName = calendarDisplayName; calendar.color = colorCode.isEmpty() ? "#800000" : colorCode; m_calendars << calendar; qDebug() << "found calendar information in response:" << calendarPath << calendarDisplayName << colorCode; return true; } if (isPrincipal) { qDebug() << "found user principal path in response:" << *parsedUserPrincipalPath; } else { qDebug() << "Unable to parse calendar from response, have details:" << calendarPath << calendarDisplayName << colorCode; } return false; } void CalDAVDiscovery::handleSslErrors(const QList &errors) { // TODO add configuration for SSL handling QNetworkReply *reply = qobject_cast(sender()); if (reply) { reply->ignoreSslErrors(errors); } } void CalDAVDiscovery::emitNetworkReplyError(const QNetworkReply &reply) { const int httpCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qDebug() << QString("QNetworkReply error: %1 with HTTP code: %2").arg(reply.error()).arg(httpCode); switch (reply.error()) { case QNetworkReply::AuthenticationRequiredError: emitError(SignInError); break; case QNetworkReply::ContentNotFoundError: emitError(ContentNotFoundError); break; default: emitError(NetworkRequestFailedError); break; } } void CalDAVDiscovery::emitError(Error errorCode) { switch (errorCode) { case NoError: return; case InvalidUrlError: qWarning() << "The server address or path is incorrect."; break; case SignInError: qWarning() << "The username or password is incorrect."; break; case NetworkRequestFailedError: qWarning() << "The network request was unsuccessful."; break; case ContentNotFoundError: // We may get this error if an incorrect username means that we made a request with an invalid server URL qWarning() << "The server request was unsuccessful. Make sure the username is correct."; break; case ServiceUnavailableError: // Some servers respond with this if the server path is wrong qWarning() << "The server request was unsuccessful. Make sure the server path is correct."; break; case InvalidServerResponseError: qWarning() << "The server response could not be processed."; break; case CurrentUserPrincipalNotFoundError: qWarning() << "The server response did not provide the user details for the specified username."; break; case CalendarHomeNotFoundError: qWarning() << "The server response did not provide the calendar home location for the specified username."; break; default: qWarning() << "An error has occurred."; break; } emit error(); } void CalDAVDiscovery::setStatus(Status status) { if (status != m_status) { m_status = status; if (status == CalDAVDiscovery::Finished) { emit success(); } } } buteo-sync-plugin-carddav-0.1.12/tools/cdavtool/helpers.h000066400000000000000000000123521475112063300233540ustar00rootroot00000000000000/* * Copyright (C) 2016 Jolla Ltd. * Contact: Chris Adams * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef CDAVTOOL_HELPERS_H #define CDAVTOOL_HELPERS_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class OnlineCalendar { public: OnlineCalendar() : enabled(false) {} ~OnlineCalendar() {} OnlineCalendar(const OnlineCalendar &other) { operator=(other); } OnlineCalendar &operator=(const OnlineCalendar &other) { serverPath = other.serverPath; displayName = other.displayName; color = other.color; enabled = other.enabled; return *this; } bool operator==(const OnlineCalendar &other) const { if (other == *this) { return true; } return other.serverPath == serverPath; } QString serverPath; QString displayName; QString color; bool enabled; }; class CalDAVDiscovery : public QObject { Q_OBJECT Q_ENUMS(Error) public: enum Status { UnknownStatus, SigningIn, RequestingUserPrincipalUrl, RequestingCalendarHomeUrl, RequestingCalendarListing, Finalizing, Finished }; enum Error { NoError, InternalError, InvalidUrlError, SignInError, NetworkRequestFailedError, ContentNotFoundError, ServiceUnavailableError, InvalidServerResponseError, CurrentUserPrincipalNotFoundError, CalendarHomeNotFoundError }; CalDAVDiscovery(const QString &serviceName, const QString &username, const QString &password, Accounts::Account *account, Accounts::Manager *accountManager, QNetworkAccessManager *networkManager, QObject *parent = 0); ~CalDAVDiscovery(); void setVerbose(bool verbose) { m_verbose = verbose; } void start(const QString &serverAddress, const QString &calendarHomePath = QString()); static void writeCalendars(Accounts::Account *account, const Accounts::Service &srv, const QList &calendars); Q_SIGNALS: void error(); void success(); private Q_SLOTS: void handleSslErrors(const QList &errors); void requestUserPrincipalUrlFinished(); void requestCalendarHomeUrlFinished(); void requestCalendarListFinished(); private: void startRequests(); void requestUserPrincipalUrl(const QString &discoveryPath); void requestCalendarHomeUrl(const QString &userPrincipalPath); void requestCalendarList(const QString &calendarHomePath); bool addNextCalendar(QXmlStreamReader *reader, QString *parsedUserPrincipalPath); void emitNetworkReplyError(const QNetworkReply &reply); void emitError(Error errorCode); void setStatus(Status status); QNetworkRequest templateRequest(const QString &destUrlString = QString()) const; QHash m_pendingReplies; QList m_calendars; Accounts::Account *m_account; Accounts::Manager *m_accountManager; QNetworkAccessManager *m_networkAccessManager; Status m_status; QString m_serviceName; QString m_username; QString m_password; QString m_serverAddress; QString m_calendarHomePath; QSet m_userPrincipalPaths; bool m_verbose; }; #endif // CDAVTOOL_HELPERS_H buteo-sync-plugin-carddav-0.1.12/tools/cdavtool/main.cpp000066400000000000000000000155331475112063300231750ustar00rootroot00000000000000/* * Copyright (C) 2016 Jolla Ltd. * Contact: Chris Adams * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #include #include #include #include #include #include #include #include #include "worker.h" #define RETURN_SUCCESS 0 #define RETURN_ERROR 1 int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); CDavToolWorker worker; QObject::connect(&worker, &CDavToolWorker::done, &app, &QCoreApplication::quit); const QString usage = QStringLiteral( "usage:\n" "cdavtool --create-account --type carddav|caldav|both --username --password --host [--calendar-path ] [--addressbook-path ] [--verbose]\n" "cdavtool --with-account [--clear-remote-calendars|--clear-remote-addressbooks] [--verbose]\n" "cdavtool --delete-account [--verbose]\n" "\n" "examples:\n" "cdavtool --create-account --type both --username testuser --password testpass --host http://8.1.tst.merproject.org/ --verbose\n" "cdavtool --with-account 5 --clear-remote-calendars\n" "cdavtool --delete-account 5\n"); QStringList args = app.arguments(); if (args.last() == QStringLiteral("--verbose")) { args.removeLast(); worker.setVerbose(true); } if (args.size() < 3 || args.size() > 14) { printf("%s\n", "Too few or many arguments."); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } else if (args[1] == QStringLiteral("--create-account")) { if (args.size() < 10 || args[2] != QStringLiteral("--type") || args[4] != QStringLiteral("--username") || args[6] != QStringLiteral("--password") || args[8] != QStringLiteral("--host")) { printf("%s\n", "Incorrect switches for --create-account:"); printf("%s\n", "Missing --type, --username, --password or --host arguments."); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } // parse all args. QString type = args[3], username = args[5], password = args[7], host = args[9]; CDavToolWorker::CreateMode mode = CDavToolWorker::CreateBoth; if (type == QStringLiteral("carddav")) { mode = CDavToolWorker::CreateCardDAV; } else if (type == QStringLiteral("caldav")) { mode = CDavToolWorker::CreateCalDAV; } // create the account. if (args.size() == 10) { worker.createAccount(username, password, mode, host); } else if (args.size() == 12) { if (args[10] == QStringLiteral("--calendar-path")) { worker.createAccount(username, password, mode, host, args[11], QString()); } else { worker.createAccount(username, password, mode, host, QString(), args[11]); } } else if (args.size() == 14) { if (args[10] == QStringLiteral("--calendar-path")) { worker.createAccount(username, password, mode, host, args[11], args[13]); } else { worker.createAccount(username, password, mode, host, args[13], args[11]); } } else { printf("%s\n", "Invalid switches for --create-account"); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } } else if (args[1] == QStringLiteral("--with-account")) { if (args.size() != 4) { printf("%s\n", "Incorrect switches for --with-account"); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } bool ok = false; int accountId = args[2].toInt(&ok); if (!ok || accountId <= 0) { printf("%s\n", "Invalid switches for --with-account (id)"); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } if (args[3] == QStringLiteral("--clear-remote-calendars")) { worker.clearRemoteCalendars(accountId); } else if (args[3] == QStringLiteral("--clear-remote-addressbooks")) { worker.clearRemoteAddressbooks(accountId); } else { printf("%s\n", "Invalid switches for --with-account (method)"); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } } else if (args[1] == QStringLiteral("--delete-account")) { if (args.size() != 3) { printf("%s\n", "Incorrect switches for --delete-account"); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } bool ok = false; int accountId = args[2].toInt(&ok); if (!ok || accountId <= 0) { printf("%s\n", "Invalid switches for --delete-account (id)"); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } else { worker.deleteAccount(accountId); } } else { printf("%s\n", "Invalid operation specified."); printf("%s\n", usage.toLatin1().constData()); return RETURN_ERROR; } (void)app.exec(); return worker.errorOccurred() ? RETURN_ERROR : RETURN_SUCCESS; } buteo-sync-plugin-carddav-0.1.12/tools/cdavtool/worker.cpp000066400000000000000000000657471475112063300235760ustar00rootroot00000000000000/* * Copyright (C) 2016 Jolla Ltd. * Contact: Chris Adams * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #include "worker.h" #include #include namespace { QVariantMap elementToVMap(QXmlStreamReader &reader) { QVariantMap element; // store the attributes of the element QXmlStreamAttributes attrs = reader.attributes(); while (attrs.size()) { QXmlStreamAttribute attr = attrs.takeFirst(); element.insert(attr.name().toString(), attr.value().toString()); } while (reader.readNext() != QXmlStreamReader::EndElement) { if (reader.isCharacters()) { // store the text of the element, if any QString elementText = reader.text().toString(); if (!elementText.isEmpty()) { element.insert(QLatin1String("@text"), elementText); } } else if (reader.isStartElement()) { // recurse if necessary. QString subElementName = reader.name().toString(); QVariantMap subElement = elementToVMap(reader); if (element.contains(subElementName)) { // already have an element with this name. // create a variantlist and append. QVariant existing = element.value(subElementName); QVariantList subElementList; if (existing.type() == QVariant::Map) { // we need to convert the value into a QVariantList subElementList << existing.toMap(); } else if (existing.type() == QVariant::List) { subElementList = existing.toList(); } subElementList << subElement; element.insert(subElementName, subElementList); } else { // first element with this name. insert as a map. element.insert(subElementName, subElement); } } } return element; } QVariantMap xmlToVMap(QXmlStreamReader &reader) { QVariantMap retn; while (!reader.atEnd() && !reader.hasError() && reader.readNextStartElement()) { QString elementName = reader.name().toString(); QVariantMap element = elementToVMap(reader); retn.insert(elementName, element); } return retn; } } CDavToolWorker::CDavToolWorker(QObject *parent) : QObject(parent) , m_carddavSyncer(Q_NULLPTR) , m_carddavDiscovery(Q_NULLPTR) , m_caldavDiscovery(Q_NULLPTR) , m_networkManager(new QNetworkAccessManager(this)) , m_profileManager(new Buteo::ProfileManager) , m_accountManager(new Accounts::Manager(this)) , m_account(Q_NULLPTR) , m_identity(Q_NULLPTR) , m_createMode(CDavToolWorker::CreateBoth) , m_operationMode(CDavToolWorker::CreateAccount) , m_errorOccurred(false) , m_verbose(false) { } CDavToolWorker::~CDavToolWorker() { delete m_profileManager; } void CDavToolWorker::createAccount( const QString &username, const QString &password, CDavToolWorker::CreateMode mode, const QString &hostAddress, const QString &calendarPath, const QString &addressbookPath) { // cache some URL data we will need later. m_username = username; m_password = password; m_createMode = mode; m_hostAddress = hostAddress; m_calendarPath = calendarPath; m_addressbookPath = addressbookPath; // create an account m_account = m_accountManager->createAccount(QStringLiteral("onlinesync")); if (!m_account) { m_errorOccurred = true; emit done(); return; } // find the CalDAV and CardDAV services Accounts::ServiceList services = m_account->services(); Q_FOREACH (const Accounts::Service &s, services) { if (s.serviceType().toLower() == QStringLiteral("caldav") && (m_createMode == CDavToolWorker::CreateBoth || m_createMode == CDavToolWorker::CreateCalDAV)) { m_caldavService = s; } else if (s.serviceType().toLower() == QStringLiteral("carddav") && (m_createMode == CDavToolWorker::CreateBoth || m_createMode == CDavToolWorker::CreateCardDAV)) { m_carddavService = s; } } // create a set of credentials for the account QMap methodMechanisms; methodMechanisms.insert(QStringLiteral("password"), QStringList(QStringLiteral("password"))); m_credentials = SignOn::IdentityInfo("jolla", username, methodMechanisms); m_credentials.setSecret(password, true); m_identity = SignOn::Identity::newIdentity(m_credentials); if (!m_identity) { m_account->remove(); m_account->syncAndBlock(); m_errorOccurred = true; emit done(); return; } // store the credentials into an identity which will later be associated with the account. connect(m_identity, SIGNAL(error(SignOn::Error)), this, SLOT(handleError(SignOn::Error)), Qt::UniqueConnection); connect(m_identity, SIGNAL(credentialsStored(quint32)), this, SLOT(handleCredentialsStored(quint32)), Qt::UniqueConnection); printf("%s\n", "Storing account credentials..."); m_identity->storeCredentials(m_credentials); } void CDavToolWorker::handleError(const SignOn::Error &err) { printf("Error: %d: %s\n", (int)err.type(), err.message().toLatin1().constData()); if (m_identity) { m_identity->signOut(); } if (m_operationMode == CDavToolWorker::CreateAccount) { if (m_identity) { m_identity->remove(); } if (m_account) { m_account->remove(); m_account->syncAndBlock(); } } m_errorOccurred = true; // the Identity operations are asynchronous... give them time to complete. QTimer::singleShot(1000, this, SIGNAL(done())); } void CDavToolWorker::handleCredentialsStored(quint32 credentialsId) { if (m_identity->id() == 0) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("Identity has no id, but stored credentials: %1").arg(credentialsId))); return; } else { printf("%s%d\n", "Successfully stored credentials: ", credentialsId); } // associate the Identity with the Account, and set a variety of other required keys. const QString segCredKey = QStringLiteral("jolla/segregated_credentials/Jolla"); const QString credKey = QStringLiteral("CredentialsId"); const QString servAddrKey = QStringLiteral("server_address"); const QString abookPath = QStringLiteral("addressbook_path"); m_account->selectService(Accounts::Service()); m_account->setValue(segCredKey, m_identity->id()); m_account->setValue(credKey, m_identity->id()); m_account->syncAndBlock(); if (m_createMode == CDavToolWorker::CreateBoth || m_createMode == CDavToolWorker::CreateCardDAV) { m_account->selectService(m_carddavService); m_account->setValue(credKey, m_identity->id()); m_account->setValue(servAddrKey, m_hostAddress); if (!m_addressbookPath.isEmpty()) { m_account->setValue(abookPath, m_addressbookPath); } m_account->syncAndBlock(); } if (m_createMode == CDavToolWorker::CreateBoth || m_createMode == CDavToolWorker::CreateCalDAV) { m_account->selectService(m_caldavService); m_account->setValue(credKey, m_identity->id()); m_account->setValue(servAddrKey, m_hostAddress); m_account->selectService(Accounts::Service()); m_account->syncAndBlock(); m_caldavDiscovery = new CalDAVDiscovery(m_caldavService.name(), m_username, m_password, m_account, m_accountManager, m_networkManager, this); connect(m_caldavDiscovery, &CalDAVDiscovery::error, this, &CDavToolWorker::discoveryError); connect(m_caldavDiscovery, &CalDAVDiscovery::success, this, &CDavToolWorker::accountDone); printf("%s\n", "Performing calendar discovery..."); m_caldavDiscovery->setVerbose(m_verbose); m_caldavDiscovery->start(m_hostAddress, m_calendarPath); } else { accountDone(); } } void CDavToolWorker::discoveryError() { if (m_calendarPath.isEmpty()) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("Unable to discover CalDAV calendars!"))); } else { // set the calendar path directly, assume that discovery is not possible. m_account->setValue(QStringLiteral("calendars"), QVariant::fromValue(QStringList(m_calendarPath))); m_account->setValue(QStringLiteral("enabled_calendars"), QVariant::fromValue(QStringList(m_calendarPath))); m_account->setValue(QStringLiteral("calendar_colors"), QVariant::fromValue(QStringList(QStringLiteral("#b90e28")))); m_account->syncAndBlock(); accountDone(); } } void CDavToolWorker::accountDone() { // ok, the account is mostly set up, we just need to generate sync profiles and then enable it. printf("%s\n", "Generating sync profiles..."); if (m_createMode == CDavToolWorker::CreateBoth || m_createMode == CDavToolWorker::CreateCardDAV) { if (!createSyncProfiles(m_account, m_carddavService)) { // error will be generated by that function. return; } m_account->selectService(Accounts::Service()); m_account->setEnabled(true); } if (m_createMode == CDavToolWorker::CreateBoth || m_createMode == CDavToolWorker::CreateCalDAV) { if (!createSyncProfiles(m_account, m_caldavService)) { // error will be generated by that function. return; } m_account->selectService(Accounts::Service()); m_account->setEnabled(true); } m_account->selectService(Accounts::Service()); m_account->setEnabled(true); m_account->setDisplayName(QStringLiteral("cdavtool")); m_account->syncAndBlock(); // success! printf("%s\n%d\n", "Successfully created account:", m_account->id()); QTimer::singleShot(1000, this, SIGNAL(done())); } bool CDavToolWorker::createSyncProfiles(Accounts::Account *account, const Accounts::Service &service) { account->selectService(service); QStringList templates = account->value(QStringLiteral("sync_profile_templates")).toStringList(); Q_FOREACH (const QString &templateProfileName, templates) { Buteo::SyncProfile *templateProfile = m_profileManager->syncProfile(templateProfileName); if (!templateProfile) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("Unable to create template profile: %1").arg(templateProfileName))); return false; } Buteo::SyncProfile *perAccountProfile = templateProfile->clone(); if (!perAccountProfile) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("Unable to create per-account profile: %1").arg(templateProfileName))); return false; } QString accountIdStr = QString::number(account->id()); perAccountProfile->setName(templateProfileName + "-" + accountIdStr); perAccountProfile->setKey(Buteo::KEY_DISPLAY_NAME, templateProfileName + "-" + account->displayName().toHtmlEscaped()); perAccountProfile->setKey(Buteo::KEY_ACCOUNT_ID, accountIdStr); perAccountProfile->setBoolKey(Buteo::KEY_USE_ACCOUNTS, true); perAccountProfile->setEnabled(true); QString profileName = m_profileManager->updateProfile(*perAccountProfile); if (profileName.isEmpty()) { profileName = perAccountProfile->name(); } if (profileName.isEmpty()) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("Unable to store per-account profile: %1").arg(templateProfileName))); } account->setValue(QStringLiteral("%1/%2").arg(templateProfileName).arg(Buteo::KEY_PROFILE_ID), profileName); delete perAccountProfile; delete templateProfile; } return true; } void CDavToolWorker::deleteAccount(int accountId) { Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!account) { m_errorOccurred = true; } else { // remove associated credentials. account->selectService(Accounts::Service()); QStringList configurationKeys = account->allKeys(); foreach (const QString &key, configurationKeys) { if (key.contains(QStringLiteral("CredentialsId"))) { int identityId = account->valueAsInt(key, 0); if (identityId) { SignOn::Identity *doomedIdentity = SignOn::Identity::existingIdentity(identityId, this); if (doomedIdentity) { doomedIdentity->signOut(); doomedIdentity->remove(); } } } } account->remove(); account->syncAndBlock(); } // the Identity operations are asynchronous... give them time to complete. QTimer::singleShot(1000, this, SIGNAL(done())); } void CDavToolWorker::clearRemoteCalendars(int accountId) { m_operationMode = CDavToolWorker::ClearAllRemoteCalendars; m_account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!m_account) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("No such account"))); return; } // get username + password... m_identity = SignOn::Identity::existingIdentity(m_account->value(QStringLiteral("CredentialsId")).toInt(), this); m_session = m_identity->createSession(QStringLiteral("password")); connect(m_session, SIGNAL(response(SignOn::SessionData)), this, SLOT(gotCredentials(SignOn::SessionData)), Qt::UniqueConnection); connect(m_session, SIGNAL(error(SignOn::Error)), this, SLOT(handleError(SignOn::Error)), Qt::UniqueConnection); m_session->process(SignOn::SessionData(SignOn::SessionData()), QStringLiteral("password")); } void CDavToolWorker::clearRemoteAddressbooks(int accountId) { m_operationMode = CDavToolWorker::ClearAllRemoteAddressbooks; m_account = Accounts::Account::fromId(m_accountManager, accountId, this); if (!m_account) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("No such account"))); return; } // get username + password... m_identity = SignOn::Identity::existingIdentity(m_account->value(QStringLiteral("CredentialsId")).toInt(), this); m_session = m_identity->createSession(QStringLiteral("password")); connect(m_session, SIGNAL(response(SignOn::SessionData)), this, SLOT(gotCredentials(SignOn::SessionData)), Qt::UniqueConnection); connect(m_session, SIGNAL(error(SignOn::Error)), this, SLOT(handleError(SignOn::Error)), Qt::UniqueConnection); m_session->process(SignOn::SessionData(SignOn::SessionData()), QStringLiteral("password")); } void CDavToolWorker::gotCredentials(const SignOn::SessionData &response) { m_username = response.toMap().value(QStringLiteral("UserName")).toString(); m_password = response.toMap().value(QStringLiteral("Secret")).toString(); Accounts::ServiceList services = m_account->services(); Q_FOREACH (const Accounts::Service &s, services) { if (s.serviceType().toLower() == QStringLiteral("caldav")) { m_caldavService = s; } else if (s.serviceType().toLower() == QStringLiteral("carddav")) { m_carddavService = s; } } if (m_operationMode == CDavToolWorker::ClearAllRemoteCalendars) { if (m_caldavService.name().isEmpty()) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("No caldav service found!"))); return; } m_account->selectService(m_caldavService); m_hostAddress = m_account->value(QStringLiteral("server_address")).toString(); if (m_hostAddress.isEmpty()) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("No host address known!"))); return; } const QStringList calendarPaths = m_account->value(QStringLiteral("calendars")).toStringList(); gotCollectionsList(calendarPaths); } else if (m_operationMode == CDavToolWorker::ClearAllRemoteAddressbooks) { if (m_carddavService.name().isEmpty()) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("No carddav service found!"))); return; } m_account->selectService(m_carddavService); m_hostAddress = m_account->value(QStringLiteral("server_address")).toString(); if (m_hostAddress.isEmpty()) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("No host address known!"))); return; } m_carddavSyncer = new Syncer(this, Q_NULLPTR, m_account->id()); m_carddavDiscovery = new CardDav(m_carddavSyncer, m_hostAddress, m_addressbookPath, m_username, m_password); connect(m_carddavDiscovery, &CardDav::addressbooksList, this, [this] (const QList &addressbooks) { QStringList paths; for (const ReplyParser::AddressBookInformation &ab : addressbooks) { paths.append(ab.url); } this->gotCollectionsList(paths); }); connect(m_carddavDiscovery, &CardDav::error, this, &CDavToolWorker::handleCardDAVError); m_carddavDiscovery->determineAddressbooksList(); } } void CDavToolWorker::handleCardDAVError(int code) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("Error while retrieving addressbook list: %1").arg(code))); } void CDavToolWorker::gotCollectionsList(const QStringList &paths) { Q_FOREACH (const QString &cpath, paths) { QString requestStr = QStringLiteral( "" "" "" "" ""); QNetworkReply *getEtagsReply = generateRequest(m_hostAddress, cpath, QLatin1String("1"), QLatin1String("PROPFIND"), requestStr); connect(getEtagsReply, &QNetworkReply::finished, this, &CDavToolWorker::gotEtags); m_replies.append(getEtagsReply); } if (!m_replies.size()) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("No collections to clear!"))); } } void CDavToolWorker::gotEtags() { QNetworkReply *reply = qobject_cast(sender()); if (reply->error() != QNetworkReply::NoError) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("Error occurred when fetching etags: %1: %2").arg(reply->error()).arg(reply->errorString()))); return; } m_replies.removeOne(reply); QXmlStreamReader reader(reply->readAll()); QVariantMap vmap = xmlToVMap(reader); QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); QVariantList responses; if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { // multiple updates in the delta. responses = multistatusMap[QLatin1String("response")].toList(); } else { // only one update in the delta. QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); responses << response; } Q_FOREACH (const QVariant &rv, responses) { QVariantMap rmap = rv.toMap(); QString href = rmap.value("href").toMap().value("@text").toString(); QUrl uri = QUrl::fromPercentEncoding(href.toUtf8()); QString etag = rmap.value("propstat").toMap().value("prop").toMap().value("getetag").toMap().value("@text").toString(); QString status = rmap.value("propstat").toMap().value("status").toMap().value("@text").toString(); if (status.isEmpty()) { status = rmap.value("status").toMap().value("@text").toString(); } if (!href.endsWith(QStringLiteral(".vcf"), Qt::CaseInsensitive) && !href.endsWith(QStringLiteral(".vcs")) && !href.endsWith(QStringLiteral(".ics"))) { // this is probably a response for a collection resource, // rather than for a contact or event resource within the collection. qWarning() << "ignoring probable collection resource:" << uri.toString() << etag << status; continue; } else { qWarning() << "DELETING:" << m_hostAddress << href << etag; QNetworkReply *deletionRequest = generateUpsyncRequest(m_hostAddress, href, etag, QString(), QStringLiteral("DELETE"), QString()); connect(deletionRequest, &QNetworkReply::finished, this, &CDavToolWorker::finishedDeletion); m_replies.append(deletionRequest); } } if (m_replies.isEmpty()) { // collections are already empty. qWarning() << "All collections are empty?"; emit done(); } } void CDavToolWorker::finishedDeletion() { QNetworkReply *reply = qobject_cast(sender()); if (reply->error() != QNetworkReply::NoError) { handleError(SignOn::Error(SignOn::Error::Unknown, QStringLiteral("Error occurred when deleting event/contact: %1: %2").arg(reply->error()).arg(reply->errorString()))); return; } m_replies.removeOne(reply); if (m_replies.isEmpty()) { // this last deletion is complete! emit done(); } } QNetworkReply *CDavToolWorker::generateRequest(const QString &url, const QString &path, const QString &depth, const QString &requestType, const QString &request) const { QByteArray requestData(request.toUtf8()); QUrl reqUrl(url); if (!path.isEmpty()) { // override the path from the given url with the path argument. // this is because the initial URL may be a user-principals URL // but subsequent paths are not relative to that one, but instead // are relative to the root path / if (path.startsWith('/')) { reqUrl.setPath(path); } else { reqUrl.setPath('/' + path); } } if (!m_username.isEmpty() && !m_password.isEmpty()) { reqUrl.setUserName(m_username); reqUrl.setPassword(m_password); } QNetworkRequest req(reqUrl); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8"); req.setHeader(QNetworkRequest::ContentLengthHeader, requestData.length()); if (!depth.isEmpty()) { req.setRawHeader("Depth", depth.toUtf8()); } QBuffer *requestDataBuffer = new QBuffer(parent()); requestDataBuffer->setData(requestData); qWarning() << "generateRequest():" << reqUrl << depth << requestType << QString::fromUtf8(requestData); return m_networkManager->sendCustomRequest(req, requestType.toLatin1(), requestDataBuffer); } QNetworkReply *CDavToolWorker::generateUpsyncRequest(const QString &url, const QString &path, const QString &ifMatch, const QString &contentType, const QString &requestType, const QString &request) const { QByteArray requestData(request.toUtf8()); QUrl reqUrl(url); if (!path.isEmpty()) { // override the path from the given url with the path argument. // this is because the initial URL may be a user-principals URL // but subsequent paths are not relative to that one, but instead // are relative to the root path / reqUrl.setPath(path); } if (!m_username.isEmpty() && !m_password.isEmpty()) { reqUrl.setUserName(m_username); reqUrl.setPassword(m_password); } QNetworkRequest req(reqUrl); if (!contentType.isEmpty()) { req.setHeader(QNetworkRequest::ContentTypeHeader, contentType); } if (!request.isEmpty()) { req.setHeader(QNetworkRequest::ContentLengthHeader, requestData.length()); } if (!ifMatch.isEmpty()) { req.setRawHeader("If-Match", ifMatch.toUtf8()); } qWarning() << "generateUpsyncRequest():" << reqUrl << ":" << requestData.length() << "bytes"; Q_FOREACH (const QByteArray &headerName, req.rawHeaderList()) { qWarning() << " " << headerName << "=" << req.rawHeader(headerName); } if (!request.isEmpty()) { QBuffer *requestDataBuffer = new QBuffer(parent()); requestDataBuffer->setData(requestData); return m_networkManager->sendCustomRequest(req, requestType.toLatin1(), requestDataBuffer); } return m_networkManager->sendCustomRequest(req, requestType.toLatin1()); } buteo-sync-plugin-carddav-0.1.12/tools/cdavtool/worker.h000066400000000000000000000121541475112063300232230ustar00rootroot00000000000000/* * Copyright (C) 2016 Jolla Ltd. * Contact: Chris Adams * * You may use this file under the terms of the BSD license as follows: * * "Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * Neither the name of Nemo Mobile nor the names of its contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." */ #ifndef CDAVTOOL_WORKER_H #define CDAVTOOL_WORKER_H #include "helpers.h" #include "syncer_p.h" #include "carddav_p.h" #include #include #include #include // accounts&sso #include #include #include #include #include #include #include #include // buteo-syncfw #include #include #include #include #include class CDavToolWorker : public QObject { Q_OBJECT public: enum CreateMode { CreateBoth = 0, CreateCardDAV, CreateCalDAV }; enum OperationMode { CreateAccount = 0, DeleteAccount, ClearAllRemoteCalendars, ClearAllRemoteAddressbooks }; CDavToolWorker(QObject *parent = Q_NULLPTR); ~CDavToolWorker(); void setVerbose(bool verbose) { m_verbose = verbose; } void createAccount(const QString &username, const QString &password, CreateMode mode, const QString &hostAddress, const QString &calendarPath = QString(), const QString &addressbookPath = QString()); void deleteAccount(int accountId); void clearRemoteCalendars(int accountId); void clearRemoteAddressbooks(int accountId); bool errorOccurred() const { return m_errorOccurred; } Q_SIGNALS: void done(); private: bool createSyncProfiles(Accounts::Account *account, const Accounts::Service &service); QNetworkReply *generateRequest(const QString &url, const QString &path, const QString &depth, const QString &requestType, const QString &request) const; QNetworkReply *generateUpsyncRequest(const QString &url, const QString &path, const QString &ifMatch, const QString &contentType, const QString &requestType, const QString &request) const; private Q_SLOTS: void handleCredentialsStored(quint32); void accountDone(); void discoveryError(); void handleError(const SignOn::Error &err); void handleCardDAVError(int code); void gotCredentials(const SignOn::SessionData &response); void gotCollectionsList(const QStringList &paths); void gotEtags(); void finishedDeletion(); private: Syncer *m_carddavSyncer; CardDav *m_carddavDiscovery; CalDAVDiscovery *m_caldavDiscovery; QNetworkAccessManager *m_networkManager; Buteo::ProfileManager *m_profileManager; Accounts::Manager *m_accountManager; Accounts::Account *m_account; SignOn::AuthSession *m_session; SignOn::Identity *m_identity; SignOn::IdentityInfo m_credentials; Accounts::Service m_caldavService; Accounts::Service m_carddavService; QString m_username; QString m_password; QString m_hostAddress; QString m_calendarPath; QString m_addressbookPath; CreateMode m_createMode; OperationMode m_operationMode; bool m_errorOccurred; bool m_verbose; QList m_replies; }; #endif // CDAVTOOL_WORKER_H buteo-sync-plugin-carddav-0.1.12/tools/tools.pro000066400000000000000000000000431475112063300216020ustar00rootroot00000000000000TEMPLATE=subdirs SUBDIRS+=cdavtool