Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ts=2 sw=2 et: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | #include "nsCookiePermission.h" |
michael@0 | 8 | |
michael@0 | 9 | #include "mozIThirdPartyUtil.h" |
michael@0 | 10 | #include "nsICookie2.h" |
michael@0 | 11 | #include "nsIServiceManager.h" |
michael@0 | 12 | #include "nsICookiePromptService.h" |
michael@0 | 13 | #include "nsICookieManager2.h" |
michael@0 | 14 | #include "nsNetUtil.h" |
michael@0 | 15 | #include "nsIURI.h" |
michael@0 | 16 | #include "nsIPrefService.h" |
michael@0 | 17 | #include "nsIPrefBranch.h" |
michael@0 | 18 | #include "nsIChannel.h" |
michael@0 | 19 | #include "nsIHttpChannelInternal.h" |
michael@0 | 20 | #include "nsIDOMWindow.h" |
michael@0 | 21 | #include "nsIPrincipal.h" |
michael@0 | 22 | #include "nsString.h" |
michael@0 | 23 | #include "nsCRT.h" |
michael@0 | 24 | #include "nsILoadContext.h" |
michael@0 | 25 | #include "nsIScriptObjectPrincipal.h" |
michael@0 | 26 | #include "nsNetCID.h" |
michael@0 | 27 | |
michael@0 | 28 | /**************************************************************** |
michael@0 | 29 | ************************ nsCookiePermission ******************** |
michael@0 | 30 | ****************************************************************/ |
michael@0 | 31 | |
michael@0 | 32 | // values for mCookiesLifetimePolicy |
michael@0 | 33 | // 0 == accept normally |
michael@0 | 34 | // 1 == ask before accepting |
michael@0 | 35 | // 2 == downgrade to session |
michael@0 | 36 | // 3 == limit lifetime to N days |
michael@0 | 37 | static const uint32_t ACCEPT_NORMALLY = 0; |
michael@0 | 38 | static const uint32_t ASK_BEFORE_ACCEPT = 1; |
michael@0 | 39 | static const uint32_t ACCEPT_SESSION = 2; |
michael@0 | 40 | static const uint32_t ACCEPT_FOR_N_DAYS = 3; |
michael@0 | 41 | |
michael@0 | 42 | static const bool kDefaultPolicy = true; |
michael@0 | 43 | static const char kCookiesLifetimePolicy[] = "network.cookie.lifetimePolicy"; |
michael@0 | 44 | static const char kCookiesLifetimeDays[] = "network.cookie.lifetime.days"; |
michael@0 | 45 | static const char kCookiesAlwaysAcceptSession[] = "network.cookie.alwaysAcceptSessionCookies"; |
michael@0 | 46 | |
michael@0 | 47 | static const char kCookiesPrefsMigrated[] = "network.cookie.prefsMigrated"; |
michael@0 | 48 | // obsolete pref names for migration |
michael@0 | 49 | static const char kCookiesLifetimeEnabled[] = "network.cookie.lifetime.enabled"; |
michael@0 | 50 | static const char kCookiesLifetimeBehavior[] = "network.cookie.lifetime.behavior"; |
michael@0 | 51 | static const char kCookiesAskPermission[] = "network.cookie.warnAboutCookies"; |
michael@0 | 52 | |
michael@0 | 53 | static const char kPermissionType[] = "cookie"; |
michael@0 | 54 | |
michael@0 | 55 | NS_IMPL_ISUPPORTS(nsCookiePermission, |
michael@0 | 56 | nsICookiePermission, |
michael@0 | 57 | nsIObserver) |
michael@0 | 58 | |
michael@0 | 59 | bool |
michael@0 | 60 | nsCookiePermission::Init() |
michael@0 | 61 | { |
michael@0 | 62 | // Initialize nsIPermissionManager and fetch relevant prefs. This is only |
michael@0 | 63 | // required for some methods on nsICookiePermission, so it should be done |
michael@0 | 64 | // lazily. |
michael@0 | 65 | nsresult rv; |
michael@0 | 66 | mPermMgr = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); |
michael@0 | 67 | if (NS_FAILED(rv)) return false; |
michael@0 | 68 | mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID, &rv); |
michael@0 | 69 | if (NS_FAILED(rv)) return false; |
michael@0 | 70 | |
michael@0 | 71 | // failure to access the pref service is non-fatal... |
michael@0 | 72 | nsCOMPtr<nsIPrefBranch> prefBranch = |
michael@0 | 73 | do_GetService(NS_PREFSERVICE_CONTRACTID); |
michael@0 | 74 | if (prefBranch) { |
michael@0 | 75 | prefBranch->AddObserver(kCookiesLifetimePolicy, this, false); |
michael@0 | 76 | prefBranch->AddObserver(kCookiesLifetimeDays, this, false); |
michael@0 | 77 | prefBranch->AddObserver(kCookiesAlwaysAcceptSession, this, false); |
michael@0 | 78 | PrefChanged(prefBranch, nullptr); |
michael@0 | 79 | |
michael@0 | 80 | // migration code for original cookie prefs |
michael@0 | 81 | bool migrated; |
michael@0 | 82 | rv = prefBranch->GetBoolPref(kCookiesPrefsMigrated, &migrated); |
michael@0 | 83 | if (NS_FAILED(rv) || !migrated) { |
michael@0 | 84 | bool warnAboutCookies = false; |
michael@0 | 85 | prefBranch->GetBoolPref(kCookiesAskPermission, &warnAboutCookies); |
michael@0 | 86 | |
michael@0 | 87 | // if the user is using ask before accepting, we'll use that |
michael@0 | 88 | if (warnAboutCookies) |
michael@0 | 89 | prefBranch->SetIntPref(kCookiesLifetimePolicy, ASK_BEFORE_ACCEPT); |
michael@0 | 90 | |
michael@0 | 91 | bool lifetimeEnabled = false; |
michael@0 | 92 | prefBranch->GetBoolPref(kCookiesLifetimeEnabled, &lifetimeEnabled); |
michael@0 | 93 | |
michael@0 | 94 | // if they're limiting lifetime and not using the prompts, use the |
michael@0 | 95 | // appropriate limited lifetime pref |
michael@0 | 96 | if (lifetimeEnabled && !warnAboutCookies) { |
michael@0 | 97 | int32_t lifetimeBehavior; |
michael@0 | 98 | prefBranch->GetIntPref(kCookiesLifetimeBehavior, &lifetimeBehavior); |
michael@0 | 99 | if (lifetimeBehavior) |
michael@0 | 100 | prefBranch->SetIntPref(kCookiesLifetimePolicy, ACCEPT_FOR_N_DAYS); |
michael@0 | 101 | else |
michael@0 | 102 | prefBranch->SetIntPref(kCookiesLifetimePolicy, ACCEPT_SESSION); |
michael@0 | 103 | } |
michael@0 | 104 | prefBranch->SetBoolPref(kCookiesPrefsMigrated, true); |
michael@0 | 105 | } |
michael@0 | 106 | } |
michael@0 | 107 | |
michael@0 | 108 | return true; |
michael@0 | 109 | } |
michael@0 | 110 | |
michael@0 | 111 | void |
michael@0 | 112 | nsCookiePermission::PrefChanged(nsIPrefBranch *aPrefBranch, |
michael@0 | 113 | const char *aPref) |
michael@0 | 114 | { |
michael@0 | 115 | int32_t val; |
michael@0 | 116 | |
michael@0 | 117 | #define PREF_CHANGED(_P) (!aPref || !strcmp(aPref, _P)) |
michael@0 | 118 | |
michael@0 | 119 | if (PREF_CHANGED(kCookiesLifetimePolicy) && |
michael@0 | 120 | NS_SUCCEEDED(aPrefBranch->GetIntPref(kCookiesLifetimePolicy, &val))) |
michael@0 | 121 | mCookiesLifetimePolicy = val; |
michael@0 | 122 | |
michael@0 | 123 | if (PREF_CHANGED(kCookiesLifetimeDays) && |
michael@0 | 124 | NS_SUCCEEDED(aPrefBranch->GetIntPref(kCookiesLifetimeDays, &val))) |
michael@0 | 125 | // save cookie lifetime in seconds instead of days |
michael@0 | 126 | mCookiesLifetimeSec = val * 24 * 60 * 60; |
michael@0 | 127 | |
michael@0 | 128 | bool bval; |
michael@0 | 129 | if (PREF_CHANGED(kCookiesAlwaysAcceptSession) && |
michael@0 | 130 | NS_SUCCEEDED(aPrefBranch->GetBoolPref(kCookiesAlwaysAcceptSession, &bval))) |
michael@0 | 131 | mCookiesAlwaysAcceptSession = bval; |
michael@0 | 132 | } |
michael@0 | 133 | |
michael@0 | 134 | NS_IMETHODIMP |
michael@0 | 135 | nsCookiePermission::SetAccess(nsIURI *aURI, |
michael@0 | 136 | nsCookieAccess aAccess) |
michael@0 | 137 | { |
michael@0 | 138 | // Lazily initialize ourselves |
michael@0 | 139 | if (!EnsureInitialized()) |
michael@0 | 140 | return NS_ERROR_UNEXPECTED; |
michael@0 | 141 | |
michael@0 | 142 | // |
michael@0 | 143 | // NOTE: nsCookieAccess values conveniently match up with |
michael@0 | 144 | // the permission codes used by nsIPermissionManager. |
michael@0 | 145 | // this is nice because it avoids conversion code. |
michael@0 | 146 | // |
michael@0 | 147 | return mPermMgr->Add(aURI, kPermissionType, aAccess, |
michael@0 | 148 | nsIPermissionManager::EXPIRE_NEVER, 0); |
michael@0 | 149 | } |
michael@0 | 150 | |
michael@0 | 151 | NS_IMETHODIMP |
michael@0 | 152 | nsCookiePermission::CanAccess(nsIURI *aURI, |
michael@0 | 153 | nsIChannel *aChannel, |
michael@0 | 154 | nsCookieAccess *aResult) |
michael@0 | 155 | { |
michael@0 | 156 | // Check this protocol doesn't allow cookies |
michael@0 | 157 | bool hasFlags; |
michael@0 | 158 | nsresult rv = |
michael@0 | 159 | NS_URIChainHasFlags(aURI, nsIProtocolHandler::URI_FORBIDS_COOKIE_ACCESS, |
michael@0 | 160 | &hasFlags); |
michael@0 | 161 | if (NS_FAILED(rv) || hasFlags) { |
michael@0 | 162 | *aResult = ACCESS_DENY; |
michael@0 | 163 | return NS_OK; |
michael@0 | 164 | } |
michael@0 | 165 | |
michael@0 | 166 | // Lazily initialize ourselves |
michael@0 | 167 | if (!EnsureInitialized()) |
michael@0 | 168 | return NS_ERROR_UNEXPECTED; |
michael@0 | 169 | |
michael@0 | 170 | // finally, check with permission manager... |
michael@0 | 171 | rv = mPermMgr->TestPermission(aURI, kPermissionType, (uint32_t *) aResult); |
michael@0 | 172 | if (NS_SUCCEEDED(rv)) { |
michael@0 | 173 | if (*aResult == nsICookiePermission::ACCESS_SESSION) { |
michael@0 | 174 | *aResult = nsICookiePermission::ACCESS_ALLOW; |
michael@0 | 175 | } |
michael@0 | 176 | } |
michael@0 | 177 | |
michael@0 | 178 | return rv; |
michael@0 | 179 | } |
michael@0 | 180 | |
michael@0 | 181 | NS_IMETHODIMP |
michael@0 | 182 | nsCookiePermission::CanSetCookie(nsIURI *aURI, |
michael@0 | 183 | nsIChannel *aChannel, |
michael@0 | 184 | nsICookie2 *aCookie, |
michael@0 | 185 | bool *aIsSession, |
michael@0 | 186 | int64_t *aExpiry, |
michael@0 | 187 | bool *aResult) |
michael@0 | 188 | { |
michael@0 | 189 | NS_ASSERTION(aURI, "null uri"); |
michael@0 | 190 | |
michael@0 | 191 | *aResult = kDefaultPolicy; |
michael@0 | 192 | |
michael@0 | 193 | // Lazily initialize ourselves |
michael@0 | 194 | if (!EnsureInitialized()) |
michael@0 | 195 | return NS_ERROR_UNEXPECTED; |
michael@0 | 196 | |
michael@0 | 197 | uint32_t perm; |
michael@0 | 198 | mPermMgr->TestPermission(aURI, kPermissionType, &perm); |
michael@0 | 199 | bool isThirdParty = false; |
michael@0 | 200 | switch (perm) { |
michael@0 | 201 | case nsICookiePermission::ACCESS_SESSION: |
michael@0 | 202 | *aIsSession = true; |
michael@0 | 203 | |
michael@0 | 204 | case nsICookiePermission::ACCESS_ALLOW: |
michael@0 | 205 | *aResult = true; |
michael@0 | 206 | break; |
michael@0 | 207 | |
michael@0 | 208 | case nsICookiePermission::ACCESS_DENY: |
michael@0 | 209 | *aResult = false; |
michael@0 | 210 | break; |
michael@0 | 211 | |
michael@0 | 212 | case nsICookiePermission::ACCESS_ALLOW_FIRST_PARTY_ONLY: |
michael@0 | 213 | mThirdPartyUtil->IsThirdPartyChannel(aChannel, aURI, &isThirdParty); |
michael@0 | 214 | // If it's third party, we can't set the cookie |
michael@0 | 215 | if (isThirdParty) |
michael@0 | 216 | *aResult = false; |
michael@0 | 217 | break; |
michael@0 | 218 | |
michael@0 | 219 | case nsICookiePermission::ACCESS_LIMIT_THIRD_PARTY: |
michael@0 | 220 | mThirdPartyUtil->IsThirdPartyChannel(aChannel, aURI, &isThirdParty); |
michael@0 | 221 | // If it's third party, check whether cookies are already set |
michael@0 | 222 | if (isThirdParty) { |
michael@0 | 223 | nsresult rv; |
michael@0 | 224 | nsCOMPtr<nsICookieManager2> cookieManager = do_GetService(NS_COOKIEMANAGER_CONTRACTID, &rv); |
michael@0 | 225 | if (NS_FAILED(rv)) { |
michael@0 | 226 | *aResult = false; |
michael@0 | 227 | break; |
michael@0 | 228 | } |
michael@0 | 229 | uint32_t priorCookieCount = 0; |
michael@0 | 230 | nsAutoCString hostFromURI; |
michael@0 | 231 | aURI->GetHost(hostFromURI); |
michael@0 | 232 | cookieManager->CountCookiesFromHost(hostFromURI, &priorCookieCount); |
michael@0 | 233 | *aResult = priorCookieCount != 0; |
michael@0 | 234 | } |
michael@0 | 235 | break; |
michael@0 | 236 | |
michael@0 | 237 | default: |
michael@0 | 238 | // the permission manager has nothing to say about this cookie - |
michael@0 | 239 | // so, we apply the default prefs to it. |
michael@0 | 240 | NS_ASSERTION(perm == nsIPermissionManager::UNKNOWN_ACTION, "unknown permission"); |
michael@0 | 241 | |
michael@0 | 242 | // now we need to figure out what type of accept policy we're dealing with |
michael@0 | 243 | // if we accept cookies normally, just bail and return |
michael@0 | 244 | if (mCookiesLifetimePolicy == ACCEPT_NORMALLY) { |
michael@0 | 245 | *aResult = true; |
michael@0 | 246 | return NS_OK; |
michael@0 | 247 | } |
michael@0 | 248 | |
michael@0 | 249 | // declare this here since it'll be used in all of the remaining cases |
michael@0 | 250 | int64_t currentTime = PR_Now() / PR_USEC_PER_SEC; |
michael@0 | 251 | int64_t delta = *aExpiry - currentTime; |
michael@0 | 252 | |
michael@0 | 253 | // check whether the user wants to be prompted |
michael@0 | 254 | if (mCookiesLifetimePolicy == ASK_BEFORE_ACCEPT) { |
michael@0 | 255 | // if it's a session cookie and the user wants to accept these |
michael@0 | 256 | // without asking, or if we are in private browsing mode, just |
michael@0 | 257 | // accept the cookie and return |
michael@0 | 258 | if ((*aIsSession && mCookiesAlwaysAcceptSession) || |
michael@0 | 259 | (aChannel && NS_UsePrivateBrowsing(aChannel))) { |
michael@0 | 260 | *aResult = true; |
michael@0 | 261 | return NS_OK; |
michael@0 | 262 | } |
michael@0 | 263 | |
michael@0 | 264 | // default to rejecting, in case the prompting process fails |
michael@0 | 265 | *aResult = false; |
michael@0 | 266 | |
michael@0 | 267 | nsAutoCString hostPort; |
michael@0 | 268 | aURI->GetHostPort(hostPort); |
michael@0 | 269 | |
michael@0 | 270 | if (!aCookie) { |
michael@0 | 271 | return NS_ERROR_UNEXPECTED; |
michael@0 | 272 | } |
michael@0 | 273 | // If there is no host, use the scheme, and append "://", |
michael@0 | 274 | // to make sure it isn't a host or something. |
michael@0 | 275 | // This is done to make the dialog appear for javascript cookies from |
michael@0 | 276 | // file:// urls, and make the text on it not too weird. (bug 209689) |
michael@0 | 277 | if (hostPort.IsEmpty()) { |
michael@0 | 278 | aURI->GetScheme(hostPort); |
michael@0 | 279 | if (hostPort.IsEmpty()) { |
michael@0 | 280 | // still empty. Just return the default. |
michael@0 | 281 | return NS_OK; |
michael@0 | 282 | } |
michael@0 | 283 | hostPort = hostPort + NS_LITERAL_CSTRING("://"); |
michael@0 | 284 | } |
michael@0 | 285 | |
michael@0 | 286 | // we don't cache the cookiePromptService - it's not used often, so not |
michael@0 | 287 | // worth the memory. |
michael@0 | 288 | nsresult rv; |
michael@0 | 289 | nsCOMPtr<nsICookiePromptService> cookiePromptService = |
michael@0 | 290 | do_GetService(NS_COOKIEPROMPTSERVICE_CONTRACTID, &rv); |
michael@0 | 291 | if (NS_FAILED(rv)) return rv; |
michael@0 | 292 | |
michael@0 | 293 | // get some useful information to present to the user: |
michael@0 | 294 | // whether a previous cookie already exists, and how many cookies this host |
michael@0 | 295 | // has set |
michael@0 | 296 | bool foundCookie = false; |
michael@0 | 297 | uint32_t countFromHost; |
michael@0 | 298 | nsCOMPtr<nsICookieManager2> cookieManager = do_GetService(NS_COOKIEMANAGER_CONTRACTID, &rv); |
michael@0 | 299 | if (NS_SUCCEEDED(rv)) { |
michael@0 | 300 | nsAutoCString rawHost; |
michael@0 | 301 | aCookie->GetRawHost(rawHost); |
michael@0 | 302 | rv = cookieManager->CountCookiesFromHost(rawHost, &countFromHost); |
michael@0 | 303 | |
michael@0 | 304 | if (NS_SUCCEEDED(rv) && countFromHost > 0) |
michael@0 | 305 | rv = cookieManager->CookieExists(aCookie, &foundCookie); |
michael@0 | 306 | } |
michael@0 | 307 | if (NS_FAILED(rv)) return rv; |
michael@0 | 308 | |
michael@0 | 309 | // check if the cookie we're trying to set is already expired, and return; |
michael@0 | 310 | // but only if there's no previous cookie, because then we need to delete the previous |
michael@0 | 311 | // cookie. we need this check to avoid prompting the user for already-expired cookies. |
michael@0 | 312 | if (!foundCookie && !*aIsSession && delta <= 0) { |
michael@0 | 313 | // the cookie has already expired. accept it, and let the backend figure |
michael@0 | 314 | // out it's expired, so that we get correct logging & notifications. |
michael@0 | 315 | *aResult = true; |
michael@0 | 316 | return rv; |
michael@0 | 317 | } |
michael@0 | 318 | |
michael@0 | 319 | bool rememberDecision = false; |
michael@0 | 320 | int32_t dialogRes = nsICookiePromptService::DENY_COOKIE; |
michael@0 | 321 | rv = cookiePromptService->CookieDialog(nullptr, aCookie, hostPort, |
michael@0 | 322 | countFromHost, foundCookie, |
michael@0 | 323 | &rememberDecision, &dialogRes); |
michael@0 | 324 | if (NS_FAILED(rv)) return rv; |
michael@0 | 325 | |
michael@0 | 326 | *aResult = !!dialogRes; |
michael@0 | 327 | if (dialogRes == nsICookiePromptService::ACCEPT_SESSION_COOKIE) |
michael@0 | 328 | *aIsSession = true; |
michael@0 | 329 | |
michael@0 | 330 | if (rememberDecision) { |
michael@0 | 331 | switch (dialogRes) { |
michael@0 | 332 | case nsICookiePromptService::DENY_COOKIE: |
michael@0 | 333 | mPermMgr->Add(aURI, kPermissionType, (uint32_t) nsIPermissionManager::DENY_ACTION, |
michael@0 | 334 | nsIPermissionManager::EXPIRE_NEVER, 0); |
michael@0 | 335 | break; |
michael@0 | 336 | case nsICookiePromptService::ACCEPT_COOKIE: |
michael@0 | 337 | mPermMgr->Add(aURI, kPermissionType, (uint32_t) nsIPermissionManager::ALLOW_ACTION, |
michael@0 | 338 | nsIPermissionManager::EXPIRE_NEVER, 0); |
michael@0 | 339 | break; |
michael@0 | 340 | case nsICookiePromptService::ACCEPT_SESSION_COOKIE: |
michael@0 | 341 | mPermMgr->Add(aURI, kPermissionType, nsICookiePermission::ACCESS_SESSION, |
michael@0 | 342 | nsIPermissionManager::EXPIRE_NEVER, 0); |
michael@0 | 343 | break; |
michael@0 | 344 | default: |
michael@0 | 345 | break; |
michael@0 | 346 | } |
michael@0 | 347 | } |
michael@0 | 348 | } else { |
michael@0 | 349 | // we're not prompting, so we must be limiting the lifetime somehow |
michael@0 | 350 | // if it's a session cookie, we do nothing |
michael@0 | 351 | if (!*aIsSession && delta > 0) { |
michael@0 | 352 | if (mCookiesLifetimePolicy == ACCEPT_SESSION) { |
michael@0 | 353 | // limit lifetime to session |
michael@0 | 354 | *aIsSession = true; |
michael@0 | 355 | } else if (delta > mCookiesLifetimeSec) { |
michael@0 | 356 | // limit lifetime to specified time |
michael@0 | 357 | *aExpiry = currentTime + mCookiesLifetimeSec; |
michael@0 | 358 | } |
michael@0 | 359 | } |
michael@0 | 360 | } |
michael@0 | 361 | |
michael@0 | 362 | // TODO: Why don't we just use this here: |
michael@0 | 363 | // httpChannelInternal->GetDocumentURI(aURI); |
michael@0 | 364 | } |
michael@0 | 365 | |
michael@0 | 366 | return NS_OK; |
michael@0 | 367 | } |
michael@0 | 368 | |
michael@0 | 369 | NS_IMETHODIMP |
michael@0 | 370 | nsCookiePermission::Observe(nsISupports *aSubject, |
michael@0 | 371 | const char *aTopic, |
michael@0 | 372 | const char16_t *aData) |
michael@0 | 373 | { |
michael@0 | 374 | nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject); |
michael@0 | 375 | NS_ASSERTION(!nsCRT::strcmp(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, aTopic), |
michael@0 | 376 | "unexpected topic - we only deal with pref changes!"); |
michael@0 | 377 | |
michael@0 | 378 | if (prefBranch) |
michael@0 | 379 | PrefChanged(prefBranch, NS_LossyConvertUTF16toASCII(aData).get()); |
michael@0 | 380 | return NS_OK; |
michael@0 | 381 | } |