package org.wordpress.android.ui; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.webkit.WebViewClient; import android.widget.ProgressBar; import android.widget.Toast; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.fluxc.model.PostModel; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.ui.reader.ReaderActivityLauncher; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.URLFilteredWebViewClient; import org.wordpress.android.util.UrlUtils; import org.wordpress.android.util.WPUrlUtils; import org.wordpress.android.util.WPWebViewClient; import org.wordpress.android.util.helpers.WPWebChromeClient; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import javax.inject.Inject; /** * Activity for opening external WordPress links in a webview. * <p/> * Try to use one of the methods below to open the webview: * - openURL * - openUrlByUsingMainWPCOMCredentials * - openUrlByUsingWPCOMCredentials * - openUrlByUsingBlogCredentials (for self hosted sites) * <p/> * If you need to start the activity with delay, start activity with result, or none of the methods above are enough * for your needs, * you can start the activity by passing the required parameters, depending on what you need to do. * <p/> * 1. Load a simple URL (without any kind of authentication) * - Start the activity with the parameter URL_TO_LOAD set to the URL to load. * <p/> * 2. Load a WordPress.com URL * Start the activity with the following parameters: * - URL_TO_LOAD: target URL to load in the webview. * - AUTHENTICATION_URL: The address of the WordPress.com authentication endpoint. Please use WPCOM_LOGIN_URL. * - AUTHENTICATION_USER: username. * - AUTHENTICATION_PASSWD: password. * <p/> * 3. Load a WordPress.org URL with authentication * - URL_TO_LOAD: target URL to load in the webview. * - AUTHENTICATION_URL: The address of the authentication endpoint. Please use the value of getSiteLoginUrl() * to retrieve the correct address of the authentication endpoint. * - AUTHENTICATION_USER: username. * - AUTHENTICATION_PASSWD: password. * - LOCAL_BLOG_ID: local id of the blog in the app database. This is required since some blogs could have HTTP Auth, * or self-signed certs in place. * - REFERRER_URL: url to add as an HTTP referrer header, currently only used for non-authed reader posts */ public class WPWebViewActivity extends WebViewActivity { public static final String AUTHENTICATION_URL = "authenticated_url"; public static final String AUTHENTICATION_USER = "authenticated_user"; public static final String AUTHENTICATION_PASSWD = "authenticated_passwd"; public static final String USE_GLOBAL_WPCOM_USER = "USE_GLOBAL_WPCOM_USER"; public static final String URL_TO_LOAD = "url_to_load"; public static final String WPCOM_LOGIN_URL = "https://wordpress.com/wp-login.php"; public static final String LOCAL_BLOG_ID = "local_blog_id"; public static final String SHAREABLE_URL = "shareable_url"; public static final String SHARE_SUBJECT = "share_subject"; public static final String REFERRER_URL = "referrer_url"; public static final String DISABLE_LINKS_ON_PAGE = "DISABLE_LINKS_ON_PAGE"; public static final String ALLOWED_URLS = "allowed_urls"; private static final String ENCODING_UTF8 = "UTF-8"; @Inject AccountStore mAccountStore; @Inject SiteStore mSiteStore; @Override public void onCreate(Bundle savedInstanceState) { ((WordPress) getApplication()).component().inject(this); super.onCreate(savedInstanceState); } public static void openUrlByUsingGlobalWPCOMCredentials(Context context, String url) { openWPCOMURL(context, url, null, null); } public static void openPostUrlByUsingGlobalWPCOMCredentials(Context context, String url, String shareableUrl, String shareSubject) { openWPCOMURL(context, url, shareableUrl, shareSubject); } // frameNonce is used to show drafts, without it "no page found" error would be thrown public static void openJetpackBlogPostPreview(Context context, String url, String shareableUrl, String shareSubject, String frameNonce) { if (!TextUtils.isEmpty(frameNonce)) { url += "&frame-nonce=" + frameNonce; } Intent intent = new Intent(context, WPWebViewActivity.class); intent.putExtra(WPWebViewActivity.URL_TO_LOAD, url); intent.putExtra(WPWebViewActivity.DISABLE_LINKS_ON_PAGE, false); if (!TextUtils.isEmpty(shareableUrl)) { intent.putExtra(WPWebViewActivity.SHAREABLE_URL, shareableUrl); } if (!TextUtils.isEmpty(shareSubject)) { intent.putExtra(WPWebViewActivity.SHARE_SUBJECT, shareSubject); } context.startActivity(intent); } // Note: The webview has links disabled (excepted for urls in the whitelist: listOfAllowedURLs) public static void openUrlByUsingBlogCredentials(Context context, SiteModel site, PostModel post, String url, String[] listOfAllowedURLs) { if (context == null) { AppLog.e(AppLog.T.UTILS, "Context is null"); return; } if (site == null) { AppLog.e(AppLog.T.UTILS, "Site is null"); return; } if (TextUtils.isEmpty(url)) { AppLog.e(AppLog.T.UTILS, "Empty or null URL"); Toast.makeText(context, context.getResources().getText(R.string.invalid_site_url_message), Toast.LENGTH_SHORT).show(); return; } String authURL = WPWebViewActivity.getSiteLoginUrl(site); Intent intent = new Intent(context, WPWebViewActivity.class); intent.putExtra(WPWebViewActivity.AUTHENTICATION_USER, site.getUsername()); intent.putExtra(WPWebViewActivity.AUTHENTICATION_PASSWD, site.getPassword()); intent.putExtra(WPWebViewActivity.URL_TO_LOAD, url); intent.putExtra(WPWebViewActivity.AUTHENTICATION_URL, authURL); intent.putExtra(WPWebViewActivity.LOCAL_BLOG_ID, site.getId()); intent.putExtra(WPWebViewActivity.DISABLE_LINKS_ON_PAGE, true); intent.putExtra(ALLOWED_URLS, listOfAllowedURLs); if (post != null) { intent.putExtra(WPWebViewActivity.SHAREABLE_URL, post.getLink()); if (!TextUtils.isEmpty(post.getTitle())) { intent.putExtra(WPWebViewActivity.SHARE_SUBJECT, post.getTitle()); } } context.startActivity(intent); } public static void openURL(Context context, String url) { openURL(context, url, null); } public static void openURL(Context context, String url, String referrer) { if (context == null) { AppLog.e(AppLog.T.UTILS, "Context is null"); return; } if (TextUtils.isEmpty(url)) { AppLog.e(AppLog.T.UTILS, "Empty or null URL"); Toast.makeText(context, context.getResources().getText(R.string.invalid_site_url_message), Toast.LENGTH_SHORT).show(); return; } Intent intent = new Intent(context, WPWebViewActivity.class); intent.putExtra(WPWebViewActivity.URL_TO_LOAD, url); if (!TextUtils.isEmpty(referrer)) { intent.putExtra(REFERRER_URL, referrer); } context.startActivity(intent); } private static boolean checkContextAndUrl(Context context, String url) { if (context == null) { AppLog.e(AppLog.T.UTILS, "Context is null"); return false; } if (TextUtils.isEmpty(url)) { AppLog.e(AppLog.T.UTILS, "Empty or null URL passed to openUrlByUsingMainWPCOMCredentials"); Toast.makeText(context, context.getResources().getText(R.string.invalid_site_url_message), Toast.LENGTH_SHORT).show(); return false; } return true; } private static void openWPCOMURL(Context context, String url, String shareableUrl, String shareSubject) { if (!checkContextAndUrl(context, url)) { return; } Intent intent = new Intent(context, WPWebViewActivity.class); intent.putExtra(WPWebViewActivity.USE_GLOBAL_WPCOM_USER, true); intent.putExtra(WPWebViewActivity.URL_TO_LOAD, url); intent.putExtra(WPWebViewActivity.AUTHENTICATION_URL, WPCOM_LOGIN_URL); if (!TextUtils.isEmpty(shareableUrl)) { intent.putExtra(WPWebViewActivity.SHAREABLE_URL, shareableUrl); } if (!TextUtils.isEmpty(shareSubject)) { intent.putExtra(WPWebViewActivity.SHARE_SUBJECT, shareSubject); } context.startActivity(intent); } @SuppressLint("SetJavaScriptEnabled") @Override protected void configureWebView() { mWebView.getSettings().setJavaScriptEnabled(true); mWebView.getSettings().setDomStorageEnabled(true); WebViewClient webViewClient; Bundle extras = getIntent().getExtras(); // Configure the allowed URLs if available ArrayList<String> allowedURL = null; if (extras != null && extras.getBoolean(DISABLE_LINKS_ON_PAGE, false)) { String addressToLoad = extras.getString(URL_TO_LOAD); String authURL = extras.getString(AUTHENTICATION_URL); allowedURL = new ArrayList<>(); if (!TextUtils.isEmpty(addressToLoad)) { allowedURL.add(addressToLoad); } if (!TextUtils.isEmpty(authURL)) { allowedURL.add(authURL); } if(extras.getStringArray(ALLOWED_URLS) != null) { String[] urls = extras.getStringArray(ALLOWED_URLS); for (String currentURL: urls) { allowedURL.add(currentURL); } } } if (getIntent().hasExtra(LOCAL_BLOG_ID)) { SiteModel site = mSiteStore.getSiteByLocalId(getIntent().getIntExtra(LOCAL_BLOG_ID, -1)); if (site == null) { AppLog.e(AppLog.T.UTILS, "No valid blog passed to WPWebViewActivity"); finish(); } webViewClient = new WPWebViewClient(site, mAccountStore.getAccessToken(), allowedURL); } else { webViewClient = new URLFilteredWebViewClient(allowedURL); } mWebView.setWebViewClient(webViewClient); mWebView.setWebChromeClient(new WPWebChromeClient(this, (ProgressBar) findViewById(R.id.progress_bar))); } @Override protected void loadContent() { Bundle extras = getIntent().getExtras(); if (extras == null) { AppLog.e(AppLog.T.UTILS, "No valid parameters passed to WPWebViewActivity"); finish(); return; } String addressToLoad = extras.getString(URL_TO_LOAD); String username = extras.getString(AUTHENTICATION_USER, ""); String password = extras.getString(AUTHENTICATION_PASSWD, ""); String authURL = extras.getString(AUTHENTICATION_URL); if (extras.getBoolean(USE_GLOBAL_WPCOM_USER, false)) { username = mAccountStore.getAccount().getUserName(); } if (TextUtils.isEmpty(addressToLoad) || !UrlUtils.isValidUrlAndHostNotNull(addressToLoad)) { AppLog.e(AppLog.T.UTILS, "Empty or null or invalid URL passed to WPWebViewActivity"); Toast.makeText(this, getText(R.string.invalid_site_url_message), Toast.LENGTH_SHORT).show(); finish(); } if (TextUtils.isEmpty(authURL) && TextUtils.isEmpty(username) && TextUtils.isEmpty(password)) { // Only the URL to load is passed to this activity. Use the normal un-authenticated // loader, optionally with our referrer header String referrerUrl = extras.getString(REFERRER_URL); if (!TextUtils.isEmpty(referrerUrl)) { Map<String, String> headers = new HashMap<>(); headers.put("Referer", referrerUrl); loadUrl(addressToLoad, headers); } else { loadUrl(addressToLoad); } } else { if (TextUtils.isEmpty(authURL) || !UrlUtils.isValidUrlAndHostNotNull(authURL)) { AppLog.e(AppLog.T.UTILS, "Empty or null or invalid auth URL passed to WPWebViewActivity"); Toast.makeText(this, getText(R.string.invalid_site_url_message), Toast.LENGTH_SHORT).show(); finish(); } if (TextUtils.isEmpty(username)) { AppLog.e(AppLog.T.UTILS, "Username empty/null"); Toast.makeText(this, getText(R.string.incorrect_credentials), Toast.LENGTH_SHORT).show(); finish(); } loadAuthenticatedUrl(authURL, addressToLoad, username, password); } } /** * Login to the WordPress.com and load the specified URL. */ protected void loadAuthenticatedUrl(String authenticationURL, String urlToLoad, String username, String password) { String postData = getAuthenticationPostData(authenticationURL, urlToLoad, username, password, mAccountStore.getAccessToken()); mWebView.postUrl(authenticationURL, postData.getBytes()); } public static String getAuthenticationPostData(String authenticationUrl, String urlToLoad, String username, String password, String token) { if (TextUtils.isEmpty(authenticationUrl)) return ""; try { String postData = String.format("log=%s&pwd=%s&redirect_to=%s", URLEncoder.encode(StringUtils.notNullStr(username), ENCODING_UTF8), URLEncoder.encode(StringUtils.notNullStr(password), ENCODING_UTF8), URLEncoder.encode(StringUtils.notNullStr(urlToLoad), ENCODING_UTF8) ); // Add token authorization when signing in to WP.com if (WPUrlUtils.safeToAddWordPressComAuthToken(authenticationUrl) && authenticationUrl.contains("wordpress.com/wp-login.php") && !TextUtils.isEmpty(token)) { postData += "&authorization=Bearer " + URLEncoder.encode(token, ENCODING_UTF8); } return postData; } catch (UnsupportedEncodingException e) { AppLog.e(AppLog.T.UTILS, e); } return ""; } /** * Get the URL of the WordPress login page. * * @return URL of the login page. */ public static String getSiteLoginUrl(SiteModel site) { String loginURL = site.getLoginUrl(); // Try to guess the login URL if blogOptions is null (blog not added to the app), or WP version is < 3.6 if (loginURL == null) { if (site.getUrl() != null) { return site.getUrl() + "/wp-login.php"; } else { return site.getXmlRpcUrl().replace("xmlrpc.php", "wp-login.php"); } } return loginURL; } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.webview, menu); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { if (mWebView == null) { return false; } int itemID = item.getItemId(); if (itemID == R.id.menu_refresh) { mWebView.reload(); return true; } else if (itemID == R.id.menu_share) { Intent share = new Intent(Intent.ACTION_SEND); share.setType("text/plain"); // Use the preferred shareable URL or the default webview URL Bundle extras = getIntent().getExtras(); String shareableUrl = extras.getString(SHAREABLE_URL, null); if (TextUtils.isEmpty(shareableUrl)) { shareableUrl = mWebView.getUrl(); } share.putExtra(Intent.EXTRA_TEXT, shareableUrl); String shareSubject = extras.getString(SHARE_SUBJECT, null); if (!TextUtils.isEmpty(shareSubject)) { share.putExtra(Intent.EXTRA_SUBJECT, shareSubject); } startActivity(Intent.createChooser(share, getText(R.string.share_link))); return true; } else if (itemID == R.id.menu_browser) { ReaderActivityLauncher.openUrl(this, mWebView.getUrl(), ReaderActivityLauncher.OpenUrlType.EXTERNAL); return true; } return super.onOptionsItemSelected(item); } }