package org.wordpress.android.util;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Base64;
import org.wordpress.android.BuildConfig;
import org.wordpress.android.fluxc.Dispatcher;
import org.wordpress.android.fluxc.generated.AccountActionBuilder;
import org.wordpress.android.fluxc.generated.PostActionBuilder;
import org.wordpress.android.fluxc.generated.SiteActionBuilder;
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.util.AppLog.T;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
public class WPLegacyMigrationUtils {
private static final String DEPRECATED_DATABASE_NAME = "wordpress";
private static final String DEPRECATED_ACCOUNT_TABLE = "tbl_accounts";
private static final String DEPRECATED_ACCESS_TOKEN_COLUMN = "access_token";
private static final String DEPRECATED_ACCESS_TOKEN_PREFERENCE = "wp_pref_wpcom_access_token";
private static final String DEPRECATED_BLOGS_TABLE = "accounts";
private static final String DEPRECATED_POSTS_TABLE = "posts";
private static final String DEPRECATED_DB_PASSWORD_SECRET = BuildConfig.DB_SECRET;
/**
* Moves an existing access token from a previous version of WPAndroid into FluxC's AccountStore.
* The access token has historically existed in preferences and two DB tables.
*/
public static String migrateAccessTokenToAccountStore(Context context, Dispatcher dispatcher) {
String token = getLatestDeprecatedAccessToken(context.getApplicationContext());
// updating from previous app version
if (!TextUtils.isEmpty(token)) {
AccountStore.UpdateTokenPayload payload = new AccountStore.UpdateTokenPayload(token);
dispatcher.dispatch(AccountActionBuilder.newUpdateAccessTokenAction(payload));
}
return token;
}
/**
* Copies existing self-hosted sites from a previous version of WPAndroid into FluxC's SiteStore.
* Any Jetpack sites are ignored - those connected to the logged-in WP.com account will be pulled through the
* REST API after migration. Other Jetpack sites will not be migrated.
* Existing sites are retained in the deprecated accounts table after migration.
*/
public static List<SiteModel> migrateSelfHostedSitesFromDeprecatedDB(Context context, Dispatcher dispatcher) {
List<SiteModel> siteList = getSelfHostedSitesFromDeprecatedDB(context.getApplicationContext());
if (siteList != null) {
AppLog.i(T.DB, "Starting migration of " + siteList.size() + " self-hosted sites to FluxC");
for (SiteModel siteModel : siteList) {
AppLog.i(T.DB, "Migrating self-hosted site with url: " + siteModel.getXmlRpcUrl()
+ " username: " + siteModel.getUsername());
dispatcher.dispatch(SiteActionBuilder.newUpdateSiteAction(siteModel));
}
}
return siteList;
}
/**
* Copies existing drafts and locally changed posts from a previous version of WPAndroid into FluxC's PostStore.
* Existing posts are retained in the deprecated posts table after migration.
*/
public static void migrateDraftsFromDeprecatedDB(Context context, Dispatcher dispatcher, SiteStore siteStore) {
List<PostModel> postList = getDraftsFromDeprecatedDB(context.getApplicationContext(), siteStore);
if (postList != null) {
AppLog.i(T.DB, "Starting migration of " + postList.size() + " drafts to FluxC");
for (PostModel postModel : postList) {
AppLog.i(T.DB, "Migrating draft with title: " + postModel.getTitle()
+ " and local site ID: " + postModel.getLocalSiteId());
dispatcher.dispatch(PostActionBuilder.newUpdatePostAction(postModel));
}
}
}
private static String getDeprecatedPreferencesAccessToken(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String token = prefs.getString(DEPRECATED_ACCESS_TOKEN_PREFERENCE, null);
return token;
}
public static String getLatestDeprecatedAccessToken(Context context) {
String latestToken = getAccessTokenFromTable(context, DEPRECATED_ACCOUNT_TABLE);
if (TextUtils.isEmpty(latestToken)) {
latestToken = getDeprecatedPreferencesAccessToken(context);
}
return latestToken;
}
static String getAccessTokenFromTable(Context context, String tableName) {
String token = null;
try {
SQLiteDatabase db = context.openOrCreateDatabase(DEPRECATED_DATABASE_NAME, 0, null);
Cursor c = db.rawQuery("SELECT " + DEPRECATED_ACCESS_TOKEN_COLUMN
+ " FROM " + tableName + " WHERE local_id=0", null);
if (c.moveToFirst() && c.getColumnIndex(DEPRECATED_ACCESS_TOKEN_COLUMN) != -1) {
token = c.getString(c.getColumnIndex(DEPRECATED_ACCESS_TOKEN_COLUMN));
}
c.close();
db.close();
} catch (SQLException e) {
// DB doesn't exist
}
return token;
}
public static boolean hasSelfHostedSiteToMigrate(Context context) {
try {
SQLiteDatabase db = context.openOrCreateDatabase(DEPRECATED_DATABASE_NAME, 0, null);
String[] fields = new String[]{"username", "password", "url", "homeURL", "blogId", "api_blogid"};
// To exclude the jetpack sites we need to check for empty password
String byString = String.format("dotcomFlag=0 AND NOT(dotcomFlag=0 AND password='%s')",
encryptPassword(""));
Cursor c = db.query(DEPRECATED_BLOGS_TABLE, fields, byString, null, null, null, null);
int numRows = c.getCount();
c.moveToFirst();
for (int i = 0; i < numRows; i++) {
long apiBlogId = StringUtils.stringToLong(c.getString(5));
if (apiBlogId > 0) {
// If the api_blogid field is set, that's probably a Jetpack site that is not connected to the main
// account, so we want to skip it.
c.moveToNext();
continue;
}
c.close();
return true;
}
c.close();
return false;
} catch (SQLException e) {
return false;
}
}
static List<SiteModel> getSelfHostedSitesFromDeprecatedDB(Context context) {
List<SiteModel> siteList = new ArrayList<>();
try {
SQLiteDatabase db = context.openOrCreateDatabase(DEPRECATED_DATABASE_NAME, 0, null);
String[] fields = new String[]{"username", "password", "url", "homeURL", "blogId", "api_blogid", "isAdmin"};
// To exclude the jetpack sites we need to check for empty password
String byString = String.format("dotcomFlag=0 AND NOT(dotcomFlag=0 AND password='%s')",
encryptPassword(""));
Cursor c = db.query(DEPRECATED_BLOGS_TABLE, fields, byString, null, null, null, null);
int numRows = c.getCount();
c.moveToFirst();
for (int i = 0; i < numRows; i++) {
long apiBlogId = StringUtils.stringToLong(c.getString(5));
if (apiBlogId > 0) {
// If the api_blogid field is set, that's probably a Jetpack site that is not connected to the main
// account, so we want to skip it.
c.moveToNext();
continue;
}
String username = c.getString(0);
String encryptedPwd = c.getString(1);
String xmlrpcUrl = c.getString(2);
if (TextUtils.isEmpty(username)) {
AppLog.d(T.DB, "Found a self-hosted site with no username - skipping it.");
c.moveToNext();
continue;
}
if (TextUtils.isEmpty(xmlrpcUrl)) {
AppLog.d(T.DB, "Found a self-hosted site with no XML-RPC URL - skipping it.");
c.moveToNext();
continue;
}
SiteModel siteModel = new SiteModel();
siteModel.setUsername(username);
// Decrypt password before migrating since we no longer encrypt passwords in FluxC
siteModel.setPassword(decryptPassword(encryptedPwd));
siteModel.setXmlRpcUrl(xmlrpcUrl);
String url = c.getString(3);
if (!TextUtils.isEmpty(url)) {
siteModel.setUrl(url);
}
siteModel.setSelfHostedSiteId(c.getLong(4));
siteModel.setIsSelfHostedAdmin(SqlUtils.sqlToBool(c.getInt(6)));
siteList.add(siteModel);
c.moveToNext();
}
c.close();
} catch (SQLException e) {
// DB doesn't exist
}
return siteList;
}
public static boolean hasDraftsToMigrate(Context context) {
try {
SQLiteDatabase db = context.openOrCreateDatabase(DEPRECATED_DATABASE_NAME, 0, null);
String byString = "localDraft=1 OR isLocalChange=1";
Cursor c = db.query(DEPRECATED_POSTS_TABLE, null, byString, null, null, null, null);
if (c.getCount() > 0) {
c.close();
return true;
}
c.close();
return false;
} catch (SQLException e) {
return false;
}
}
static List<PostModel> getDraftsFromDeprecatedDB(Context context, SiteStore siteStore) {
List<PostModel> postList = new ArrayList<>();
try {
SQLiteDatabase db = context.openOrCreateDatabase(DEPRECATED_DATABASE_NAME, 0, null);
String byString = "localDraft=1 OR isLocalChange=1";
Cursor c = db.query(DEPRECATED_POSTS_TABLE, null, byString, null, null, null, null);
int numRows = c.getCount();
c.moveToFirst();
for (int i = 0; i < numRows; i++) {
PostModel postModel = new PostModel();
Cursor siteCursor = db.query(DEPRECATED_BLOGS_TABLE,
new String[]{"dotcomFlag","blogId","url","api_blogid"},
String.format("id=%s", c.getInt(c.getColumnIndex("blogID"))), null, null, null, null);
if (siteCursor.getCount() > 0) {
siteCursor.moveToFirst();
boolean dotcomFlag = siteCursor.getInt(0) == 1;
int blogId = siteCursor.getInt(1);
String xmlrpcUrl = siteCursor.getString(2);
long apiBlogId = StringUtils.stringToLong(siteCursor.getString(3));
int migratedSiteLocalId;
if (dotcomFlag) {
// WP.com site - identify it by WP.com site ID
migratedSiteLocalId = siteStore.getLocalIdForRemoteSiteId(blogId);
} else if (apiBlogId > 0) {
// Jetpack site - identify it by WP.com site ID
migratedSiteLocalId = siteStore.getLocalIdForRemoteSiteId(apiBlogId);
} else {
// Self-hosted site - identify it by its self-hosted site ID and XML-RPC URL
migratedSiteLocalId = siteStore.getLocalIdForSelfHostedSiteIdAndXmlRpcUrl(blogId, xmlrpcUrl);
}
postModel.setLocalSiteId(migratedSiteLocalId);
siteCursor.close();
} else {
AppLog.d(T.DB, "Couldn't find site corresponding to draft in deprecated DB! " +
"Site local id " + c.getInt(c.getColumnIndex("blogID")) +
" - Post title: " + c.getString(c.getColumnIndex("title")));
c.moveToNext();
siteCursor.close();
continue;
}
postModel.setIsLocalDraft(SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("localDraft"))));
postModel.setIsLocallyChanged(SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("isLocalChange"))));
String postId = c.getString(c.getColumnIndex("postid"));
if (!TextUtils.isEmpty(postId)) {
postModel.setRemotePostId(StringUtils.stringToLong(postId));
}
postModel.setTitle(StringUtils.unescapeHTML(c.getString(c.getColumnIndex("title"))));
String descriptionText = c.getString(c.getColumnIndex("description"));
String moreText = c.getString(c.getColumnIndex("mt_text_more"));
if (TextUtils.isEmpty(moreText)) {
postModel.setContent(descriptionText);
} else {
postModel.setContent(descriptionText + "\n<!--more-->\n" + moreText);
}
long dateCreated = c.getLong(c.getColumnIndex("date_created_gmt"));
if (dateCreated > 0) {
postModel.setDateCreated(DateTimeUtils.iso8601UTCFromTimestamp(dateCreated / 1000));
}
// Safety check as 'dateLastUpdated' was somewhat recently added and a user migrating from an old
// version of the app might not have it
int dateLastUpdatedIndex = c.getColumnIndex("dateLastUpdated");
long dateLocallyChanged = dateLastUpdatedIndex > 0 ? c.getLong(dateLastUpdatedIndex) : 0;
if (dateLocallyChanged > 0) {
postModel.setDateLocallyChanged(DateTimeUtils.iso8601UTCFromTimestamp(dateLocallyChanged / 1000));
}
int featuredImageIndex = c.getColumnIndex("wp_post_thumbnail");
long featuredImageId = featuredImageIndex > 0 ? c.getLong(featuredImageIndex) : 0;
postModel.setFeaturedImageId(featuredImageId);
postModel.setExcerpt(c.getString(c.getColumnIndex("mt_excerpt")));
postModel.setLink(c.getString(c.getColumnIndex("link")));
postModel.setTagNames(c.getString(c.getColumnIndex("mt_keywords")));
postModel.setStatus(c.getString(c.getColumnIndex("post_status")));
postModel.setPassword(c.getString(c.getColumnIndex("wp_password")));
postModel.setPostFormat(c.getString(c.getColumnIndex("wp_post_format")));
postModel.setIsPage(SqlUtils.sqlToBool(c.getInt(c.getColumnIndex("isPage"))));
int latColumnIndex = c.getColumnIndex("latitude");
int lngColumnIndex = c.getColumnIndex("longitude");
if (!c.isNull(latColumnIndex) && !c.isNull(lngColumnIndex)) {
postModel.setLocation(c.getDouble(latColumnIndex), c.getDouble(lngColumnIndex));
}
postModel.setCustomFields(c.getString(c.getColumnIndex("custom_fields")));
postList.add(postModel);
c.moveToNext();
}
c.close();
} catch (SQLException e) {
// DB doesn't exist
}
return postList;
}
private static String encryptPassword(String clearText) {
try {
DESKeySpec keySpec = new DESKeySpec(
DEPRECATED_DB_PASSWORD_SECRET.getBytes("UTF-8"));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(keySpec);
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.ENCRYPT_MODE, key);
return Base64.encodeToString(cipher.doFinal(clearText.getBytes("UTF-8")), Base64.DEFAULT);
} catch (Exception e) {
}
return clearText;
}
private static String decryptPassword(String encryptedPwd) {
try {
DESKeySpec keySpec = new DESKeySpec(
DEPRECATED_DB_PASSWORD_SECRET.getBytes("UTF-8"));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(keySpec);
byte[] encryptedWithoutB64 = Base64.decode(encryptedPwd, Base64.DEFAULT);
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] plainTextPwdBytes = cipher.doFinal(encryptedWithoutB64);
return new String(plainTextPwdBytes);
} catch (Exception e) {
}
return encryptedPwd;
}
}