blob: 3389b9e8ee6eac142836fba367748b553d946053 [file] [log] [blame]
package org.wordpress.android.ui.prefs;
import android.app.Activity;
import android.text.TextUtils;
import org.wordpress.android.R;
import org.wordpress.android.analytics.AnalyticsTracker;
import org.wordpress.android.datasets.SiteSettingsTable;
import org.wordpress.android.models.Blog;
import org.wordpress.android.models.CategoryModel;
import org.wordpress.android.models.SiteSettingsModel;
import org.wordpress.android.util.LanguageUtils;
import org.wordpress.android.util.AnalyticsUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.MapUtils;
import org.xmlrpc.android.ApiHelper.Method;
import org.xmlrpc.android.XMLRPCCallback;
import org.xmlrpc.android.XMLRPCClientInterface;
import org.xmlrpc.android.XMLRPCException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
class SelfHostedSiteSettings extends SiteSettingsInterface {
// XML-RPC wp.getOptions keys
public static final String PRIVACY_KEY = "blog_public";
public static final String DEF_CATEGORY_KEY = "default_category";
public static final String DEF_POST_FORMAT_KEY = "default_post_format";
public static final String ALLOW_COMMENTS_KEY = "default_comment_status";
public static final String SEND_PINGBACKS_KEY = "default_pingback_flag";
public static final String RECEIVE_PINGBACKS_KEY = "default_ping_status";
public static final String CLOSE_OLD_COMMENTS_KEY = "close_comments_for_old_posts";
public static final String CLOSE_OLD_COMMENTS_DAYS_KEY = "close_comments_days_old";
public static final String THREAD_COMMENTS_KEY = "thread_comments";
public static final String THREAD_COMMENTS_DEPTH_KEY = "thread_comments_depth";
public static final String PAGE_COMMENTS_KEY = "page_comments";
public static final String PAGE_COMMENT_COUNT_KEY = "comments_per_page";
public static final String COMMENT_SORT_ORDER_KEY = "comment_order";
public static final String COMMENT_MODERATION_KEY = "comment_moderation";
public static final String REQUIRE_IDENTITY_KEY = "require_name_email";
public static final String REQUIRE_USER_ACCOUNT_KEY = "comment_registration";
public static final String WHITELIST_KNOWN_USERS_KEY = "comment_whitelist";
public static final String MAX_LINKS_KEY = "comment_max_links";
public static final String MODERATION_KEYS_KEY = "moderation_keys";
public static final String BLACKLIST_KEYS_KEY = "blacklist_keys";
public static final String SOFTWARE_VERSION_KEY = "software_version";
private static final String BLOG_URL_KEY = "blog_url";
private static final String BLOG_TITLE_KEY = "blog_title";
private static final String BLOG_USERNAME_KEY = "username";
private static final String BLOG_PASSWORD_KEY = "password";
private static final String BLOG_TAGLINE_KEY = "blog_tagline";
private static final String BLOG_CATEGORY_ID_KEY = "categoryId";
private static final String BLOG_CATEGORY_PARENT_ID_KEY = "parentId";
private static final String BLOG_CATEGORY_DESCRIPTION_KEY = "categoryDescription";
private static final String BLOG_CATEGORY_NAME_KEY = "categoryName";
// Requires WordPress 4.5.x or higher
private static final int REQUIRED_MAJOR_VERSION = 4;
private static final int REQUIRED_MINOR_VERSION = 3;
private static final String OPTION_ALLOWED = "open";
private static final String OPTION_DISALLOWED = "closed";
SelfHostedSiteSettings(Activity host, Blog blog, SiteSettingsListener listener) {
super(host, blog, listener);
}
@Override
public SiteSettingsInterface init(boolean fetch) {
super.init(fetch);
if (mSettings.defaultCategory == 0) {
mSettings.defaultCategory = siteSettingsPreferences(mActivity).getInt(DEF_CATEGORY_PREF_KEY, 0);
}
if (TextUtils.isEmpty(mSettings.defaultPostFormat) || mSettings.defaultPostFormat.equals("0")) {
mSettings.defaultPostFormat = siteSettingsPreferences(mActivity).getString(DEF_FORMAT_PREF_KEY, "0");
}
mSettings.language = siteSettingsPreferences(mActivity).getString(LANGUAGE_PREF_KEY, LanguageUtils.getPatchedCurrentDeviceLanguage(null));
return this;
}
@Override
public void saveSettings() {
super.saveSettings();
final Map<String, String> params = serializeSelfHostedParams();
if (params == null || params.isEmpty()) return;
XMLRPCCallback callback = new XMLRPCCallback() {
@Override
public void onSuccess(long id, final Object result) {
notifySavedOnUiThread(null);
mRemoteSettings.copyFrom(mSettings);
if (result != null) {
HashMap<String, Object> properties = new HashMap<>();
if (result instanceof Map) {
Map<String, Object> resultMap = (Map) result;
Set<String> keys = resultMap.keySet();
for (String key : keys) {
Object currentValue = resultMap.get(key);
if (currentValue != null) {
properties.put(SAVED_ITEM_PREFIX + key, currentValue);
}
}
}
AnalyticsUtils.trackWithCurrentBlogDetails(
AnalyticsTracker.Stat.SITE_SETTINGS_SAVED_REMOTELY, properties);
}
}
@Override
public void onFailure(long id, final Exception error) {
notifySavedOnUiThread(error);
}
};
final Object[] callParams = {
mBlog.getRemoteBlogId(), mSettings.username, mSettings.password, params
};
XMLRPCClientInterface xmlrpcInterface = instantiateInterface();
if (xmlrpcInterface == null) return;
xmlrpcInterface.callAsync(callback, Method.SET_OPTIONS, callParams);
}
/**
* Request remote site data via XML-RPC.
*/
@Override
protected void fetchRemoteData() {
new Thread() {
@Override
public void run() {
Object[] params = {mBlog.getRemoteBlogId(), mBlog.getUsername(), mBlog.getPassword()};
// Need two interfaces or the first call gets aborted
instantiateInterface().callAsync(mOptionsCallback, Method.GET_OPTIONS, params);
instantiateInterface().callAsync(mCategoriesCallback, Method.GET_CATEGORIES, params);
}
}.start();
}
/**
* Handles response to fetching self-hosted site categories via XML-RPC.
*/
private final XMLRPCCallback mCategoriesCallback = new XMLRPCCallback() {
@Override
public void onSuccess(long id, Object result) {
if (result instanceof Object[]) {
AppLog.d(AppLog.T.API, "Received Categories XML-RPC response.");
credentialsVerified(true);
mRemoteSettings.localTableId = mBlog.getRemoteBlogId();
deserializeCategoriesResponse(mRemoteSettings, (Object[]) result);
mSettings.categories = mRemoteSettings.categories;
SiteSettingsTable.saveCategories(mSettings.categories);
notifyUpdatedOnUiThread(null);
} else {
// Response is considered an error if we are unable to parse it
AppLog.w(AppLog.T.API, "Error parsing Categories XML-RPC response: " + result);
notifyUpdatedOnUiThread(new XMLRPCException("Unknown response object"));
}
}
@Override
public void onFailure(long id, Exception error) {
AppLog.w(AppLog.T.API, "Error Categories XML-RPC response: " + error);
notifyUpdatedOnUiThread(error);
}
};
/**
* Handles response to fetching self-hosted site options via XML-RPC.
*/
private final XMLRPCCallback mOptionsCallback = new XMLRPCCallback() {
@Override
public void onSuccess(long id, final Object result) {
if (result instanceof Map) {
AppLog.d(AppLog.T.API, "Received Options XML-RPC response.");
if (!versionSupported((Map) result) && mActivity != null) {
notifyUpdatedOnUiThread(new XMLRPCException(mActivity.getString(R.string.site_settings_unsupported_version_error)));
return;
}
credentialsVerified(true);
deserializeOptionsResponse(mRemoteSettings, (Map) result);
// postFormats setting is not returned by this api call so copy it over
final Map<String, String> currentPostFormats = mSettings.postFormats;
mSettings.copyFrom(mRemoteSettings);
mSettings.postFormats = currentPostFormats;
SiteSettingsTable.saveSettings(mSettings);
notifyUpdatedOnUiThread(null);
} else {
// Response is considered an error if we are unable to parse it
AppLog.w(AppLog.T.API, "Error parsing Options XML-RPC response: " + result);
notifyUpdatedOnUiThread(new XMLRPCException("Unknown response object"));
}
}
@Override
public void onFailure(long id, final Exception error) {
AppLog.w(AppLog.T.API, "Error Options XML-RPC response: " + error);
notifyUpdatedOnUiThread(error);
}
};
private boolean versionSupported(Map map) {
String version = getNestedMapValue(map, SOFTWARE_VERSION_KEY);
if (TextUtils.isEmpty(version)) return false;
String[] split = version.split("\\.");
return split.length > 0 &&
Integer.valueOf(split[0]) >= REQUIRED_MAJOR_VERSION &&
Integer.valueOf(split[1]) >= REQUIRED_MINOR_VERSION;
}
private Map<String, String> serializeSelfHostedParams() {
Map<String, String> params = new HashMap<>();
if (mSettings.title != null && !mSettings.title.equals(mRemoteSettings.title)) {
params.put(BLOG_TITLE_KEY, mSettings.title);
}
if (mSettings.tagline != null && !mSettings.tagline.equals(mRemoteSettings.tagline)) {
params.put(BLOG_TAGLINE_KEY, mSettings.tagline);
}
if (mSettings.privacy != mRemoteSettings.privacy) {
params.put(PRIVACY_KEY, String.valueOf(mSettings.privacy));
}
if (mSettings.defaultCategory != mRemoteSettings.defaultCategory) {
params.put(DEF_CATEGORY_KEY, String.valueOf(mSettings.defaultCategory));
}
if (mSettings.defaultPostFormat != null && !mSettings.defaultPostFormat.equals(mRemoteSettings.defaultPostFormat)) {
params.put(DEF_POST_FORMAT_KEY, mSettings.defaultPostFormat);
}
if (mSettings.allowComments != mRemoteSettings.allowComments) {
params.put(ALLOW_COMMENTS_KEY, String.valueOf(mSettings.allowComments));
}
if (mSettings.sendPingbacks != mRemoteSettings.sendPingbacks) {
params.put(SEND_PINGBACKS_KEY, mSettings.sendPingbacks ? "1" : "0");
}
if (mSettings.receivePingbacks != mRemoteSettings.receivePingbacks) {
params.put(RECEIVE_PINGBACKS_KEY, mSettings.receivePingbacks ? OPTION_ALLOWED : OPTION_DISALLOWED);
}
if (mSettings.commentApprovalRequired != mRemoteSettings.commentApprovalRequired) {
params.put(COMMENT_MODERATION_KEY, String.valueOf(mSettings.commentApprovalRequired));
}
if (mSettings.closeCommentAfter != mRemoteSettings.closeCommentAfter) {
if (mSettings.closeCommentAfter <= 0) {
params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(0));
} else {
params.put(CLOSE_OLD_COMMENTS_KEY, String.valueOf(1));
params.put(CLOSE_OLD_COMMENTS_DAYS_KEY, String.valueOf(mSettings.closeCommentAfter));
}
}
if (mSettings.sortCommentsBy != mRemoteSettings.sortCommentsBy) {
if (mSettings.sortCommentsBy == ASCENDING_SORT) {
params.put(COMMENT_SORT_ORDER_KEY, "asc");
} else if (mSettings.sortCommentsBy == DESCENDING_SORT) {
params.put(COMMENT_SORT_ORDER_KEY, "desc");
}
}
if (mSettings.threadingLevels != mRemoteSettings.threadingLevels) {
if (mSettings.threadingLevels <= 1) {
params.put(THREAD_COMMENTS_KEY, String.valueOf(0));
} else {
params.put(PAGE_COMMENTS_KEY, String.valueOf(1));
params.put(THREAD_COMMENTS_DEPTH_KEY, String.valueOf(mSettings.threadingLevels));
}
}
if (mSettings.commentsPerPage != mRemoteSettings.commentsPerPage) {
if (mSettings.commentsPerPage <= 0) {
params.put(PAGE_COMMENTS_KEY, String.valueOf(0));
} else{
params.put(PAGE_COMMENTS_KEY, String.valueOf(1));
params.put(PAGE_COMMENT_COUNT_KEY, String.valueOf(mSettings.commentsPerPage));
}
}
if (mSettings.commentsRequireIdentity != mRemoteSettings.commentsRequireIdentity) {
params.put(REQUIRE_IDENTITY_KEY, String.valueOf(mSettings.commentsRequireIdentity ? 1 : 0));
}
if (mSettings.commentsRequireUserAccount != mRemoteSettings.commentsRequireUserAccount) {
params.put(REQUIRE_USER_ACCOUNT_KEY, String.valueOf(mSettings.commentsRequireUserAccount ? 1 : 0));
}
if (mSettings.commentAutoApprovalKnownUsers != mRemoteSettings.commentAutoApprovalKnownUsers) {
params.put(WHITELIST_KNOWN_USERS_KEY, String.valueOf(mSettings.commentAutoApprovalKnownUsers));
}
if (mSettings.maxLinks != mRemoteSettings.maxLinks) {
params.put(MAX_LINKS_KEY, String.valueOf(mSettings.maxLinks));
}
if (mSettings.holdForModeration != null && !mSettings.holdForModeration.equals(mRemoteSettings.holdForModeration)) {
StringBuilder builder = new StringBuilder();
for (String key : mSettings.holdForModeration) {
builder.append(key);
builder.append("\n");
}
if (builder.length() > 1) {
params.put(MODERATION_KEYS_KEY, builder.substring(0, builder.length() - 1));
} else {
params.put(MODERATION_KEYS_KEY, "");
}
}
if (mSettings.blacklist != null && !mSettings.blacklist.equals(mRemoteSettings.blacklist)) {
StringBuilder builder = new StringBuilder();
for (String key : mSettings.blacklist) {
builder.append(key);
builder.append("\n");
}
if (builder.length() > 1) {
params.put(BLACKLIST_KEYS_KEY, builder.substring(0, builder.length() - 1));
} else {
params.put(BLACKLIST_KEYS_KEY, "");
}
}
return params;
}
/**
* Sets values from a self-hosted XML-RPC response object.
*/
private void deserializeOptionsResponse(SiteSettingsModel model, Map response) {
if (mBlog == null || response == null) return;
model.username = mBlog.getUsername();
model.password = mBlog.getPassword();
model.address = getNestedMapValue(response, BLOG_URL_KEY);
model.title = getNestedMapValue(response, BLOG_TITLE_KEY);
model.tagline = getNestedMapValue(response, BLOG_TAGLINE_KEY);
model.privacy = Integer.valueOf(getNestedMapValue(response, PRIVACY_KEY));
model.defaultCategory = Integer.valueOf(getNestedMapValue(response, DEF_CATEGORY_KEY));
model.defaultPostFormat = getNestedMapValue(response, DEF_POST_FORMAT_KEY);
model.allowComments = OPTION_ALLOWED.equals(getNestedMapValue(response, ALLOW_COMMENTS_KEY));
model.receivePingbacks = OPTION_ALLOWED.equals(getNestedMapValue(response, RECEIVE_PINGBACKS_KEY));
String sendPingbacks = getNestedMapValue(response, SEND_PINGBACKS_KEY);
String approvalRequired = getNestedMapValue(response, COMMENT_MODERATION_KEY);
String identityRequired = getNestedMapValue(response, REQUIRE_IDENTITY_KEY);
String accountRequired = getNestedMapValue(response, REQUIRE_USER_ACCOUNT_KEY);
String knownUsers = getNestedMapValue(response, WHITELIST_KNOWN_USERS_KEY);
model.sendPingbacks = !TextUtils.isEmpty(sendPingbacks) && Integer.valueOf(sendPingbacks) > 0;
model.commentApprovalRequired = !TextUtils.isEmpty(approvalRequired) && Boolean.valueOf(approvalRequired);
model.commentsRequireIdentity = !TextUtils.isEmpty(identityRequired) && Integer.valueOf(identityRequired) > 0;
model.commentsRequireUserAccount = !TextUtils.isEmpty(accountRequired) && Integer.valueOf(identityRequired) > 0;
model.commentAutoApprovalKnownUsers = !TextUtils.isEmpty(knownUsers) && Boolean.valueOf(knownUsers);
model.maxLinks = Integer.valueOf(getNestedMapValue(response, MAX_LINKS_KEY));
mRemoteSettings.holdForModeration = new ArrayList<>();
mRemoteSettings.blacklist = new ArrayList<>();
String modKeys = getNestedMapValue(response, MODERATION_KEYS_KEY);
if (modKeys.length() > 0) {
Collections.addAll(mRemoteSettings.holdForModeration, modKeys.split("\n"));
}
String blacklistKeys = getNestedMapValue(response, BLACKLIST_KEYS_KEY);
if (blacklistKeys.length() > 0) {
Collections.addAll(mRemoteSettings.blacklist, blacklistKeys.split("\n"));
}
String close = getNestedMapValue(response, CLOSE_OLD_COMMENTS_KEY);
if (!TextUtils.isEmpty(close) && Boolean.valueOf(close)) {
mRemoteSettings.closeCommentAfter = Integer.valueOf(getNestedMapValue(response, CLOSE_OLD_COMMENTS_DAYS_KEY));
} else {
mRemoteSettings.closeCommentAfter = 0;
}
String thread = getNestedMapValue(response, THREAD_COMMENTS_KEY);
if (!TextUtils.isEmpty(thread) && Integer.valueOf(thread) > 0) {
mRemoteSettings.threadingLevels = Integer.valueOf(getNestedMapValue(response, THREAD_COMMENTS_DEPTH_KEY));
} else {
mRemoteSettings.threadingLevels = 0;
}
String page = getNestedMapValue(response, PAGE_COMMENTS_KEY);
if (!TextUtils.isEmpty(page) && Boolean.valueOf(page)) {
mRemoteSettings.commentsPerPage = Integer.valueOf(getNestedMapValue(response, PAGE_COMMENT_COUNT_KEY));
} else {
mRemoteSettings.commentsPerPage = 0;
}
if (getNestedMapValue(response, COMMENT_SORT_ORDER_KEY).equals("asc")) {
mRemoteSettings.sortCommentsBy = ASCENDING_SORT;
} else {
mRemoteSettings.sortCommentsBy = DESCENDING_SORT;
}
}
private void deserializeCategoriesResponse(SiteSettingsModel model, Object[] response) {
model.categories = new CategoryModel[response.length];
for (int i = 0; i < response.length; ++i) {
if (response[i] instanceof Map) {
Map category = (Map) response[i];
CategoryModel categoryModel = new CategoryModel();
categoryModel.id = MapUtils.getMapInt(category, BLOG_CATEGORY_ID_KEY);
categoryModel.parentId = MapUtils.getMapInt(category, BLOG_CATEGORY_PARENT_ID_KEY);
categoryModel.description = MapUtils.getMapStr(category, BLOG_CATEGORY_DESCRIPTION_KEY);
categoryModel.name = MapUtils.getMapStr(category, BLOG_CATEGORY_NAME_KEY);
model.categories[i] = categoryModel;
}
}
}
/**
* Helper method to get a value from a nested Map. Used to parse self-hosted response objects.
*/
private String getNestedMapValue(Map map, String key) {
if (map != null && key != null) {
return MapUtils.getMapStr((Map) map.get(key), "value");
}
return "";
}
}