[gnome-software/wip/hughsie/working-shell-extensions] shell-extensions: Fix regressions when switching to libxmlb
- From: Richard Hughes <rhughes src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/wip/hughsie/working-shell-extensions] shell-extensions: Fix regressions when switching to libxmlb
- Date: Wed, 21 Nov 2018 16:17:49 +0000 (UTC)
commit 0f9c9ee3611c66b5ff69da198bdae883ef672b2a
Author: Richard Hughes <richard hughsie com>
Date: Mon Nov 19 13:35:31 2018 +0000
shell-extensions: Fix regressions when switching to libxmlb
plugins/shell-extensions/gs-appstream.c | 1 +
plugins/shell-extensions/gs-appstream.h | 1 +
.../shell-extensions/gs-plugin-shell-extensions.c | 377 ++++++++++++++-------
plugins/shell-extensions/gs-self-test.c | 15 +-
plugins/shell-extensions/meson.build | 5 +-
5 files changed, 268 insertions(+), 131 deletions(-)
---
diff --git a/plugins/shell-extensions/gs-appstream.c b/plugins/shell-extensions/gs-appstream.c
new file mode 120000
index 00000000..96326ab0
--- /dev/null
+++ b/plugins/shell-extensions/gs-appstream.c
@@ -0,0 +1 @@
+../core/gs-appstream.c
\ No newline at end of file
diff --git a/plugins/shell-extensions/gs-appstream.h b/plugins/shell-extensions/gs-appstream.h
new file mode 120000
index 00000000..4eabcb3c
--- /dev/null
+++ b/plugins/shell-extensions/gs-appstream.h
@@ -0,0 +1 @@
+../core/gs-appstream.h
\ No newline at end of file
diff --git a/plugins/shell-extensions/gs-plugin-shell-extensions.c
b/plugins/shell-extensions/gs-plugin-shell-extensions.c
index cf69c298..36de3b06 100644
--- a/plugins/shell-extensions/gs-plugin-shell-extensions.c
+++ b/plugins/shell-extensions/gs-plugin-shell-extensions.c
@@ -1,6 +1,6 @@
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
*
- * Copyright (C) 2016 Richard Hughes <richard hughsie com>
+ * Copyright (C) 2016-2018 Richard Hughes <richard hughsie com>
* Copyright (C) 2018 Kalev Lember <klember redhat com>
*
* Licensed under the GNU General Public License Version 2
@@ -24,12 +24,13 @@
#include <errno.h>
#include <glib/gi18n.h>
-#include <glib/gstdio.h>
#include <json-glib/json-glib.h>
#include <xmlb.h>
#include <gnome-software.h>
+#include "gs-appstream.h"
+
#define SHELL_EXTENSIONS_API_URI "https://extensions.gnome.org/"
/*
@@ -48,6 +49,7 @@ struct GsPluginData {
gchar *shell_version;
GsApp *cached_origin;
GSettings *settings;
+ XbSilo *silo;
};
typedef enum {
@@ -67,6 +69,8 @@ typedef enum {
GS_PLUGIN_SHELL_EXTENSION_KIND_LAST
} GsPluginShellExtensionKind;
+static gboolean _check_silo (GsPlugin *plugin, GCancellable *cancellable, GError **error);
+
void
gs_plugin_initialize (GsPlugin *plugin)
{
@@ -93,6 +97,8 @@ gs_plugin_destroy (GsPlugin *plugin)
g_free (priv->shell_version);
if (priv->proxy != NULL)
g_object_unref (priv->proxy);
+ if (priv->silo != NULL)
+ g_object_unref (priv->silo);
g_object_unref (priv->cached_origin);
g_object_unref (priv->settings);
}
@@ -405,7 +411,15 @@ gs_plugin_refine_app (GsPlugin *plugin,
GCancellable *cancellable,
GError **error)
{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
const gchar *uuid;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(XbNode) component = NULL;
+
+ /* repo not enabled */
+ if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
+ return TRUE;
/* only process this app if was created by this plugin */
if (g_strcmp0 (gs_app_get_management_plugin (app),
@@ -433,6 +447,77 @@ gs_plugin_refine_app (GsPlugin *plugin,
if (gs_app_get_size_download (app) == 0)
gs_app_set_size_download (app, GS_APP_SIZE_UNKNOWABLE);
+
+ /* find the component using the UUID */
+ if (!_check_silo (plugin, cancellable, error))
+ return FALSE;
+ xpath = g_strdup_printf ("components/component/custom/"
+ "value[@key='shell-extensions::uuid'][text()='%s']/../..",
+ uuid);
+ component = xb_silo_query_first (priv->silo, xpath, &error_local);
+ if (component == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ return gs_appstream_refine_app (plugin, app, priv->silo, component, flags, error);
+}
+
+gboolean
+gs_plugin_refine_wildcard (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ const gchar *id;
+ g_autofree gchar *xpath = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) components = NULL;
+
+ /* repo not enabled */
+ if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
+ return TRUE;
+
+ /* check silo is valid */
+ if (!_check_silo (plugin, cancellable, error))
+ return FALSE;
+
+ /* not enough info to find */
+ id = gs_app_get_id (app);
+ if (id == NULL)
+ return TRUE;
+
+ /* find all apps */
+ xpath = g_strdup_printf ("components/component/id[text()='%s']/..", id);
+ components = xb_silo_query (priv->silo, xpath, 0, &error_local);
+ if (components == NULL) {
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return TRUE;
+ if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ return FALSE;
+ }
+ for (guint i = 0; i < components->len; i++) {
+ XbNode *component = g_ptr_array_index (components, i);
+ g_autoptr(GsApp) new = NULL;
+ new = gs_appstream_create_app (plugin, priv->silo, component, error);
+ if (new == NULL)
+ return FALSE;
+ gs_app_subsume_metadata (new, app);
+ if (!gs_appstream_refine_app (plugin, new, priv->silo, component,
+ refine_flags, error))
+ return FALSE;
+ gs_app_list_add (list, new);
+ }
+
+ /* success */
return TRUE;
}
@@ -468,12 +553,8 @@ gs_plugin_shell_extensions_parse_version (GsPlugin *plugin,
}
/* FIXME: mark as incompatible? */
- if (json_ver == NULL) {
- g_debug ("no version_map for %s: %s",
- component_id,
- priv->shell_version);
+ if (json_ver == NULL)
return TRUE;
- }
/* parse the version */
version = json_object_get_int_member (json_ver, "version");
@@ -504,12 +585,16 @@ gs_plugin_shell_extensions_parse_app (GsPlugin *plugin,
const gchar *tmp;
g_autofree gchar *component_id = NULL;
g_autoptr(XbBuilderNode) app = NULL;
+ g_autoptr(XbBuilderNode) categories = NULL;
g_autoptr(XbBuilderNode) metadata = NULL;
app = xb_builder_node_new ("component");
xb_builder_node_set_attr (app, "kind", "shell-extension");
xb_builder_node_insert_text (app, "project_license", "GPL-2.0+", NULL);
- metadata = xb_builder_node_insert (app, "metadata", NULL);
+ categories = xb_builder_node_insert (app, "categories", NULL);
+ xb_builder_node_insert_text (categories, "category", "Addon", NULL);
+ xb_builder_node_insert_text (categories, "category", "ShellExtension", NULL);
+ metadata = xb_builder_node_insert (app, "custom", NULL);
tmp = json_object_get_string_member (json_app, "description");
if (tmp != NULL) {
@@ -575,31 +660,27 @@ gs_plugin_shell_extensions_parse_app (GsPlugin *plugin,
return g_steal_pointer (&app);
}
-static XbBuilderNode *
-gs_plugin_shell_extensions_parse_apps (GsPlugin *plugin,
- const gchar *data,
- gssize data_len,
- GError **error)
+static GInputStream *
+gs_plugin_appstream_load_json_cb (XbBuilderSource *self,
+ GFile *file,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
{
+ GsPlugin *plugin = GS_PLUGIN (user_data);
JsonArray *json_extensions_array;
JsonNode *json_extensions;
JsonNode *json_root;
JsonObject *json_item;
+ gchar *xml;
+ g_autofree gchar *fn = g_file_get_path (file);
+ g_autoptr(AsApp) app = as_app_new ();
g_autoptr(JsonParser) json_parser = NULL;
g_autoptr(XbBuilderNode) apps = NULL;
- /* nothing */
- if (data == NULL) {
- g_set_error_literal (error,
- GS_PLUGIN_ERROR,
- GS_PLUGIN_ERROR_INVALID_FORMAT,
- "server returned no data");
- return NULL;
- }
-
/* parse the data and find the success */
json_parser = json_parser_new ();
- if (!json_parser_load_from_data (json_parser, data, data_len, error)) {
+ if (!json_parser_load_from_file (json_parser, fn, error)) {
gs_utils_error_convert_json_glib (error);
return NULL;
}
@@ -661,139 +742,121 @@ gs_plugin_shell_extensions_parse_apps (GsPlugin *plugin,
xb_builder_node_add_child (apps, component);
}
- return g_steal_pointer (&apps);
+ /* convert back to XML */
+ xml = xb_builder_node_export (apps, XB_NODE_EXPORT_FLAG_NONE, error);
+ if (xml == NULL)
+ return NULL;
+ return g_memory_input_stream_new_from_data (xml, -1, g_free);
}
-static XbBuilderNode *
-gs_plugin_shell_extensions_get_apps (GsPlugin *plugin,
- guint cache_age,
- GCancellable *cancellable,
- GError **error)
+static gboolean
+gs_plugin_shell_extensions_refresh (GsPlugin *plugin,
+ guint cache_age,
+ GCancellable *cancellable,
+ GError **error)
{
GsPluginData *priv = gs_plugin_get_data (plugin);
- g_autofree gchar *cachefn = NULL;
+ g_autofree gchar *fn = NULL;
g_autofree gchar *uri = NULL;
- g_autoptr(GBytes) data = NULL;
- g_autoptr(GFile) cachefn_file = NULL;
+ g_autoptr(GFile) file = NULL;
g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin));
- g_autoptr(XbBuilderNode) apps = NULL;
- /* look in the cache */
- cachefn = gs_utils_get_cache_filename ("shell-extensions",
- "gnome.json",
- GS_UTILS_CACHE_FLAG_WRITEABLE,
- error);
- if (cachefn == NULL)
- return NULL;
- cachefn_file = g_file_new_for_path (cachefn);
- if (gs_utils_get_file_age (cachefn_file) < cache_age) {
- g_autofree gchar *json_data = NULL;
- if (!g_file_get_contents (cachefn, &json_data, NULL, error))
- return NULL;
- g_debug ("got cached extension data from %s", cachefn);
- return gs_plugin_shell_extensions_parse_apps (plugin,
- json_data,
- -1, error);
+ /* get cache filename */
+ fn = gs_utils_get_cache_filename ("shell-extensions",
+ "gnome.json",
+ GS_UTILS_CACHE_FLAG_WRITEABLE,
+ error);
+ if (fn == NULL)
+ return FALSE;
+
+ /* check age */
+ file = g_file_new_for_path (fn);
+ if (g_file_query_exists (file, NULL)) {
+ guint age = gs_utils_get_file_age (file);
+ if (age < cache_age) {
+ g_debug ("%s is only %u seconds old, ignoring", fn, age);
+ return TRUE;
+ }
}
- /* create the GET data */
+ /* download the file */
uri = g_strdup_printf ("%s/static/extensions.json",
SHELL_EXTENSIONS_API_URI);
gs_app_set_summary_missing (app_dl,
/* TRANSLATORS: status text when downloading */
_("Downloading shell extension metadata…"));
- data = gs_plugin_download_data (plugin, app_dl, uri, cancellable, error);
- if (data == NULL) {
+ if (!gs_plugin_download_file (plugin, app_dl, uri, fn, cancellable, error)) {
gs_utils_error_add_unique_id (error, priv->cached_origin);
- return NULL;
- }
- apps = gs_plugin_shell_extensions_parse_apps (plugin,
- g_bytes_get_data (data, NULL),
- (gssize) g_bytes_get_size (data),
- error);
- if (apps == NULL) {
- gsize len = g_bytes_get_size (data);
- g_autofree gchar *tmp = NULL;
-
- /* truncate the string if long */
- if (len > 100)
- len = 100;
- tmp = g_strndup (g_bytes_get_data (data, NULL), len);
- g_prefix_error (error, "Failed to parse '%s': ", tmp);
- return NULL;
+ return FALSE;
}
- /* save to the cache */
- if (!g_file_set_contents (cachefn,
- g_bytes_get_data (data, NULL),
- (guint) g_bytes_get_size (data),
- error))
- return NULL;
-
- return g_steal_pointer (&apps);
+ /* be explicit */
+ if (priv->silo != NULL)
+ xb_silo_invalidate (priv->silo);
+ return TRUE;
}
static gboolean
-gs_plugin_shell_extensions_refresh (GsPlugin *plugin,
- guint cache_age,
- GCancellable *cancellable,
- GError **error)
+_check_silo (GsPlugin *plugin, GCancellable *cancellable, GError **error)
{
GsPluginData *priv = gs_plugin_get_data (plugin);
- gboolean repo_enabled;
- const gchar *fn_test;
+ g_autofree gchar *blobfn = NULL;
g_autofree gchar *fn = NULL;
g_autoptr(GError) error_local = NULL;
+ g_autoptr(GFile) blobfile = NULL;
g_autoptr(GFile) file = NULL;
g_autoptr(XbBuilder) builder = xb_builder_new ();
- g_autoptr(XbBuilderNode) apps = NULL;
- g_autoptr(XbSilo) silo = NULL;
+ g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
- /* check age */
- fn_test = g_getenv ("GS_SELF_TEST_SHELL_EXTENSIONS_XML_FN");
- if (fn_test != NULL) {
- fn = g_strdup (fn_test);
- } else {
- fn = gs_utils_get_cache_filename ("shell-extensions",
- "extensions-web.xmlb",
- GS_UTILS_CACHE_FLAG_WRITEABLE,
- error);
- if (fn == NULL)
- return FALSE;
+ /* everything is okay */
+ if (priv->silo != NULL && xb_silo_is_valid (priv->silo)) {
+ g_debug ("silo already valid");
+ return TRUE;
}
- /* remove old appstream data if the repo is disabled */
- repo_enabled = g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo");
- if (!repo_enabled) {
- g_unlink (fn);
- return TRUE;
+ /* drat! silo needs regenerating */
+ g_clear_object (&priv->silo);
+
+ /* verbose profiling */
+ if (g_getenv ("GS_XMLB_VERBOSE") != NULL) {
+ xb_builder_set_profile_flags (builder,
+ XB_SILO_PROFILE_FLAG_XPATH |
+ XB_SILO_PROFILE_FLAG_DEBUG);
}
+ /* add support for JSON files */
+ fn = gs_utils_get_cache_filename ("shell-extensions",
+ "gnome.json",
+ GS_UTILS_CACHE_FLAG_WRITEABLE,
+ error);
+ if (fn == NULL)
+ return FALSE;
+ xb_builder_source_add_converter (source,
+ "application/json",
+ gs_plugin_appstream_load_json_cb,
+ plugin, NULL);
file = g_file_new_for_path (fn);
- if (g_file_query_exists (file, NULL)) {
- guint age = gs_utils_get_file_age (file);
- if (age < cache_age) {
- g_debug ("%s is only %u seconds old, ignoring", fn, age);
- return TRUE;
- }
+ if (!xb_builder_source_load_file (source, file,
+ XB_BUILDER_SOURCE_FLAG_WATCH_FILE,
+ cancellable,
+ error)) {
+ return FALSE;
}
-
- /* get data */
- apps = gs_plugin_shell_extensions_get_apps (plugin,
- cache_age,
- cancellable,
- error);
- if (apps == NULL)
+ xb_builder_import_source (builder, source);
+
+ /* create binary cache */
+ blobfn = gs_utils_get_cache_filename ("shell-extensions",
+ "extensions-web.xmlb",
+ GS_UTILS_CACHE_FLAG_WRITEABLE,
+ error);
+ if (blobfn == NULL)
return FALSE;
-
- /* add to builder */
- xb_builder_import_node (builder, apps);
-
- /* save to disk */
- silo = xb_builder_ensure (builder, file,
- XB_BUILDER_COMPILE_FLAG_NONE,
- cancellable, &error_local);
- if (silo == NULL) {
+ blobfile = g_file_new_for_path (blobfn);
+ g_debug ("ensuring %s", blobfn);
+ priv->silo = xb_builder_ensure (builder, blobfile,
+ XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID,
+ NULL, &error_local);
+ if (priv->silo == NULL) {
g_set_error (error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_FAILED,
@@ -801,6 +864,57 @@ gs_plugin_shell_extensions_refresh (GsPlugin *plugin,
error_local->message);
return FALSE;
}
+
+ /* success */
+ return TRUE;
+}
+
+static void
+_claim_components (GsPlugin *plugin, GsAppList *list)
+{
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ gs_app_set_kind (app, AS_APP_KIND_SHELL_EXTENSION);
+ gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
+ gs_app_set_summary (app, GS_APP_QUALITY_LOWEST,
+ /* TRANSLATORS: the one-line summary */
+ _("GNOME Shell Extension"));
+ }
+}
+
+gboolean
+gs_plugin_add_search (GsPlugin *plugin, gchar **values, GsAppList *list,
+ GCancellable *cancellable, GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
+ return TRUE;
+ if (!_check_silo (plugin, cancellable, error))
+ return FALSE;
+ if (!gs_appstream_search (plugin, priv->silo, values, list_tmp,
+ cancellable, error))
+ return FALSE;
+ _claim_components (plugin, list_tmp);
+ gs_app_list_add_list (list, list_tmp);
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_category_apps (GsPlugin *plugin, GsCategory *category, GsAppList *list,
+ GCancellable *cancellable, GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GsAppList) list_tmp = gs_app_list_new ();
+ if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
+ return TRUE;
+ if (!_check_silo (plugin, cancellable, error))
+ return FALSE;
+ if (!gs_appstream_add_category_apps (plugin, priv->silo, category,
+ list_tmp, cancellable, error))
+ return FALSE;
+ _claim_components (plugin, list_tmp);
+ gs_app_list_add_list (list, list_tmp);
return TRUE;
}
@@ -810,10 +924,15 @@ gs_plugin_refresh (GsPlugin *plugin,
GCancellable *cancellable,
GError **error)
{
- return gs_plugin_shell_extensions_refresh (plugin,
- cache_age,
- cancellable,
- error);
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
+ return TRUE;
+ if (!gs_plugin_shell_extensions_refresh (plugin,
+ cache_age,
+ cancellable,
+ error))
+ return FALSE;
+ return _check_silo (plugin, cancellable, error);
}
gboolean
@@ -994,6 +1113,12 @@ gs_plugin_add_categories (GsPlugin *plugin,
GCancellable *cancellable,
GError **error)
{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ /* repo not enabled */
+ if (!g_settings_get_boolean (priv->settings, "enable-shell-extensions-repo"))
+ return TRUE;
+
/* just ensure there is any data, no matter how old */
return gs_plugin_shell_extensions_refresh (plugin,
G_MAXUINT,
diff --git a/plugins/shell-extensions/gs-self-test.c b/plugins/shell-extensions/gs-self-test.c
index 29db7106..80a74f7c 100644
--- a/plugins/shell-extensions/gs-self-test.c
+++ b/plugins/shell-extensions/gs-self-test.c
@@ -78,8 +78,9 @@ gs_plugins_shell_extensions_installed_func (GsPluginLoader *plugin_loader)
static void
gs_plugins_shell_extensions_remote_func (GsPluginLoader *plugin_loader)
{
- const gchar *xml_fn = "/var/tmp/self-test/extensions-web.xmlb";
+ const gchar *cachedir = "/var/tmp/gs-self-test";
gboolean ret;
+ g_autofree gchar *fn = NULL;
g_autoptr(GError) error = NULL;
g_autoptr(GFile) file = NULL;
g_autoptr(GPtrArray) components = NULL;
@@ -93,10 +94,10 @@ gs_plugins_shell_extensions_remote_func (GsPluginLoader *plugin_loader)
}
/* ensure files are removed */
- g_unlink (xml_fn);
+ g_setenv ("GS_SELF_TEST_CACHEDIR", cachedir, TRUE);
+ gs_utils_rmtree (cachedir, NULL);
/* refresh the metadata */
- g_setenv ("GS_SELF_TEST_SHELL_EXTENSIONS_XML_FN", xml_fn, TRUE);
plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
"age", (guint64) G_MAXUINT,
NULL);
@@ -106,7 +107,13 @@ gs_plugins_shell_extensions_remote_func (GsPluginLoader *plugin_loader)
/* ensure file was populated */
silo = xb_silo_new ();
- file = g_file_new_for_path (xml_fn);
+ fn = gs_utils_get_cache_filename ("shell-extensions",
+ "extensions-web.xmlb",
+ GS_UTILS_CACHE_FLAG_WRITEABLE,
+ &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (fn);
+ file = g_file_new_for_path (fn);
ret = xb_silo_load_from_file (silo, file,
XB_SILO_LOAD_FLAG_NONE,
NULL, &error);
diff --git a/plugins/shell-extensions/meson.build b/plugins/shell-extensions/meson.build
index dec9d97d..d30ebc5b 100644
--- a/plugins/shell-extensions/meson.build
+++ b/plugins/shell-extensions/meson.build
@@ -3,7 +3,10 @@ cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"']
shared_module(
'gs_plugin_shell-extensions',
-sources : 'gs-plugin-shell-extensions.c',
+ sources : [
+ 'gs-appstream.c',
+ 'gs-plugin-shell-extensions.c'
+ ],
include_directories : [
include_directories('../..'),
include_directories('../../lib'),
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]