• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2291

02 May 2024 08:09PM UTC coverage: 90.759% (+0.01%) from 90.747%
2291

push

Evergreen

web-flow
Fix a deadlock when accessing current user from inside an App listener (#7671)

App::switch_user() emitted changes without first releasing the lock on
m_user_mutex, leading to a deadlock if anyone inside the listener tried to
acquire the mutex. The rest of the places where we emitted changes were
correct.

The newly added wrapper catches this error when building with clang.

101952 of 180246 branches covered (56.56%)

14 of 17 new or added lines in 2 files covered. (82.35%)

59 existing lines in 14 files now uncovered.

212566 of 234210 relevant lines covered (90.76%)

5856712.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.33
/src/realm/object-store/sync/app.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2020 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or utilied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/object-store/sync/app.hpp>
20

21
#include <realm/sync/network/http.hpp>
22
#include <realm/util/base64.hpp>
23
#include <realm/util/flat_map.hpp>
24
#include <realm/util/platform_info.hpp>
25
#include <realm/util/uri.hpp>
26
#include <realm/object-store/sync/app_user.hpp>
27
#include <realm/object-store/sync/app_utils.hpp>
28
#include <realm/object-store/sync/impl/app_metadata.hpp>
29
#include <realm/object-store/sync/impl/sync_file.hpp>
30
#include <realm/object-store/sync/sync_manager.hpp>
31

32
#ifdef __EMSCRIPTEN__
33
#include <realm/object-store/sync/impl/emscripten/network_transport.hpp>
34
#endif
35

36
#include <external/json/json.hpp>
37
#include <sstream>
38
#include <string>
39

40
using namespace realm;
41
using namespace realm::app;
42
using namespace bson;
43
using util::Optional;
44
using util::UniqueFunction;
45

46
namespace {
47
// MARK: - Helpers
48

49
REALM_COLD
50
REALM_NOINLINE
51
REALM_NORETURN
52
void throw_json_error(ErrorCodes::Error ec, std::string_view message)
53
{
2✔
54
    throw AppError(ec, std::string(message));
2✔
55
}
2✔
56

57
template <typename T>
58
T as(const Bson& bson)
59
{
65,413✔
60
    if (holds_alternative<T>(bson)) {
65,413✔
61
        return static_cast<T>(bson);
65,413✔
62
    }
65,413✔
63
    throw_json_error(ErrorCodes::MalformedJson, "?");
×
64
}
×
65

66
template <typename T>
67
T get(const BsonDocument& doc, const std::string& key)
68
{
46,682✔
69
    if (auto val = doc.find(key)) {
46,682✔
70
        return as<T>(*val);
46,682✔
71
    }
46,682✔
72
    throw_json_error(ErrorCodes::MissingJsonKey, key);
×
73
    return {};
×
74
}
46,682✔
75

76
template <typename T>
77
void read_field(const BsonDocument& data, const std::string& key, T& value)
78
{
64✔
79
    if (auto val = data.find(key)) {
64✔
80
        value = as<T>(*val);
64✔
81
    }
64✔
82
    else {
×
83
        throw_json_error(ErrorCodes::MissingJsonKey, key);
×
84
    }
×
85
}
64✔
86

87
template <>
88
void read_field(const BsonDocument& data, const std::string& key, ObjectId& value)
89
{
32✔
90
    value = ObjectId(get<std::string>(data, key).c_str());
32✔
91
}
32✔
92

93
template <typename T>
94
void read_field(const BsonDocument& data, const std::string& key, Optional<T>& value)
95
{
4,303✔
96
    if (auto val = data.find(key)) {
4,303✔
97
        value = as<T>(*val);
16✔
98
    }
16✔
99
}
4,303✔
100

101
template <typename T>
102
T parse(std::string_view str)
103
{
13,857✔
104
    try {
13,857✔
105
        return as<T>(bson::parse(str));
13,857✔
106
    }
13,857✔
107
    catch (const std::exception& e) {
13,857✔
108
        throw_json_error(ErrorCodes::MalformedJson, e.what());
2✔
109
    }
2✔
110
}
13,857✔
111

112
struct UserAPIKeyResponseHandler {
113
    UniqueFunction<void(App::UserAPIKey&&, Optional<AppError>)> completion;
114
    void operator()(const Response& response)
115
    {
44✔
116
        if (auto error = AppUtils::check_for_errors(response)) {
44✔
117
            return completion({}, std::move(error));
22✔
118
        }
22✔
119

120
        try {
22✔
121
            auto json = parse<BsonDocument>(response.body);
22✔
122
            completion(read_user_api_key(json), {});
22✔
123
        }
22✔
124
        catch (AppError& e) {
22✔
125
            completion({}, std::move(e));
×
126
        }
×
127
    }
22✔
128

129
    static App::UserAPIKey read_user_api_key(const BsonDocument& doc)
130
    {
32✔
131
        App::UserAPIKey user_api_key;
32✔
132
        read_field(doc, "_id", user_api_key.id);
32✔
133
        read_field(doc, "key", user_api_key.key);
32✔
134
        read_field(doc, "name", user_api_key.name);
32✔
135
        read_field(doc, "disabled", user_api_key.disabled);
32✔
136
        return user_api_key;
32✔
137
    }
32✔
138
};
139

140
// generate the request headers for a HTTP call, by default it will generate
141
// headers with a refresh token if a user is passed
142
HttpHeaders get_request_headers(const std::shared_ptr<User>& user, RequestTokenType token_type)
143
{
15,085✔
144
    HttpHeaders headers{{"Content-Type", "application/json;charset=utf-8"}, {"Accept", "application/json"}};
15,085✔
145
    if (user) {
15,085✔
146
        switch (token_type) {
5,591✔
147
            case RequestTokenType::NoAuth:
✔
148
                break;
×
149
            case RequestTokenType::AccessToken:
5,369✔
150
                headers.insert({"Authorization", util::format("Bearer %1", user->access_token())});
5,369✔
151
                break;
5,369✔
152
            case RequestTokenType::RefreshToken:
222✔
153
                headers.insert({"Authorization", util::format("Bearer %1", user->refresh_token())});
222✔
154
                break;
222✔
155
        }
5,591✔
156
    }
5,591✔
157
    return headers;
15,085✔
158
}
15,085✔
159

160
UniqueFunction<void(const Response&)> handle_default_response(UniqueFunction<void(Optional<AppError>)>&& completion)
161
{
24✔
162
    return [completion = std::move(completion)](const Response& response) {
24✔
163
        completion(AppUtils::check_for_errors(response));
24✔
164
    };
24✔
165
}
24✔
166

167
constexpr static std::string_view s_base_path = "/api/client/v2.0";
168
constexpr static std::string_view s_app_path = "/app";
169
constexpr static std::string_view s_auth_path = "/auth";
170
constexpr static std::string_view s_sync_path = "/realm-sync";
171
constexpr static uint64_t s_default_timeout_ms = 60000;
172
constexpr static std::string_view s_username_password_provider_key = "local-userpass";
173
constexpr static std::string_view s_user_api_key_provider_key_path = "api_keys";
174
constexpr static int s_max_http_redirects = 20;
175
static util::FlatMap<std::string, util::FlatMap<std::string, SharedApp>> s_apps_cache; // app_id -> base_url -> app
176
std::mutex s_apps_mutex;
177
} // anonymous namespace
178

179
namespace realm::app {
180

181
std::string_view App::default_base_url()
182
{
4,331✔
183
    return "https://services.cloud.mongodb.com";
4,331✔
184
}
4,331✔
185

186
// NO_THREAD_SAFETY_ANALYSIS because clang generates a false positive.
187
// "Calling function configure requires negative capability '!app->m_route_mutex'"
188
// But 'app' is an object just created in this static method so it is not possible to annotate this in the header.
189
SharedApp App::get_app(CacheMode mode, const AppConfig& config) NO_THREAD_SAFETY_ANALYSIS
190
{
4,285✔
191
    if (mode == CacheMode::Enabled) {
4,285✔
192
        std::lock_guard lock(s_apps_mutex);
10✔
193
        auto& app = s_apps_cache[config.app_id][config.base_url.value_or(std::string(App::default_base_url()))];
10✔
194
        if (!app) {
10✔
195
            app = std::make_shared<App>(Private(), config);
6✔
196
        }
6✔
197
        return app;
10✔
198
    }
10✔
199
    REALM_ASSERT(mode == CacheMode::Disabled);
4,275✔
200
    return std::make_shared<App>(Private(), config);
4,275✔
201
}
4,285✔
202

203
SharedApp App::get_cached_app(const std::string& app_id, const std::optional<std::string>& base_url)
204
{
10✔
205
    std::lock_guard lock(s_apps_mutex);
10✔
206
    if (auto it = s_apps_cache.find(app_id); it != s_apps_cache.end()) {
10✔
207
        const auto& apps_by_url = it->second;
10✔
208

209
        auto app_it = base_url ? apps_by_url.find(*base_url) : apps_by_url.begin();
10✔
210
        if (app_it != apps_by_url.end()) {
10✔
211
            return app_it->second;
8✔
212
        }
8✔
213
    }
10✔
214

215
    return nullptr;
2✔
216
}
10✔
217

218
void App::clear_cached_apps()
219
{
4,451✔
220
    std::lock_guard lock(s_apps_mutex);
4,451✔
221
    s_apps_cache.clear();
4,451✔
222
}
4,451✔
223

224
void App::close_all_sync_sessions()
225
{
×
226
    std::lock_guard lock(s_apps_mutex);
×
227
    for (auto& apps_by_url : s_apps_cache) {
×
228
        for (auto& app : apps_by_url.second) {
×
229
            app.second->sync_manager()->close_all_sessions();
×
230
        }
×
231
    }
×
232
}
×
233

234
App::App(Private, const AppConfig& config)
235
    : m_config(config)
2,132✔
236
    , m_base_url(m_config.base_url.value_or(std::string(App::default_base_url())))
2,132✔
237
    , m_request_timeout_ms(m_config.default_request_timeout_ms.value_or(s_default_timeout_ms))
2,132✔
238
    , m_file_manager(std::make_unique<SyncFileManager>(config))
2,132✔
239
    , m_metadata_store(create_metadata_store(config, *m_file_manager))
2,132✔
240
    , m_sync_manager(SyncManager::create(config.sync_client_config))
2,132✔
241
{
4,281✔
242
#ifdef __EMSCRIPTEN__
243
    if (!m_config.transport) {
244
        m_config.transport = std::make_shared<_impl::EmscriptenNetworkTransport>();
245
    }
246
#endif
247
    REALM_ASSERT(m_config.transport);
4,281✔
248

249
    // if a base url is provided, then verify the value
250
    if (m_config.base_url) {
4,281✔
251
        util::Uri::parse(*m_config.base_url);
575✔
252
    }
575✔
253
    // Setup a baseline set of routes using the provided or default base url
254
    // These will be updated when the location info is refreshed prior to sending the
255
    // first AppServices HTTP request.
256
    configure_route(m_base_url, "");
4,281✔
257
    m_sync_manager->set_sync_route(make_sync_route(), false);
4,281✔
258

259
    if (m_config.device_info.platform_version.empty()) {
4,281✔
260
        throw InvalidArgument("You must specify the Platform Version in App::Config::device_info");
×
261
    }
×
262

263
    if (m_config.device_info.sdk.empty()) {
4,281✔
264
        throw InvalidArgument("You must specify the SDK Name in App::Config::device_info");
×
265
    }
×
266

267
    if (m_config.device_info.sdk_version.empty()) {
4,281✔
268
        throw InvalidArgument("You must specify the SDK Version in App::Config::device_info");
×
269
    }
×
270
}
4,281✔
271

272
App::~App() {}
4,281✔
273

274
bool App::init_logger()
275
{
45,403✔
276
    if (!m_logger_ptr) {
45,403✔
277
        m_logger_ptr = m_sync_manager->get_logger();
4,273✔
278
        if (!m_logger_ptr) {
4,273✔
279
            m_logger_ptr = util::Logger::get_default_logger();
×
280
        }
×
281
    }
4,273✔
282
    return bool(m_logger_ptr);
45,403✔
283
}
45,403✔
284

285
bool App::would_log(util::Logger::Level level)
286
{
5,940✔
287
    return init_logger() && m_logger_ptr->would_log(util::LogCategory::app, level);
5,940✔
288
}
5,940✔
289

290
template <class... Params>
291
void App::log_debug(const char* message, Params&&... params)
292
{
39,329✔
293
    if (init_logger()) {
39,329✔
294
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::debug, message,
39,329✔
295
                          std::forward<Params>(params)...);
39,329✔
296
    }
39,329✔
297
}
39,329✔
298

299
template <class... Params>
300
void App::log_error(const char* message, Params&&... params)
301
{
134✔
302
    if (init_logger()) {
134✔
303
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::error, message,
134✔
304
                          std::forward<Params>(params)...);
134✔
305
    }
134✔
306
}
134✔
307

308
std::string App::auth_route()
309
{
9,484✔
310
    util::CheckedLockGuard guard(m_route_mutex);
9,484✔
311
    return m_auth_route;
9,484✔
312
}
9,484✔
313

314
std::string App::base_url()
315
{
×
316
    util::CheckedLockGuard guard(m_route_mutex);
×
317
    return m_base_url;
×
318
}
×
319

320
std::string App::get_host_url()
321
{
4,337✔
322
    util::CheckedLockGuard guard(m_route_mutex);
4,337✔
323
    return m_host_url;
4,337✔
324
}
4,337✔
325

326
std::string App::get_ws_host_url()
327
{
76✔
328
    util::CheckedLockGuard guard(m_route_mutex);
76✔
329
    return m_ws_host_url;
76✔
330
}
76✔
331

332
std::string App::make_sync_route(Optional<std::string> ws_host_url)
333
{
8,544✔
334
    return util::format("%1%2%3/%4%5", ws_host_url.value_or(m_ws_host_url), s_base_path, s_app_path, m_config.app_id,
8,544✔
335
                        s_sync_path);
8,544✔
336
}
8,544✔
337

338
void App::configure_route(const std::string& host_url, const std::string& ws_host_url)
339
{
8,552✔
340
    m_host_url = host_url;
8,552✔
341
    m_ws_host_url = ws_host_url;
8,552✔
342
    if (m_ws_host_url.empty())
8,552✔
343
        m_ws_host_url = App::create_ws_host_url(m_host_url);
4,281✔
344

345
    // host_url is the url to the server: e.g., https://services.cloud.mongodb.com or https://localhost:9090
346
    // base_route is the baseline client api path: e.g. <host_url>/api/client/v2.0
347
    m_base_route = util::format("%1%2", m_host_url, s_base_path);
8,552✔
348
    // app_route is the cloud app URL: <host_url>/api/client/v2.0/app/<app_id>
349
    m_app_route = util::format("%1%2/%3", m_base_route, s_app_path, m_config.app_id);
8,552✔
350
    // auth_route is cloud app auth URL: <host_url>/api/client/v2.0/app/<app_id>/auth
351
    m_auth_route = util::format("%1%2", m_app_route, s_auth_path);
8,552✔
352
}
8,552✔
353

354
// Create a temporary websocket URL domain using the given host URL
355
// This updates the URL based on the following assumptions:
356
// If the URL doesn't start with 'http' => <host-url>
357
// http[s]://[region-prefix]realm.mongodb.com => ws[s]://ws.[region-prefix]realm.mongodb.com
358
// http[s]://[region-prefix]services.cloud.mongodb.com => ws[s]://[region-prefix].ws.services.cloud.mongodb.com
359
// All others => http[s]://<host-url> => ws[s]://<host-url>
360
std::string App::create_ws_host_url(std::string_view host_url)
361
{
4,427✔
362
    constexpr static std::string_view old_base_domain = "realm.mongodb.com";
4,427✔
363
    constexpr static std::string_view new_base_domain = "services.cloud.mongodb.com";
4,427✔
364
    const size_t base_len = std::char_traits<char>::length("http://");
4,427✔
365

366
    // Doesn't contain 7 or more characters (length of 'http://') or start with http,
367
    // just return provided string
368
    if (host_url.length() < base_len || host_url.substr(0, 4) != "http") {
4,427✔
369
        return std::string(host_url);
2✔
370
    }
2✔
371
    // If it starts with 'https' then ws url will start with 'wss'
372
    bool https = host_url[4] == 's';
4,425✔
373
    size_t prefix_len = base_len + (https ? 1 : 0);
4,425✔
374
    std::string_view prefix = https ? "wss://" : "ws://";
4,425✔
375

376
    // http[s]://[<region-prefix>]realm.mongodb.com[/<path>] =>
377
    //     ws[s]://ws.[<region-prefix>]realm.mongodb.com[/<path>]
378
    if (host_url.find(old_base_domain) != std::string_view::npos) {
4,425✔
379
        return util::format("%1ws.%2", prefix, host_url.substr(prefix_len));
12✔
380
    }
12✔
381
    // http[s]://[<region-prefix>]services.cloud.mongodb.com[/<path>] =>
382
    //     ws[s]://[<region-prefix>].ws.services.cloud.mongodb.com[/<path>]
383
    if (auto start = host_url.find(new_base_domain); start != std::string_view::npos) {
4,413✔
384
        return util::format("%1%2ws.%3", prefix, host_url.substr(prefix_len, start - prefix_len),
3,732✔
385
                            host_url.substr(start));
3,732✔
386
    }
3,732✔
387

388
    // All others => http[s]://<host-url>[/<path>] => ws[s]://<host-url>[/<path>]
389
    return util::format("ws%1", host_url.substr(4));
681✔
390
}
4,413✔
391

392
void App::update_hostname(const std::string& host_url, const std::string& ws_host_url,
393
                          const std::string& new_base_url)
394
{
4,271✔
395
    log_debug("App: update_hostname: %1 | %2 | %3", host_url, ws_host_url, new_base_url);
4,271✔
396
    m_base_url = new_base_url;
4,271✔
397
    // If a new host url was returned from the server, use it to configure the routes
398
    // Otherwise, use the m_base_url value
399
    std::string base_url = host_url.length() > 0 ? host_url : m_base_url;
4,271✔
400
    configure_route(base_url, ws_host_url);
4,271✔
401
}
4,271✔
402

403
// MARK: - Template specializations
404

405
template <>
406
App::UsernamePasswordProviderClient App::provider_client<App::UsernamePasswordProviderClient>()
407
{
4,688✔
408
    return App::UsernamePasswordProviderClient(shared_from_this());
4,688✔
409
}
4,688✔
410

411
template <>
412
App::UserAPIKeyProviderClient App::provider_client<App::UserAPIKeyProviderClient>()
413
{
20✔
414
    return App::UserAPIKeyProviderClient(*this);
20✔
415
}
20✔
416

417
// MARK: - UsernamePasswordProviderClient
418

419
void App::UsernamePasswordProviderClient::register_email(const std::string& email, const std::string& password,
420
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
421
{
4,692✔
422
    m_parent->log_debug("App: register_email: %1", email);
4,692✔
423
    m_parent->post(util::format("%1/providers/%2/register", m_parent->auth_route(), s_username_password_provider_key),
4,692✔
424
                   std::move(completion), {{"email", email}, {"password", password}});
4,692✔
425
}
4,692✔
426

427
void App::UsernamePasswordProviderClient::confirm_user(const std::string& token, const std::string& token_id,
428
                                                       UniqueFunction<void(Optional<AppError>)>&& completion)
429
{
2✔
430
    m_parent->log_debug("App: confirm_user");
2✔
431
    m_parent->post(util::format("%1/providers/%2/confirm", m_parent->auth_route(), s_username_password_provider_key),
2✔
432
                   std::move(completion), {{"token", token}, {"tokenId", token_id}});
2✔
433
}
2✔
434

435
void App::UsernamePasswordProviderClient::resend_confirmation_email(
436
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
437
{
2✔
438
    m_parent->log_debug("App: resend_confirmation_email: %1", email);
2✔
439
    m_parent->post(
2✔
440
        util::format("%1/providers/%2/confirm/send", m_parent->auth_route(), s_username_password_provider_key),
2✔
441
        std::move(completion), {{"email", email}});
2✔
442
}
2✔
443

444
void App::UsernamePasswordProviderClient::retry_custom_confirmation(
445
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
446
{
4✔
447
    m_parent->log_debug("App: retry_custom_confirmation: %1", email);
4✔
448
    m_parent->post(
4✔
449
        util::format("%1/providers/%2/confirm/call", m_parent->auth_route(), s_username_password_provider_key),
4✔
450
        std::move(completion), {{"email", email}});
4✔
451
}
4✔
452

453
void App::UsernamePasswordProviderClient::send_reset_password_email(
454
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
455
{
×
456
    m_parent->log_debug("App: send_reset_password_email: %1", email);
×
457
    m_parent->post(
×
458
        util::format("%1/providers/%2/reset/send", m_parent->auth_route(), s_username_password_provider_key),
×
459
        std::move(completion), {{"email", email}});
×
460
}
×
461

462
void App::UsernamePasswordProviderClient::reset_password(const std::string& password, const std::string& token,
463
                                                         const std::string& token_id,
464
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
465
{
2✔
466
    m_parent->log_debug("App: reset_password");
2✔
467
    m_parent->post(util::format("%1/providers/%2/reset", m_parent->auth_route(), s_username_password_provider_key),
2✔
468
                   std::move(completion), {{"password", password}, {"token", token}, {"tokenId", token_id}});
2✔
469
}
2✔
470

471
void App::UsernamePasswordProviderClient::call_reset_password_function(
472
    const std::string& email, const std::string& password, const BsonArray& args,
473
    UniqueFunction<void(Optional<AppError>)>&& completion)
474
{
6✔
475
    m_parent->log_debug("App: call_reset_password_function: %1", email);
6✔
476
    m_parent->post(
6✔
477
        util::format("%1/providers/%2/reset/call", m_parent->auth_route(), s_username_password_provider_key),
6✔
478
        std::move(completion), {{"email", email}, {"password", password}, {"arguments", args}});
6✔
479
}
6✔
480

481
// MARK: - UserAPIKeyProviderClient
482

483
std::string App::UserAPIKeyProviderClient::url_for_path(const std::string& path = "") const
484
{
82✔
485
    if (!path.empty()) {
82✔
486
        return m_auth_request_client.url_for_path(
58✔
487
            util::format("%1/%2/%3", s_auth_path, s_user_api_key_provider_key_path, path));
58✔
488
    }
58✔
489

490
    return m_auth_request_client.url_for_path(util::format("%1/%2", s_auth_path, s_user_api_key_provider_key_path));
24✔
491
}
82✔
492

493
void App::UserAPIKeyProviderClient::create_api_key(
494
    const std::string& name, const std::shared_ptr<User>& user,
495
    UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
496
{
10✔
497
    m_auth_request_client.do_authenticated_request(
10✔
498
        HttpMethod::post, url_for_path(), Bson(BsonDocument{{"name", name}}).to_string(), user,
10✔
499
        RequestTokenType::RefreshToken, UserAPIKeyResponseHandler{std::move(completion)});
10✔
500
}
10✔
501

502
void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, const std::shared_ptr<User>& user,
503
                                                  UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
504
{
34✔
505
    m_auth_request_client.do_authenticated_request(HttpMethod::get, url_for_path(id.to_string()), "", user,
34✔
506
                                                   RequestTokenType::RefreshToken,
34✔
507
                                                   UserAPIKeyResponseHandler{std::move(completion)});
34✔
508
}
34✔
509

510
void App::UserAPIKeyProviderClient::fetch_api_keys(
511
    const std::shared_ptr<User>& user,
512
    UniqueFunction<void(std::vector<UserAPIKey>&&, Optional<AppError>)>&& completion)
513
{
14✔
514
    m_auth_request_client.do_authenticated_request(
14✔
515
        HttpMethod::get, url_for_path(), "", user, RequestTokenType::RefreshToken,
14✔
516
        [completion = std::move(completion)](const Response& response) {
14✔
517
            if (auto error = AppUtils::check_for_errors(response)) {
14✔
518
                return completion({}, std::move(error));
4✔
519
            }
4✔
520

521
            try {
10✔
522
                auto json = parse<BsonArray>(response.body);
10✔
523
                std::vector<UserAPIKey> keys;
10✔
524
                keys.reserve(json.size());
10✔
525
                for (auto&& api_key_json : json) {
10✔
526
                    keys.push_back(UserAPIKeyResponseHandler::read_user_api_key(as<BsonDocument>(api_key_json)));
10✔
527
                }
10✔
528
                return completion(std::move(keys), {});
10✔
529
            }
10✔
530
            catch (AppError& e) {
10✔
531
                completion({}, std::move(e));
×
532
            }
×
533
        });
10✔
534
}
14✔
535

536
void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr<User>& user,
537
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
538
{
8✔
539
    m_auth_request_client.do_authenticated_request(HttpMethod::del, url_for_path(id.to_string()), "", user,
8✔
540
                                                   RequestTokenType::RefreshToken,
8✔
541
                                                   handle_default_response(std::move(completion)));
8✔
542
}
8✔
543

544
void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, const std::shared_ptr<User>& user,
545
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
546
{
8✔
547
    m_auth_request_client.do_authenticated_request(
8✔
548
        HttpMethod::put, url_for_path(util::format("%1/enable", id.to_string())), "", user,
8✔
549
        RequestTokenType::RefreshToken, handle_default_response(std::move(completion)));
8✔
550
}
8✔
551

552
void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, const std::shared_ptr<User>& user,
553
                                                    UniqueFunction<void(Optional<AppError>)>&& completion)
554
{
8✔
555
    m_auth_request_client.do_authenticated_request(
8✔
556
        HttpMethod::put, url_for_path(util::format("%1/disable", id.to_string())), "", user,
8✔
557
        RequestTokenType::RefreshToken, handle_default_response(std::move(completion)));
8✔
558
}
8✔
559
// MARK: - App
560

561
// The user cache can have an expired pointer to an object if another thread is
562
// currently waiting for the mutex so that it can unregister the object, which
563
// will result in shared_from_this() throwing. We could instead do
564
// `weak_from_this().lock()`, but that is more expensive in the much more common
565
// case where the pointer is valid.
566
//
567
// Storing weak_ptrs in m_user would also avoid this problem, but would introduce
568
// a different one where the natural way to use the users could result in us
569
// trying to release the final strong reference while holding the lock, which
570
// would lead to a deadlock
571
static std::shared_ptr<User> try_lock(User& user)
572
{
3,823✔
573
    try {
3,823✔
574
        return user.shared_from_this();
3,823✔
575
    }
3,823✔
576
    catch (const std::bad_weak_ptr&) {
3,823✔
577
        return nullptr;
×
578
    }
×
579
}
3,823✔
580

581
std::shared_ptr<User> App::get_user_for_id(const std::string& user_id)
582
{
9,319✔
583
    if (auto& user = m_users[user_id]) {
9,319✔
584
        if (auto locked = try_lock(*user)) {
52✔
585
            return locked;
52✔
586
        }
52✔
587
    }
52✔
588
    return User::make(shared_from_this(), user_id);
9,267✔
589
}
9,319✔
590

591
void App::user_data_updated(const std::string& user_id)
592
{
4,744✔
593
    if (auto it = m_users.find(user_id); it != m_users.end()) {
4,744✔
594
        it->second->update_backing_data(m_metadata_store->get_user(user_id));
6✔
595
    }
6✔
596
}
4,744✔
597

598
std::shared_ptr<User> App::current_user()
599
{
8,328✔
600
    util::CheckedLockGuard lock(m_user_mutex);
8,328✔
601
    if (m_current_user && m_current_user->is_logged_in()) {
8,328✔
602
        if (auto user = try_lock(*m_current_user)) {
3,763✔
603
            return user;
3,763✔
604
        }
3,763✔
605
    }
3,763✔
606
    if (auto user_id = m_metadata_store->get_current_user(); !user_id.empty()) {
4,565✔
607
        auto user = get_user_for_id(user_id);
4,523✔
608
        m_current_user = user.get();
4,523✔
609
        return user;
4,523✔
610
    }
4,523✔
611
    return nullptr;
42✔
612
}
4,565✔
613

614
std::shared_ptr<User> App::get_existing_logged_in_user(std::string_view user_id)
615
{
2✔
616
    util::CheckedLockGuard lock(m_user_mutex);
2✔
617
    if (auto it = m_users.find(std::string(user_id)); it != m_users.end() && it->second->is_logged_in()) {
2!
618
        if (auto user = try_lock(*it->second)) {
×
619
            return user;
×
620
        }
×
621
    }
×
622
    if (m_metadata_store->has_logged_in_user(user_id)) {
2✔
623
        return User::make(shared_from_this(), user_id);
2✔
624
    }
2✔
625
    return nullptr;
×
626
}
2✔
627

628
std::string App::get_base_url() const
629
{
76✔
630
    util::CheckedLockGuard guard(m_route_mutex);
76✔
631
    return m_base_url;
76✔
632
}
76✔
633

634
void App::update_base_url(std::string_view new_base_url, UniqueFunction<void(Optional<AppError>)>&& completion)
635
{
12✔
636
    if (new_base_url.empty()) {
12✔
637
        // Treat an empty string the same as requesting the default base url
638
        new_base_url = App::default_base_url();
6✔
639
        log_debug("App::update_base_url: empty => %1", new_base_url);
6✔
640
    }
6✔
641
    else {
6✔
642
        log_debug("App::update_base_url: %1", new_base_url);
6✔
643
    }
6✔
644

645
    // Validate the new base_url
646
    util::Uri::parse(new_base_url);
12✔
647

648
    bool update_not_needed;
12✔
649
    {
12✔
650
        util::CheckedLockGuard guard(m_route_mutex);
12✔
651
        // Update the location if the base_url is different or a location update is already needed
652
        m_location_updated = (new_base_url == m_base_url) && m_location_updated;
12!
653
        update_not_needed = m_location_updated;
12✔
654
    }
12✔
655
    // If the new base_url is the same as the current base_url and the location has already been updated,
656
    // then we're done
657
    if (update_not_needed) {
12✔
658
        completion(util::none);
×
659
        return;
×
660
    }
×
661

662
    // Otherwise, request the location information at the new base URL
663
    request_location(std::move(completion), std::string(new_base_url));
12✔
664
}
12✔
665

666
std::vector<std::shared_ptr<User>> App::all_users()
667
{
82✔
668
    util::CheckedLockGuard lock(m_user_mutex);
82✔
669
    auto user_ids = m_metadata_store->get_all_users();
82✔
670
    std::vector<std::shared_ptr<User>> users;
82✔
671
    users.reserve(user_ids.size());
82✔
672
    for (auto& user_id : user_ids) {
82✔
673
        users.push_back(get_user_for_id(user_id));
52✔
674
    }
52✔
675
    return users;
82✔
676
}
82✔
677

678
void App::get_profile(const std::shared_ptr<User>& user,
679
                      UniqueFunction<void(const std::shared_ptr<User>&, Optional<AppError>)>&& completion)
680
{
4,752✔
681
    do_authenticated_request(
4,752✔
682
        HttpMethod::get, url_for_path("/auth/profile"), "", user, RequestTokenType::AccessToken,
4,752✔
683
        [completion = std::move(completion), self = shared_from_this(), user,
4,752✔
684
         this](const Response& profile_response) {
4,752✔
685
            if (auto error = AppUtils::check_for_errors(profile_response)) {
4,752✔
686
                return completion(nullptr, std::move(error));
×
687
            }
×
688

689
            try {
4,752✔
690
                auto profile_json = parse<BsonDocument>(profile_response.body);
4,752✔
691
                auto identities_json = get<BsonArray>(profile_json, "identities");
4,752✔
692

693
                std::vector<UserIdentity> identities;
4,752✔
694
                identities.reserve(identities_json.size());
4,752✔
695
                for (auto& identity_json : identities_json) {
4,786✔
696
                    auto doc = as<BsonDocument>(identity_json);
4,786✔
697
                    identities.push_back({get<std::string>(doc, "id"), get<std::string>(doc, "provider_type")});
4,786✔
698
                }
4,786✔
699

700
                if (auto data = m_metadata_store->get_user(user->user_id())) {
4,752✔
701
                    data->identities = std::move(identities);
4,752✔
702
                    data->profile = UserProfile(get<BsonDocument>(profile_json, "data"));
4,752✔
703
                    m_metadata_store->update_user(user->user_id(), *data);
4,752✔
704
                    user->update_backing_data(std::move(data));
4,752✔
705
                }
4,752✔
706
            }
4,752✔
707
            catch (const AppError& err) {
4,752✔
708
                return completion(nullptr, err);
×
709
            }
×
710

711
            return completion(user, {});
4,752✔
712
        });
4,752✔
713
}
4,752✔
714

715
void App::attach_auth_options(BsonDocument& body)
716
{
4,776✔
717
    log_debug("App: version info: platform: %1  version: %2 - sdk: %3 - sdk version: %4 - core version: %5",
4,776✔
718
              util::get_library_platform(), m_config.device_info.platform_version, m_config.device_info.sdk,
4,776✔
719
              m_config.device_info.sdk_version, REALM_VERSION_STRING);
4,776✔
720

721
    BsonDocument options;
4,776✔
722
    options["appId"] = m_config.app_id;
4,776✔
723
    options["platform"] = util::get_library_platform();
4,776✔
724
    options["platformVersion"] = m_config.device_info.platform_version;
4,776✔
725
    options["sdk"] = m_config.device_info.sdk;
4,776✔
726
    options["sdkVersion"] = m_config.device_info.sdk_version;
4,776✔
727
    options["cpuArch"] = util::get_library_cpu_arch();
4,776✔
728
    options["deviceName"] = m_config.device_info.device_name;
4,776✔
729
    options["deviceVersion"] = m_config.device_info.device_version;
4,776✔
730
    options["frameworkName"] = m_config.device_info.framework_name;
4,776✔
731
    options["frameworkVersion"] = m_config.device_info.framework_version;
4,776✔
732
    options["coreVersion"] = REALM_VERSION_STRING;
4,776✔
733
    options["bundleId"] = m_config.device_info.bundle_id;
4,776✔
734

735
    body["options"] = BsonDocument({{"device", options}});
4,776✔
736
}
4,776✔
737

738
void App::log_in_with_credentials(const AppCredentials& credentials, const std::shared_ptr<User>& linking_user,
739
                                  UniqueFunction<void(const std::shared_ptr<User>&, Optional<AppError>)>&& completion)
740
{
4,784✔
741
    if (would_log(util::Logger::Level::debug)) {
4,784✔
742
        auto app_info = util::format("app_id: %1", m_config.app_id);
4,784✔
743
        log_debug("App: log_in_with_credentials: %1", app_info);
4,784✔
744
    }
4,784✔
745
    // if we try logging in with an anonymous user while there
746
    // is already an anonymous session active, reuse it
747
    std::shared_ptr<User> anon_user;
4,784✔
748
    if (credentials.provider() == AuthProvider::ANONYMOUS) {
4,784✔
749
        util::CheckedLockGuard lock(m_user_mutex);
86✔
750
        for (auto& [_, user] : m_users) {
86✔
751
            if (user->is_anonymous()) {
14✔
752
                anon_user = try_lock(*user);
8✔
753
                if (!anon_user)
8✔
754
                    continue;
×
755
                m_current_user = user;
8✔
756
                m_metadata_store->set_current_user(user->user_id());
8✔
757
                break;
8✔
758
            }
8✔
759
        }
14✔
760
    }
86✔
761

762
    if (anon_user) {
4,784✔
763
        emit_change_to_subscribers();
8✔
764
        completion(anon_user, util::none);
8✔
765
        return;
8✔
766
    }
8✔
767

768
    if (linking_user) {
4,776✔
769
        util::CheckedLockGuard lock(m_user_mutex);
10✔
770
        if (!verify_user_present(linking_user)) {
10✔
771
            return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user was not found."));
×
772
        }
×
773
    }
10✔
774

775
    // construct the route
776
    std::string route = util::format("%1/providers/%2/login%3", auth_route(), credentials.provider_as_string(),
4,776✔
777
                                     linking_user ? "?link=true" : "");
4,776✔
778

779
    BsonDocument body = credentials.serialize_as_bson();
4,776✔
780
    attach_auth_options(body);
4,776✔
781

782
    do_request(
4,776✔
783
        make_request(HttpMethod::post, std::move(route), linking_user, RequestTokenType::AccessToken,
4,776✔
784
                     Bson(body).to_string()),
4,776✔
785
        [completion = std::move(completion), credentials, linking_user, self = shared_from_this(),
4,776✔
786
         this](auto&&, const Response& response) mutable {
4,776✔
787
            if (auto error = AppUtils::check_for_errors(response)) {
4,776✔
788
                log_error("App: log_in_with_credentials failed: %1 message: %2", response.http_status_code,
20✔
789
                          error->what());
20✔
790
                return completion(nullptr, std::move(error));
20✔
791
            }
20✔
792

793
            std::shared_ptr<User> user = linking_user;
4,756✔
794
            try {
4,756✔
795
                auto json = parse<BsonDocument>(response.body);
4,756✔
796
                if (linking_user) {
4,756✔
797
                    if (auto user_data = m_metadata_store->get_user(linking_user->user_id())) {
10✔
798
                        user_data->access_token = RealmJWT(get<std::string>(json, "access_token"));
10✔
799
                        // maybe a callback for this?
800
                        m_metadata_store->update_user(linking_user->user_id(), *user_data);
10✔
801
                        linking_user->update_backing_data(std::move(user_data));
10✔
802
                    }
10✔
803
                }
10✔
804
                else {
4,746✔
805
                    auto user_id = get<std::string>(json, "user_id");
4,746✔
806
                    m_metadata_store->create_user(user_id, get<std::string>(json, "refresh_token"),
4,746✔
807
                                                  get<std::string>(json, "access_token"),
4,746✔
808
                                                  get<std::string>(json, "device_id"));
4,746✔
809
                    util::CheckedLockGuard lock(m_user_mutex);
4,746✔
810
                    user_data_updated(user_id); // FIXME: needs to be callback from metadata store
4,746✔
811
                    user = get_user_for_id(user_id);
4,746✔
812
                }
4,746✔
813
            }
4,756✔
814
            catch (const AppError& e) {
4,756✔
815
                return completion(nullptr, e);
2✔
816
            }
2✔
817
            // If the user has not been logged in, then there is a problem with the token
818
            if (!user->is_logged_in()) {
4,754✔
819
                return completion(nullptr,
2✔
820
                                  AppError(ErrorCodes::BadToken, "Could not log in user: received malformed JWT"));
2✔
821
            }
2✔
822
            switch_user(user);
4,752✔
823
            get_profile(user, std::move(completion));
4,752✔
824
        },
4,752✔
825
        false);
4,776✔
826
}
4,776✔
827

828
void App::log_in_with_credentials(
829
    const AppCredentials& credentials,
830
    util::UniqueFunction<void(const std::shared_ptr<User>&, Optional<AppError>)>&& completion)
831
{
4,774✔
832
    App::log_in_with_credentials(credentials, nullptr, std::move(completion));
4,774✔
833
}
4,774✔
834

835
void App::log_out(const std::shared_ptr<User>& user, SyncUser::State new_state,
836
                  UniqueFunction<void(Optional<AppError>)>&& completion)
837
{
52✔
838
    if (!user || user->state() == new_state || user->state() == SyncUser::State::Removed) {
52✔
839
        if (completion) {
6✔
840
            completion(util::none);
4✔
841
        }
4✔
842
        return;
6✔
843
    }
6✔
844

845
    log_debug("App: log_out(%1)", user->user_id());
46✔
846
    auto request =
46✔
847
        make_request(HttpMethod::del, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, "");
46✔
848

849
    m_metadata_store->log_out(user->user_id(), new_state);
46✔
850
    user->update_backing_data(m_metadata_store->get_user(user->user_id()));
46✔
851

852
    do_request(std::move(request),
46✔
853
               [self = shared_from_this(), completion = std::move(completion)](auto&&, const Response& response) {
46✔
854
                   auto error = AppUtils::check_for_errors(response);
46✔
855
                   if (!error) {
46✔
856
                       self->emit_change_to_subscribers();
46✔
857
                   }
46✔
858
                   if (completion) {
46✔
859
                       completion(error);
41✔
860
                   }
41✔
861
               });
46✔
862
}
46✔
863

864
void App::log_out(const std::shared_ptr<User>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
865
{
37✔
866
    auto new_state = user && user->is_anonymous() ? SyncUser::State::Removed : SyncUser::State::LoggedOut;
37✔
867
    log_out(user, new_state, std::move(completion));
37✔
868
}
37✔
869

870
void App::log_out(UniqueFunction<void(Optional<AppError>)>&& completion)
871
{
18✔
872
    log_out(current_user(), std::move(completion));
18✔
873
}
18✔
874

875
bool App::verify_user_present(const std::shared_ptr<User>& user) const
876
{
4,793✔
877
    for (auto& [_, u] : m_users) {
4,795✔
878
        if (u == user.get())
4,795✔
879
            return true;
4,793✔
880
    }
4,795✔
881
    return false;
×
882
}
4,793✔
883

884
void App::switch_user(const std::shared_ptr<User>& user)
885
{
4,760✔
886
    if (!user || user->state() != SyncUser::State::LoggedIn) {
4,760✔
887
        throw AppError(ErrorCodes::ClientUserNotLoggedIn, "User is no longer valid or is logged out");
2✔
888
    }
2✔
889
    {
4,758✔
890
        util::CheckedLockGuard lock(m_user_mutex);
4,758✔
891
        if (!verify_user_present(user)) {
4,758✔
NEW
892
            throw AppError(ErrorCodes::ClientUserNotFound, "User does not exist");
×
NEW
893
        }
×
894

895
        m_current_user = user.get();
4,758✔
896
        m_metadata_store->set_current_user(user->user_id());
4,758✔
897
    }
4,758✔
NEW
898
    emit_change_to_subscribers();
×
899
}
4,758✔
900

901
void App::remove_user(const std::shared_ptr<User>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
902
{
21✔
903
    if (!user || user->state() == SyncUser::State::Removed) {
21✔
904
        return completion(AppError(ErrorCodes::ClientUserNotFound, "User has already been removed"));
4✔
905
    }
4✔
906

907
    {
17✔
908
        util::CheckedLockGuard lock(m_user_mutex);
17✔
909
        if (!verify_user_present(user)) {
17✔
910
            return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found"));
×
911
        }
×
912
    }
17✔
913

914
    if (user->is_logged_in()) {
17✔
915
        log_out(
15✔
916
            user, SyncUser::State::Removed,
15✔
917
            [user, completion = std::move(completion), self = shared_from_this()](const Optional<AppError>& error) {
15✔
918
                user->update_backing_data(std::nullopt);
15✔
919
                if (completion) {
15✔
920
                    completion(error);
14✔
921
                }
14✔
922
            });
15✔
923
    }
15✔
924
    else {
2✔
925
        m_metadata_store->log_out(user->user_id(), SyncUser::State::Removed);
2✔
926
        user->update_backing_data(std::nullopt);
2✔
927
        if (completion) {
2✔
928
            completion(std::nullopt);
2✔
929
        }
2✔
930
    }
2✔
931
}
17✔
932

933
void App::delete_user(const std::shared_ptr<User>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
934
{
12✔
935
    if (!user) {
12✔
936
        return completion(AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
×
937
    }
×
938
    if (user->state() != SyncUser::State::LoggedIn) {
12✔
939
        return completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "User must be logged in to be deleted."));
4✔
940
    }
4✔
941

942
    {
8✔
943
        util::CheckedLockGuard lock(m_user_mutex);
8✔
944
        if (!verify_user_present(user)) {
8✔
945
            return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found."));
×
946
        }
×
947
    }
8✔
948

949
    do_authenticated_request(
8✔
950
        HttpMethod::del, url_for_path("/auth/delete"), "", user, RequestTokenType::AccessToken,
8✔
951
        [self = shared_from_this(), completion = std::move(completion), user, this](const Response& response) {
8✔
952
            auto error = AppUtils::check_for_errors(response);
8✔
953
            if (!error) {
8✔
954
                auto user_id = user->user_id();
8✔
955
                user->detach_and_tear_down();
8✔
956
                m_metadata_store->delete_user(*m_file_manager, user_id);
8✔
957
                emit_change_to_subscribers();
8✔
958
            }
8✔
959
            completion(std::move(error));
8✔
960
        });
8✔
961
}
8✔
962

963
void App::link_user(const std::shared_ptr<User>& user, const AppCredentials& credentials,
964
                    UniqueFunction<void(const std::shared_ptr<User>&, Optional<AppError>)>&& completion)
965
{
12✔
966
    if (!user) {
12✔
967
        return completion(nullptr,
×
968
                          AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
×
969
    }
×
970
    if (user->state() != SyncUser::State::LoggedIn) {
12✔
971
        return completion(nullptr,
2✔
972
                          AppError(ErrorCodes::ClientUserNotLoggedIn, "The specified user is not logged in."));
2✔
973
    }
2✔
974
    if (credentials.provider() == AuthProvider::ANONYMOUS) {
10✔
975
        return completion(nullptr, AppError(ErrorCodes::ClientUserAlreadyNamed,
×
976
                                            "Cannot add anonymous credentials to an existing user."));
×
977
    }
×
978

979
    log_in_with_credentials(credentials, user, std::move(completion));
10✔
980
}
10✔
981

982
std::shared_ptr<User> App::create_fake_user_for_testing(const std::string& user_id, const std::string& access_token,
983
                                                        const std::string& refresh_token)
984
{
×
985
    std::shared_ptr<User> user;
×
986
    {
×
987
        m_metadata_store->create_user(user_id, refresh_token, access_token, "fake_device");
×
988
        util::CheckedLockGuard lock(m_user_mutex);
×
989
        user_data_updated(user_id); // FIXME: needs to be callback from metadata store
×
990
        user = get_user_for_id(user_id);
×
991
    }
×
992

993
    switch_user(user);
×
994
    return user;
×
995
}
×
996

997

998
void App::refresh_custom_data(const std::shared_ptr<User>& user,
999
                              UniqueFunction<void(Optional<AppError>)>&& completion)
1000
{
14✔
1001
    refresh_access_token(user, false, std::move(completion));
14✔
1002
}
14✔
1003

1004
void App::refresh_custom_data(const std::shared_ptr<User>& user, bool update_location,
1005
                              UniqueFunction<void(Optional<AppError>)>&& completion)
1006
{
10✔
1007
    refresh_access_token(user, update_location, std::move(completion));
10✔
1008
}
10✔
1009

1010
std::string App::url_for_path(const std::string& path = "") const
1011
{
5,010✔
1012
    util::CheckedLockGuard guard(m_route_mutex);
5,010✔
1013
    return util::format("%1%2", m_base_route, path);
5,010✔
1014
}
5,010✔
1015

1016
std::string App::get_app_route(const Optional<std::string>& hostname) const
1017
{
4,415✔
1018
    if (hostname) {
4,415✔
1019
        return util::format("%1%2%3/%4", *hostname, s_base_path, s_app_path, m_config.app_id);
118✔
1020
    }
118✔
1021
    return m_app_route;
4,297✔
1022
}
4,415✔
1023

1024
void App::request_location(UniqueFunction<void(std::optional<AppError>)>&& completion,
1025
                           std::optional<std::string>&& new_hostname, std::optional<std::string>&& redir_location,
1026
                           int redirect_count)
1027
{
4,415✔
1028
    // Request the new location information at the new base url hostname; or redir response location if a redirect
1029
    // occurred during the initial location request. redirect_count is used to track the number of sequential
1030
    // redirect responses received during the location update and return an error if this count exceeds
1031
    // max_http_redirects. If neither new_hostname nor redir_location is provided, the current value of m_base_url
1032
    // will be used.
1033
    std::string app_route;
4,415✔
1034
    std::string base_url;
4,415✔
1035
    {
4,415✔
1036
        util::CheckedUniqueLock lock(m_route_mutex);
4,415✔
1037
        // Skip if the location info has already been initialized and a new hostname is not provided
1038
        if (!new_hostname && !redir_location && m_location_updated) {
4,415✔
1039
            // Release the lock before calling the completion function
1040
            lock.unlock();
×
1041
            completion(util::none);
×
1042
            return;
×
1043
        }
×
1044
        base_url = new_hostname.value_or(m_base_url);
4,415✔
1045
        // If this is for a redirect after querying new_hostname, then use the redirect location
1046
        if (redir_location)
4,415✔
1047
            app_route = get_app_route(redir_location);
106✔
1048
        // If this is querying the new_hostname, then use that location
1049
        else if (new_hostname)
4,309✔
1050
            app_route = get_app_route(new_hostname);
12✔
1051
        else
4,297✔
1052
            app_route = get_app_route();
4,297✔
1053
        REALM_ASSERT(!app_route.empty());
4,415✔
1054
    }
4,415✔
1055

1056
    Request req;
×
1057
    req.method = HttpMethod::get;
4,415✔
1058
    req.url = util::format("%1/location", app_route);
4,415✔
1059
    req.timeout_ms = m_request_timeout_ms;
4,415✔
1060

1061
    log_debug("App: request location: %1", req.url);
4,415✔
1062

1063
    m_config.transport->send_request_to_server(req, [self = shared_from_this(), completion = std::move(completion),
4,415✔
1064
                                                     base_url = std::move(base_url),
4,415✔
1065
                                                     redirect_count](const Response& response) mutable {
4,415✔
1066
        // Check to see if a redirect occurred
1067
        if (AppUtils::is_redirect_status_code(response.http_status_code)) {
4,415✔
1068
            // Make sure we don't do too many redirects (max_http_redirects (20) is an arbitrary number)
1069
            if (redirect_count >= s_max_http_redirects) {
114✔
1070
                completion(AppError{ErrorCodes::ClientTooManyRedirects,
4✔
1071
                                    util::format("number of redirections exceeded %1", s_max_http_redirects),
4✔
1072
                                    {},
4✔
1073
                                    response.http_status_code});
4✔
1074
                return;
4✔
1075
            }
4✔
1076
            // Handle the redirect response when requesting the location - extract the
1077
            // new location header field and resend the request.
1078
            auto redir_location = AppUtils::extract_redir_location(response.headers);
110✔
1079
            if (!redir_location) {
110✔
1080
                // Location not found in the response, pass error response up the chain
1081
                completion(AppError{ErrorCodes::ClientRedirectError,
4✔
1082
                                    "Redirect response missing location header",
4✔
1083
                                    {},
4✔
1084
                                    response.http_status_code});
4✔
1085
                return;
4✔
1086
            }
4✔
1087
            // try to request the location info at the new location in the redirect response
1088
            // retry_count is passed in to track the number of subsequent redirection attempts
1089
            self->request_location(std::move(completion), std::move(base_url), std::move(redir_location),
106✔
1090
                                   redirect_count + 1);
106✔
1091
            return;
106✔
1092
        }
110✔
1093

1094
        // Location request was successful - update the location info
1095
        auto update_response = self->update_location(response, base_url);
4,301✔
1096
        if (update_response) {
4,301✔
1097
            self->log_error("App: request location failed (%1%2): %3", update_response->code_string(),
30✔
1098
                            update_response->additional_status_code
30✔
1099
                                ? util::format(" %1", *update_response->additional_status_code)
30✔
1100
                                : "",
30✔
1101
                            update_response->reason());
30✔
1102
        }
30✔
1103
        completion(update_response);
4,301✔
1104
    });
4,301✔
1105
}
4,415✔
1106

1107
std::optional<AppError> App::update_location(const Response& response, const std::string& base_url)
1108
{
4,301✔
1109
    // Validate the location info response for errors and update the stored location info if it is
1110
    // a valid response. base_url is the new hostname or m_base_url value when request_location()
1111
    // was called.
1112

1113
    if (auto error = AppUtils::check_for_errors(response)) {
4,301✔
1114
        return error;
30✔
1115
    }
30✔
1116

1117
    // Update the location info with the data from the response
1118
    try {
4,271✔
1119
        auto json = parse<BsonDocument>(response.body);
4,271✔
1120
        auto hostname = get<std::string>(json, "hostname");
4,271✔
1121
        auto ws_hostname = get<std::string>(json, "ws_hostname");
4,271✔
1122
        std::optional<std::string> sync_route;
4,271✔
1123
        read_field(json, "sync_route", sync_route);
4,271✔
1124

1125
        util::CheckedLockGuard guard(m_route_mutex);
4,271✔
1126
        // Update the local hostname and path information
1127
        update_hostname(hostname, ws_hostname, base_url);
4,271✔
1128
        m_location_updated = true;
4,271✔
1129
        if (!sync_route) {
4,271✔
1130
            sync_route = make_sync_route();
4,263✔
1131
        }
4,263✔
1132
        m_sync_manager->set_sync_route(*sync_route, true);
4,271✔
1133
    }
4,271✔
1134
    catch (const AppError& ex) {
4,271✔
1135
        return ex;
×
1136
    }
×
1137
    return util::none;
4,271✔
1138
}
4,271✔
1139

1140
void App::update_location_and_resend(std::unique_ptr<Request>&& request, IntermediateCompletion&& completion,
1141
                                     Optional<std::string>&& redir_location)
1142
{
4,297✔
1143
    // Update the location information if a redirect response was received or m_location_updated == false
1144
    // and then send the request to the server with request.url updated to the new AppServices hostname.
1145
    request_location(
4,297✔
1146
        [completion = std::move(completion), request = std::move(request),
4,297✔
1147
         self = shared_from_this()](Optional<AppError> error) mutable {
4,297✔
1148
            if (error) {
4,297✔
1149
                // Operation failed, pass it up the chain
1150
                return completion(std::move(request), AppUtils::make_apperror_response(*error));
36✔
1151
            }
36✔
1152

1153
            // If the location info was updated, update the original request to point
1154
            // to the new location URL.
1155
            auto url = util::Uri::parse(request->url);
4,261✔
1156
            request->url =
4,261✔
1157
                util::format("%1%2%3%4", self->get_host_url(), url.get_path(), url.get_query(), url.get_frag());
4,261✔
1158

1159
            self->log_debug("App: send_request(after location update): %1 %2", request->method, request->url);
4,261✔
1160
            // Retry the original request with the updated url
1161
            auto& request_ref = *request;
4,261✔
1162
            self->m_config.transport->send_request_to_server(
4,261✔
1163
                request_ref, [self = std::move(self), completion = std::move(completion),
4,261✔
1164
                              request = std::move(request)](const Response& response) mutable {
4,261✔
1165
                    self->check_for_redirect_response(std::move(request), response, std::move(completion));
4,261✔
1166
                });
4,261✔
1167
        },
4,261✔
1168
        // The base_url is not changing for this request
1169
        util::none, std::move(redir_location));
4,297✔
1170
}
4,297✔
1171

1172
void App::post(std::string&& route, UniqueFunction<void(Optional<AppError>)>&& completion, const BsonDocument& body)
1173
{
4,708✔
1174
    do_request(
4,708✔
1175
        make_request(HttpMethod::post, std::move(route), nullptr, RequestTokenType::NoAuth, Bson(body).to_string()),
4,708✔
1176
        [completion = std::move(completion)](auto&&, const Response& response) {
4,708✔
1177
            completion(AppUtils::check_for_errors(response));
4,708✔
1178
        });
4,708✔
1179
}
4,708✔
1180

1181
void App::do_request(std::unique_ptr<Request>&& request, IntermediateCompletion&& completion, bool update_location)
1182
{
15,085✔
1183
    // Verify the request URL to make sure it is valid
1184
    util::Uri::parse(request->url);
15,085✔
1185

1186
    // Refresh the location info when app is created or when requested (e.g. after a websocket redirect)
1187
    // to ensure the http and websocket URL information is up to date.
1188
    {
15,085✔
1189
        util::CheckedUniqueLock lock(m_route_mutex);
15,085✔
1190
        if (update_location) {
15,085✔
1191
            // If requesting a location update, force the location to be updated before sending the request.
1192
            m_location_updated = false;
48✔
1193
        }
48✔
1194
        if (!m_location_updated) {
15,085✔
1195
            lock.unlock();
4,297✔
1196
            // Location info needs to be requested, update the location info and then send the request
1197
            update_location_and_resend(std::move(request), std::move(completion));
4,297✔
1198
            return;
4,297✔
1199
        }
4,297✔
1200
    }
15,085✔
1201

1202
    log_debug("App: do_request: %1 %2", request->method, request->url);
10,788✔
1203
    // If location info has already been updated, then send the request directly
1204
    auto& request_ref = *request;
10,788✔
1205
    m_config.transport->send_request_to_server(
10,788✔
1206
        request_ref, [self = shared_from_this(), completion = std::move(completion),
10,788✔
1207
                      request = std::move(request)](const Response& response) mutable {
10,788✔
1208
            self->check_for_redirect_response(std::move(request), response, std::move(completion));
10,788✔
1209
        });
10,788✔
1210
}
10,788✔
1211

1212
void App::check_for_redirect_response(std::unique_ptr<Request>&& request, const Response& response,
1213
                                      IntermediateCompletion&& completion)
1214
{
15,049✔
1215
    // If this isn't a redirect response, then we're done
1216
    if (!AppUtils::is_redirect_status_code(response.http_status_code)) {
15,049✔
1217
        return completion(std::move(request), response);
15,049✔
1218
    }
15,049✔
1219

1220
    // Handle a redirect response when sending the original request - extract the location
1221
    // header field and resend the request.
1222
    auto redir_location = AppUtils::extract_redir_location(response.headers);
×
1223
    if (!redir_location) {
×
1224
        // Location not found in the response, pass error response up the chain
1225
        return completion(std::move(request),
×
1226
                          AppUtils::make_clienterror_response(ErrorCodes::ClientRedirectError,
×
1227
                                                              "Redirect response missing location header",
×
1228
                                                              response.http_status_code));
×
1229
    }
×
1230

1231
    // Request the location info at the new location - once this is complete, the original
1232
    // request will be sent to the new server
1233
    update_location_and_resend(std::move(request), std::move(completion), std::move(redir_location));
×
1234
}
×
1235

1236
void App::do_authenticated_request(HttpMethod method, std::string&& route, std::string&& body,
1237
                                   const std::shared_ptr<User>& user, RequestTokenType token_type,
1238
                                   util::UniqueFunction<void(const Response&)>&& completion)
1239
{
5,439✔
1240
    auto request = make_request(method, std::move(route), user, token_type, std::move(body));
5,439✔
1241
    do_request(std::move(request), [token_type, user, completion = std::move(completion), self = shared_from_this()](
5,439✔
1242
                                       std::unique_ptr<Request>&& request, const Response& response) mutable {
5,439✔
1243
        if (auto error = AppUtils::check_for_errors(response)) {
5,439✔
1244
            self->handle_auth_failure(std::move(*error), std::move(request), response, user, token_type,
66✔
1245
                                      std::move(completion));
66✔
1246
        }
66✔
1247
        else {
5,373✔
1248
            completion(response);
5,373✔
1249
        }
5,373✔
1250
    });
5,439✔
1251
}
5,439✔
1252

1253
void App::handle_auth_failure(const AppError& error, std::unique_ptr<Request>&& request, const Response& response,
1254
                              const std::shared_ptr<User>& user, RequestTokenType token_type,
1255
                              util::UniqueFunction<void(const Response&)>&& completion)
1256
{
66✔
1257
    // Only handle auth failures
1258
    if (*error.additional_status_code != 401) {
66✔
1259
        completion(response);
40✔
1260
        return;
40✔
1261
    }
40✔
1262

1263
    // If the refresh token is invalid then the user needs to be logged back
1264
    // in to be able to use it again
1265
    if (token_type == RequestTokenType::RefreshToken) {
26✔
1266
        if (user && user->is_logged_in()) {
18!
1267
            user->log_out();
×
1268
        }
×
1269
        completion(response);
18✔
1270
        return;
18✔
1271
    }
18✔
1272

1273
    // Otherwise we may be able to request a new access token and have the request succeed with that
1274
    refresh_access_token(user, false,
8✔
1275
                         [self = shared_from_this(), request = std::move(request), completion = std::move(completion),
8✔
1276
                          response = std::move(response), user](Optional<AppError>&& error) mutable {
8✔
1277
                             if (error) {
8✔
1278
                                 // pass the error back up the chain
1279
                                 completion(response);
4✔
1280
                                 return;
4✔
1281
                             }
4✔
1282

1283
                             // Reissue the request with the new access token
1284
                             request->headers = get_request_headers(user, RequestTokenType::AccessToken);
4✔
1285
                             self->do_request(std::move(request),
4✔
1286
                                              [completion = std::move(completion)](auto&&, auto& response) {
4✔
1287
                                                  completion(response);
4✔
1288
                                              });
4✔
1289
                         });
4✔
1290
}
8✔
1291

1292
/// MARK: - refresh access token
1293
void App::refresh_access_token(const std::shared_ptr<User>& user, bool update_location,
1294
                               util::UniqueFunction<void(Optional<AppError>)>&& completion)
1295
{
116✔
1296
    if (!user) {
116✔
1297
        completion(AppError(ErrorCodes::ClientUserNotFound, "No current user exists"));
2✔
1298
        return;
2✔
1299
    }
2✔
1300

1301
    if (!user->is_logged_in()) {
114✔
1302
        completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "The user is not logged in"));
2✔
1303
        return;
2✔
1304
    }
2✔
1305

1306
    log_debug("App: refresh_access_token: user_id: %1%2", user->user_id(),
112✔
1307
              update_location ? " (updating location)" : "");
112✔
1308

1309
    // If update_location is set, force the location info to be updated before sending the request
1310
    do_request(
112✔
1311
        make_request(HttpMethod::post, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, ""),
112✔
1312
        [completion = std::move(completion), self = shared_from_this(), user](auto&&, const Response& response) {
112✔
1313
            if (auto error = AppUtils::check_for_errors(response)) {
112✔
1314
                self->log_error("App: refresh_access_token: %1 -> %2 ERROR: %3", user->user_id(),
66✔
1315
                                response.http_status_code, error->what());
66✔
1316

1317
                return completion(std::move(error));
66✔
1318
            }
66✔
1319

1320
            try {
46✔
1321
                auto json = parse<BsonDocument>(response.body);
46✔
1322
                RealmJWT access_token{get<std::string>(json, "access_token")};
46✔
1323
                if (auto data = self->m_metadata_store->get_user(user->user_id())) {
46✔
1324
                    data->access_token = access_token;
44✔
1325
                    self->m_metadata_store->update_user(user->user_id(), *data);
44✔
1326
                    user->update_backing_data(std::move(data));
44✔
1327
                }
44✔
1328
            }
46✔
1329
            catch (AppError& err) {
46✔
1330
                return completion(std::move(err));
2✔
1331
            }
2✔
1332

1333
            return completion(util::none);
44✔
1334
        },
46✔
1335
        update_location);
112✔
1336
}
112✔
1337

1338
std::string App::function_call_url_path() const
1339
{
595✔
1340
    util::CheckedLockGuard guard(m_route_mutex);
595✔
1341
    return util::format("%1/functions/call", m_app_route);
595✔
1342
}
595✔
1343

1344
void App::call_function(const std::shared_ptr<User>& user, const std::string& name, std::string_view args_ejson,
1345
                        const Optional<std::string>& service_name_opt,
1346
                        UniqueFunction<void(const std::string*, Optional<AppError>)>&& completion)
1347
{
587✔
1348
    auto service_name = service_name_opt ? *service_name_opt : "<none>";
587✔
1349
    if (would_log(util::Logger::Level::debug)) {
587✔
1350
        log_debug("App: call_function: %1 service_name: %2 args_bson: %3", name, service_name, args_ejson);
587✔
1351
    }
587✔
1352

1353
    auto args = util::format("{\"arguments\":%1,\"name\":%2%3}", args_ejson, nlohmann::json(name).dump(),
587✔
1354
                             service_name_opt ? (",\"service\":" + nlohmann::json(service_name).dump()) : "");
587✔
1355

1356
    do_authenticated_request(HttpMethod::post, function_call_url_path(), std::move(args), user,
587✔
1357
                             RequestTokenType::AccessToken,
587✔
1358
                             [self = shared_from_this(), name = name, service_name = std::move(service_name),
587✔
1359
                              completion = std::move(completion)](const Response& response) {
587✔
1360
                                 if (auto error = AppUtils::check_for_errors(response)) {
587✔
1361
                                     self->log_error("App: call_function: %1 service_name: %2 -> %3 ERROR: %4", name,
18✔
1362
                                                     service_name, response.http_status_code, error->what());
18✔
1363
                                     return completion(nullptr, error);
18✔
1364
                                 }
18✔
1365
                                 completion(&response.body, util::none);
569✔
1366
                             });
569✔
1367
}
587✔
1368

1369
void App::call_function(const std::shared_ptr<User>& user, const std::string& name, const BsonArray& args_bson,
1370
                        const Optional<std::string>& service_name,
1371
                        UniqueFunction<void(Optional<Bson>&&, Optional<AppError>)>&& completion)
1372
{
587✔
1373
    auto service_name2 = service_name ? *service_name : "<none>";
587✔
1374
    std::stringstream args_ejson;
587✔
1375
    args_ejson << "[";
587✔
1376
    bool not_first = false;
587✔
1377
    for (auto&& arg : args_bson) {
603✔
1378
        if (not_first)
603✔
1379
            args_ejson << ',';
16✔
1380
        args_ejson << arg.toJson();
603✔
1381
        not_first = true;
603✔
1382
    }
603✔
1383
    args_ejson << "]";
587✔
1384

1385
    call_function(user, name, std::move(args_ejson).str(), service_name,
587✔
1386
                  [self = shared_from_this(), name, service_name = std::move(service_name2),
587✔
1387
                   completion = std::move(completion)](const std::string* response, util::Optional<AppError> err) {
587✔
1388
                      if (err) {
587✔
1389
                          return completion({}, err);
18✔
1390
                      }
18✔
1391
                      if (!response) {
569✔
1392
                          return completion({}, AppError{ErrorCodes::AppUnknownError, "Empty response from server"});
×
1393
                      }
×
1394
                      util::Optional<Bson> body_as_bson;
569✔
1395
                      try {
569✔
1396
                          body_as_bson = bson::parse(*response);
569✔
1397
                          if (self->would_log(util::Logger::Level::debug)) {
569✔
1398
                              self->log_debug("App: call_function: %1 service_name: %2 - results: %3", name,
569✔
1399
                                              service_name, body_as_bson ? body_as_bson->to_string() : "<none>");
569✔
1400
                          }
569✔
1401
                      }
569✔
1402
                      catch (const std::exception& e) {
569✔
1403
                          self->log_error("App: call_function: %1 service_name: %2 - error parsing result: %3", name,
×
1404
                                          service_name, e.what());
×
1405
                          return completion(util::none, AppError(ErrorCodes::BadBsonParse, e.what()));
×
1406
                      };
569✔
1407
                      completion(std::move(body_as_bson), util::none);
569✔
1408
                  });
569✔
1409
}
587✔
1410

1411
void App::call_function(const std::shared_ptr<User>& user, const std::string& name, const BsonArray& args_bson,
1412
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
1413
{
4✔
1414
    call_function(user, name, args_bson, util::none, std::move(completion));
4✔
1415
}
4✔
1416

1417
void App::call_function(const std::string& name, const BsonArray& args_bson,
1418
                        const Optional<std::string>& service_name,
1419
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
1420
{
×
1421
    call_function(current_user(), name, args_bson, service_name, std::move(completion));
×
1422
}
×
1423

1424
void App::call_function(const std::string& name, const BsonArray& args_bson,
1425
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
1426
{
2✔
1427
    call_function(current_user(), name, args_bson, std::move(completion));
2✔
1428
}
2✔
1429

1430
Request App::make_streaming_request(const std::shared_ptr<User>& user, const std::string& name,
1431
                                    const BsonArray& args_bson, const Optional<std::string>& service_name) const
1432
{
8✔
1433
    auto args = BsonDocument{
8✔
1434
        {"arguments", args_bson},
8✔
1435
        {"name", name},
8✔
1436
    };
8✔
1437
    if (service_name) {
8✔
1438
        args["service"] = *service_name;
8✔
1439
    }
8✔
1440
    const auto args_json = Bson(args).to_string();
8✔
1441

1442
    auto args_base64 = std::string(util::base64_encoded_size(args_json.size()), '\0');
8✔
1443
    util::base64_encode(args_json, args_base64);
8✔
1444

1445
    auto url = function_call_url_path() + "?baas_request=" + util::uri_percent_encode(args_base64);
8✔
1446
    if (user) {
8✔
1447
        url += "&baas_at=";
2✔
1448
        url += user->access_token(); // doesn't need url encoding
2✔
1449
    }
2✔
1450

1451
    return Request{
8✔
1452
        HttpMethod::get,
8✔
1453
        url,
8✔
1454
        m_request_timeout_ms,
8✔
1455
        {{"Accept", "text/event-stream"}},
8✔
1456
    };
8✔
1457
}
8✔
1458

1459
std::unique_ptr<Request> App::make_request(HttpMethod method, std::string&& url, const std::shared_ptr<User>& user,
1460
                                           RequestTokenType token_type, std::string&& body) const
1461
{
15,081✔
1462
    auto request = std::make_unique<Request>();
15,081✔
1463
    request->method = method;
15,081✔
1464
    request->url = std::move(url);
15,081✔
1465
    request->body = std::move(body);
15,081✔
1466
    request->headers = get_request_headers(user, token_type);
15,081✔
1467
    request->timeout_ms = m_request_timeout_ms;
15,081✔
1468
    return request;
15,081✔
1469
}
15,081✔
1470

1471
PushClient App::push_notification_client(const std::string& service_name)
1472
{
10✔
1473
    return PushClient(service_name, m_config.app_id, std::shared_ptr<AuthRequestClient>(shared_from_this(), this));
10✔
1474
}
10✔
1475

1476
void App::emit_change_to_subscribers()
1477
{
4,820✔
1478
    // This wrapper is needed only to be able to add the `REQUIRES(!m_user_mutex)`
1479
    // annotation. Calling this function with the lock held leads to a deadlock
1480
    // if any of the listeners try to access us.
1481
    Subscribable<App>::emit_change_to_subscribers(*this);
4,820✔
1482
}
4,820✔
1483

1484
// MARK: - UserProvider
1485

1486
void App::register_sync_user(User& user)
1487
{
9,269✔
1488
    auto& tracked_user = m_users[user.user_id()];
9,269✔
1489
    REALM_ASSERT(!tracked_user || !tracked_user->weak_from_this().lock());
9,269!
1490
    tracked_user = &user;
9,269✔
1491
    user.update_backing_data(m_metadata_store->get_user(user.user_id()));
9,269✔
1492
}
9,269✔
1493

1494
void App::unregister_sync_user(User& user)
1495
{
9,269✔
1496
    util::CheckedLockGuard lock(m_user_mutex);
9,269✔
1497
    auto it = m_users.find(user.user_id());
9,269✔
1498
    REALM_ASSERT(it != m_users.end());
9,269✔
1499
    // If the user was requested while we were waiting for the lock, it may
1500
    // have already been replaced with a new instance for the same user id
1501
    if (it != m_users.end() && it->second == &user) {
9,269✔
1502
        m_users.erase(it);
9,269✔
1503
    }
9,269✔
1504
    if (m_current_user == &user) {
9,269✔
1505
        m_current_user = nullptr;
9,053✔
1506
    }
9,053✔
1507
}
9,269✔
1508

1509
bool App::immediately_run_file_actions(std::string_view realm_path)
1510
{
10✔
1511
    return m_metadata_store->immediately_run_file_actions(*m_file_manager, realm_path);
10✔
1512
}
10✔
1513

1514
std::string App::path_for_realm(const SyncConfig& config, std::optional<std::string> custom_file_name) const
1515
{
28✔
1516
    return m_file_manager->path_for_realm(config, std::move(custom_file_name));
28✔
1517
}
28✔
1518

1519
} // namespace realm::app
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc