From a9aa9a186f775a3aeb53fd47810517ba00260a87 Mon Sep 17 00:00:00 2001 From: Calixte Date: Tue, 14 Nov 2023 14:48:03 +0000 Subject: [PATCH] Bug 1815739 - Add a protocol handler for content uris in GV r=geckoview-reviewers,owlish,tthibaud,necko-reviewers,kershaw On Android, contents are shared in using uris with a content scheme, hence this patch adds a new protocol handler for 'content://', a new type of input stream in order to read them and a new type of channel. Differential Revision: https://phabricator.services.mozilla.com/D174782 --- .../geckoview/GeckoViewContentChannel.cpp | 47 +++++ .../geckoview/GeckoViewContentChannel.h | 23 +++ .../GeckoViewContentProtocolHandler.cpp | 54 ++++++ .../GeckoViewContentProtocolHandler.h | 27 +++ .../geckoview/GeckoViewInputStream.cpp | 110 ++++++++++++ .../geckoview/GeckoViewInputStream.h | 43 +++++ .../components/geckoview/components.conf | 14 ++ mobile/android/components/geckoview/moz.build | 6 + .../mozilla/geckoview/test/PdfCreationTest.kt | 19 ++ .../mozilla/geckoview/ContentInputStream.java | 139 +++++++++++++++ .../geckoview/GeckoViewInputStream.java | 163 ++++++++++++++++++ .../geckoview/SessionPdfFileSaver.java | 3 +- .../geckoview/GeckoViewNavigation.sys.mjs | 2 +- widget/android/moz.build | 2 + 14 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 mobile/android/components/geckoview/GeckoViewContentChannel.cpp create mode 100644 mobile/android/components/geckoview/GeckoViewContentChannel.h create mode 100644 mobile/android/components/geckoview/GeckoViewContentProtocolHandler.cpp create mode 100644 mobile/android/components/geckoview/GeckoViewContentProtocolHandler.h create mode 100644 mobile/android/components/geckoview/GeckoViewInputStream.cpp create mode 100644 mobile/android/components/geckoview/GeckoViewInputStream.h create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentInputStream.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoViewInputStream.java diff --git a/mobile/android/components/geckoview/GeckoViewContentChannel.cpp b/mobile/android/components/geckoview/GeckoViewContentChannel.cpp new file mode 100644 index 000000000000..49236b074cb9 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewContentChannel.cpp @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GeckoViewContentChannel.h" +#include "GeckoViewInputStream.h" +#include "mozilla/java/ContentInputStreamWrappers.h" + +using namespace mozilla; + +GeckoViewContentChannel::GeckoViewContentChannel(nsIURI* aURI) { + SetURI(aURI); + SetOriginalURI(aURI); +} + +NS_IMETHODIMP +GeckoViewContentChannel::OpenContentStream(bool aAsync, + nsIInputStream** aResult, + nsIChannel** aChannel) { + nsCOMPtr uri; + nsresult rv = GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI); + + nsAutoCString spec; + rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + bool isReadable = GeckoViewContentInputStream::isReadable(spec); + if (!isReadable) { + return NS_ERROR_FILE_NOT_FOUND; + } + + nsCOMPtr inputStream; + rv = GeckoViewContentInputStream::getInstance(spec, + getter_AddRefs(inputStream)); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_WARN_IF(!inputStream)) { + return NS_ERROR_MALFORMED_URI; + } + + inputStream.forget(aResult); + + return NS_OK; +} diff --git a/mobile/android/components/geckoview/GeckoViewContentChannel.h b/mobile/android/components/geckoview/GeckoViewContentChannel.h new file mode 100644 index 000000000000..70252e713459 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewContentChannel.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GeckoViewContentChannel_h__ +#define GeckoViewContentChannel_h__ + +#include "nsBaseChannel.h" + +class GeckoViewContentChannel final : public nsBaseChannel { + public: + explicit GeckoViewContentChannel(nsIURI* aUri); + + private: + ~GeckoViewContentChannel() = default; + + nsresult OpenContentStream(bool async, nsIInputStream** result, + nsIChannel** channel) override; +}; + +#endif // !GeckoViewContentChannel_h__ diff --git a/mobile/android/components/geckoview/GeckoViewContentProtocolHandler.cpp b/mobile/android/components/geckoview/GeckoViewContentProtocolHandler.cpp new file mode 100644 index 000000000000..dffd84e60893 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewContentProtocolHandler.cpp @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +// vim:ts=4 sw=2 sts=2 et cin: +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GeckoViewContentProtocolHandler.h" +#include "GeckoViewContentChannel.h" +#include "nsStandardURL.h" +#include "nsURLHelper.h" +#include "nsIURIMutator.h" + +#include "nsNetUtil.h" + +#include "mozilla/ResultExtensions.h" + +//----------------------------------------------------------------------------- + +nsresult GeckoViewContentProtocolHandler::Init() { return NS_OK; } + +NS_IMPL_ISUPPORTS(GeckoViewContentProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsIProtocolHandler methods: + +NS_IMETHODIMP +GeckoViewContentProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral("content"); + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewContentProtocolHandler::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + nsresult rv; + RefPtr chan = new GeckoViewContentChannel(uri); + + rv = chan->SetLoadInfo(aLoadInfo); + if (NS_FAILED(rv)) { + return rv; + } + + chan.forget(result); + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewContentProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* result) { + // don't override anything. + *result = false; + return NS_OK; +} diff --git a/mobile/android/components/geckoview/GeckoViewContentProtocolHandler.h b/mobile/android/components/geckoview/GeckoViewContentProtocolHandler.h new file mode 100644 index 000000000000..1763aaf121b2 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewContentProtocolHandler.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GeckoViewContentProtocolHandler_h__ +#define GeckoViewContentProtocolHandler_h__ + +#include "nsIProtocolHandler.h" +#include "nsWeakReference.h" + +class nsIURIMutator; + +class GeckoViewContentProtocolHandler : public nsIProtocolHandler, + public nsSupportsWeakReference { + virtual ~GeckoViewContentProtocolHandler() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + + GeckoViewContentProtocolHandler() = default; + + [[nodiscard]] nsresult Init(); +}; + +#endif // !GeckoViewContentProtocolHandler_h__ diff --git a/mobile/android/components/geckoview/GeckoViewInputStream.cpp b/mobile/android/components/geckoview/GeckoViewInputStream.cpp new file mode 100644 index 000000000000..187a0ce10c50 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewInputStream.cpp @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GeckoViewInputStream.h" +#include "nsStreamUtils.h" + +NS_IMPL_ISUPPORTS(GeckoViewInputStream, nsIInputStream); + +NS_IMETHODIMP +GeckoViewInputStream::Close() { + mClosed = true; + mInstance->Close(); + + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewInputStream::Available(uint64_t* aCount) { + if (mClosed) { + return NS_BASE_STREAM_CLOSED; + } + + *aCount = static_cast(mInstance->Available()); + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewInputStream::StreamStatus() { + return mClosed ? NS_BASE_STREAM_CLOSED : NS_OK; +} + +NS_IMETHODIMP +GeckoViewInputStream::Read(char* aBuf, uint32_t aCount, uint32_t* aReadCount) { + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, aReadCount); +} + +NS_IMETHODIMP +GeckoViewInputStream::ReadSegments(nsWriteSegmentFun writer, void* aClosure, + uint32_t aCount, uint32_t* result) { + NS_ASSERTION(result, "null ptr"); + + if (mClosed) { + return NS_BASE_STREAM_CLOSED; + } + + auto bufferAddress = + static_cast(mInstance->MBuffer()->Address()); + uint32_t segmentPos = static_cast(mInstance->MPos()); + int32_t dataLength = 0; + nsresult rv; + + *result = 0; + while (aCount) { + rv = mInstance->Read(static_cast(aCount), &dataLength); + if (NS_FAILED(rv)) { + return NS_BASE_STREAM_OSERROR; + } + + if (dataLength == -1) { + break; + } + + uint32_t uDataLength = static_cast(dataLength); + uint32_t written; + rv = writer(this, aClosure, bufferAddress + segmentPos, *result, + uDataLength, &written); + + if (NS_FAILED(rv)) { + // InputStreams do not propagate errors to caller. + break; + } + + NS_ASSERTION(written > 0, "Must have written something"); + + *result += written; + aCount -= written; + + segmentPos = static_cast(mInstance->ConsumedData(written)); + } + + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewInputStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = true; + return NS_OK; +} + +bool GeckoViewInputStream::isClosed() const { return mInstance->IsClosed(); } + +bool GeckoViewContentInputStream::isReadable(const nsAutoCString& aUri) { + return mozilla::java::ContentInputStream::IsReadable( + mozilla::jni::StringParam(aUri)); +} + +nsresult GeckoViewContentInputStream::getInstance(const nsAutoCString& aUri, + nsIInputStream** aInstance) { + RefPtr instance = + new GeckoViewContentInputStream(aUri); + if (instance->isClosed()) { + return NS_ERROR_FILE_NOT_FOUND; + } + *aInstance = instance.forget().take(); + + return NS_OK; +} diff --git a/mobile/android/components/geckoview/GeckoViewInputStream.h b/mobile/android/components/geckoview/GeckoViewInputStream.h new file mode 100644 index 000000000000..ac48e0db259b --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewInputStream.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GeckoViewInputStream_h__ +#define GeckoViewInputStream_h__ + +#include "mozilla/java/GeckoViewInputStreamWrappers.h" +#include "mozilla/java/ContentInputStreamWrappers.h" +#include "nsIInputStream.h" + +class GeckoViewInputStream : public nsIInputStream { + NS_DECL_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + + explicit GeckoViewInputStream( + mozilla::java::GeckoViewInputStream::LocalRef aInstance) + : mInstance(aInstance){}; + bool isClosed() const; + + protected: + virtual ~GeckoViewInputStream() = default; + + private: + mozilla::java::GeckoViewInputStream::LocalRef mInstance; + bool mClosed{false}; +}; + +class GeckoViewContentInputStream final : public GeckoViewInputStream { + public: + static nsresult getInstance(const nsAutoCString& aUri, + nsIInputStream** aInstance); + static bool isReadable(const nsAutoCString& aUri); + + private: + explicit GeckoViewContentInputStream(const nsAutoCString& aUri) + : GeckoViewInputStream(mozilla::java::ContentInputStream::GetInstance( + mozilla::jni::StringParam(aUri))) {} +}; + +#endif // !GeckoViewInputStream_h__ diff --git a/mobile/android/components/geckoview/components.conf b/mobile/android/components/geckoview/components.conf index ebae2456a0a4..ea9b9eba090d 100644 --- a/mobile/android/components/geckoview/components.conf +++ b/mobile/android/components/geckoview/components.conf @@ -77,6 +77,20 @@ Classes = [ 'headers': ['GeckoViewExternalAppService.h'], 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS, }, + { + 'cid': '{a8f4582e-4b47-4e06-970d-b94b76977bf7}', + 'contract_ids': ['@mozilla.org/network/protocol;1?name=content'], + 'type': 'GeckoViewContentProtocolHandler', + 'headers': ['./GeckoViewContentProtocolHandler.h'], + 'protocol_config': { + 'scheme': 'content', + 'flags': [ + 'URI_IS_POTENTIALLY_TRUSTWORTHY', + 'URI_IS_LOCAL_RESOURCE', + 'URI_DANGEROUS_TO_LOAD', + ], + }, + }, ] if defined('MOZ_ANDROID_HISTORY'): diff --git a/mobile/android/components/geckoview/moz.build b/mobile/android/components/geckoview/moz.build index 93d5210f3b98..7b115ed03b52 100644 --- a/mobile/android/components/geckoview/moz.build +++ b/mobile/android/components/geckoview/moz.build @@ -5,13 +5,19 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. SOURCES += [ + "GeckoViewContentChannel.cpp", + "GeckoViewContentProtocolHandler.cpp", "GeckoViewExternalAppService.cpp", + "GeckoViewInputStream.cpp", "GeckoViewOutputStream.cpp", "GeckoViewStreamListener.cpp", ] EXPORTS += [ + "GeckoViewContentChannel.h", + "GeckoViewContentProtocolHandler.h", "GeckoViewExternalAppService.h", + "GeckoViewInputStream.h", "GeckoViewOutputStream.h", "GeckoViewStreamListener.h", ] diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PdfCreationTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PdfCreationTest.kt index efd6d43e285f..627c076fc486 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PdfCreationTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/PdfCreationTest.kt @@ -15,6 +15,7 @@ import androidx.test.filters.LargeTest import org.hamcrest.Matchers.equalTo import org.junit.After import org.junit.Assert.assertTrue +import org.junit.Assume.assumeThat import org.junit.Before import org.junit.Ignore import org.junit.Rule @@ -158,4 +159,22 @@ class PdfCreationTest : BaseSessionTest() { } } } + + @NullDelegate(Autofill.Delegate::class) + @Test + fun saveAContentPdfDocument() { + // Bug 1864622. + assumeThat(sessionRule.env.isIsolatedProcess, equalTo(false)) + activityRule.scenario.onActivity { + val originalBytes = getTestBytes(HELLO_PDF_WORLD_PDF_PATH) + TestContentProvider.setTestData(originalBytes, "application/pdf") + mainSession.loadUri("content://org.mozilla.geckoview.test.provider/pdf") + mainSession.waitForPageStop() + + val response = mainSession.pdfFileSaver.save() + sessionRule.waitForResult(response).let { + assertThat("The PDF File must the same as the original one.", it.body?.readBytes(), equalTo(originalBytes)) + } + } + } } diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentInputStream.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentInputStream.java new file mode 100644 index 000000000000..d126f0b1af23 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/ContentInputStream.java @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.os.Process; +import android.util.Log; +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import java.io.IOException; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.annotation.WrapForJNI; + +/** + * This class provides an {@link OutputStream} wrapper for a Gecko nsIOutputStream (or really, + * nsIRequest). + */ +/* package */ class ContentInputStream extends GeckoViewInputStream { + private static final String LOGTAG = "ContentInputStream"; + + private static final byte[][] HEADERS = {{'%', 'P', 'D', 'F', '-'}}; + + private AssetFileDescriptor mFd; + + ContentInputStream(final @NonNull String aUri) { + final Uri uri = Uri.parse(aUri); + final Context context = GeckoAppShell.getApplicationContext(); + final ContentResolver cr = context.getContentResolver(); + + try { + mFd = cr.openAssetFileDescriptor(uri, "r"); + setInputStream(mFd.createInputStream()); + + if (!checkHeaders(HEADERS)) { + Log.e(LOGTAG, "Cannot open the uri: " + aUri + " (invalid header)"); + close(); + } + } catch (final IOException | SecurityException e) { + Log.e(LOGTAG, "Cannot open the uri: " + aUri, e); + close(); + } + } + + @Override + public void close() { + if (mFd != null) { + try { + mFd.close(); + } catch (final IOException e) { + Log.e(LOGTAG, "Cannot close the file descriptor", e); + } finally { + mFd = null; + } + } + super.close(); + } + + private static boolean wasGrantedPermission( + final @NonNull Context aCtx, final @NonNull Uri aUri) { + // For reference: + // https://developer.android.com/topic/security/risks/content-resolver#mitigations_2 + final int pid = Process.myPid(); + final int uid = Process.myUid(); + return aCtx.checkUriPermission(aUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED; + } + + private static boolean isExported(final @NonNull Context aCtx, final @NonNull Uri aUri) { + // For reference: + // https://developer.android.com/topic/security/risks/content-resolver#mitigations_2 + final String authority = aUri.getAuthority(); + final PackageManager packageManager = aCtx.getPackageManager(); + if (authority == null || packageManager == null) { + return false; + } + final ProviderInfo info = packageManager.resolveContentProvider(authority, 0); + if (info == null) { + return false; + } + + // We check that the provider is exported: + // https://developer.android.com/reference/android/content/pm/ComponentInfo?hl=en#exported + return info.exported; + } + + private static boolean belongsToCurrentApplication( + final @NonNull Context aCtx, final @NonNull Uri aUri) { + // For reference: + // https://developer.android.com/topic/security/risks/content-resolver#mitigations_2 + final String authority = aUri.getAuthority(); + final PackageManager packageManager = aCtx.getPackageManager(); + if (authority == null || packageManager == null) { + return false; + } + final ProviderInfo info = packageManager.resolveContentProvider(authority, 0); + if (info == null) { + return false; + } + + // We check that the provider is GV itself (when testing GV, the provider is GV itself). + final String packageName = aCtx.getPackageName(); + return packageName != null && packageName.equals(info.packageName); + } + + @WrapForJNI + @AnyThread + private static boolean isReadable(final @NonNull String aUri) { + final Uri uri = Uri.parse(aUri); + final Context context = GeckoAppShell.getApplicationContext(); + + try { + if ((isExported(context, uri) && wasGrantedPermission(context, uri)) + || belongsToCurrentApplication(context, uri)) { + final ContentResolver cr = context.getContentResolver(); + cr.openAssetFileDescriptor(uri, "r").close(); + return true; + } + } catch (final IOException | SecurityException e) { + // A SecurityException could happen if the uri is no more valid or if + // we're in an isolated process. + Log.e(LOGTAG, "Cannot read the uri: " + uri, e); + } + return false; + } + + @WrapForJNI + @AnyThread + private static @NonNull GeckoViewInputStream getInstance(final @NonNull String aUri) { + return new ContentInputStream(aUri); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoViewInputStream.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoViewInputStream.java new file mode 100644 index 000000000000..97b14f628d9c --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoViewInputStream.java @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.geckoview; + +import android.util.Log; +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import org.mozilla.gecko.annotation.WrapForJNI; + +/** This class provides a Gecko nsIInputStream wrapper for a Java {@link InputStream}. */ +@WrapForJNI +@AnyThread +/* package */ class GeckoViewInputStream { + private static final String LOGTAG = "GeckoViewInputStream"; + private static final int BUFFER_SIZE = 4096; + + protected final ByteBuffer mBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE); + private ReadableByteChannel mChannel; + private InputStream mIs = null; + private boolean mMustGetData = true; + private int mPos = 0; + private int mSize; + + /** + * Set an input stream. + * + * @param is the {@link InputStream} to set. + */ + protected void setInputStream(final @NonNull InputStream is) { + mIs = is; + mChannel = Channels.newChannel(is); + } + + /** + * Check if there is a stream. + * + * @return true if there is no stream. + */ + public boolean isClosed() { + return mIs == null; + } + + /** + * Called by native code to get the number of available bytes in the underlying stream. + * + * @return the number of available bytes. + */ + public int available() { + if (mIs == null || mSize == -1) { + return 0; + } + try { + return Math.max(mIs.available(), mMustGetData ? 0 : mSize - mPos); + } catch (final IOException e) { + Log.e(LOGTAG, "Cannot get the number of available bytes", e); + return 0; + } + } + + /** Close the underlying stream. */ + public void close() { + if (mIs == null) { + return; + } + try { + mChannel.close(); + } catch (final IOException e) { + Log.e(LOGTAG, "Cannot close the channel", e); + } finally { + mChannel = null; + } + + try { + mIs.close(); + } catch (final IOException e) { + Log.e(LOGTAG, "Cannot close the stream", e); + } finally { + mIs = null; + } + } + + /** + * Called by native code to notify that the data have been consumed. + * + * @param length the number of consumed bytes. + * @return the position in the buffer. + */ + public long consumedData(final int length) { + mPos += length; + if (mPos >= mSize) { + mPos = 0; + mMustGetData = true; + } + return mPos; + } + + /** + * Check that the underlying stream starts with one of the given headers. + * + * @param headers the headers to check. + * @return true if one of the headers is found. + */ + protected boolean checkHeaders(final @NonNull byte[][] headers) throws IOException { + read(0); + for (final byte[] header : headers) { + final int headerLength = header.length; + if (mSize < headerLength) { + continue; + } + int i = 0; + for (; i < headerLength; i++) { + if (mBuffer.get(i) != header[i]) { + break; + } + } + if (i == headerLength) { + return true; + } + } + return false; + } + + /** + * Called by native code to read some bytes in the underlying stream. + * + * @param aCount the number of bytes to read. + * @return the number of read bytes, -1 in case of EOF. + * @throws IOException if an error occurs. + */ + @WrapForJNI(exceptionMode = "nsresult") + public int read(final long aCount) throws IOException { + if (mIs == null) { + Log.e(LOGTAG, "The stream is closed."); + throw new IllegalStateException("Stream is closed"); + } + + if (!mMustGetData) { + return (int) Math.min((long) (mSize - mPos), aCount); + } + + mMustGetData = false; + mBuffer.clear(); + + try { + mSize = mChannel.read(mBuffer); + } catch (final IOException e) { + Log.e(LOGTAG, "Cannot read some bytes", e); + throw e; + } + if (mSize == -1) { + return -1; + } + + return (int) Math.min((long) mSize, aCount); + } +} diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionPdfFileSaver.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionPdfFileSaver.java index 6e7c93ca8b3c..3d92b11e8100 100644 --- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionPdfFileSaver.java +++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionPdfFileSaver.java @@ -79,7 +79,8 @@ public final class SessionPdfFileSaver { public GeckoResult onValue(final WebResponse response) { final int statusCode = response.statusCode != 0 ? response.statusCode : 200; return GeckoResult.fromValue( - new WebResponse.Builder(originalUrl) + new WebResponse.Builder( + originalUrl.startsWith("content://") ? url : originalUrl) .statusCode(statusCode) .body(response.body) .skipConfirmation(skipConfirmation) diff --git a/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs b/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs index 81d25bff1d42..13880be7b49c 100644 --- a/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs +++ b/mobile/android/modules/geckoview/GeckoViewNavigation.sys.mjs @@ -205,7 +205,7 @@ export class GeckoViewNavigation extends GeckoViewModule { // a privileged principal. const isExternal = navFlags & Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; - if (!isExternal) { + if (!isExternal || Services.io.newURI(uri).schemeIs("content")) { // Always use the system principal as the triggering principal // for user-initiated (ie. no referrer session and not external) // loads. See discussion in bug 1573860. diff --git a/widget/android/moz.build b/widget/android/moz.build index ca05de848b19..1f2ae1eba592 100644 --- a/widget/android/moz.build +++ b/widget/android/moz.build @@ -33,6 +33,7 @@ classes_with_WrapForJNI = [ "Clipboard", "CodecProxy", "CompositorSurfaceManager", + "ContentInputStream", "EnterpriseRoots", "EventCallback", "EventDispatcher", @@ -58,6 +59,7 @@ classes_with_WrapForJNI = [ "GeckoSurfaceTexture", "GeckoSystemStateListener", "GeckoThread", + "GeckoViewInputStream", "GeckoVRManager", "GeckoVideoInfo", "GeckoWebExecutor",