mirror of
https://gitlab.freedesktop.org/NetworkManager/NetworkManager
synced 2024-09-06 17:14:02 +00:00
wwan: add service-providers.xml parser
This allows up to look up a default APN if the user doesn't pick one.
This commit is contained in:
parent
adf0254369
commit
6632c77094
|
@ -3315,7 +3315,10 @@ src_devices_wwan_libnm_wwan_la_SOURCES = \
|
||||||
src/devices/wwan/nm-modem-manager.c \
|
src/devices/wwan/nm-modem-manager.c \
|
||||||
src/devices/wwan/nm-modem-manager.h \
|
src/devices/wwan/nm-modem-manager.h \
|
||||||
src/devices/wwan/nm-modem.c \
|
src/devices/wwan/nm-modem.c \
|
||||||
src/devices/wwan/nm-modem.h
|
src/devices/wwan/nm-modem.h \
|
||||||
|
src/devices/wwan/nm-service-providers.c \
|
||||||
|
src/devices/wwan/nm-service-providers.h \
|
||||||
|
$(NULL)
|
||||||
|
|
||||||
if WITH_OFONO
|
if WITH_OFONO
|
||||||
src_devices_wwan_libnm_wwan_la_SOURCES += \
|
src_devices_wwan_libnm_wwan_la_SOURCES += \
|
||||||
|
|
|
@ -2,6 +2,7 @@ sources = files(
|
||||||
'nm-modem-broadband.c',
|
'nm-modem-broadband.c',
|
||||||
'nm-modem.c',
|
'nm-modem.c',
|
||||||
'nm-modem-manager.c',
|
'nm-modem-manager.c',
|
||||||
|
'nm-service-providers.c',
|
||||||
)
|
)
|
||||||
|
|
||||||
deps = [
|
deps = [
|
||||||
|
|
460
src/devices/wwan/nm-service-providers.c
Normal file
460
src/devices/wwan/nm-service-providers.c
Normal file
|
@ -0,0 +1,460 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-2.1+
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2009 Novell, Inc.
|
||||||
|
* Author: Tambet Ingo (tambet@gmail.com).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2009 - 2019 Red Hat, Inc.
|
||||||
|
* Copyright (C) 2012 Lanedo GmbH
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "nm-default.h"
|
||||||
|
|
||||||
|
#include "nm-service-providers.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PARSER_TOPLEVEL = 0,
|
||||||
|
PARSER_COUNTRY,
|
||||||
|
PARSER_PROVIDER,
|
||||||
|
PARSER_METHOD_GSM,
|
||||||
|
PARSER_METHOD_GSM_APN,
|
||||||
|
PARSER_METHOD_CDMA,
|
||||||
|
PARSER_DONE,
|
||||||
|
PARSER_ERROR
|
||||||
|
} ParseContextState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *mccmnc;
|
||||||
|
NMServiceProvidersGsmApnCallback callback;
|
||||||
|
gpointer user_data;
|
||||||
|
GCancellable *cancellable;
|
||||||
|
GMarkupParseContext *ctx;
|
||||||
|
char buffer[4096];
|
||||||
|
|
||||||
|
char *text_buffer;
|
||||||
|
ParseContextState state;
|
||||||
|
|
||||||
|
gboolean mccmnc_matched;
|
||||||
|
gboolean found_internet_apn;
|
||||||
|
char *apn;
|
||||||
|
char *username;
|
||||||
|
char *password;
|
||||||
|
char *gateway;
|
||||||
|
char *auth_method;
|
||||||
|
GSList *dns;
|
||||||
|
} ParseContext;
|
||||||
|
|
||||||
|
/*****************************************************************************/
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_toplevel_start (ParseContext *parse_context,
|
||||||
|
const char *name,
|
||||||
|
const char **attribute_names,
|
||||||
|
const char **attribute_values)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (strcmp (name, "serviceproviders") == 0) {
|
||||||
|
for (i = 0; attribute_names && attribute_names[i]; i++) {
|
||||||
|
if (strcmp (attribute_names[i], "format") == 0) {
|
||||||
|
if (strcmp (attribute_values[i], "2.0")) {
|
||||||
|
g_warning ("%s: mobile broadband provider database format '%s'"
|
||||||
|
" not supported.", __func__, attribute_values[i]);
|
||||||
|
parse_context->state = PARSER_ERROR;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp (name, "country") == 0) {
|
||||||
|
parse_context->state = PARSER_COUNTRY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_country_start (ParseContext *parse_context,
|
||||||
|
const char *name,
|
||||||
|
const char **attribute_names,
|
||||||
|
const char **attribute_values)
|
||||||
|
{
|
||||||
|
if (strcmp (name, "provider") == 0)
|
||||||
|
parse_context->state = PARSER_PROVIDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_provider_start (ParseContext *parse_context,
|
||||||
|
const char *name,
|
||||||
|
const char **attribute_names,
|
||||||
|
const char **attribute_values)
|
||||||
|
{
|
||||||
|
parse_context->mccmnc_matched = FALSE;
|
||||||
|
if (strcmp (name, "gsm") == 0)
|
||||||
|
parse_context->state = PARSER_METHOD_GSM;
|
||||||
|
else if (strcmp (name, "cdma") == 0)
|
||||||
|
parse_context->state = PARSER_METHOD_CDMA;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_gsm_start (ParseContext *parse_context,
|
||||||
|
const char *name,
|
||||||
|
const char **attribute_names,
|
||||||
|
const char **attribute_values)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (strcmp (name, "network-id") == 0) {
|
||||||
|
const char *mcc = NULL, *mnc = NULL;
|
||||||
|
|
||||||
|
for (i = 0; attribute_names && attribute_names[i]; i++) {
|
||||||
|
if (strcmp (attribute_names[i], "mcc") == 0)
|
||||||
|
mcc = attribute_values[i];
|
||||||
|
else if (strcmp (attribute_names[i], "mnc") == 0)
|
||||||
|
mnc = attribute_values[i];
|
||||||
|
if (mcc && strlen (mcc) && mnc && strlen (mnc)) {
|
||||||
|
char *mccmnc = g_strdup_printf ("%s%s", mcc, mnc);
|
||||||
|
|
||||||
|
if (strcmp (mccmnc, parse_context->mccmnc) == 0)
|
||||||
|
parse_context->mccmnc_matched = TRUE;
|
||||||
|
g_free (mccmnc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp (name, "apn") == 0) {
|
||||||
|
parse_context->found_internet_apn = FALSE;
|
||||||
|
g_clear_pointer (&parse_context->apn, g_free);
|
||||||
|
g_clear_pointer (&parse_context->username, g_free);
|
||||||
|
g_clear_pointer (&parse_context->password, g_free);
|
||||||
|
g_clear_pointer (&parse_context->gateway, g_free);
|
||||||
|
g_clear_pointer (&parse_context->auth_method, g_free);
|
||||||
|
g_slist_free_full (parse_context->dns, g_free);
|
||||||
|
parse_context->dns = NULL;
|
||||||
|
|
||||||
|
for (i = 0; attribute_names && attribute_names[i]; i++) {
|
||||||
|
if (strcmp (attribute_names[i], "value") == 0) {
|
||||||
|
parse_context->state = PARSER_METHOD_GSM_APN;
|
||||||
|
parse_context->apn = g_strstrip (g_strdup (attribute_values[i]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_gsm_apn_start (ParseContext *parse_context,
|
||||||
|
const char *name,
|
||||||
|
const char **attribute_names,
|
||||||
|
const char **attribute_values)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (strcmp (name, "usage") == 0) {
|
||||||
|
for (i = 0; attribute_names && attribute_names[i]; i++) {
|
||||||
|
if ( (strcmp (attribute_names[i], "type") == 0)
|
||||||
|
&& (strcmp (attribute_values[i], "internet") == 0)) {
|
||||||
|
parse_context->found_internet_apn = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp (name, "authentication") == 0) {
|
||||||
|
for (i = 0; attribute_names && attribute_names[i]; i++) {
|
||||||
|
if (strcmp (attribute_names[i], "method") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->auth_method, g_free);
|
||||||
|
parse_context->auth_method = g_strstrip (g_strdup (attribute_values[i]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_start_element (GMarkupParseContext *context,
|
||||||
|
const char *element_name,
|
||||||
|
const char **attribute_names,
|
||||||
|
const char **attribute_values,
|
||||||
|
gpointer user_data,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
ParseContext *parse_context = user_data;
|
||||||
|
|
||||||
|
g_clear_pointer (&parse_context->text_buffer, g_free);
|
||||||
|
|
||||||
|
switch (parse_context->state) {
|
||||||
|
case PARSER_TOPLEVEL:
|
||||||
|
parser_toplevel_start (parse_context, element_name, attribute_names, attribute_values);
|
||||||
|
break;
|
||||||
|
case PARSER_COUNTRY:
|
||||||
|
parser_country_start (parse_context, element_name, attribute_names, attribute_values);
|
||||||
|
break;
|
||||||
|
case PARSER_PROVIDER:
|
||||||
|
parser_provider_start (parse_context, element_name, attribute_names, attribute_values);
|
||||||
|
break;
|
||||||
|
case PARSER_METHOD_GSM:
|
||||||
|
parser_gsm_start (parse_context, element_name, attribute_names, attribute_values);
|
||||||
|
break;
|
||||||
|
case PARSER_METHOD_GSM_APN:
|
||||||
|
parser_gsm_apn_start (parse_context, element_name, attribute_names, attribute_values);
|
||||||
|
break;
|
||||||
|
case PARSER_METHOD_CDMA:
|
||||||
|
break;
|
||||||
|
case PARSER_ERROR:
|
||||||
|
break;
|
||||||
|
case PARSER_DONE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_country_end (ParseContext *parse_context,
|
||||||
|
const char *name)
|
||||||
|
{
|
||||||
|
if (strcmp (name, "country") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->text_buffer, g_free);
|
||||||
|
parse_context->state = PARSER_TOPLEVEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_provider_end (ParseContext *parse_context,
|
||||||
|
const char *name)
|
||||||
|
{
|
||||||
|
if (strcmp (name, "provider") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->text_buffer, g_free);
|
||||||
|
parse_context->state = PARSER_COUNTRY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_gsm_end (ParseContext *parse_context,
|
||||||
|
const char *name)
|
||||||
|
{
|
||||||
|
if (strcmp (name, "gsm") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->text_buffer, g_free);
|
||||||
|
parse_context->state = PARSER_PROVIDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_gsm_apn_end (ParseContext *parse_context,
|
||||||
|
const char *name)
|
||||||
|
{
|
||||||
|
if (strcmp (name, "username") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->username, g_free);
|
||||||
|
parse_context->username = g_steal_pointer (&parse_context->text_buffer);
|
||||||
|
} else if (strcmp (name, "password") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->password, g_free);
|
||||||
|
parse_context->password = g_steal_pointer (&parse_context->text_buffer);
|
||||||
|
} else if (strcmp (name, "dns") == 0) {
|
||||||
|
parse_context->dns = g_slist_prepend (parse_context->dns,
|
||||||
|
g_steal_pointer (&parse_context->text_buffer));
|
||||||
|
} else if (strcmp (name, "gateway") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->gateway, g_free);
|
||||||
|
parse_context->gateway = g_steal_pointer (&parse_context->text_buffer);
|
||||||
|
} else if (strcmp (name, "apn") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->text_buffer, g_free);
|
||||||
|
|
||||||
|
if (parse_context->mccmnc_matched && parse_context->found_internet_apn)
|
||||||
|
parse_context->state = PARSER_DONE;
|
||||||
|
else
|
||||||
|
parse_context->state = PARSER_METHOD_GSM;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_cdma_end (ParseContext *parse_context,
|
||||||
|
const char *name)
|
||||||
|
{
|
||||||
|
if (strcmp (name, "cdma") == 0) {
|
||||||
|
g_clear_pointer (&parse_context->text_buffer, g_free);
|
||||||
|
parse_context->state = PARSER_PROVIDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_end_element (GMarkupParseContext *context,
|
||||||
|
const char *element_name,
|
||||||
|
gpointer user_data,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
ParseContext *parse_context = user_data;
|
||||||
|
|
||||||
|
switch (parse_context->state) {
|
||||||
|
case PARSER_TOPLEVEL:
|
||||||
|
break;
|
||||||
|
case PARSER_COUNTRY:
|
||||||
|
parser_country_end (parse_context, element_name);
|
||||||
|
break;
|
||||||
|
case PARSER_PROVIDER:
|
||||||
|
parser_provider_end (parse_context, element_name);
|
||||||
|
break;
|
||||||
|
case PARSER_METHOD_GSM:
|
||||||
|
parser_gsm_end (parse_context, element_name);
|
||||||
|
break;
|
||||||
|
case PARSER_METHOD_GSM_APN:
|
||||||
|
parser_gsm_apn_end (parse_context, element_name);
|
||||||
|
break;
|
||||||
|
case PARSER_METHOD_CDMA:
|
||||||
|
parser_cdma_end (parse_context, element_name);
|
||||||
|
break;
|
||||||
|
case PARSER_ERROR:
|
||||||
|
break;
|
||||||
|
case PARSER_DONE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
parser_text (GMarkupParseContext *context,
|
||||||
|
const char *text,
|
||||||
|
gsize text_len,
|
||||||
|
gpointer user_data,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
ParseContext *parse_context = user_data;
|
||||||
|
|
||||||
|
g_free (parse_context->text_buffer);
|
||||||
|
parse_context->text_buffer = g_strdup (text);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const GMarkupParser parser = {
|
||||||
|
.start_element = parser_start_element,
|
||||||
|
.end_element = parser_end_element,
|
||||||
|
.text = parser_text,
|
||||||
|
.passthrough = NULL,
|
||||||
|
.error = NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*****************************************************************************/
|
||||||
|
|
||||||
|
static void
|
||||||
|
finish_parse_context (ParseContext *parse_context, GError *error)
|
||||||
|
{
|
||||||
|
if (parse_context->callback) {
|
||||||
|
if (error) {
|
||||||
|
parse_context->callback (NULL, NULL, NULL, NULL, NULL,
|
||||||
|
NULL, error,
|
||||||
|
parse_context->user_data);
|
||||||
|
} else {
|
||||||
|
parse_context->callback (parse_context->apn,
|
||||||
|
parse_context->username,
|
||||||
|
parse_context->password,
|
||||||
|
parse_context->gateway,
|
||||||
|
parse_context->auth_method,
|
||||||
|
parse_context->dns,
|
||||||
|
error,
|
||||||
|
parse_context->user_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_free (parse_context->mccmnc);
|
||||||
|
g_markup_parse_context_free (parse_context->ctx);
|
||||||
|
|
||||||
|
g_free (parse_context->text_buffer);
|
||||||
|
g_free (parse_context->apn);
|
||||||
|
g_free (parse_context->username);
|
||||||
|
g_free (parse_context->password);
|
||||||
|
g_free (parse_context->gateway);
|
||||||
|
g_free (parse_context->auth_method);
|
||||||
|
g_slist_free_full (parse_context->dns, g_free);
|
||||||
|
|
||||||
|
g_slice_free (ParseContext, parse_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
read_next_chunk (GInputStream *stream, ParseContext *parse_context);
|
||||||
|
|
||||||
|
static void
|
||||||
|
stream_read_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
|
||||||
|
{
|
||||||
|
GInputStream *stream = G_INPUT_STREAM (source_object);
|
||||||
|
ParseContext *parse_context = user_data;
|
||||||
|
gssize len;
|
||||||
|
GError *error = NULL;
|
||||||
|
|
||||||
|
len = g_input_stream_read_finish (stream, res, &error);
|
||||||
|
if (len == -1) {
|
||||||
|
g_prefix_error (&error, "Error reading service provider database: ");
|
||||||
|
finish_parse_context (parse_context, error);
|
||||||
|
g_clear_error (&error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len == 0) {
|
||||||
|
g_set_error (&error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN,
|
||||||
|
"Operator ID '%s' not found in service provider database",
|
||||||
|
parse_context->mccmnc);
|
||||||
|
finish_parse_context (parse_context, error);
|
||||||
|
g_clear_error (&error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_markup_parse_context_parse (parse_context->ctx, parse_context->buffer, len, &error)) {
|
||||||
|
g_prefix_error (&error, "Error parsing service provider database: ");
|
||||||
|
finish_parse_context (parse_context, error);
|
||||||
|
g_clear_error (&error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parse_context->state == PARSER_DONE) {
|
||||||
|
finish_parse_context (parse_context, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
read_next_chunk (stream, parse_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
read_next_chunk (GInputStream *stream, ParseContext *parse_context)
|
||||||
|
{
|
||||||
|
g_input_stream_read_async (stream,
|
||||||
|
parse_context->buffer,
|
||||||
|
sizeof (parse_context->buffer),
|
||||||
|
G_PRIORITY_DEFAULT,
|
||||||
|
parse_context->cancellable,
|
||||||
|
stream_read_cb,
|
||||||
|
parse_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
file_read_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
|
||||||
|
{
|
||||||
|
GFile *file = G_FILE (source_object);
|
||||||
|
ParseContext *parse_context = user_data;
|
||||||
|
GFileInputStream *stream;
|
||||||
|
gs_free_error GError *error = NULL;
|
||||||
|
|
||||||
|
stream = g_file_read_finish (file, res, &error);
|
||||||
|
if (!stream) {
|
||||||
|
g_prefix_error (&error, "Error opening service provider database: ");
|
||||||
|
finish_parse_context (parse_context, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
read_next_chunk (G_INPUT_STREAM (stream), parse_context);
|
||||||
|
|
||||||
|
g_object_unref (stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************************************************************/
|
||||||
|
|
||||||
|
void
|
||||||
|
nm_service_providers_find_gsm_apn (const char *service_providers,
|
||||||
|
const char *mccmnc,
|
||||||
|
GCancellable *cancellable,
|
||||||
|
NMServiceProvidersGsmApnCallback callback,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
GFile *file;
|
||||||
|
ParseContext *parse_context;
|
||||||
|
|
||||||
|
parse_context = g_slice_new0 (ParseContext);
|
||||||
|
parse_context->mccmnc = g_strdup (mccmnc);
|
||||||
|
parse_context->cancellable = cancellable;
|
||||||
|
parse_context->callback = callback;
|
||||||
|
parse_context->user_data = user_data;
|
||||||
|
parse_context->ctx = g_markup_parse_context_new (&parser, 0, parse_context, NULL);
|
||||||
|
|
||||||
|
file = g_file_new_for_path (service_providers);
|
||||||
|
|
||||||
|
g_file_read_async (file, G_PRIORITY_DEFAULT, cancellable, file_read_cb, parse_context);
|
||||||
|
|
||||||
|
g_object_unref (file);
|
||||||
|
}
|
24
src/devices/wwan/nm-service-providers.h
Normal file
24
src/devices/wwan/nm-service-providers.h
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-2.1+
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Red Hat, Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __NETWORKMANAGER_SERVICE_PROVIDERS_H__
|
||||||
|
#define __NETWORKMANAGER_SERVICE_PROVIDERS_H__
|
||||||
|
|
||||||
|
typedef void (*NMServiceProvidersGsmApnCallback) (const char *apn,
|
||||||
|
const char *username,
|
||||||
|
const char *password,
|
||||||
|
const char *gateway,
|
||||||
|
const char *auth_method,
|
||||||
|
const GSList *dns,
|
||||||
|
GError *error,
|
||||||
|
gpointer user_data);
|
||||||
|
|
||||||
|
void nm_service_providers_find_gsm_apn (const char *service_providers,
|
||||||
|
const char *mccmnc,
|
||||||
|
GCancellable *cancellable,
|
||||||
|
NMServiceProvidersGsmApnCallback callback,
|
||||||
|
gpointer user_data);
|
||||||
|
|
||||||
|
#endif /* __NETWORKMANAGER_SERVICE_PROVIDERS_H__ */
|
Loading…
Reference in a new issue