Re: Support WKS publishing from within Kleopatra

Felix Tiede <[email protected]>
Newsgroups gmane.comp.kde.devel.pim
Message-ID <3301521.Luzu6u9BVM@pip>
Hey Ingo,

Am Dienstag, 5. April 2022, 09:24:20 CEST schrieb Ingo Klöcker:
> Hi Felix,
> 
> On Montag, 4. April 2022 17:42:15 CEST Felix Tiede wrote:
> > Kmail's account wizard already supports it, yet it is unavailable for
> > existing keys and accounts: Publish a PGP key on the mail provider's WKD
> > using the WKS process.
> 
> Yes, that's still missing in Kleopatra. See https://dev.gnupg.org/T5334
> which is the high-level task for improving the support for WKS/WKD in
> Kleopatra.

Interesting.
My patch so far only supports publishing a key, not searching/downloading it.

>
> > Some time ago I fiddled with it, basically copying src/commands/
> > exportopenpgpcertstoserver.*, modifying it to create a publish request
> > with
> > a command line call to gpg-wks-client, then reading that request from a
> > temporary file, building an email from it and attempting to send it using
> > the Akonadi transport associated with the sender mail address.
> > That patch was against Kleopatra-18.x.
> > 
> > Back then it was only supported to publish at the key's first user id's
> > provider and address, it has meanwhile been amended to support publishing
> > for each key's user id.
> 
> Sounds good.

Thanks.
It's currently implemented as a context menu entry for each User ID, like the 
entry for the key itself in the keylist, where the latter uses the key's first 
user id at all times, the former tries to get a mail transport for the ID 
before trying.

> 
> > The other day I finally got round to reproduce the same with
> > Kleopatra-21.12, which in turn required a patch for Libkleo-21.12, since I
> > need Libkleo to provide me with the path to the gpg-wks-client binary.
> > 
> > And now for the point of writing it all:
> > I want to write a MR for Kleopatra to get the feature into the main
> > distribution of Kleopatra.
> > So, as a first step I created a MR for Libkleo to provide Kleopatra with
> > the path to the gpg-wks-client binary - and in the course of this MR I've
> > learned about QGpgME namespace which provides a direct interface to
> > GnuPG's WKS client.
> > 
> > However, since I'm not really that proficient in rewriting my old patch to
> > work with QGpgME::WKSPublishJob, I am afraid I'd need assistance with
> > that.
> 
> That's what this mailing list is about. For more direct assistance we could
> also meet at the #kontact IRC channel at Libera.Chat.

I'm rarely on IRC these days, but it's an option, yes.

> 
> > So, would a MR with "the old way" of doing it - calling an external
> > binary,
> > creating, writing, reading, parsing a temporary file - be accepted into
> > Kleopatra or do I need to fully rewrite it for QGpgME::WKSPublishJob
> > before
> > creating the MR?
> 
> I'm afraid that I won't accept such an MR. Using QGpgME Jobs is the
> recommended way to interact with GnuPG via GpgME. (Another way is via Assuan
> transactions, e.g. for smartcard support, but that's not an option here.)

I've attached the (old) kleopatra-21.12.3 patch to this mail, so you can have 
a look at what I did and we can discuss porting it to QGpgME.

Or I can create a MR anyway and we can discuss via the review process, 
whatever works better.

> 
> BTW, I don't know your current status, e.g. whether you are a student, but
> in case you are interested: I'm pretty sure g10 Code GmbH would pay for
> your work. They are looking for developers and maybe even student workers.

Actually, I'm neither a professional developer nor looking for a job.
If g10 Code is indeed interested in paying me for this work, it would be 
possible for me to accept payment in a legal manner with appropriate taxing 
information, though.

Regards,
Felix
kleopatra-21.12.3-wks.patch (text/x-patch, 16.4 KB)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2cb93570..8f74be25 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -29,6 +29,8 @@ option(DISABLE_KWATCHGNUPG "Don't build the kwatchgnupg tool [default=OFF]" OFF)
 
 # Standalone build. Find / include everything necessary.
 set(KF5_MIN_VERSION "5.87.0")
+set(KIDENTITYMANAGEMENT_VERSION "5.19.3")
+set(KMAILTRANSPORT_VERSION "5.19.3")
 set(KMIME_VERSION "5.19.3")
 set(LIBKLEO_VERSION "5.19.3")
 set(QT_REQUIRED_VERSION "5.15.2")
@@ -112,6 +114,9 @@ endif()
 # Kdepimlibs packages
 find_package(KF5Libkleo ${LIBKLEO_VERSION} CONFIG REQUIRED)
 find_package(KF5Mime ${KMIME_WANT_VERSION} CONFIG REQUIRED)
+find_package(KF5IdentityManagement ${KIDENTITYMANAGEMENT_VERSION} CONFIG REQUIRED)
+find_package(KF5MailTransport ${KMAILTRANSPORT_VERSION} CONFIG REQUIRED)
+find_package(KF5MailTransportAkonadi ${KMAILTRANSPORT_VERSION} CONFIG REQUIRED)
 
 find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Widgets Test Network PrintSupport)
 
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 52fe5f16..ad025c08 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -242,6 +242,7 @@ set(_kleopatra_SRCS
   commands/selftestcommand.cpp
   commands/exportsecretkeycommand.cpp
   commands/exportopenpgpcertstoservercommand.cpp
+  commands/exportopenpgpcerttoprovidercommand.cpp
   commands/adduseridcommand.cpp
   commands/newcertificatecommand.cpp
   commands/setinitialpincommand.cpp
@@ -370,6 +371,9 @@ target_link_libraries(kleopatra_bin
   KF5::CoreAddons
   KF5::ItemModels
   KF5::Crash
+  KF5::IdentityManagement # Export OpenPGP keys using WKS
+  KF5::MailTransport
+  KF5::MailTransportAkonadi
   Qt::Network
   Qt::PrintSupport # Printing secret keys
   ${_kleopatra_uiserver_extra_libs}
diff --git a/src/commands/exportopenpgpcerttoprovidercommand.cpp b/src/commands/exportopenpgpcerttoprovidercommand.cpp
new file mode 100644
index 00000000..d50f7ade
--- /dev/null
+++ b/src/commands/exportopenpgpcerttoprovidercommand.cpp
@@ -0,0 +1,188 @@
+/* -*- mode: c++; c-basic-offset:4 -*-
+    commands/exportopenpgpcerttoprovidercommand.cpp
+
+    This file is part of Kleopatra, the KDE keymanager
+    SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
+    SPDX-FileCopyrightText: 2019-2022 Felix Tiede
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include <config-kleopatra.h>
+
+#include "exportopenpgpcerttoprovidercommand.h"
+
+#include "command_p.h"
+
+#include <Libkleo/GnuPG>
+
+#include <gpgme++/key.h>
+
+#include <kidentitymanagement/identity.h>
+#include <MailTransport/TransportManager>
+#include <MailTransportAkonadi/MessageQueueJob>
+
+#include <KLocalizedString>
+#include <KMessageBox>
+
+#include <QString>
+
+using namespace Kleo;
+using namespace Kleo::Commands;
+using namespace GpgME;
+
+const KIdentityManagement::IdentityManager *ExportOpenPGPCertToProviderCommand::mailIdManager = new KIdentityManagement::IdentityManager(true);
+
+ExportOpenPGPCertToProviderCommand::ExportOpenPGPCertToProviderCommand(KeyListController *c)
+    : GnuPGProcessCommand(c),
+      uid(GpgME::UserID())
+{
+    wksMail.open();
+    wksMail.close();
+}
+
+ExportOpenPGPCertToProviderCommand::ExportOpenPGPCertToProviderCommand(QAbstractItemView *v, KeyListController *c)
+    : GnuPGProcessCommand(v, c),
+      uid(GpgME::UserID())
+{
+    wksMail.open();
+    wksMail.close();
+}
+
+ExportOpenPGPCertToProviderCommand::ExportOpenPGPCertToProviderCommand(const Key &key)
+    : GnuPGProcessCommand(key),
+      uid(GpgME::UserID())
+{
+    wksMail.open();
+    wksMail.close();
+}
+
+ExportOpenPGPCertToProviderCommand::ExportOpenPGPCertToProviderCommand(const UserID &uid)
+    : GnuPGProcessCommand(uid.parent()),
+      uid(uid)
+{
+    wksMail.open();
+    wksMail.close();
+}
+
+ExportOpenPGPCertToProviderCommand::~ExportOpenPGPCertToProviderCommand() {}
+
+bool ExportOpenPGPCertToProviderCommand::preStartHook(QWidget *parent) const
+{
+    QString sender;
+    if (uid.isNull())
+      sender = QString::fromLatin1(d->keys().at(0).userID(0).addrSpec().data());
+    else
+      sender = QString::fromLatin1(uid.addrSpec().data());
+
+    KIdentityManagement::Identity identity = ExportOpenPGPCertToProviderCommand::mailIdManager->identityForAddress(sender);
+    if (identity.isNull())
+      identity = ExportOpenPGPCertToProviderCommand::mailIdManager->defaultIdentity();
+
+    if (identity.transport().isEmpty()) {
+        KMessageBox::error(parent,
+         xi18nc("@warning",
+                "<para><email>%1</email> has no usable transport for mailing a key available, "
+                "WKS upload not possible.</para>", sender),
+         xi18nc("@title:window", "OpenPGP Certificate Export"));
+        return false;
+    }
+    return KMessageBox::warningContinueCancel(parent,
+            xi18nc("@info",
+                   "<para>Not every mail provider supports WKS, so any key being "
+                   "exported this way may fail individually.</para><para>If exported, "
+                   "a confirmation request mail will be sent to <email>%1</email> "
+                   "which needs to be acknowledged with a mail program to complete the "
+                   "export process.</para><para><application>KMail</application> "
+                   "can handle these mails, but not all mail programs can.</para>"
+                   "<para>Once exported, the standard does not (yet) allow for "
+                   "automated removal of a published key.</para>"
+                   "<para>Are you sure you want to continue?</para>", sender),
+            xi18nc("@title:window", "OpenPGP Certificate Export"),
+            KStandardGuiItem::cont(), KStandardGuiItem::cancel(),
+            QStringLiteral("warn-export-openpgp-wks-unsupported"))
+            == KMessageBox::Continue;
+}
+
+void ExportOpenPGPCertToProviderCommand::postSuccessHook(QWidget *parent)
+{
+    QString sender;
+    if (uid.isNull())
+      sender = QString::fromLatin1(d->keys().at(0).userID(0).addrSpec().data());
+    else
+      sender = QString::fromLatin1(uid.addrSpec().data());
+
+    KIdentityManagement::Identity identity = ExportOpenPGPCertToProviderCommand::mailIdManager->identityForAddress(sender);
+    if (identity.isNull())
+      identity = ExportOpenPGPCertToProviderCommand::mailIdManager->defaultIdentity();
+    MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportByName(
+        identity.transport());
+
+    if (!transport)
+        return;
+
+    wksMail.open();
+    KMime::Message *msg = new KMime::Message();
+
+    msg->setContent(KMime::CRLFtoLF(wksMail.readAll()));
+    msg->parse();
+    wksMail.close();
+
+    MailTransport::MessageQueueJob *job = new MailTransport::MessageQueueJob(parent);
+    job->transportAttribute().setTransportId(transport->id());
+    job->addressAttribute().setFrom(msg->from()->asUnicodeString());
+    job->addressAttribute().setTo(msg->to()->displayNames());
+    job->setMessage(KMime::Message::Ptr(msg));
+
+    job->start();
+}
+
+QStringList ExportOpenPGPCertToProviderCommand::arguments() const
+{
+    QStringList result;
+    result << gpgWksClientPath();
+    result << QStringLiteral("--output") << wksMail.fileName();
+    result << QStringLiteral("--create");
+    Q_FOREACH (const Key &key, d->keys()) {
+        result << QLatin1String(key.primaryFingerprint());
+        if (!uid.isNull()) {
+            result << QLatin1String(uid.addrSpec().data());
+        }
+        else {
+            result << QLatin1String(key.userID(0).email());
+        }
+    }
+    return result;
+}
+
+QString ExportOpenPGPCertToProviderCommand::errorCaption() const
+{
+    return i18nc("@title:window", "OpenPGP Certificate Export Error");
+}
+
+QString ExportOpenPGPCertToProviderCommand::successCaption() const
+{
+    return i18nc("@title:window", "OpenPGP Certificate Export Finished");
+}
+
+QString ExportOpenPGPCertToProviderCommand::crashExitMessage(const QStringList &args) const
+{
+    return xi18nc("@info",
+                  "<para>The GPG process that tried to export OpenPGP certificates "
+                  "ended prematurely because of an unexpected error.</para>"
+                  "<para>Please check the output of <icode>%1</icode> for details.</para>", args.join(QLatin1Char(' ')));
+}
+
+QString ExportOpenPGPCertToProviderCommand::errorExitMessage(const QStringList &args) const
+{
+    return xi18nc("@info",
+                  "<para>An error occurred while trying to export OpenPGP certificates.</para> "
+                  "<para>The output from <command>%1</command> was: <message>%2</message></para>",
+                  args[0], errorString());
+}
+
+QString ExportOpenPGPCertToProviderCommand::successMessage(const QStringList&) const
+{
+    return i18nc("@info", "OpenPGP certificates exported successfully.");
+}
+
diff --git a/src/commands/exportopenpgpcerttoprovidercommand.h b/src/commands/exportopenpgpcerttoprovidercommand.h
new file mode 100644
index 00000000..f7e1798d
--- /dev/null
+++ b/src/commands/exportopenpgpcerttoprovidercommand.h
@@ -0,0 +1,65 @@
+/* -*- mode: c++; c-basic-offset:4 -*-
+    commands/exportopenpgpcertstoservercommand.h
+
+    This file is part of Kleopatra, the KDE keymanager
+    SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
+    SPDX-FileCopyrightText: 2019 Felix Tiede
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef __KLEOPATRA_COMMMANDS_EXPORTOPENPGPCERTTOPROVIDERCOMMAND_H__
+#define __KLEOPATRA_COMMMANDS_EXPORTOPENPGPCERTTOPROVIDERCOMMAND_H__
+
+#include <commands/gnupgprocesscommand.h>
+
+#include <kidentitymanagement/identitymanager.h>
+
+#include <QtCore/QTemporaryFile>
+
+#include <gpgme++/key.h>
+
+namespace Kleo
+{
+namespace Commands
+{
+
+class ExportOpenPGPCertToProviderCommand : public GnuPGProcessCommand
+{
+    Q_OBJECT
+public:
+    explicit ExportOpenPGPCertToProviderCommand(QAbstractItemView *view, KeyListController *parent);
+    explicit ExportOpenPGPCertToProviderCommand(KeyListController *parent);
+    explicit ExportOpenPGPCertToProviderCommand(const GpgME::Key &key);
+    explicit ExportOpenPGPCertToProviderCommand(const GpgME::UserID &uid);
+
+    ~ExportOpenPGPCertToProviderCommand() override;
+
+    static Restrictions restrictions()
+    {
+        return OnlyOneKey | NeedSecretKey | MustBeOpenPGP;
+    }
+
+private:
+    bool preStartHook(QWidget *) const override;
+    void postSuccessHook(QWidget *) override;
+
+    QStringList arguments() const override;
+
+    QString errorCaption() const override;
+    QString successCaption() const override;
+
+    QString crashExitMessage(const QStringList &) const override;
+    QString errorExitMessage(const QStringList &) const override;
+    QString successMessage(const QStringList &) const override;
+
+    GpgME::UserID uid;
+
+    QTemporaryFile wksMail;
+    static const KIdentityManagement::IdentityManager *mailIdManager;
+};
+
+}
+}
+
+#endif // __KLEOPATRA_COMMMANDS_EXPORTOPENPGPCERTTOPROVIDERCOMMAND_H__
diff --git a/src/dialogs/certificatedetailswidget.cpp b/src/dialogs/certificatedetailswidget.cpp
index f637075e..608e5ead 100644
--- a/src/dialogs/certificatedetailswidget.cpp
+++ b/src/dialogs/certificatedetailswidget.cpp
@@ -1,5 +1,6 @@
 /*  SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB
     SPDX-FileCopyrightText: 2017 Intevation GmbH
+    SPDX-FileCopyrightText: 2019-2022 Felix Tiede
 
     SPDX-License-Identifier: GPL-2.0-or-later
 */
@@ -17,6 +18,7 @@
 #include "commands/changepassphrasecommand.h"
 #include "commands/changeexpirycommand.h"
 #include "commands/certifycertificatecommand.h"
+#include "commands/exportopenpgpcerttoprovidercommand.h"
 #include "commands/revokecertificationcommand.h"
 #include "commands/adduseridcommand.h"
 #include "commands/genrevokecommand.h"
@@ -90,6 +92,7 @@ public:
     void webOfTrustClicked();
     void exportClicked();
     void addUserID();
+    void exportUserIDToProvider();
     void changePassphrase();
     void changeExpiration();
     void keysMayHaveChanged();
@@ -599,6 +602,16 @@ void CertificateDetailsWidget::Private::addUserID()
     cmd->start();
 }
 
+void CertificateDetailsWidget::Private::exportUserIDToProvider()
+{
+    auto userID = key.userID(0);
+
+    auto item = ui.userIDTable->currentItem();
+    if (item) {
+        userID = item->data(0, Qt::UserRole).value<GpgME::UserID>();
+    }
+}
+
 void CertificateDetailsWidget::Private::keysMayHaveChanged()
 {
     auto newKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint());
@@ -643,6 +656,17 @@ void CertificateDetailsWidget::Private::userIDTableContextMenuRequested(const QP
         });
         cmd->start();
     });
+    if (key.hasSecret() && key.protocol() == GpgME::OpenPGP) {
+        menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export-provider")),
+                        i18n("Publish at mail provider ..."),
+                        q, [this, userID]() {
+            auto cmd = new Kleo::Commands::ExportOpenPGPCertToProviderCommand(userID);
+            ui.userIDTable->setEnabled(false);
+            connect(cmd, &Kleo::Commands::ExportOpenPGPCertToProviderCommand::finished,
+                    q, [this]() { ui.userIDTable->setEnabled(true); });
+            cmd->start();
+        });
+    }
     if (Kleo::Commands::RevokeCertificationCommand::isSupported()) {
         menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")),
                         i18n("Revoke Certification..."),
diff --git a/src/kleopatra.rc b/src/kleopatra.rc
index 13281f5d..79434dbf 100644
--- a/src/kleopatra.rc
+++ b/src/kleopatra.rc
@@ -12,6 +12,7 @@
             <Action name="file_export_secret_keys"/>
             <Action name="file_export_paper_key"/>
             <Action name="file_export_certificates_to_server"/>
+            <Action name="file_export_certificate_to_provider"/>
             <Separator/>
             <Action name="file_decrypt_verify_files"/>
             <Action name="file_sign_encrypt_files"/>
@@ -126,6 +127,7 @@
         <Action name="file_export_secret_keys"/>
         <Action name="file_export_paper_key"/>
         <Action name="file_export_certificates_to_server"/>
+        <Action name="file_export_certificate_to_provider"/>
         <Separator/>
         <Action name="view_certificate_details"/>
     </Menu>
diff --git a/src/view/keylistcontroller.cpp b/src/view/keylistcontroller.cpp
index ba1d8004..7deaea6d 100644
--- a/src/view/keylistcontroller.cpp
+++ b/src/view/keylistcontroller.cpp
@@ -3,6 +3,7 @@
 
     This file is part of Kleopatra, the KDE keymanager
     SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
+    SPDX-FileCopyrightText: 2019-2022 Felix Tiede
 
     SPDX-License-Identifier: GPL-2.0-or-later
 */
@@ -20,6 +21,7 @@
 #include "kleopatra_debug.h"
 #include "commands/exportcertificatecommand.h"
 #include "commands/exportopenpgpcertstoservercommand.h"
+#include "commands/exportopenpgpcerttoprovidercommand.h"
 #include "commands/exportsecretkeycommand.h"
 #include "commands/importcertificatefromfilecommand.h"
 #include "commands/changepassphrasecommand.h"
@@ -358,6 +360,10 @@ void KeyListController::createActions(KActionCollection *coll)
             "file_export_certificates_to_server", i18n("Publish on Server..."), i18n("Publish the selected certificate (public key) on a public keyserver"),
             "view-certificate-export-server", nullptr, nullptr, QStringLiteral("Ctrl+Shift+E"), false, true
         },
+        {
+            "file_export_certificate_to_provider", i18n("Publish at Mailprovider..."), i18n("Publish the selected certificate (public key) at mail provider's Web Key Directory if offered"),
+            "view-certificate-export-provider", nullptr, nullptr, QString(), false, true
+        },
         {
             "file_export_secret_keys", i18n("Backup Secret Keys..."), QString(),
             "view-certificate-export-secret", nullptr, nullptr, QString(), false, true
@@ -491,6 +497,7 @@ void KeyListController::createActions(KActionCollection *coll)
     registerActionForCommand<ExportSecretKeyCommand>(coll->action(QStringLiteral("file_export_secret_keys")));
     registerActionForCommand<ExportPaperKeyCommand>(coll->action(QStringLiteral("file_export_paper_key")));
     registerActionForCommand<ExportOpenPGPCertsToServerCommand>(coll->action(QStringLiteral("file_export_certificates_to_server")));
+    registerActionForCommand<ExportOpenPGPCertToProviderCommand>(coll->action(QStringLiteral("file_export_certificate_to_provider")));
     //---
     registerActionForCommand<DecryptVerifyFilesCommand>(coll->action(QStringLiteral("file_decrypt_verify_files")));
     registerActionForCommand<SignEncryptFilesCommand>(coll->action(QStringLiteral("file_sign_encrypt_files")));
signature.asc (application/pgp-signature, 833 B)
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEJijiGzDq8UDHCPC0QGDR0So3spMFAmJMJ7UACgkQQGDR0So3
spP8yxAArk5gaHW65wjVJaPdkhEgovCKWngclMXGK9oapzYkXvRi2Cj3hGOwkzfx
tkW/FLJlFiWAHyOvQ97wDkkB4qLERiAdHD6L9zWSlfNpwv4P+lVjW7n8V/8m2eKD
6WFjL+61gtPDhpv9qdB1bDJXZxBBrS+EI1L7cqvdeD2f/t8ucX5cTCK+CaxMYgmp
dUBJBdfdQvuqPkd2lY0sl491iFTK1TzjFA7o2oolcPUdujGmDgDyf9zovE+Eg3JM
v9ly8SXHMkDQNG2elX9xbt/NiuV5mPmPQt4k5lOo4MqyM8mY3G8sp9cn3lPXjlnX
ng3WUOtJ/75Fh/IgI65MehpVWPvfUypU28eOV6UZh7n+3En4Lwa2guhaBZXl51tE
wvELRm5fXcTbseqekoKH4kmsbuSWSt8Fpu6AXciY+qJkY/z8axoRW4xu7+NAy0lT
G5E8XhSQQF5FJV0WGGLzhiW+rg03C52jjsa6g8bhBrFDPiCyYFfcil4/Ml3P6cDw
NkytnyvzfaGqKkL9LUs+/x09RtssssK4wfIwVMWIAfJhjYMqsTMwTLRFYwqRBeZ7
M4Qb42XGCgM+iJ/AdU8FhqzGQB3JvZYNApoYFyXhiWOmUYyX60o3/aYHvGa/YvVS
+L3JuLZlbjAVAnivUPXoltLjI274nrTkdMOBeKAwlAzkUTfX/GI=
=xOie
-----END PGP SIGNATURE-----
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.