Commit c4f6507c authored by Michael Thiessen's avatar Michael Thiessen Committed by Commit Bot

Migrate UrlFormatter#fixupUrl to GURL

Also adds unit tests for the UrlFormatter java wrapper which didn't
previously exist.

Bug: 783819
Change-Id: Ib98bd5daa64a48bb06d6593185f53f49ad22b3c7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2036730
Commit-Queue: Michael Thiessen <mthiesse@chromium.org>
Reviewed-by: default avatarTommy Li <tommycli@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarChris Palmer <palmer@chromium.org>
Cr-Commit-Position: refs/heads/master@{#738268}
parent f4a37521
...@@ -20,6 +20,7 @@ import org.chromium.chrome.browser.bookmarks.BookmarkBridge.BookmarkModelObserve ...@@ -20,6 +20,7 @@ import org.chromium.chrome.browser.bookmarks.BookmarkBridge.BookmarkModelObserve
import org.chromium.components.bookmarks.BookmarkId; import org.chromium.components.bookmarks.BookmarkId;
import org.chromium.components.browser_ui.widget.TintedDrawable; import org.chromium.components.browser_ui.widget.TintedDrawable;
import org.chromium.components.url_formatter.UrlFormatter; import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.url.GURL;
/** /**
* The activity that enables the user to modify the title, url and parent folder of a bookmark. * The activity that enables the user to modify the title, url and parent folder of a bookmark.
...@@ -136,8 +137,7 @@ public class BookmarkEditActivity extends SynchronousInitializationActivity { ...@@ -136,8 +137,7 @@ public class BookmarkEditActivity extends SynchronousInitializationActivity {
@Override @Override
protected void onStop() { protected void onStop() {
if (mModel.doesBookmarkExist(mBookmarkId)) { if (mModel.doesBookmarkExist(mBookmarkId)) {
final String originalUrl = final GURL originalUrl = new GURL(mModel.getBookmarkById(mBookmarkId).getUrl());
mModel.getBookmarkById(mBookmarkId).getUrl();
final String title = mTitleEditText.getTrimmedText(); final String title = mTitleEditText.getTrimmedText();
final String url = mUrlEditText.getTrimmedText(); final String url = mUrlEditText.getTrimmedText();
...@@ -147,9 +147,9 @@ public class BookmarkEditActivity extends SynchronousInitializationActivity { ...@@ -147,9 +147,9 @@ public class BookmarkEditActivity extends SynchronousInitializationActivity {
if (!mUrlEditText.isEmpty() if (!mUrlEditText.isEmpty()
&& mModel.getBookmarkById(mBookmarkId).isUrlEditable()) { && mModel.getBookmarkById(mBookmarkId).isUrlEditable()) {
String fixedUrl = UrlFormatter.fixupUrl(url); GURL fixedUrl = UrlFormatter.fixupUrl(url);
if (fixedUrl != null && !fixedUrl.equals(originalUrl)) { if (fixedUrl.isValid() && !fixedUrl.equals(originalUrl)) {
mModel.setBookmarkUrl(mBookmarkId, fixedUrl); mModel.setBookmarkUrl(mBookmarkId, fixedUrl.getSpec());
} }
} }
} }
......
...@@ -33,6 +33,7 @@ import org.chromium.content_public.browser.WebContents; ...@@ -33,6 +33,7 @@ import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.Referrer; import org.chromium.content_public.common.Referrer;
import org.chromium.network.mojom.ReferrerPolicy; import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
/** /**
* This class attempts to preload the tab if the url is known from the intent when the profile * This class attempts to preload the tab if the url is known from the intent when the profile
...@@ -168,13 +169,13 @@ public class StartupTabPreloader implements ProfileManager.Observer, Destroyable ...@@ -168,13 +169,13 @@ public class StartupTabPreloader implements ProfileManager.Observer, Destroyable
private void loadTab() { private void loadTab() {
Intent intent = mIntentSupplier.get(); Intent intent = mIntentSupplier.get();
String url = UrlFormatter.fixupUrl(getUrlFromIntent(intent)); GURL url = UrlFormatter.fixupUrl(getUrlFromIntent(intent));
ChromeTabCreator chromeTabCreator = ChromeTabCreator chromeTabCreator =
(ChromeTabCreator) mTabCreatorManager.getTabCreator(false); (ChromeTabCreator) mTabCreatorManager.getTabCreator(false);
WebContents webContents = WebContentsFactory.createWebContents(false, false); WebContents webContents = WebContentsFactory.createWebContents(false, false);
mLoadUrlParams = new LoadUrlParams(url); mLoadUrlParams = new LoadUrlParams(url.getValidSpecOrEmpty());
String referrer = IntentHandler.getReferrerUrlIncludingExtraHeaders(intent); String referrer = IntentHandler.getReferrerUrlIncludingExtraHeaders(intent);
if (referrer != null && !referrer.isEmpty()) { if (referrer != null && !referrer.isEmpty()) {
mLoadUrlParams.setReferrer(new Referrer(referrer, ReferrerPolicy.DEFAULT)); mLoadUrlParams.setReferrer(new Referrer(referrer, ReferrerPolicy.DEFAULT));
......
...@@ -48,6 +48,7 @@ import org.chromium.content_public.common.ContentUrlConstants; ...@@ -48,6 +48,7 @@ import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.base.ActivityKeyboardVisibilityDelegate; import org.chromium.ui.base.ActivityKeyboardVisibilityDelegate;
import org.chromium.ui.base.ActivityWindowAndroid; import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.modaldialog.ModalDialogManager; import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.url.GURL;
/** Queries the user's default search engine and shows autocomplete suggestions. */ /** Queries the user's default search engine and shows autocomplete suggestions. */
public class SearchActivity extends AsyncInitializationActivity public class SearchActivity extends AsyncInitializationActivity
...@@ -324,8 +325,8 @@ public class SearchActivity extends AsyncInitializationActivity ...@@ -324,8 +325,8 @@ public class SearchActivity extends AsyncInitializationActivity
if (TextUtils.isEmpty(url)) return; if (TextUtils.isEmpty(url)) return;
// Fix up the URL and send it to the full browser. // Fix up the URL and send it to the full browser.
String fixedUrl = UrlFormatter.fixupUrl(url); GURL fixedUrl = UrlFormatter.fixupUrl(url);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(fixedUrl)); Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(fixedUrl.getValidSpecOrEmpty()));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
intent.setClass(this, ChromeLauncherActivity.class); intent.setClass(this, ChromeLauncherActivity.class);
IntentHandler.addTrustedIntentExtras(intent); IntentHandler.addTrustedIntentExtras(intent);
......
...@@ -18,6 +18,7 @@ import org.chromium.chrome.R; ...@@ -18,6 +18,7 @@ import org.chromium.chrome.R;
import org.chromium.chrome.browser.partnercustomizations.HomepageManager; import org.chromium.chrome.browser.partnercustomizations.HomepageManager;
import org.chromium.chrome.browser.settings.SettingsUtils; import org.chromium.chrome.browser.settings.SettingsUtils;
import org.chromium.components.url_formatter.UrlFormatter; import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.url.GURL;
/** /**
* Provides the Java-UI for editing the homepage preference. * Provides the Java-UI for editing the homepage preference.
...@@ -77,8 +78,8 @@ public class HomepageEditor extends Fragment implements TextWatcher { ...@@ -77,8 +78,8 @@ public class HomepageEditor extends Fragment implements TextWatcher {
mSaveButton.setOnClickListener(new View.OnClickListener() { mSaveButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
mHomepageManager.setPrefHomepageCustomUri( GURL url = UrlFormatter.fixupUrl(mHomepageUrlEdit.getText().toString());
UrlFormatter.fixupUrl(mHomepageUrlEdit.getText().toString())); mHomepageManager.setPrefHomepageCustomUri(url.getValidSpecOrEmpty());
mHomepageManager.setPrefHomepageUseDefaultUri(false); mHomepageManager.setPrefHomepageUseDefaultUri(false);
getActivity().finish(); getActivity().finish();
} }
......
...@@ -34,6 +34,7 @@ import org.chromium.content_public.browser.WebContents; ...@@ -34,6 +34,7 @@ import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.Referrer; import org.chromium.content_public.common.Referrer;
import org.chromium.ui.base.PageTransition; import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.WindowAndroid; import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
/** /**
* This class creates various kinds of new tabs and adds them to the right {@link TabModel}. * This class creates various kinds of new tabs and adds them to the right {@link TabModel}.
...@@ -109,8 +110,10 @@ public class ChromeTabCreator extends TabCreatorManager.TabCreator { ...@@ -109,8 +110,10 @@ public class ChromeTabCreator extends TabCreatorManager.TabCreator {
TraceEvent.begin("ChromeTabCreator.createNewTab"); TraceEvent.begin("ChromeTabCreator.createNewTab");
int parentId = parent != null ? parent.getId() : Tab.INVALID_TAB_ID; int parentId = parent != null ? parent.getId() : Tab.INVALID_TAB_ID;
GURL url = UrlFormatter.fixupUrl(loadUrlParams.getUrl());
// Sanitize the url. // Sanitize the url.
loadUrlParams.setUrl(UrlFormatter.fixupUrl(loadUrlParams.getUrl())); loadUrlParams.setUrl(url.getValidSpecOrEmpty());
loadUrlParams.setTransitionType(getTransitionType(type, intent)); loadUrlParams.setTransitionType(getTransitionType(type, intent));
// Check if the tab is being created asynchronously. // Check if the tab is being created asynchronously.
......
...@@ -325,6 +325,7 @@ test("components_unittests") { ...@@ -325,6 +325,7 @@ test("components_unittests") {
"//components/signin/core/browser/android:java", "//components/signin/core/browser/android:java",
"//components/signin/public/android:java", "//components/signin/public/android:java",
"//components/spellcheck/browser/android:java", "//components/spellcheck/browser/android:java",
"//components/url_formatter/android:native_java_unittests_java",
"//components/variations/android:variations_java", "//components/variations/android:variations_java",
"//content/public/android:content_java", "//content/public/android:content_java",
"//content/public/browser", "//content/public/browser",
......
...@@ -54,7 +54,10 @@ jumbo_static_library("url_formatter") { ...@@ -54,7 +54,10 @@ jumbo_static_library("url_formatter") {
public_deps = [ "//third_party/icu" ] public_deps = [ "//third_party/icu" ]
if (is_android) { if (is_android) {
deps += [ "android:jni_headers" ] deps += [
"android:jni_headers",
"//url:gurl_android",
]
} }
} }
...@@ -84,4 +87,12 @@ jumbo_source_set("unit_tests") { ...@@ -84,4 +87,12 @@ jumbo_source_set("unit_tests") {
"//ui/gfx", "//ui/gfx",
"//url", "//url",
] ]
if (is_android) {
sources += [ "url_formatter_android_unittest.cc" ]
deps += [
"android:native_j_unittests_jni_headers",
"android:native_java_unittests_java",
]
}
} }
...@@ -8,6 +8,7 @@ android_library("url_formatter_java") { ...@@ -8,6 +8,7 @@ android_library("url_formatter_java") {
deps = [ deps = [
"//base:base_java", "//base:base_java",
"//base:jni_java", "//base:jni_java",
"//url:gurl_java",
] ]
annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ] annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
...@@ -21,3 +22,23 @@ generate_jni("jni_headers") { ...@@ -21,3 +22,23 @@ generate_jni("jni_headers") {
sources = sources =
[ "java/src/org/chromium/components/url_formatter/UrlFormatter.java" ] [ "java/src/org/chromium/components/url_formatter/UrlFormatter.java" ]
} }
android_library("native_java_unittests_java") {
testonly = true
deps = [
":url_formatter_java",
"//base:base_java",
"//third_party/junit",
"//url:gurl_java",
]
sources = [ "native_java_unittests/src/org/chromium/components/url_formatter/UrlFormatterUnitTest.java" ]
annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
}
# See https://bugs.chromium.org/p/chromium/issues/detail?id=908819 for why we
# can't put 'java' in the name here.
generate_jni("native_j_unittests_jni_headers") {
testonly = true
sources = [ "native_java_unittests/src/org/chromium/components/url_formatter/UrlFormatterUnitTest.java" ]
}
...@@ -9,24 +9,29 @@ import android.text.TextUtils; ...@@ -9,24 +9,29 @@ import android.text.TextUtils;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.annotations.NativeMethods; import org.chromium.base.annotations.NativeMethods;
import org.chromium.url.GURL;
/** /**
* Wrapper for utilities in url_formatter. * Wrapper for utilities in url_formatter.
*/ */
@JNINamespace("url_formatter::android") @JNINamespace("url_formatter::android")
@MainDex
public final class UrlFormatter { public final class UrlFormatter {
/** /**
* Refer to url_formatter::FixupURL. * Refer to url_formatter::FixupURL.
* *
* Given a URL-like string, returns a real URL or null. For example: * Given a URL-like string, returns a possibly-invalid GURL. For example:
* - "google.com" -> "http://google.com/" * - "google.com" -> "http://google.com/"
* - "about:" -> "chrome://version/" * - "about:" -> "chrome://version/"
* - "//mail.google.com:/" -> "file:///mail.google.com:/" * - "//mail.google.com:/" -> "file:///mail.google.com:/"
* - "..." -> null * - "0x100.0" -> "http://0x100.0/" (invalid)
*/ */
public static String fixupUrl(String uri) { public static GURL fixupUrl(String uri) {
return TextUtils.isEmpty(uri) ? null : UrlFormatterJni.get().fixupUrl(uri); if (TextUtils.isEmpty(uri)) return GURL.emptyGURL();
GURL.ensureNativeInitializedForGURL();
return UrlFormatterJni.get().fixupUrl(uri);
} }
/** /**
...@@ -125,7 +130,7 @@ public final class UrlFormatter { ...@@ -125,7 +130,7 @@ public final class UrlFormatter {
@VisibleForTesting @VisibleForTesting
@NativeMethods @NativeMethods
public interface Natives { public interface Natives {
String fixupUrl(String url); GURL fixupUrl(String url);
String formatUrlForDisplayOmitScheme(String url); String formatUrlForDisplayOmitScheme(String url);
String formatUrlForDisplayOmitHTTPScheme(String url); String formatUrlForDisplayOmitHTTPScheme(String url);
String formatUrlForDisplayOmitSchemeOmitTrivialSubdomains(String url); String formatUrlForDisplayOmitSchemeOmitTrivialSubdomains(String url);
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.url_formatter;
import org.junit.Assert;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.CalledByNativeJavaTest;
/**
* Unit tests for {@link UrlFormatter}.
*
* These tests are basic sanity checks to ensure the plumbing is working correctly. The wrapped
* functions are tested much more thoroughly elsewhere.
*/
public class UrlFormatterUnitTest {
@CalledByNative
private UrlFormatterUnitTest() {}
@CalledByNativeJavaTest
public void testFixupUrl() {
Assert.assertEquals("http://google.com/", UrlFormatter.fixupUrl("google.com").getSpec());
Assert.assertEquals("chrome://version/", UrlFormatter.fixupUrl("about:").getSpec());
Assert.assertEquals("file:///mail.google.com:/",
UrlFormatter.fixupUrl("//mail.google.com:/").getSpec());
Assert.assertFalse(UrlFormatter.fixupUrl("0x100.0").isValid());
}
}
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
#include "components/url_formatter/elide_url.h" #include "components/url_formatter/elide_url.h"
#include "components/url_formatter/url_fixer.h" #include "components/url_formatter/url_fixer.h"
#include "components/url_formatter/url_formatter.h" #include "components/url_formatter/url_formatter.h"
#include "url/android/gurl_android.h"
#include "url/gurl.h" #include "url/gurl.h"
using base::android::JavaParamRef; using base::android::JavaParamRef;
...@@ -27,16 +28,14 @@ namespace url_formatter { ...@@ -27,16 +28,14 @@ namespace url_formatter {
namespace android { namespace android {
static ScopedJavaLocalRef<jstring> JNI_UrlFormatter_FixupUrl( static ScopedJavaLocalRef<jobject> JNI_UrlFormatter_FixupUrl(
JNIEnv* env, JNIEnv* env,
const JavaParamRef<jstring>& url) { const JavaParamRef<jstring>& url) {
DCHECK(url); DCHECK(url);
GURL fixed_url = url_formatter::FixupURL( GURL fixed_url = url_formatter::FixupURL(
base::android::ConvertJavaStringToUTF8(env, url), std::string()); base::android::ConvertJavaStringToUTF8(env, url), std::string());
return fixed_url.is_valid() return url::GURLAndroid::FromNativeGURL(env, fixed_url);
? base::android::ConvertUTF8ToJavaString(env, fixed_url.spec())
: ScopedJavaLocalRef<jstring>();
} }
static ScopedJavaLocalRef<jstring> static ScopedJavaLocalRef<jstring>
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/android/jni_android.h"
#include "components/url_formatter/android/native_j_unittests_jni_headers/UrlFormatterUnitTest_jni.h"
#include "testing/gtest/include/gtest/gtest.h"
using base::android::AttachCurrentThread;
class UrlFormatterUnitTest : public ::testing::Test {
public:
UrlFormatterUnitTest()
: j_test_(Java_UrlFormatterUnitTest_Constructor(AttachCurrentThread())) {}
const base::android::ScopedJavaGlobalRef<jobject>& j_test() {
return j_test_;
}
private:
base::android::ScopedJavaGlobalRef<jobject> j_test_;
};
JAVA_TESTS(UrlFormatterUnitTest, j_test())
...@@ -187,6 +187,7 @@ to the classpath for downstream development. See "additional_entries" below. ...@@ -187,6 +187,7 @@ to the classpath for downstream development. See "additional_entries" below.
<classpathentry kind="src" path="components/sync/test/android/javatests/src"/> <classpathentry kind="src" path="components/sync/test/android/javatests/src"/>
<classpathentry kind="src" path="components/test/android/browsertests_apk/src"/> <classpathentry kind="src" path="components/test/android/browsertests_apk/src"/>
<classpathentry kind="src" path="components/url_formatter/android/java/src"/> <classpathentry kind="src" path="components/url_formatter/android/java/src"/>
<classpathentry kind="src" path="components/url_formatter/android/native_java_unittests/src"/>
<classpathentry kind="src" path="components/variations/android/java/src"/> <classpathentry kind="src" path="components/variations/android/java/src"/>
<classpathentry kind="src" path="components/variations/android/junit/src"/> <classpathentry kind="src" path="components/variations/android/junit/src"/>
<classpathentry kind="src" path="components/version_info/android/java/src"/> <classpathentry kind="src" path="components/version_info/android/java/src"/>
......
...@@ -67,6 +67,14 @@ std::unique_ptr<GURL> GURLAndroid::ToNativeGURL( ...@@ -67,6 +67,14 @@ std::unique_ptr<GURL> GURLAndroid::ToNativeGURL(
reinterpret_cast<GURL*>(Java_GURL_toNativeGURL(env, j_gurl))); reinterpret_cast<GURL*>(Java_GURL_toNativeGURL(env, j_gurl)));
} }
// static
ScopedJavaLocalRef<jobject> GURLAndroid::FromNativeGURL(JNIEnv* env,
const GURL& gurl) {
ScopedJavaLocalRef<jobject> j_gurl = Java_GURL_Constructor(env);
InitFromGURL(env, gurl, j_gurl);
return j_gurl;
}
static void JNI_GURL_GetOrigin(JNIEnv* env, static void JNI_GURL_GetOrigin(JNIEnv* env,
const JavaParamRef<jstring>& j_spec, const JavaParamRef<jstring>& j_spec,
jboolean is_valid, jboolean is_valid,
......
...@@ -17,6 +17,9 @@ class GURLAndroid { ...@@ -17,6 +17,9 @@ class GURLAndroid {
static std::unique_ptr<GURL> ToNativeGURL( static std::unique_ptr<GURL> ToNativeGURL(
JNIEnv* env, JNIEnv* env,
const base::android::JavaRef<jobject>& j_gurl); const base::android::JavaRef<jobject>& j_gurl);
static base::android::ScopedJavaLocalRef<jobject> FromNativeGURL(
JNIEnv* env,
const GURL& gurl);
}; };
} // namespace url } // namespace url
......
...@@ -34,21 +34,39 @@ public class GURL { ...@@ -34,21 +34,39 @@ public class GURL {
private boolean mIsValid; private boolean mIsValid;
private Parsed mParsed; private Parsed mParsed;
private static class Holder { private static GURL sEmptyGURL = new GURL(""); }
public static GURL emptyGURL() {
return Holder.sEmptyGURL;
}
/** /**
* Create a new GURL. * Create a new GURL.
* *
* @param uri The string URI representation to parse into a GURL. * @param uri The string URI representation to parse into a GURL.
*/ */
public GURL(String uri) { public GURL(String uri) {
ensureNativeInitializedForGURL();
GURLJni.get().init(uri, this);
}
@CalledByNative
protected GURL() {}
/**
* Ensures that the native library is sufficiently loaded for GURL usage.
*
* This function is public so that GURL-related usage like the UrlFormatter also counts towards
* the "Startup.Android.GURLEnsureMainDexInitialized" histogram.
*/
public static void ensureNativeInitializedForGURL() {
if (LibraryLoader.getInstance().isInitialized()) return;
long time = SystemClock.elapsedRealtime(); long time = SystemClock.elapsedRealtime();
LibraryLoader.getInstance().ensureMainDexInitialized(); LibraryLoader.getInstance().ensureMainDexInitialized();
RecordHistogram.recordTimesHistogram("Startup.Android.GURLEnsureMainDexInitialized", RecordHistogram.recordTimesHistogram("Startup.Android.GURLEnsureMainDexInitialized",
SystemClock.elapsedRealtime() - time); SystemClock.elapsedRealtime() - time);
GURLJni.get().init(uri, this);
} }
protected GURL() {}
@CalledByNative @CalledByNative
private void init(String spec, boolean isValid, Parsed parsed) { private void init(String spec, boolean isValid, Parsed parsed) {
mSpec = spec; mSpec = spec;
...@@ -79,6 +97,14 @@ public class GURL { ...@@ -79,6 +97,14 @@ public class GURL {
return ""; return "";
} }
/**
* @return Either a valid Spec (see {@link #getSpec}), or an empty string.
*/
public String getValidSpecOrEmpty() {
if (isValid()) return mSpec;
return "";
}
/** /**
* See native GURL::possibly_invalid_spec(). * See native GURL::possibly_invalid_spec().
*/ */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment