With data-client added as library module (#3656)

* With data-client added as library module

* Fix build
This commit is contained in:
Vivek Maskara 2020-04-15 03:00:13 -07:00 committed by GitHub
parent 9ee04f3df4
commit 32ee0b4f9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
258 changed files with 34820 additions and 2 deletions

View file

@ -0,0 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.wikipedia.dataclient">
<application android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true">
</application>
</manifest>

View file

@ -0,0 +1,38 @@
package org.wikipedia;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.login.LoginResult;
import okhttp3.OkHttpClient;
public abstract class AppAdapter {
public abstract String getMediaWikiBaseUrl();
public abstract String getRestbaseUriFormat();
public abstract OkHttpClient getOkHttpClient(@NonNull WikiSite wikiSite);
public abstract int getDesiredLeadImageDp();
public abstract boolean isLoggedIn();
public abstract String getUserName();
public abstract String getPassword();
public abstract void updateAccount(@NonNull LoginResult result);
public abstract SharedPreferenceCookieManager getCookies();
public abstract void setCookies(@NonNull SharedPreferenceCookieManager cookies);
public abstract boolean logErrorsInsteadOfCrashing();
private static AppAdapter INSTANCE;
public static void set(AppAdapter instance) {
INSTANCE = instance;
}
public static AppAdapter get() {
if (INSTANCE == null) {
throw new RuntimeException("Please provide an instance of AppAdapter when using this library.");
}
return INSTANCE;
}
}

View file

@ -0,0 +1,19 @@
package org.wikipedia.captcha;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.mwapi.MwResponse;
public class Captcha extends MwResponse {
@SuppressWarnings("unused,NullableProblems") @NonNull private FancyCaptchaReload fancycaptchareload;
@NonNull String captchaId() {
return fancycaptchareload.index();
}
private static class FancyCaptchaReload {
@SuppressWarnings("unused,NullableProblems") @NonNull private String index;
@NonNull String index() {
return index;
}
}
}

View file

@ -0,0 +1,49 @@
package org.wikipedia.captcha;
import android.os.Parcel;
import android.os.Parcelable;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.edit.EditResult;
// Handles only Image Captchas
public class CaptchaResult extends EditResult {
private final String captchaId;
public CaptchaResult(String captchaId) {
super("Failure");
this.captchaId = captchaId;
}
protected CaptchaResult(Parcel in) {
super(in);
captchaId = in.readString();
}
public String getCaptchaId() {
return captchaId;
}
public String getCaptchaUrl(WikiSite wiki) {
return wiki.url("index.php") + "?title=Special:Captcha/image&wpCaptchaId=" + captchaId;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(captchaId);
}
public static final Parcelable.Creator<CaptchaResult> CREATOR
= new Parcelable.Creator<CaptchaResult>() {
@Override
public CaptchaResult createFromParcel(Parcel in) {
return new CaptchaResult(in);
}
@Override
public CaptchaResult[] newArray(int size) {
return new CaptchaResult[size];
}
};
}

View file

@ -0,0 +1,26 @@
package org.wikipedia.concurrency;
import androidx.annotation.NonNull;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.CheckReturnValue;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
public class RxBus {
private final Subject<Object> bus = PublishSubject.create().toSerialized();
private final Observable<Object> observable = bus.observeOn(AndroidSchedulers.mainThread());
public void post(Object o) {
bus.onNext(o);
}
@CheckReturnValue
public Disposable subscribe(@NonNull Consumer<Object> consumer) {
return observable.subscribe(consumer);
}
}

View file

@ -0,0 +1,12 @@
package org.wikipedia.createaccount;
import androidx.annotation.NonNull;
/**
* Exception thrown when an account creation request FAILs
*/
public class CreateAccountException extends RuntimeException {
CreateAccountException(@NonNull String message) {
super(message);
}
}

View file

@ -0,0 +1,57 @@
package org.wikipedia.createaccount;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class CreateAccountResult implements Parcelable {
@NonNull private final String status;
@NonNull private final String message;
public CreateAccountResult(@NonNull String status, @NonNull String message) {
this.status = status;
this.message = message;
}
@NonNull
public String getStatus() {
return status;
}
@NonNull
public String getMessage() {
return message;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(status);
parcel.writeString(message);
}
@Override
public int describeContents() {
return 0;
}
protected CreateAccountResult(Parcel in) {
status = in.readString();
message = in.readString();
}
@NonNull
public static final Parcelable.Creator<CreateAccountResult> CREATOR
= new Parcelable.Creator<CreateAccountResult>() {
@Override
public CreateAccountResult createFromParcel(Parcel in) {
return new CreateAccountResult(in);
}
@Override
public CreateAccountResult[] newArray(int size) {
return new CreateAccountResult[size];
}
};
}

View file

@ -0,0 +1,43 @@
package org.wikipedia.createaccount;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
class CreateAccountSuccessResult extends CreateAccountResult implements Parcelable {
private String username;
CreateAccountSuccessResult(@NonNull String username) {
super("PASS", "Account created");
this.username = username;
}
String getUsername() {
return username;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
super.writeToParcel(parcel, flags);
parcel.writeString(username);
}
private CreateAccountSuccessResult(Parcel in) {
super(in);
username = in.readString();
}
public static final Creator<CreateAccountSuccessResult> CREATOR
= new Creator<CreateAccountSuccessResult>() {
@Override
public CreateAccountSuccessResult createFromParcel(Parcel in) {
return new CreateAccountSuccessResult(in);
}
@Override
public CreateAccountSuccessResult[] newArray(int size) {
return new CreateAccountSuccessResult[size];
}
};
}

View file

@ -0,0 +1,213 @@
package org.wikipedia.csrf;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.wikipedia.AppAdapter;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.login.LoginClient;
import org.wikipedia.login.LoginResult;
import org.wikipedia.util.log.L;
import java.io.IOException;
import retrofit2.Call;
import retrofit2.Response;
public class CsrfTokenClient {
private static final String ANON_TOKEN = "+\\";
private static final int MAX_RETRIES = 1;
private static final int MAX_RETRIES_OF_LOGIN_BLOCKING = 2;
@NonNull private final WikiSite csrfWikiSite;
@NonNull private final WikiSite loginWikiSite;
private int retries = 0;
@Nullable private Call<MwQueryResponse> csrfTokenCall;
@NonNull private LoginClient loginClient = new LoginClient();
public CsrfTokenClient(@NonNull WikiSite csrfWikiSite, @NonNull WikiSite loginWikiSite) {
this.csrfWikiSite = csrfWikiSite;
this.loginWikiSite = loginWikiSite;
}
public void request(@NonNull final Callback callback) {
request(false, callback);
}
public void request(boolean forceLogin, @NonNull final Callback callback) {
cancel();
if (forceLogin) {
retryWithLogin(new RuntimeException("Forcing login..."), callback);
return;
}
csrfTokenCall = request(ServiceFactory.get(csrfWikiSite), callback);
}
public void cancel() {
loginClient.cancel();
if (csrfTokenCall != null) {
csrfTokenCall.cancel();
csrfTokenCall = null;
}
}
@VisibleForTesting
@NonNull
Call<MwQueryResponse> request(@NonNull Service service, @NonNull final Callback cb) {
return requestToken(service, new CsrfTokenClient.Callback() {
@Override public void success(@NonNull String token) {
if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) {
retryWithLogin(new RuntimeException("App believes we're logged in, but got anonymous token."), cb);
} else {
cb.success(token);
}
}
@Override public void failure(@NonNull Throwable caught) {
retryWithLogin(caught, cb);
}
@Override
public void twoFactorPrompt() {
cb.twoFactorPrompt();
}
});
}
private void retryWithLogin(@NonNull Throwable caught, @NonNull final Callback callback) {
if (retries < MAX_RETRIES
&& !TextUtils.isEmpty(AppAdapter.get().getUserName())
&& !TextUtils.isEmpty(AppAdapter.get().getPassword())) {
retries++;
SharedPreferenceCookieManager.getInstance().clearAllCookies();
login(AppAdapter.get().getUserName(), AppAdapter.get().getPassword(), () -> {
L.i("retrying...");
request(callback);
}, callback);
} else {
callback.failure(caught);
}
}
private void login(@NonNull final String username, @NonNull final String password,
@NonNull final RetryCallback retryCallback,
@NonNull final Callback callback) {
new LoginClient().request(loginWikiSite, username, password,
new LoginClient.LoginCallback() {
@Override
public void success(@NonNull LoginResult loginResult) {
if (loginResult.pass()) {
AppAdapter.get().updateAccount(loginResult);
retryCallback.retry();
} else {
callback.failure(new LoginClient.LoginFailedException(loginResult.getMessage()));
}
}
@Override
public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) {
callback.twoFactorPrompt();
}
@Override public void passwordResetPrompt(@Nullable String token) {
// Should not happen here, but call the callback just in case.
callback.failure(new LoginClient.LoginFailedException("Logged in with temporary password."));
}
@Override
public void error(@NonNull Throwable caught) {
callback.failure(caught);
}
});
}
@NonNull public String getTokenBlocking() throws Throwable {
String token = "";
Service service = ServiceFactory.get(csrfWikiSite);
for (int retry = 0; retry < MAX_RETRIES_OF_LOGIN_BLOCKING; retry++) {
try {
if (retry > 0) {
// Log in explicitly
new LoginClient().loginBlocking(loginWikiSite, AppAdapter.get().getUserName(),
AppAdapter.get().getPassword(), "");
}
Response<MwQueryResponse> response = service.getCsrfTokenCall().execute();
if (response.body() == null || response.body().query() == null
|| TextUtils.isEmpty(response.body().query().csrfToken())) {
continue;
}
token = response.body().query().csrfToken();
if (AppAdapter.get().isLoggedIn() && token.equals(ANON_TOKEN)) {
throw new RuntimeException("App believes we're logged in, but got anonymous token.");
}
break;
} catch (Throwable t) {
L.w(t);
}
}
if (TextUtils.isEmpty(token) || token.equals(ANON_TOKEN)) {
throw new IOException("Invalid token, or login failure.");
}
return token;
}
@VisibleForTesting @NonNull Call<MwQueryResponse> requestToken(@NonNull Service service,
@NonNull final Callback cb) {
Call<MwQueryResponse> call = service.getCsrfTokenCall();
call.enqueue(new retrofit2.Callback<MwQueryResponse>() {
@Override
public void onResponse(@NonNull Call<MwQueryResponse> call, @NonNull Response<MwQueryResponse> response) {
if (call.isCanceled()) {
return;
}
cb.success(response.body().query().csrfToken());
}
@Override
public void onFailure(@NonNull Call<MwQueryResponse> call, @NonNull Throwable t) {
if (call.isCanceled()) {
return;
}
cb.failure(t);
}
});
return call;
}
public interface Callback {
void success(@NonNull String token);
void failure(@NonNull Throwable caught);
void twoFactorPrompt();
}
public static class DefaultCallback implements Callback {
@Override
public void success(@NonNull String token) {
}
@Override
public void failure(@NonNull Throwable caught) {
L.e(caught);
}
@Override
public void twoFactorPrompt() {
// TODO:
}
}
private interface RetryCallback {
void retry();
}
}

View file

@ -0,0 +1,194 @@
package org.wikipedia.dataclient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.restbase.RbDefinition;
import org.wikipedia.dataclient.restbase.RbRelatedPages;
import org.wikipedia.dataclient.restbase.page.RbPageLead;
import org.wikipedia.dataclient.restbase.page.RbPageRemaining;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.feed.aggregated.AggregatedFeedContent;
import org.wikipedia.feed.announcement.AnnouncementList;
import org.wikipedia.feed.configure.FeedAvailability;
import org.wikipedia.feed.onthisday.OnThisDay;
import org.wikipedia.gallery.Gallery;
import org.wikipedia.readinglist.sync.SyncedReadingLists;
import java.util.Map;
import io.reactivex.Observable;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface RestService {
String REST_API_PREFIX = "api/rest_v1/";
String ACCEPT_HEADER_PREFIX = "accept: application/json; charset=utf-8; profile=\"https://www.mediawiki.org/wiki/Specs/";
String ACCEPT_HEADER_SUMMARY = ACCEPT_HEADER_PREFIX + "Summary/1.2.0\"";
String ACCEPT_HEADER_MOBILE_SECTIONS = ACCEPT_HEADER_PREFIX + "mobile-sections/0.12.4\"";
String ACCEPT_HEADER_DEFINITION = ACCEPT_HEADER_PREFIX + "definition/0.7.2\"";
String REST_PAGE_SECTIONS_URL = "page/mobile-sections-remaining/{title}";
/**
* Gets a page summary for a given title -- for link previews
*
* @param title the page title to be used including prefix
*/
@Headers({
"x-analytics: preview=1",
ACCEPT_HEADER_SUMMARY
})
@GET("page/summary/{title}")
@NonNull
Observable<RbPageSummary> getSummary(@Nullable @Header("Referer") String referrerUrl,
@NonNull @Path("title") String title);
/**
* Gets the lead section and initial metadata of a given title.
*
* @param title the page title with prefix if necessary
*/
@Headers({
"x-analytics: pageview=1",
ACCEPT_HEADER_MOBILE_SECTIONS
})
@GET("page/mobile-sections-lead/{title}")
@NonNull
Observable<Response<RbPageLead>> getLeadSection(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(Service.OFFLINE_SAVE_HEADER) String saveHeader,
@Nullable @Header("Referer") String referrerUrl,
@NonNull @Path("title") String title);
/**
* Gets the remaining sections of a given title.
*
* @param title the page title to be used including prefix
*/
@Headers(ACCEPT_HEADER_MOBILE_SECTIONS)
@GET(REST_PAGE_SECTIONS_URL)
@NonNull Observable<Response<RbPageRemaining>> getRemainingSections(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(Service.OFFLINE_SAVE_HEADER) String saveHeader,
@NonNull @Path("title") String title);
/**
* TODO: remove this if we find a way to get the request url before the observable object being executed
* Gets the remaining sections request url of a given title.
*
* @param title the page title to be used including prefix
*/
@Headers(ACCEPT_HEADER_MOBILE_SECTIONS)
@GET(REST_PAGE_SECTIONS_URL)
@NonNull Call<RbPageRemaining> getRemainingSectionsUrl(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(Service.OFFLINE_SAVE_HEADER) String saveHeader,
@NonNull @Path("title") String title);
// todo: this Content Service-only endpoint is under page/ but that implementation detail should
// probably not be reflected here. Move to WordDefinitionClient
/**
* Gets selected Wiktionary content for a given title derived from user-selected text
*
* @param title the Wiktionary page title derived from user-selected Wikipedia article text
*/
@Headers(ACCEPT_HEADER_DEFINITION)
@GET("page/definition/{title}")
@NonNull Observable<Map<String, RbDefinition.Usage[]>> getDefinition(@NonNull @Path("title") String title);
@Headers(ACCEPT_HEADER_SUMMARY)
@GET("page/random/summary")
@NonNull Observable<RbPageSummary> getRandomSummary();
@Headers(ACCEPT_HEADER_SUMMARY)
@GET("page/related/{title}")
@NonNull Observable<RbRelatedPages> getRelatedPages(@Path("title") String title);
@GET("page/media/{title}")
@NonNull Observable<Gallery> getMedia(@Path("title") String title);
@GET("feed/onthisday/events/{mm}/{dd}")
@NonNull Observable<OnThisDay> getOnThisDay(@Path("mm") int month, @Path("dd") int day);
@Headers(ACCEPT_HEADER_PREFIX + "announcements/0.1.0\"")
@GET("feed/announcements")
@NonNull Call<AnnouncementList> getAnnouncements();
@Headers(ACCEPT_HEADER_PREFIX + "aggregated-feed/0.5.0\"")
@GET("feed/featured/{year}/{month}/{day}")
@NonNull Observable<AggregatedFeedContent> getAggregatedFeed(@Path("year") String year,
@Path("month") String month,
@Path("day") String day);
@GET("feed/availability")
@NonNull Observable<FeedAvailability> getFeedAvailability();
// ------- Reading lists -------
@Headers("Cache-Control: no-cache")
@POST("data/lists/setup")
@NonNull Call<Void> setupReadingLists(@Query("csrf_token") String token);
@Headers("Cache-Control: no-cache")
@POST("data/lists/teardown")
@NonNull Call<Void> tearDownReadingLists(@Query("csrf_token") String token);
@Headers("Cache-Control: no-cache")
@GET("data/lists/")
@NonNull Call<SyncedReadingLists> getReadingLists(@Query("next") String next);
@Headers("Cache-Control: no-cache")
@POST("data/lists/")
@NonNull Call<SyncedReadingLists.RemoteIdResponse> createReadingList(@Query("csrf_token") String token,
@Body SyncedReadingLists.RemoteReadingList list);
@Headers("Cache-Control: no-cache")
@PUT("data/lists/{id}")
@NonNull Call<Void> updateReadingList(@Path("id") long listId, @Query("csrf_token") String token,
@Body SyncedReadingLists.RemoteReadingList list);
@Headers("Cache-Control: no-cache")
@DELETE("data/lists/{id}")
@NonNull Call<Void> deleteReadingList(@Path("id") long listId, @Query("csrf_token") String token);
@Headers("Cache-Control: no-cache")
@GET("data/lists/changes/since/{date}")
@NonNull Call<SyncedReadingLists> getReadingListChangesSince(@Path("date") String iso8601Date,
@Query("next") String next);
@Headers("Cache-Control: no-cache")
@GET("data/lists/pages/{project}/{title}")
@NonNull Call<SyncedReadingLists> getReadingListsContaining(@Path("project") String project,
@Path("title") String title,
@Query("next") String next);
@Headers("Cache-Control: no-cache")
@GET("data/lists/{id}/entries/")
@NonNull Call<SyncedReadingLists> getReadingListEntries(@Path("id") long listId, @Query("next") String next);
@Headers("Cache-Control: no-cache")
@POST("data/lists/{id}/entries/")
@NonNull Call<SyncedReadingLists.RemoteIdResponse> addEntryToReadingList(@Path("id") long listId,
@Query("csrf_token") String token,
@Body SyncedReadingLists.RemoteReadingListEntry entry);
@Headers("Cache-Control: no-cache")
@POST("data/lists/{id}/entries/batch")
@NonNull Call<SyncedReadingLists.RemoteIdResponseBatch> addEntriesToReadingList(@Path("id") long listId,
@Query("csrf_token") String token,
@Body SyncedReadingLists.RemoteReadingListEntryBatch batch);
@Headers("Cache-Control: no-cache")
@DELETE("data/lists/{id}/entries/{entry_id}")
@NonNull Call<Void> deleteEntryFromReadingList(@Path("id") long listId, @Path("entry_id") long entryId,
@Query("csrf_token") String token);
}

View file

@ -0,0 +1,401 @@
package org.wikipedia.dataclient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.captcha.Captcha;
import org.wikipedia.dataclient.mwapi.CreateAccountResponse;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.mwapi.SiteMatrix;
import org.wikipedia.dataclient.mwapi.page.MwMobileViewPageLead;
import org.wikipedia.dataclient.mwapi.page.MwMobileViewPageRemaining;
import org.wikipedia.dataclient.mwapi.page.MwQueryPageSummary;
import org.wikipedia.edit.Edit;
import org.wikipedia.edit.preview.EditPreview;
import org.wikipedia.login.LoginClient;
import org.wikipedia.search.PrefixSearchResponse;
import org.wikipedia.wikidata.Entities;
import io.reactivex.Observable;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
/**
* Retrofit service layer for all API interactions, including regular MediaWiki and RESTBase.
*/
public interface Service {
String WIKIPEDIA_URL = "https://wikipedia.org/";
String WIKIDATA_URL = "https://www.wikidata.org/";
String COMMONS_URL = "https://commons.wikimedia.org/";
String META_URL = "https://meta.wikimedia.org/";
String MW_API_PREFIX = "w/api.php?format=json&formatversion=2&errorformat=plaintext&";
String MW_PAGE_SECTIONS_URL = MW_API_PREFIX + "action=mobileview&prop="
+ "text|sections&onlyrequestedsections=1&sections=1-"
+ "&sectionprop=toclevel|line|anchor&noheadings=";
int PREFERRED_THUMB_SIZE = 320;
String OFFLINE_SAVE_HEADER = "X-Offline-Save";
String OFFLINE_SAVE_HEADER_SAVE = "save";
String OFFLINE_SAVE_HEADER_DELETE = "delete";
String OFFLINE_SAVE_HEADER_NONE = "none";
// ------- MobileView page content -------
/**
* Gets the lead section and initial metadata of a given title.
*
* @param title the page title with prefix if necessary
* @return a Retrofit Call which provides the populated MwMobileViewPageLead object in #success
*/
/*
Here's the rationale for this API call:
We request 10 sentences from the lead section, and then re-parse the text using our own
sentence parsing logic to end up with 2 sentences for the link preview. We trust our
parsing logic more than TextExtracts because it's better-tailored to the user's
Locale on the client side. For example, the TextExtracts extension incorrectly treats
abbreviations like "i.e.", "B.C.", "Jr.", etc. as separate sentences, whereas our parser
will leave those alone.
Also, we no longer request "excharacters" from TextExtracts, since it has an issue where
it's liable to return content that lies beyond the lead section, which might include
unparsed wikitext, which we certainly don't want.
*/
@Headers("x-analytics: preview=1")
@GET(MW_API_PREFIX + "action=query&redirects=&converttitles="
+ "&prop=extracts|pageimages|pageprops&exsentences=5&piprop=thumbnail|name"
+ "&pilicense=any&explaintext=&pithumbsize=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryPageSummary> getSummary(@Nullable @Header("Referer") String referrerUrl,
@NonNull @Query("titles") String title,
@Nullable @Query("uselang") String useLang);
/**
* Gets the lead section and initial metadata of a given title.
*
* @param title the page title with prefix if necessary
* @param leadImageWidth one of the bucket widths for the lead image
*/
@Headers("x-analytics: pageview=1")
@GET(MW_API_PREFIX + "action=mobileview&prop="
+ "text|sections|languagecount|thumb|image|id|namespace|revision"
+ "|description|lastmodified|normalizedtitle|displaytitle|protection"
+ "|editable|pageprops&pageprops=wikibase_item"
+ "&sections=0&sectionprop=toclevel|line|anchor&noheadings=")
@NonNull Observable<Response<MwMobileViewPageLead>> getLeadSection(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(OFFLINE_SAVE_HEADER) String saveHeader,
@Nullable @Header("Referer") String referrerUrl,
@NonNull @Query("page") String title,
@Query("thumbwidth") int leadImageWidth,
@Nullable @Query("uselang") String useLang);
/**
* Gets the remaining sections of a given title.
*
* @param title the page title to be used including prefix
*/
@GET(MW_PAGE_SECTIONS_URL)
@NonNull Observable<Response<MwMobileViewPageRemaining>> getRemainingSections(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(OFFLINE_SAVE_HEADER) String saveHeader,
@NonNull @Query("page") String title,
@Nullable @Query("uselang") String useLang);
/**
* TODO: remove this if we find a way to get the request url before the observable object being executed
* Gets the remaining sections request url of a given title.
*
* @param title the page title to be used including prefix
*/
@GET(MW_PAGE_SECTIONS_URL)
@NonNull Call<MwMobileViewPageRemaining> getRemainingSectionsUrl(@Nullable @Header("Cache-Control") String cacheControl,
@Nullable @Header(OFFLINE_SAVE_HEADER) String saveHeader,
@NonNull @Query("page") String title,
@Nullable @Query("uselang") String useLang);
// ------- Search -------
@GET(MW_API_PREFIX + "action=query&prop=pageimages&piprop=thumbnail"
+ "&converttitles=&pilicense=any&pithumbsize=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> getPageImages(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=query&redirects="
+ "&converttitles=&prop=description|pageimages&piprop=thumbnail"
+ "&pilicense=any&generator=prefixsearch&gpsnamespace=0&list=search&srnamespace=0"
+ "&srwhat=text&srinfo=suggestion&srprop=&sroffset=0&srlimit=1&pithumbsize=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<PrefixSearchResponse> prefixSearch(@Query("gpssearch") String title,
@Query("gpslimit") int maxResults,
@Query("srsearch") String repeat);
@GET(MW_API_PREFIX + "action=query&converttitles="
+ "&prop=description|pageimages|pageprops&ppprop=mainpage|disambiguation"
+ "&generator=search&gsrnamespace=0&gsrwhat=text"
+ "&gsrinfo=&gsrprop=redirecttitle&piprop=thumbnail&pilicense=any&pithumbsize="
+ PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> fullTextSearch(@Query("gsrsearch") String searchTerm,
@Query("gsrlimit") int gsrLimit,
@Query("continue") String cont,
@Query("gsroffset") String gsrOffset);
@GET(MW_API_PREFIX + "action=query&prop=coordinates|description|pageimages"
+ "&colimit=50&piprop=thumbnail&pilicense=any"
+ "&generator=geosearch&ggslimit=50&pithumbsize=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> nearbySearch(@NonNull @Query("ggscoord") String coord,
@Query("ggsradius") double radius);
// ------- Miscellaneous -------
@GET(MW_API_PREFIX + "action=fancycaptchareload")
@NonNull Observable<Captcha> getNewCaptcha();
@GET(MW_API_PREFIX + "action=query&prop=langlinks&lllimit=500&redirects=&converttitles=")
@NonNull Observable<MwQueryResponse> getLangLinks(@NonNull @Query("titles") String title);
@GET(MW_API_PREFIX + "action=query&prop=description|pageprops&redirects")
@NonNull Observable<MwQueryResponse> getPagePropsAndDescription(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=query&prop=description")
@NonNull Observable<MwQueryResponse> getDescription(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=query&prop=imageinfo&iiprop=timestamp|user|url|extmetadata&iiurlwidth=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> getImageExtMetadata(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=sitematrix&smtype=language&smlangprop=code|name|localname")
@NonNull Observable<SiteMatrix> getSiteMatrix();
@GET(MW_API_PREFIX + "action=query&meta=siteinfo")
@NonNull Observable<MwQueryResponse> getSiteInfo();
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&generator=random&redirects=1&grnnamespace=0&grnlimit=50&prop=pageprops|description")
@NonNull Observable<MwQueryResponse> getRandomWithPageProps();
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&generator=random&redirects=1&grnnamespace=6&grnlimit=50"
+ "&prop=description|imageinfo&iiprop=timestamp|user|url|mime&iiurlwidth=" + PREFERRED_THUMB_SIZE)
@NonNull Observable<MwQueryResponse> getRandomWithImageInfo();
@GET(MW_API_PREFIX + "action=query&prop=categories&clprop=hidden&cllimit=500")
@NonNull Observable<MwQueryResponse> getCategories(@NonNull @Query("titles") String titles);
@GET(MW_API_PREFIX + "action=query&list=categorymembers&cmlimit=500")
@NonNull Observable<MwQueryResponse> getCategoryMembers(@NonNull @Query("cmtitle") String title,
@Nullable @Query("cmcontinue") String continueStr);
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=thank")
@NonNull Observable<MwPostResponse> thank(@Nullable @Field("rev") String rev,
@Nullable @Field("log") String log,
@NonNull @Field("token") String token,
@Nullable @Field("source") String source);
// ------- CSRF, Login, and Create Account -------
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
@NonNull Call<MwQueryResponse> getCsrfTokenCall();
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
@NonNull Observable<MwQueryResponse> getCsrfToken();
@SuppressWarnings("checkstyle:parameternumber")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=createaccount&createmessageformat=html")
@NonNull Observable<CreateAccountResponse> postCreateAccount(@NonNull @Field("username") String user,
@NonNull @Field("password") String pass,
@NonNull @Field("retype") String retype,
@NonNull @Field("createtoken") String token,
@NonNull @Field("createreturnurl") String returnurl,
@Nullable @Field("email") String email,
@Nullable @Field("captchaId") String captchaId,
@Nullable @Field("captchaWord") String captchaWord);
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=login")
@NonNull Call<MwQueryResponse> getLoginToken();
@Headers("Cache-Control: no-cache")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
@NonNull Call<LoginClient.LoginResponse> postLogIn(@Field("username") String user, @Field("password") String pass,
@Field("logintoken") String token, @Field("loginreturnurl") String url);
@Headers("Cache-Control: no-cache")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=clientlogin&rememberMe=")
@NonNull Call<LoginClient.LoginResponse> postLogIn(@Field("username") String user, @Field("password") String pass,
@Field("retype") String retypedPass, @Field("OATHToken") String twoFactorCode,
@Field("logintoken") String token,
@Field("logincontinue") boolean loginContinue);
@Headers("Cache-Control: no-cache")
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=logout")
@NonNull Observable<MwPostResponse> postLogout(@NonNull @Field("token") String token);
@GET(MW_API_PREFIX + "action=query&meta=authmanagerinfo|tokens&amirequestsfor=create&type=createaccount")
@NonNull Observable<MwQueryResponse> getAuthManagerInfo();
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
@NonNull Observable<MwQueryResponse> getUserInfo(@Query("ususers") @NonNull String userName);
// ------- Notifications -------
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=notifications&notformat=model&notlimit=max")
@NonNull Observable<MwQueryResponse> getAllNotifications(@Query("notwikis") @Nullable String wikiList,
@Query("notfilter") @Nullable String filter,
@Query("notcontinue") @Nullable String continueStr);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=echomarkread")
@NonNull Observable<MwQueryResponse> markRead(@Field("token") @NonNull String token, @Field("list") @Nullable String readList, @Field("unreadlist") @Nullable String unreadList);
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=notifications&notprop=list&notfilter=!read&notlimit=1")
@NonNull Observable<MwQueryResponse> getLastUnreadNotification();
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=unreadnotificationpages&unplimit=max&unpwikis=*")
@NonNull Observable<MwQueryResponse> getUnreadNotificationWikis();
// ------- User Options -------
@GET(MW_API_PREFIX + "action=query&meta=userinfo&uiprop=options")
@NonNull Observable<MwQueryResponse> getUserOptions();
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=options")
@NonNull Observable<MwPostResponse> postUserOption(@Field("token") @NonNull String token,
@Query("optionname") @NonNull String key,
@Query("optionvalue") @Nullable String value);
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=options")
@NonNull Observable<MwPostResponse> deleteUserOption(@Field("token") @NonNull String token,
@Query("change") @NonNull String key);
// ------- Editing -------
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
@NonNull Observable<MwQueryResponse> getWikiTextForSection(@NonNull @Query("titles") String title, @Query("rvsection") int section);
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=parse&prop=text&sectionpreview=&pst=&mobileformat=")
@NonNull Observable<EditPreview> postEditPreview(@NonNull @Field("title") String title,
@NonNull @Field("text") String text);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=edit&nocreate=")
@SuppressWarnings("checkstyle:parameternumber")
@NonNull Call<Edit> postEditSubmit(@NonNull @Field("title") String title,
@Nullable @Field("section") Integer section,
@NonNull @Field("summary") String summary,
@Nullable @Field("assert") String user,
@NonNull @Field("text") String text,
@Nullable @Field("basetimestamp") String baseTimeStamp,
@NonNull @Field("token") String token,
@Nullable @Field("captchaid") String captchaId,
@Nullable @Field("captchaword") String captchaWord);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=edit&nocreate=")
@NonNull Observable<Edit> postAppendEdit(@NonNull @Field("title") String title,
@NonNull @Field("summary") String summary,
@NonNull @Field("appendtext") String text,
@NonNull @Field("token") String token);
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=edit&nocreate=")
@NonNull Observable<Edit> postPrependEdit(@NonNull @Field("title") String title,
@NonNull @Field("summary") String summary,
@NonNull @Field("prependtext") String text,
@NonNull @Field("token") String token);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=tag")
@FormUrlEncoded
Observable<MwPostResponse> addEditTag(@NonNull @Field("revid") String revId,
@NonNull @Field("add") String tagName,
@NonNull @Field("reason") String reason,
@NonNull @Field("token") String token);
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=wikimediaeditortaskscounts")
@NonNull Observable<MwQueryResponse> getEditorTaskCounts();
@GET(MW_API_PREFIX + "action=query&generator=wikimediaeditortaskssuggestions&prop=pageprops&gwetstask=missingdescriptions&gwetslimit=3")
@NonNull Observable<MwQueryResponse> getEditorTaskMissingDescriptions(@NonNull @Query("gwetstarget") String targetLanguage);
@GET(MW_API_PREFIX + "action=query&generator=wikimediaeditortaskssuggestions&prop=pageprops&gwetstask=descriptiontranslations&gwetslimit=3")
@NonNull Observable<MwQueryResponse> getEditorTaskTranslatableDescriptions(@NonNull @Query("gwetssource") String sourceLanguage,
@NonNull @Query("gwetstarget") String targetLanguage);
// ------- Wikidata -------
@GET(MW_API_PREFIX + "action=wbgetentities")
@NonNull Observable<Entities> getEntitiesByTitle(@Query("titles") @NonNull String titles,
@Query("sites") @NonNull String sites);
@GET(MW_API_PREFIX + "action=wbgetentities&props=labels&languagefallback=1")
@NonNull Call<Entities> getWikidataLabels(@Query("ids") @NonNull String idList,
@Query("languages") @NonNull String langList);
@GET(MW_API_PREFIX + "action=wbgetentities&props=descriptions|labels|sitelinks")
@NonNull Observable<Entities> getWikidataLabelsAndDescriptions(@Query("ids") @NonNull String idList);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=wbsetdescription&errorlang=uselang")
@FormUrlEncoded
@SuppressWarnings("checkstyle:parameternumber")
Observable<MwPostResponse> postDescriptionEdit(@NonNull @Field("language") String language,
@NonNull @Field("uselang") String useLang,
@NonNull @Field("site") String site,
@NonNull @Field("title") String title,
@NonNull @Field("value") String newDescription,
@Nullable @Field("summary") String summary,
@NonNull @Field("token") String token,
@Nullable @Field("assert") String user);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=wbsetlabel&errorlang=uselang")
@FormUrlEncoded
@SuppressWarnings("checkstyle:parameternumber")
Observable<MwPostResponse> postLabelEdit(@NonNull @Field("language") String language,
@NonNull @Field("uselang") String useLang,
@NonNull @Field("site") String site,
@NonNull @Field("title") String title,
@NonNull @Field("value") String newDescription,
@Nullable @Field("summary") String summary,
@NonNull @Field("token") String token,
@Nullable @Field("assert") String user);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=wbcreateclaim&errorlang=uselang")
@FormUrlEncoded
Observable<MwPostResponse> postCreateClaim(@NonNull @Field("entity") String entity,
@NonNull @Field("snaktype") String snakType,
@NonNull @Field("property") String property,
@NonNull @Field("value") String value,
@NonNull @Field("uselang") String useLang,
@NonNull @Field("token") String token);
}

View file

@ -0,0 +1,12 @@
package org.wikipedia.dataclient;
import androidx.annotation.NonNull;
/**
* The API reported an error in the payload.
*/
public interface ServiceError {
@NonNull String getTitle();
@NonNull String getDetails();
}

View file

@ -0,0 +1,68 @@
package org.wikipedia.dataclient;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.LruCache;
import org.wikipedia.AppAdapter;
import org.wikipedia.json.GsonUtil;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
public final class ServiceFactory {
private static final int SERVICE_CACHE_SIZE = 8;
private static LruCache<Long, Service> SERVICE_CACHE = new LruCache<>(SERVICE_CACHE_SIZE);
private static LruCache<Long, RestService> REST_SERVICE_CACHE = new LruCache<>(SERVICE_CACHE_SIZE);
public static Service get(@NonNull WikiSite wiki) {
long hashCode = wiki.hashCode();
if (SERVICE_CACHE.get(hashCode) != null) {
return SERVICE_CACHE.get(hashCode);
}
Retrofit r = createRetrofit(wiki, wiki.url() + "/");
Service s = r.create(Service.class);
SERVICE_CACHE.put(hashCode, s);
return s;
}
public static <T> T get(@NonNull WikiSite wiki, Class<T> service) {
return get(wiki, wiki.url() + "/", service);
}
public static <T> T get(@NonNull WikiSite wiki, @Nullable String baseUrl, Class<T> service) {
Retrofit r = createRetrofit(wiki, TextUtils.isEmpty(baseUrl) ? wiki.url() + "/" : baseUrl);
return r.create(service);
}
public static RestService getRest(@NonNull WikiSite wiki) {
long hashCode = wiki.hashCode();
if (REST_SERVICE_CACHE.get(hashCode) != null) {
return REST_SERVICE_CACHE.get(hashCode);
}
Retrofit r = createRetrofit(wiki, TextUtils.isEmpty(AppAdapter.get().getRestbaseUriFormat())
? wiki.url() + "/" + RestService.REST_API_PREFIX
: String.format(AppAdapter.get().getRestbaseUriFormat(), "https", wiki.authority()));
RestService s = r.create(RestService.class);
REST_SERVICE_CACHE.put(hashCode, s);
return s;
}
private static Retrofit createRetrofit(@NonNull WikiSite wiki, @NonNull String baseUrl) {
return new Retrofit.Builder()
.client(AppAdapter.get().getOkHttpClient(wiki))
.baseUrl(baseUrl)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson()))
.build();
}
private ServiceFactory() { }
}

View file

@ -0,0 +1,166 @@
package org.wikipedia.dataclient;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.AppAdapter;
import org.wikipedia.util.log.L;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
public final class SharedPreferenceCookieManager implements CookieJar {
private static final String CENTRALAUTH_PREFIX = "centralauth_";
private static SharedPreferenceCookieManager INSTANCE;
// Map: domain -> list of cookies
private final Map<String, List<Cookie>> cookieJar;
@NonNull
public static SharedPreferenceCookieManager getInstance() {
if (INSTANCE == null) {
try {
INSTANCE = AppAdapter.get().getCookies();
} catch (Exception e) {
L.logRemoteErrorIfProd(e);
}
}
if (INSTANCE == null) {
INSTANCE = new SharedPreferenceCookieManager();
}
return INSTANCE;
}
public SharedPreferenceCookieManager(Map<String, List<Cookie>> cookieJar) {
this.cookieJar = cookieJar;
}
private SharedPreferenceCookieManager() {
cookieJar = new HashMap<>();
}
public Map<String, List<Cookie>> getCookieJar() {
return cookieJar;
}
private void persistCookies() {
AppAdapter.get().setCookies(this);
}
public synchronized void clearAllCookies() {
cookieJar.clear();
persistCookies();
}
@Nullable public synchronized String getCookieByName(@NonNull String name) {
for (String domainSpec: cookieJar.keySet()) {
for (Cookie cookie : cookieJar.get(domainSpec)) {
if (cookie.name().equals(name)) {
return cookie.value();
}
}
}
return null;
}
@Override
public synchronized void saveFromResponse(@NonNull HttpUrl url, @NonNull List<Cookie> cookies) {
if (cookies.isEmpty()) {
return;
}
boolean cookieJarModified = false;
for (Cookie cookie : cookies) {
// Default to the URI's domain if cookie's domain is not explicitly set
String domainSpec = TextUtils.isEmpty(cookie.domain()) ? url.uri().getAuthority() : cookie.domain();
if (!cookieJar.containsKey(domainSpec)) {
cookieJar.put(domainSpec, new ArrayList<>());
}
List<Cookie> cookieList = cookieJar.get(domainSpec);
if (cookie.expiresAt() < System.currentTimeMillis() || "deleted".equals(cookie.value())) {
Iterator<Cookie> i = cookieList.iterator();
while (i.hasNext()) {
if (i.next().name().equals(cookie.name())) {
i.remove();
cookieJarModified = true;
}
}
} else {
Iterator<Cookie> i = cookieList.iterator();
boolean exists = false;
while (i.hasNext()) {
Cookie c = i.next();
if (c.equals(cookie)) {
// an identical cookie already exists, so we don't need to update it.
exists = true;
break;
} else if (c.name().equals(cookie.name())) {
// it's a cookie with the same name, but different contents, so remove the
// current cookie, so that the new one will be added.
i.remove();
}
}
if (!exists) {
cookieList.add(cookie);
cookieJarModified = true;
}
}
}
if (cookieJarModified) {
persistCookies();
}
}
@Override
public synchronized List<Cookie> loadForRequest(@NonNull HttpUrl url) {
List<Cookie> cookieList = new ArrayList<>();
String domain = url.uri().getAuthority();
Log.d("CookieManager", "Domain:" + domain);
for (String domainSpec : cookieJar.keySet()) {
List<Cookie> cookiesForDomainSpec = cookieJar.get(domainSpec);
if (domain.endsWith(domainSpec)) {
buildCookieList(cookieList, cookiesForDomainSpec, null);
} else if (domainSpec.endsWith("commons.wikimedia.org")) {
Log.d("CookieManager", "Adding centralauth cookies");
// For sites outside the wikipedia.org domain, transfer the centralauth cookies
// from commons.wikimedia.org unconditionally.
buildCookieList(cookieList, cookiesForDomainSpec, CENTRALAUTH_PREFIX);
}
}
return cookieList;
}
private void buildCookieList(@NonNull List<Cookie> outList, @NonNull List<Cookie> inList, @Nullable String prefix) {
Iterator<Cookie> i = inList.iterator();
boolean cookieJarModified = false;
while (i.hasNext()) {
Cookie cookie = i.next();
if (prefix != null && !cookie.name().startsWith(prefix)) {
continue;
}
// But wait, is the cookie expired?
if (cookie.expiresAt() < System.currentTimeMillis()) {
i.remove();
cookieJarModified = true;
} else {
outList.add(cookie);
}
}
if (cookieJarModified) {
persistCookies();
}
}
}

View file

@ -0,0 +1,320 @@
package org.wikipedia.dataclient;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.language.AppLanguageLookUpTable;
import org.wikipedia.page.PageTitle;
import org.wikipedia.util.UriUtil;
/**
* The base URL and Wikipedia language code for a MediaWiki site. Examples:
*
* <ul>
* <lh>Name: scheme / authority / language code</lh>
* <li>English Wikipedia: HTTPS / en.wikipedia.org / en</li>
* <li>Chinese Wikipedia: HTTPS / zh.wikipedia.org / zh-hans or zh-hant</li>
* <li>Meta-Wiki: HTTPS / meta.wikimedia.org / (none)</li>
* <li>Test Wikipedia: HTTPS / test.wikipedia.org / test</li>
* <li>Võro Wikipedia: HTTPS / fiu-vro.wikipedia.org / fiu-vro</li>
* <li>Simple English Wikipedia: HTTPS / simple.wikipedia.org / simple</li>
* <li>Simple English Wikipedia (beta cluster mirror): HTTP / simple.wikipedia.beta.wmflabs.org / simple</li>
* <li>Development: HTTP / 192.168.1.11:8080 / (none)</li>
* </ul>
*
* <strong>As shown above, the language code or mapping is part of the authority:</strong>
* <ul>
* <lh>Validity: authority / language code</lh>
* <li>Correct: "test.wikipedia.org" / "test"</li>
* <li>Correct: "wikipedia.org", ""</li>
* <li>Correct: "no.wikipedia.org", "nb"</li>
* <li>Incorrect: "wikipedia.org", "test"</li>
* </ul>
*/
public class WikiSite implements Parcelable {
public static final String DEFAULT_SCHEME = "https";
private static String DEFAULT_BASE_URL = Service.WIKIPEDIA_URL;
public static final Parcelable.Creator<WikiSite> CREATOR = new Parcelable.Creator<WikiSite>() {
@Override
public WikiSite createFromParcel(Parcel in) {
return new WikiSite(in);
}
@Override
public WikiSite[] newArray(int size) {
return new WikiSite[size];
}
};
// todo: remove @SerializedName. this is now in the TypeAdapter and a "uri" case may be added
@SerializedName("domain") @NonNull private final Uri uri;
@NonNull private String languageCode;
public static boolean supportedAuthority(@NonNull String authority) {
return authority.endsWith(Uri.parse(DEFAULT_BASE_URL).getAuthority());
}
public static void setDefaultBaseUrl(@NonNull String url) {
DEFAULT_BASE_URL = TextUtils.isEmpty(url) ? Service.WIKIPEDIA_URL : url;
}
public static WikiSite forLanguageCode(@NonNull String languageCode) {
Uri uri = ensureScheme(Uri.parse(DEFAULT_BASE_URL));
return new WikiSite((languageCode.isEmpty()
? "" : (languageCodeToSubdomain(languageCode) + ".")) + uri.getAuthority(),
languageCode);
}
public WikiSite(@NonNull Uri uri) {
Uri tempUri = ensureScheme(uri);
String authority = tempUri.getAuthority();
if (("wikipedia.org".equals(authority) || "www.wikipedia.org".equals(authority))
&& tempUri.getPath() != null && tempUri.getPath().startsWith("/wiki")) {
// Special case for Wikipedia only: assume English subdomain when none given.
authority = "en.wikipedia.org";
}
String langVariant = UriUtil.getLanguageVariantFromUri(tempUri);
if (!TextUtils.isEmpty(langVariant)) {
languageCode = langVariant;
} else {
languageCode = authorityToLanguageCode(authority);
}
this.uri = new Uri.Builder()
.scheme(tempUri.getScheme())
.encodedAuthority(authority)
.build();
}
public WikiSite(@NonNull String url) {
this(url.startsWith("http") ? Uri.parse(url) : url.startsWith("//")
? Uri.parse(DEFAULT_SCHEME + ":" + url) : Uri.parse(DEFAULT_SCHEME + "://" + url));
}
public WikiSite(@NonNull String authority, @NonNull String languageCode) {
this(authority);
this.languageCode = languageCode;
}
@NonNull
public String scheme() {
return TextUtils.isEmpty(uri.getScheme()) ? DEFAULT_SCHEME : uri.getScheme();
}
/**
* @return The complete wiki authority including language subdomain but not including scheme,
* authentication, port, nor trailing slash.
*
* @see <a href='https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax'>URL syntax</a>
*/
@NonNull
public String authority() {
return uri.getAuthority();
}
/**
* Like {@link #authority()} but with a "m." between the language subdomain and the rest of the host.
* Examples:
*
* <ul>
* <li>English Wikipedia: en.m.wikipedia.org</li>
* <li>Chinese Wikipedia: zh.m.wikipedia.org</li>
* <li>Meta-Wiki: meta.m.wikimedia.org</li>
* <li>Test Wikipedia: test.m.wikipedia.org</li>
* <li>Võro Wikipedia: fiu-vro.m.wikipedia.org</li>
* <li>Simple English Wikipedia: simple.m.wikipedia.org</li>
* <li>Simple English Wikipedia (beta cluster mirror): simple.m.wikipedia.beta.wmflabs.org</li>
* <li>Development: m.192.168.1.11</li>
* </ul>
*/
@NonNull
public String mobileAuthority() {
return authorityToMobile(authority());
}
/**
* @return The canonical "desktop" form of the authority. For example, if the authority
* is in a "mobile" form, e.g. en.m.wikipedia.org, this will become en.wikipedia.org.
*/
@NonNull
public String desktopAuthority() {
return authority().replace(".m.", ".");
}
@NonNull
public String subdomain() {
return languageCodeToSubdomain(languageCode);
}
/**
* @return A path without an authority for the segment including a leading "/".
*/
@NonNull
public String path(@NonNull String segment) {
return "/w/" + segment;
}
@NonNull public Uri uri() {
return uri;
}
/**
* @return The canonical URL. e.g., https://en.wikipedia.org.
*/
@NonNull public String url() {
return uri.toString();
}
/**
* @return The canonical URL for segment. e.g., https://en.wikipedia.org/w/foo.
*/
@NonNull public String url(@NonNull String segment) {
return url() + path(segment);
}
/**
* @return The wiki language code which may differ from the language subdomain. Empty if
* language code is unknown. Ex: "en", "zh-hans", ""
*
* @see AppLanguageLookUpTable
*/
@NonNull
public String languageCode() {
return languageCode;
}
// TODO: this method doesn't have much to do with WikiSite. Move to PageTitle?
/**
* Create a PageTitle object from an internal link string.
*
* @param internalLink Internal link target text (eg. /wiki/Target).
* Should be URL decoded before passing in
* @return A {@link PageTitle} object representing the internalLink passed in.
*/
public PageTitle titleForInternalLink(String internalLink) {
// Strip the /wiki/ from the href
return new PageTitle(UriUtil.removeInternalLinkPrefix(internalLink), this);
}
// TODO: this method doesn't have much to do with WikiSite. Move to PageTitle?
/**
* Create a PageTitle object from a Uri, taking into account any fragment (section title) in the link.
* @param uri Uri object to be turned into a PageTitle.
* @return {@link PageTitle} object that corresponds to the given Uri.
*/
public PageTitle titleForUri(Uri uri) {
String path = uri.getPath();
if (!TextUtils.isEmpty(uri.getFragment())) {
path += "#" + uri.getFragment();
}
return titleForInternalLink(path);
}
@NonNull public String dbName() {
return subdomain().replaceAll("-", "_") + "wiki";
}
// Auto-generated
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
WikiSite wiki = (WikiSite) o;
if (!uri.equals(wiki.uri)) {
return false;
}
return languageCode.equals(wiki.languageCode);
}
// Auto-generated
@Override
public int hashCode() {
int result = uri.hashCode();
result = 31 * result + languageCode.hashCode();
return result;
}
// Auto-generated
@Override
public String toString() {
return "WikiSite{"
+ "uri=" + uri
+ ", languageCode='" + languageCode + '\''
+ '}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(uri, 0);
dest.writeString(languageCode);
}
protected WikiSite(@NonNull Parcel in) {
this.uri = in.readParcelable(Uri.class.getClassLoader());
this.languageCode = in.readString();
}
@NonNull
private static String languageCodeToSubdomain(@NonNull String languageCode) {
switch (languageCode) {
case AppLanguageLookUpTable.SIMPLIFIED_CHINESE_LANGUAGE_CODE:
case AppLanguageLookUpTable.TRADITIONAL_CHINESE_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_CN_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_HK_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_MO_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_SG_LANGUAGE_CODE:
case AppLanguageLookUpTable.CHINESE_TW_LANGUAGE_CODE:
return AppLanguageLookUpTable.CHINESE_LANGUAGE_CODE;
case AppLanguageLookUpTable.NORWEGIAN_BOKMAL_LANGUAGE_CODE:
return AppLanguageLookUpTable.NORWEGIAN_LEGACY_LANGUAGE_CODE; // T114042
default:
return languageCode;
}
}
@NonNull private static String authorityToLanguageCode(@NonNull String authority) {
String[] parts = authority.split("\\.");
final int minLengthForSubdomain = 3;
if (parts.length < minLengthForSubdomain
|| parts.length == minLengthForSubdomain && parts[0].equals("m")) {
// ""
// wikipedia.org
// m.wikipedia.org
return "";
}
return parts[0];
}
@NonNull private static Uri ensureScheme(@NonNull Uri uri) {
if (TextUtils.isEmpty(uri.getScheme())) {
return uri.buildUpon().scheme(DEFAULT_SCHEME).build();
}
return uri;
}
/** @param authority Host and optional port. */
@NonNull private String authorityToMobile(@NonNull String authority) {
if (authority.startsWith("m.") || authority.contains(".m.")) {
return authority;
}
return authority.replaceFirst("^" + subdomain() + "\\.?", "$0m.");
}
}

View file

@ -0,0 +1,42 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class CreateAccountResponse extends MwResponse {
@SuppressWarnings("unused") @Nullable private Result createaccount;
@Nullable public String status() {
return createaccount.status();
}
@Nullable public String user() {
return createaccount.user();
}
@Nullable public String message() {
return createaccount.message();
}
public boolean hasResult() {
return createaccount != null;
}
public static class Result {
@SuppressWarnings("unused,NullableProblems") @NonNull private String status;
@SuppressWarnings("unused") @Nullable private String message;
@SuppressWarnings("unused") @Nullable private String username;
@NonNull public String status() {
return status;
}
@Nullable public String user() {
return username;
}
@Nullable public String message() {
return message;
}
}
}

View file

@ -0,0 +1,113 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.json.GsonUtil;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@SuppressWarnings("unused")
public class EditorTaskCounts {
@Nullable private JsonElement counts;
@Nullable @SerializedName("targets_passed") private JsonElement targetsPassed;
@Nullable private JsonElement targets;
@NonNull
public Map<String, Integer> getDescriptionEditsPerLanguage() {
Map<String, Integer> editsPerLanguage = null;
if (counts != null && !(counts instanceof JsonArray)) {
editsPerLanguage = GsonUtil.getDefaultGson().fromJson(counts, Counts.class).appDescriptionEdits;
}
return editsPerLanguage == null ? Collections.emptyMap() : editsPerLanguage;
}
@NonNull
public List<Integer> getDescriptionEditTargetsPassed() {
List<Integer> passedList = null;
if (targetsPassed != null && !(targetsPassed instanceof JsonArray)) {
passedList = GsonUtil.getDefaultGson().fromJson(targetsPassed, Targets.class).appDescriptionEdits;
}
return passedList == null ? Collections.emptyList() : passedList;
}
public int getDescriptionEditTargetsPassedCount() {
List<Integer> targetList = getDescriptionEditTargets();
List<Integer> passedList = getDescriptionEditTargetsPassed();
int count = 0;
if (!targetList.isEmpty() && !passedList.isEmpty()) {
for (int target : targetList) {
if (passedList.contains(target)) {
count++;
}
}
}
return count;
}
@NonNull
public List<Integer> getDescriptionEditTargets() {
List<Integer> targetList = null;
if (targets != null && !(targets instanceof JsonArray)) {
targetList = GsonUtil.getDefaultGson().fromJson(targets, Targets.class).appDescriptionEdits;
}
return targetList == null ? Collections.emptyList() : targetList;
}
@NonNull
public Map<String, Integer> getCaptionEditsPerLanguage() {
Map<String, Integer> editsPerLanguage = null;
if (counts != null && !(counts instanceof JsonArray)) {
editsPerLanguage = GsonUtil.getDefaultGson().fromJson(counts, Counts.class).appCaptionEdits;
}
return editsPerLanguage == null ? Collections.emptyMap() : editsPerLanguage;
}
@NonNull
public List<Integer> getCaptionEditTargetsPassed() {
List<Integer> passedList = null;
if (targetsPassed != null && !(targetsPassed instanceof JsonArray)) {
passedList = GsonUtil.getDefaultGson().fromJson(targetsPassed, Targets.class).appCaptionEdits;
}
return passedList == null ? Collections.emptyList() : passedList;
}
public int getCaptionEditTargetsPassedCount() {
List<Integer> targetList = getCaptionEditTargets();
List<Integer> passedList = getCaptionEditTargetsPassed();
int count = 0;
if (!targetList.isEmpty() && !passedList.isEmpty()) {
for (int target : targetList) {
if (passedList.contains(target)) {
count++;
}
}
}
return count;
}
@NonNull
public List<Integer> getCaptionEditTargets() {
List<Integer> targetList = null;
if (targets != null && !(targets instanceof JsonArray)) {
targetList = GsonUtil.getDefaultGson().fromJson(targets, Targets.class).appCaptionEdits;
}
return targetList == null ? Collections.emptyList() : targetList;
}
public class Counts {
@Nullable @SerializedName("app_description_edits") private Map<String, Integer> appDescriptionEdits;
@Nullable @SerializedName("app_caption_edits") private Map<String, Integer> appCaptionEdits;
}
public class Targets {
@Nullable @SerializedName("app_description_edits") private List<Integer> appDescriptionEdits;
@Nullable @SerializedName("app_caption_edits") private List<Integer> appCaptionEdits;
}
}

View file

@ -0,0 +1,32 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
@SuppressWarnings("unused")
public class GeoSearchItem {
@Nullable private String title;
@SerializedName("lat") private double latitude;
@SerializedName("lon") private double longitude;
@SerializedName("dist") private double distance;
@NonNull public String getTitle() {
return StringUtils.defaultString(title);
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
public double getDistance() {
return distance;
}
}

View file

@ -0,0 +1,17 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
public class ImageDetails {
@SuppressWarnings("unused") private String name;
@SuppressWarnings("unused") private String title;
@NonNull public String getName() {
return name;
}
@NonNull public String getTitle() {
return title;
}
}

View file

@ -0,0 +1,44 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@SuppressWarnings("unused")
public class ListUserResponse {
@SerializedName("name") @Nullable private String name;
private long userid;
@Nullable private List<String> groups;
@Nullable private String cancreate;
@Nullable private List<UserResponseCreateError> cancreateerror;
@Nullable public String name() {
return name;
}
public boolean canCreate() {
return cancreate != null;
}
@NonNull public Set<String> getGroups() {
return groups != null ? new ArraySet<>(groups) : Collections.emptySet();
}
public static class UserResponseCreateError {
@Nullable private String message;
@Nullable private String code;
@Nullable private String type;
@NonNull public String message() {
return StringUtils.defaultString(message);
}
}
}

View file

@ -0,0 +1,47 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.model.BaseModel;
import java.util.List;
import java.util.Map;
class MwAuthManagerInfo extends BaseModel {
@SuppressWarnings("unused,NullableProblems") @NonNull private List<Request> requests;
@NonNull List<Request> requests() {
return requests;
}
static class Request {
@SuppressWarnings("unused,NullableProblems") @NonNull private String id;
@SuppressWarnings("unused,NullableProblems") @NonNull private Map<String, String> metadata;
@SuppressWarnings("unused,NullableProblems") @NonNull private String required;
@SuppressWarnings("unused,NullableProblems") @NonNull private String provider;
@SuppressWarnings("unused,NullableProblems") @NonNull private String account;
@SuppressWarnings("unused,NullableProblems") @NonNull private Map<String, Field> fields;
@NonNull String id() {
return id;
}
@NonNull Map<String, Field> fields() {
return fields;
}
}
static class Field {
@SuppressWarnings("unused") @Nullable private String type;
@SuppressWarnings("unused") @Nullable private String value;
@SuppressWarnings("unused") @Nullable private String label;
@SuppressWarnings("unused") @Nullable private String help;
@SuppressWarnings("unused") private boolean optional;
@SuppressWarnings("unused") private boolean sensitive;
@Nullable String value() {
return value;
}
}
}

View file

@ -0,0 +1,24 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class MwException extends RuntimeException {
@SuppressWarnings("unused") @NonNull private final MwServiceError error;
public MwException(@NonNull MwServiceError error) {
this.error = error;
}
@NonNull public MwServiceError getError() {
return error;
}
@Nullable public String getTitle() {
return error.getTitle();
}
@Override @Nullable public String getMessage() {
return error.getDetails();
}
}

View file

@ -0,0 +1,21 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.Nullable;
public class MwPostResponse extends MwResponse {
@Nullable @SuppressWarnings("unused") private String options;
@SuppressWarnings("unused") private int success;
public boolean success(@Nullable String result) {
return "success".equals(result);
}
@Nullable public String getOptions() {
return options;
}
public int getSuccessVal() {
return success;
}
}

View file

@ -0,0 +1,107 @@
package org.wikipedia.dataclient.mwapi;
import org.wikipedia.model.BaseModel;
import org.wikipedia.util.DateUtil;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
@SuppressWarnings("unused")
public class MwQueryLogEvent extends BaseModel {
private int logid;
private int ns;
private int index;
private String title;
private int pageid;
private Params params;
private String type;
private String action;
private String user;
private int userid;
private String timestamp;
private String comment;
private String parsedcomment;
private List<String> tags;
public int logid() {
return logid;
}
public int ns() {
return ns;
}
public int index() {
return index;
}
public String title() {
return title;
}
public int pageid() {
return pageid;
}
public String type() {
return type;
}
public String action() {
return action;
}
public String user() {
return user;
}
public int userid() {
return userid;
}
public String timestamp() {
return timestamp;
}
public Date date(){
try {
return DateUtil.iso8601DateParse(timestamp);
} catch (ParseException e) {
return null;
}
}
public String comment() {
return comment;
}
public String parsedcomment() {
return parsedcomment;
}
public List<String> tags() {
return tags;
}
public boolean isDeleted() {
return pageid==0;
}
public Params params() {
return params;
}
public static class Params{
private String img_sha1;
private String img_timestamp;
public String img_sha1() {
return img_sha1;
}
public String img_timestamp() {
return img_timestamp;
}
}
}

View file

@ -0,0 +1,237 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.gallery.ImageInfo;
import org.wikipedia.gallery.VideoInfo;
import org.wikipedia.model.BaseModel;
import org.wikipedia.page.Namespace;
import java.util.Collections;
import java.util.List;
/**
* A class representing a standard page object as returned by the MediaWiki API.
*/
public class MwQueryPage extends BaseModel {
@SuppressWarnings("unused") private int pageid;
@SuppressWarnings("unused") private int ns;
@SuppressWarnings("unused") private int index;
@SuppressWarnings("unused,NullableProblems") @NonNull private String title;
@SuppressWarnings("unused") @Nullable private List<LangLink> langlinks;
@SuppressWarnings("unused") @Nullable private List<Revision> revisions;
@SuppressWarnings("unused") @Nullable private List<Coordinates> coordinates;
@SuppressWarnings("unused") @Nullable private List<Category> categories;
@SuppressWarnings("unused") @Nullable private PageProps pageprops;
@SuppressWarnings("unused") @Nullable private PageTerms terms;
@SuppressWarnings("unused") @Nullable private String extract;
@SuppressWarnings("unused") @Nullable private Thumbnail thumbnail;
@SuppressWarnings("unused") @Nullable private String description;
@SuppressWarnings("unused") @SerializedName("descriptionsource") @Nullable private String descriptionSource;
@SuppressWarnings("unused") @SerializedName("imageinfo") @Nullable private List<ImageInfo> imageInfo;
@SuppressWarnings("unused") @SerializedName("videoinfo") @Nullable private List<VideoInfo> videoInfo;
@Nullable private String redirectFrom;
@Nullable private String convertedFrom;
@Nullable private String convertedTo;
@NonNull public String title() {
return title;
}
public int index() {
return index;
}
@NonNull public Namespace namespace() {
return Namespace.of(ns);
}
@Nullable public List<LangLink> langLinks() {
return langlinks;
}
@Nullable public List<Revision> revisions() {
return revisions;
}
@Nullable public List<Category> categories() {
return categories;
}
@Nullable public List<Coordinates> coordinates() {
// TODO: Handle null values in lists during deserialization, perhaps with a new
// @RequiredElements annotation and corresponding TypeAdapter
if (coordinates != null) {
coordinates.removeAll(Collections.singleton(null));
}
return coordinates;
}
public List<String> labels() {
return terms != null && terms.label != null ? terms.label : Collections.emptyList();
}
public int pageId() {
return pageid;
}
@Nullable public PageProps pageProps() {
return pageprops;
}
@Nullable public String extract() {
return extract;
}
@Nullable public String thumbUrl() {
return thumbnail != null ? thumbnail.source() : null;
}
@Nullable public String description() {
return description;
}
@Nullable
public String descriptionSource() {
return descriptionSource;
}
@Nullable public ImageInfo imageInfo() {
return imageInfo != null ? imageInfo.get(0) : null;
}
@Nullable public VideoInfo videoInfo() {
return videoInfo != null ? videoInfo.get(0) : null;
}
@Nullable public String redirectFrom() {
return redirectFrom;
}
public void redirectFrom(@Nullable String from) {
redirectFrom = from;
}
@Nullable public String convertedFrom() {
return convertedFrom;
}
public void convertedFrom(@Nullable String from) {
convertedFrom = from;
}
@Nullable public String convertedTo() {
return convertedTo;
}
public void convertedTo(@Nullable String to) {
convertedTo = to;
}
public void appendTitleFragment(@Nullable String fragment) {
title += "#" + fragment;
}
public static class Revision {
@SerializedName("revid") private long revisionId;
private String user;
@SuppressWarnings("unused,NullableProblems") @SerializedName("contentformat") @NonNull private String contentFormat;
@SuppressWarnings("unused,NullableProblems") @SerializedName("contentmodel") @NonNull private String contentModel;
@SuppressWarnings("unused,NullableProblems") @SerializedName("timestamp") @NonNull private String timeStamp;
@SuppressWarnings("unused,NullableProblems") @NonNull private String content;
@NonNull public String content() {
return content;
}
@NonNull public String timeStamp() {
return StringUtils.defaultString(timeStamp);
}
public long getRevisionId() {
return revisionId;
}
@NonNull
public String getUser() {
return StringUtils.defaultString(user);
}
}
public static class LangLink {
@SuppressWarnings("unused,NullableProblems") @NonNull private String lang;
@NonNull public String lang() {
return lang;
}
@SuppressWarnings("unused,NullableProblems") @NonNull private String title;
@NonNull public String title() {
return title;
}
}
public static class Coordinates {
@SuppressWarnings("unused") @Nullable private Double lat;
@SuppressWarnings("unused") @Nullable private Double lon;
@Nullable public Double lat() {
return lat;
}
@Nullable public Double lon() {
return lon;
}
}
static class Thumbnail {
@SuppressWarnings("unused") private String source;
@SuppressWarnings("unused") private int width;
@SuppressWarnings("unused") private int height;
String source() {
return source;
}
}
public static class PageProps {
@SuppressWarnings("unused") @SerializedName("wikibase_item") @Nullable private String wikiBaseItem;
@SuppressWarnings("unused") @Nullable private String displaytitle;
@SuppressWarnings("unused") @Nullable private String disambiguation;
@Nullable public String getDisplayTitle() {
return displaytitle;
}
@NonNull public String getWikiBaseItem() {
return StringUtils.defaultString(wikiBaseItem);
}
public boolean isDisambiguation() {
return disambiguation != null;
}
}
public static class Category {
@SuppressWarnings("unused") private int ns;
@SuppressWarnings("unused,NullableProblems") @Nullable private String title;
@SuppressWarnings("unused") private boolean hidden;
public int ns() {
return ns;
}
@NonNull public String title() {
return StringUtils.defaultString(title);
}
public boolean hidden() {
return hidden;
}
}
public static class PageTerms {
@SuppressWarnings("unused") private List<String> alias;
@SuppressWarnings("unused") private List<String> label;
}
}

View file

@ -0,0 +1,37 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
public class MwQueryResponse extends MwResponse {
@SuppressWarnings("unused") @SerializedName("batchcomplete") private boolean batchComplete;
@SuppressWarnings("unused") @SerializedName("continue") @Nullable private Map<String, String> continuation;
@Nullable private MwQueryResult query;
public boolean batchComplete() {
return batchComplete;
}
@Nullable public Map<String, String> continuation() {
return continuation;
}
@Nullable public MwQueryResult query() {
return query;
}
public boolean success() {
return query != null;
}
@VisibleForTesting protected void setQuery(@Nullable MwQueryResult query) {
this.query = query;
}
}

View file

@ -0,0 +1,314 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.gallery.ImageInfo;
import org.wikipedia.gallery.VideoInfo;
import org.wikipedia.json.PostProcessingTypeAdapter;
import org.wikipedia.model.BaseModel;
import org.wikipedia.notifications.Notification;
import org.wikipedia.page.PageTitle;
import org.wikipedia.settings.SiteInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SuppressWarnings("unused")
public class MwQueryResult extends BaseModel implements PostProcessingTypeAdapter.PostProcessable {
@Nullable private List<MwQueryPage> pages;
@Nullable private List<Redirect> redirects;
@Nullable private List<ConvertedTitle> converted;
@SerializedName("userinfo") private UserInfo userInfo;
@Nullable private List<ListUserResponse> users;
@Nullable private Tokens tokens;
@SerializedName("authmanagerinfo") @Nullable private MwAuthManagerInfo amInfo;
@Nullable private MarkReadResponse echomarkread;
@Nullable private MarkReadResponse echomarkseen;
@Nullable private NotificationList notifications;
@Nullable private Map<String, Notification.UnreadNotificationWikiItem> unreadnotificationpages;
@SerializedName("general") @Nullable private SiteInfo generalSiteInfo;
@Nullable private List<RecentChange> recentchanges;
@SerializedName("wikimediaeditortaskscounts") @Nullable private EditorTaskCounts editorTaskCounts;
@SerializedName("allimages") @Nullable private List<ImageDetails> allImages;
@SerializedName("geosearch") @Nullable private List<GeoSearchItem> geoSearch;
@Nullable private List<MwQueryLogEvent> logevents;
@Nullable public List<MwQueryPage> pages() {
return pages;
}
@Nullable public MwQueryPage firstPage() {
if (pages != null && pages.size() > 0) {
return pages.get(0);
}
return null;
}
@NonNull
public List<ImageDetails> allImages() {
return allImages == null ? Collections.emptyList() : allImages;
}
@NonNull
public List<GeoSearchItem> geoSearch() {
return geoSearch == null ? Collections.emptyList() : geoSearch;
}
@Nullable public UserInfo userInfo() {
return userInfo;
}
@Nullable public String csrfToken() {
return tokens != null ? tokens.csrf() : null;
}
@Nullable public String createAccountToken() {
return tokens != null ? tokens.createAccount() : null;
}
@Nullable public String loginToken() {
return tokens != null ? tokens.login() : null;
}
@Nullable public NotificationList notifications() {
return notifications;
}
@Nullable public Map<String, Notification.UnreadNotificationWikiItem> unreadNotificationWikis() {
return unreadnotificationpages;
}
@Nullable public MarkReadResponse getEchoMarkSeen() {
return echomarkseen;
}
@Nullable public String captchaId() {
String captchaId = null;
if (amInfo != null) {
for (MwAuthManagerInfo.Request request : amInfo.requests()) {
if ("CaptchaAuthenticationRequest".equals(request.id())) {
captchaId = request.fields().get("captchaId").value();
}
}
}
return captchaId;
}
@Nullable public List<RecentChange> getRecentChanges() {
return recentchanges;
}
@Nullable public ListUserResponse getUserResponse(@NonNull String userName) {
if (users != null) {
for (ListUserResponse user : users) {
// MediaWiki user names are case sensitive, but the first letter is always capitalized.
if (StringUtils.capitalize(userName).equals(user.name())) {
return user;
}
}
}
return null;
}
@NonNull public Map<String, ImageInfo> images() {
Map<String, ImageInfo> result = new HashMap<>();
if (pages != null) {
for (MwQueryPage page : pages) {
if (page.imageInfo() != null) {
result.put(page.title(), page.imageInfo());
}
}
}
return result;
}
@NonNull public Map<String, VideoInfo> videos() {
Map<String, VideoInfo> result = new HashMap<>();
if (pages != null) {
for (MwQueryPage page : pages) {
if (page.videoInfo() != null) {
result.put(page.title(), page.videoInfo());
}
}
}
return result;
}
@NonNull public List<PageTitle> langLinks() {
List<PageTitle> result = new ArrayList<>();
if (pages == null || pages.isEmpty() || pages.get(0).langLinks() == null) {
return result;
}
// noinspection ConstantConditions
for (MwQueryPage.LangLink link : pages.get(0).langLinks()) {
PageTitle title = new PageTitle(link.title(), WikiSite.forLanguageCode(link.lang()));
result.add(title);
}
return result;
}
@NonNull public List<NearbyPage> nearbyPages(@NonNull WikiSite wiki) {
List<NearbyPage> result = new ArrayList<>();
if (pages != null) {
for (MwQueryPage page : pages) {
NearbyPage nearbyPage = new NearbyPage(page, wiki);
if (nearbyPage.getLocation() != null) {
result.add(nearbyPage);
}
}
}
return result;
}
@Nullable public SiteInfo siteInfo() {
return generalSiteInfo;
}
@Nullable public EditorTaskCounts editorTaskCounts() {
return editorTaskCounts;
}
@Override
public void postProcess() {
resolveConvertedTitles();
resolveRedirectedTitles();
}
private void resolveRedirectedTitles() {
if (redirects == null || pages == null) {
return;
}
for (MwQueryPage page : pages) {
for (MwQueryResult.Redirect redirect : redirects) {
// TODO: Looks like result pages and redirects can also be matched on the "index"
// property. Confirm in the API docs and consider updating.
if (page.title().equals(redirect.to())) {
page.redirectFrom(redirect.from());
if (redirect.toFragment() != null) {
page.appendTitleFragment(redirect.toFragment());
}
}
}
}
}
private void resolveConvertedTitles() {
if (converted == null || pages == null) {
return;
}
// noinspection ConstantConditions
for (MwQueryResult.ConvertedTitle convertedTitle : converted) {
// noinspection ConstantConditions
for (MwQueryPage page : pages) {
if (page.title().equals(convertedTitle.to())) {
page.convertedFrom(convertedTitle.from());
page.convertedTo(convertedTitle.to());
}
}
}
}
@Nullable
public List<MwQueryLogEvent> logevents() {
return logevents;
}
private static class Redirect {
@SuppressWarnings("unused") private int index;
@SuppressWarnings("unused") @Nullable private String from;
@SuppressWarnings("unused") @Nullable private String to;
@SuppressWarnings("unused") @SerializedName("tofragment") @Nullable private String toFragment;
@Nullable public String to() {
return to;
}
@Nullable public String from() {
return from;
}
@Nullable public String toFragment() {
return toFragment;
}
}
public static class ConvertedTitle {
@SuppressWarnings("unused") @Nullable private String from;
@SuppressWarnings("unused") @Nullable private String to;
@Nullable public String to() {
return to;
}
@Nullable public String from() {
return from;
}
}
private static class Tokens {
@SuppressWarnings("unused,NullableProblems") @SerializedName("csrftoken")
@Nullable private String csrf;
@SuppressWarnings("unused,NullableProblems") @SerializedName("createaccounttoken")
@Nullable private String createAccount;
@SuppressWarnings("unused,NullableProblems") @SerializedName("logintoken")
@Nullable private String login;
@Nullable private String csrf() {
return csrf;
}
@Nullable private String createAccount() {
return createAccount;
}
@Nullable private String login() {
return login;
}
}
public static class MarkReadResponse {
@SuppressWarnings("unused") @Nullable private String result;
@SuppressWarnings("unused,NullableProblems") @Nullable private String timestamp;
@Nullable public String getResult() {
return result;
}
@Nullable public String getTimestamp() {
return timestamp;
}
}
public static class NotificationList {
@SuppressWarnings("unused") private int count;
@SuppressWarnings("unused") private int rawcount;
@SuppressWarnings("unused") @Nullable private Notification.SeenTime seenTime;
@SuppressWarnings("unused") @Nullable private List<Notification> list;
@SuppressWarnings("unused") @SerializedName("continue") @Nullable private String continueStr;
@Nullable public List<Notification> list() {
return list;
}
@Nullable public String getContinue() {
return continueStr;
}
public int getCount() {
return count;
}
@Nullable public Notification.SeenTime getSeenTime() {
return seenTime;
}
}
}

View file

@ -0,0 +1,23 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.json.PostProcessingTypeAdapter;
import org.wikipedia.model.BaseModel;
import java.util.List;
public abstract class MwResponse extends BaseModel implements PostProcessingTypeAdapter.PostProcessable {
@SuppressWarnings({"unused"}) @Nullable private List<MwServiceError> errors;
@SuppressWarnings("unused,NullableProblems") @SerializedName("servedby") @NonNull private String servedBy;
@Override
public void postProcess() {
if (errors != null && !errors.isEmpty()) {
throw new MwException(errors.get(0));
}
}
}

View file

@ -0,0 +1,74 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.ServiceError;
import org.wikipedia.model.BaseModel;
import java.util.List;
/**
* Gson POJO for a MediaWiki API error.
*/
public class MwServiceError extends BaseModel implements ServiceError {
@SuppressWarnings("unused") @Nullable private String code;
@SuppressWarnings("unused") @Nullable private String text;
@SuppressWarnings("unused") @Nullable private Data data;
@Override @NonNull public String getTitle() {
return StringUtils.defaultString(code);
}
@Override @NonNull public String getDetails() {
return StringUtils.defaultString(text);
}
public boolean badToken() {
return "badtoken".equals(code);
}
public boolean badLoginState() {
return "assertuserfailed".equals(code);
}
public boolean hasMessageName(@NonNull String messageName) {
if (data != null && data.messages() != null) {
for (Message msg : data.messages()) {
if (messageName.equals(msg.name)) {
return true;
}
}
}
return false;
}
@Nullable public String getMessageHtml(@NonNull String messageName) {
if (data != null && data.messages() != null) {
for (Message msg : data.messages()) {
if (messageName.equals(msg.name)) {
return msg.html();
}
}
}
return null;
}
private static final class Data {
@SuppressWarnings("unused") @Nullable private List<Message> messages;
@Nullable private List<Message> messages() {
return messages;
}
}
private static final class Message {
@SuppressWarnings("unused") @Nullable private String name;
@SuppressWarnings("unused") @Nullable private String html;
@NonNull private String html() {
return StringUtils.defaultString(html);
}
}
}

View file

@ -0,0 +1,67 @@
package org.wikipedia.dataclient.mwapi;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.PageTitle;
import java.util.List;
public class NearbyPage {
@NonNull private PageTitle title;
@Nullable private Location location;
/** calculated externally */
private int distance;
public NearbyPage(@NonNull MwQueryPage page, @NonNull WikiSite wiki) {
title = new PageTitle(page.title(), wiki);
title.setThumbUrl(page.thumbUrl());
List<MwQueryPage.Coordinates> coordinates = page.coordinates();
if (coordinates == null || coordinates.isEmpty()) {
return;
}
if (coordinates.get(0).lat() != null && coordinates.get(0).lon() != null) {
location = new Location(title.getPrefixedText());
location.setLatitude(coordinates.get(0).lat());
location.setLongitude(coordinates.get(0).lon());
}
}
public NearbyPage(@NonNull PageTitle title, @Nullable Location location) {
this.title = title;
this.location = location;
}
@NonNull public PageTitle getTitle() {
return title;
}
@Nullable public Location getLocation() {
return location;
}
@Override public String toString() {
return "NearbyPage{"
+ "title='" + title + '\''
+ ", thumbUrl='" + title.getThumbUrl() + '\''
+ ", location=" + location + '\''
+ ", distance='" + distance
+ '}';
}
/**
* Returns the distance from the point where the device is.
* Calculated later and can change. Needs to be set first by #setDistance!
*/
public int getDistance() {
return distance;
}
public void setDistance(int distance) {
this.distance = distance;
}
}

View file

@ -0,0 +1,42 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
@SuppressWarnings("unused")
public class RecentChange {
@Nullable private String type;
@Nullable private String title;
private long pageid;
private long revid;
@SerializedName("old_revid") private long oldRevisionId;
@Nullable private String timestamp;
@NonNull public String getType() {
return StringUtils.defaultString(type);
}
@NonNull public String getTitle() {
return StringUtils.defaultString(title);
}
public long getPageId() {
return pageid;
}
public long getRevId() {
return revid;
}
public long getOldRevisionId() {
return oldRevisionId;
}
public String getTimestamp() {
return StringUtils.defaultString(timestamp);
}
}

View file

@ -0,0 +1,60 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import com.google.gson.JsonObject;
import org.wikipedia.json.GsonUtil;
import java.util.ArrayList;
import java.util.List;
public class SiteMatrix extends MwResponse {
@SuppressWarnings("unused,NullableProblems") @NonNull private JsonObject sitematrix;
public JsonObject siteMatrix() {
return sitematrix;
}
@SuppressWarnings("unused,NullableProblems")
public class SiteInfo {
@NonNull
private String code;
@NonNull
private String name;
@NonNull
private String localname;
@NonNull
public String code() {
return code;
}
@NonNull
public String name() {
return name;
}
@NonNull
public String localName() {
return localname;
}
}
public static List<SiteInfo> getSites(@NonNull SiteMatrix siteMatrix) {
List<SiteInfo> sites = new ArrayList<>();
// We have to parse the Json manually because the list of SiteInfo objects
// contains a "count" member that prevents it from being able to deserialize
// as a list automatically.
for (String key : siteMatrix.siteMatrix().keySet()) {
if (key.equals("count")) {
continue;
}
SiteInfo info = GsonUtil.getDefaultGson().fromJson(siteMatrix.siteMatrix().get(key), SiteInfo.class);
if (info != null) {
sites.add(info);
}
}
return sites;
}
}

View file

@ -0,0 +1,84 @@
package org.wikipedia.dataclient.mwapi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings("unused")
public class UserInfo {
@NonNull
private String name;
@NonNull
private int id;
//Block information
private int blockid;
private String blockedby;
private int blockedbyid;
private String blockreason;
private String blocktimestamp;
private String blockexpiry;
// Object type is any JSON type.
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
@Nullable
private Map<String, ?> options;
public int id() {
return id;
}
@NonNull
public Map<String, String> userjsOptions() {
Map<String, String> map = new HashMap<>();
if (options != null) {
for (Map.Entry<String, ?> entry : options.entrySet()) {
if (entry.getKey().startsWith("userjs-")) {
// T161866 entry.valueOf() should always return a String but doesn't
map.put(entry.getKey(), entry.getValue() == null ? "" : String.valueOf(entry.getValue()));
}
}
}
return map;
}
@NonNull
public int blockid() {
return blockid;
}
@NonNull
public String blockedby() {
if (blockedby != null)
return blockedby;
else return "";
}
@NonNull
public int blockedbyid() {
return blockedbyid;
}
@NonNull
public String blockreason() {
if (blockreason != null)
return blockreason;
else return "";
}
@NonNull
public String blocktimestamp() {
if (blocktimestamp != null)
return blocktimestamp;
else return "";
}
@NonNull
public String blockexpiry() {
if (blockexpiry != null)
return blockexpiry;
else return "";
}
}

View file

@ -0,0 +1,281 @@
package org.wikipedia.dataclient.mwapi.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.dataclient.mwapi.MwResponse;
import org.wikipedia.dataclient.page.PageLead;
import org.wikipedia.dataclient.page.PageLeadProperties;
import org.wikipedia.dataclient.page.Protection;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.Page;
import org.wikipedia.page.PageProperties;
import org.wikipedia.page.PageTitle;
import org.wikipedia.page.Section;
import org.wikipedia.util.StringUtil;
import org.wikipedia.util.UriUtil;
import java.util.Collections;
import java.util.List;
import static org.wikipedia.dataclient.Service.PREFERRED_THUMB_SIZE;
import static org.wikipedia.util.ImageUrlUtil.getUrlForSize;
/**
* Gson POJO for loading the first stage of page content.
*/
public class MwMobileViewPageLead extends MwResponse implements PageLead {
@SuppressWarnings("unused") private Mobileview mobileview;
/** Note: before using this check that #getMobileview != null */
@Override
public Page toPage(@NonNull PageTitle title) {
return new Page(adjustPageTitle(title, title.getPrefixedText()),
mobileview.getSections(),
mobileview.toPageProperties(),
false);
}
private PageTitle adjustPageTitle(@NonNull PageTitle title, @NonNull String originalPrefixedText) {
if (mobileview.getRedirected() != null) {
// Handle redirects properly.
title = new PageTitle(mobileview.getRedirected(), title.getWikiSite(),
title.getThumbUrl());
} else if (mobileview.getNormalizedTitle() != null) {
// We care about the normalized title only if we were not redirected
title = new PageTitle(mobileview.getNormalizedTitle(), title.getWikiSite(),
title.getThumbUrl());
}
if (mobileview.getDisplayTitle() != null
&& !StringUtil.removeHTMLTags(title.getDisplayText()).equals(StringUtil.removeHTMLTags(mobileview.getDisplayTitle()))) {
title = new PageTitle(StringUtil.removeHTMLTags(mobileview.getDisplayTitle()), title.getWikiSite(),
title.getThumbUrl());
}
if (mobileview.getDisplayTitle() != null
&& !mobileview.getDisplayTitle().equals(originalPrefixedText)
&& mobileview.getNormalizedTitle() == null) {
// Sometimes the MW api will not give us the "converted" or "redirected" title if switching between Chinese variants
// Ticket: https://phabricator.wikimedia.org/T206891#4672777
// We can the original prefixed title text (the one we used for calling API) to build the PageTitle
title = new PageTitle(originalPrefixedText, title.getWikiSite(), title.getThumbUrl());
}
if (mobileview.getRedirected() != null) {
title.setConvertedText(mobileview.getRedirected());
}
title.setDescription(mobileview.getDescription());
return title;
}
@Override @NonNull public String getLeadSectionContent() {
if (mobileview != null) {
return mobileview.getSections().get(0).getContent();
}
return "";
}
@Nullable
@Override
public String getTitlePronunciationUrl() {
return null;
}
@Nullable @Override public String getLeadImageUrl(int leadImageWidth) {
return mobileview == null ? null : mobileview.getLeadImageUrl(leadImageWidth);
}
@Nullable @Override public String getThumbUrl() {
return mobileview == null ? null : mobileview.getThumbUrl();
}
@Nullable @Override public String getDescription() {
return mobileview == null ? null : mobileview.getDescription();
}
@Nullable
@Override
public Location getGeo() {
return null;
}
@VisibleForTesting
public Mobileview getMobileview() {
return mobileview;
}
/**
* Almost everything is in this inner class.
*/
public static class Mobileview implements PageLeadProperties {
@SuppressWarnings("unused") private int id;
@SuppressWarnings("unused") private int namespace;
@SuppressWarnings("unused") private long revision;
@SuppressWarnings("unused") @Nullable private String lastmodified;
@SuppressWarnings("unused") @Nullable private String displaytitle;
@SuppressWarnings("unused") @Nullable private String redirected;
@SuppressWarnings("unused") @Nullable private String normalizedtitle;
@SuppressWarnings("unused") private int languagecount;
@SuppressWarnings("unused") private boolean editable;
@SuppressWarnings("unused") private boolean mainpage;
@SuppressWarnings("unused") private boolean disambiguation;
@SuppressWarnings("unused") @Nullable private String description;
@SuppressWarnings("unused") @Nullable private String descriptionsource;
@SuppressWarnings("unused") @SerializedName("image") @Nullable private PageImage pageImage;
@SuppressWarnings("unused") @SerializedName("thumb") @Nullable private PageImageThumb leadImage;
@SuppressWarnings("unused") @Nullable private Protection protection;
@SuppressWarnings("unused") @Nullable private List<Section> sections;
@SuppressWarnings("unused") @Nullable private MwQueryPage.PageProps pageprops;
/** Converter */
public PageProperties toPageProperties() {
return new PageProperties(this);
}
@Override
public int getId() {
return id;
}
@Override @NonNull public Namespace getNamespace() {
return Namespace.of(namespace);
}
@Override
public long getRevision() {
return revision;
}
@Override
@Nullable
public String getLastModified() {
return lastmodified;
}
@Override
public int getLanguageCount() {
return languagecount;
}
@Override
@Nullable
public String getDisplayTitle() {
return displaytitle;
}
@Override
@Nullable
public String getTitlePronunciationUrl() {
return null;
}
@Override
@Nullable
public Location getGeo() {
return null;
}
@Override
@Nullable
public String getRedirected() {
return redirected;
}
@Override
@Nullable
public String getNormalizedTitle() {
return normalizedtitle;
}
@Nullable
public String getDescription() {
return description;
}
@Override
@Nullable
public String getLeadImageUrl(int leadImageWidth) {
return leadImage != null ? leadImage.getUrl() : null;
}
@Override
@Nullable
public String getThumbUrl() {
return leadImage != null ? UriUtil.resolveProtocolRelativeUrl(getUrlForSize(leadImage.getUrl(), PREFERRED_THUMB_SIZE)) : null;
}
@Override
@Nullable
public String getLeadImageFileName() {
return pageImage != null ? pageImage.getFileName() : null;
}
@Override
@Nullable
public String getWikiBaseItem() {
return pageprops != null && pageprops.getWikiBaseItem() != null ? pageprops.getWikiBaseItem() : null;
}
@Override
@Nullable
public String getDescriptionSource() {
return descriptionsource;
}
@Override
@Nullable
public String getFirstAllowedEditorRole() {
return protection != null ? protection.getFirstAllowedEditorRole() : null;
}
@Override
public boolean isEditable() {
return editable;
}
@Override
public boolean isMainPage() {
return mainpage;
}
@Override
public boolean isDisambiguation() {
return disambiguation;
}
@Override @NonNull public List<Section> getSections() {
return sections == null ? Collections.emptyList() : sections;
}
}
/**
* For the lead image File: page name
*/
public static class PageImage {
@SuppressWarnings("unused") @SerializedName("file") private String fileName;
public String getFileName() {
return fileName;
}
}
/**
* For the lead image URL
*/
public static class PageImageThumb {
@SuppressWarnings("unused") private String url;
public String getUrl() {
return url;
}
}
}

View file

@ -0,0 +1,21 @@
package org.wikipedia.dataclient.mwapi.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.page.PageRemaining;
import org.wikipedia.page.Section;
import java.util.Collections;
import java.util.List;
/**
* Gson POJO for loading remaining page content.
*/
public class MwMobileViewPageRemaining implements PageRemaining {
@SuppressWarnings("unused") @Nullable private MwMobileViewPageLead.Mobileview mobileview;
@NonNull @Override public List<Section> sections() {
return mobileview == null ? Collections.emptyList() : mobileview.getSections();
}
}

View file

@ -0,0 +1,54 @@
package org.wikipedia.dataclient.mwapi.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.page.PageClient;
import org.wikipedia.dataclient.page.PageSummary;
import io.reactivex.Observable;
import okhttp3.CacheControl;
import okhttp3.Request;
import retrofit2.Response;
/**
* Retrofit web service client for MediaWiki PHP API.
*/
public class MwPageClient implements PageClient {
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<? extends PageSummary> summary(@NonNull WikiSite wiki, @NonNull String title, @Nullable String referrerUrl) {
return ServiceFactory.get(wiki).getSummary(referrerUrl, title, wiki.languageCode());
}
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<Response<MwMobileViewPageLead>> lead(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@Nullable String referrerUrl,
@NonNull String title,
int leadImageWidth) {
return ServiceFactory.get(wiki).getLeadSection(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, referrerUrl, title, leadImageWidth, wiki.languageCode());
}
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<Response<MwMobileViewPageRemaining>> sections(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title) {
return ServiceFactory.get(wiki).getRemainingSections(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, title, wiki.languageCode());
}
@SuppressWarnings("unchecked")
@NonNull @Override public Request sectionsUrl(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title) {
return ServiceFactory.get(wiki).getRemainingSectionsUrl(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, title, wiki.languageCode()).request();
}
}

View file

@ -0,0 +1,83 @@
package org.wikipedia.dataclient.mwapi.page;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.page.PageSummary;
import org.wikipedia.page.Namespace;
/**
* Useful for link previews coming from MW API.
*/
public class MwQueryPageSummary extends MwQueryResponse implements PageSummary {
@Override @Nullable public String getTitle() {
if (query() == null || query().firstPage() == null) {
return null;
}
return query().firstPage().title();
}
@Override @Nullable public String getDisplayTitle() {
if (query() == null || query().firstPage() == null) {
return null;
}
return (query().firstPage().pageProps() != null && !TextUtils.isEmpty(query().firstPage().pageProps().getDisplayTitle()))
? query().firstPage().pageProps().getDisplayTitle() : query().firstPage().title();
}
@Override @Nullable public String getConvertedTitle() {
if (query() == null || query().firstPage() == null) {
return null;
}
return (query().firstPage().convertedTo() != null && !TextUtils.isEmpty(query().firstPage().convertedTo()))
? query().firstPage().convertedTo() : query().firstPage().title();
}
@Override @Nullable
public String getExtract() {
if (query() == null || query().firstPage() == null) {
return null;
}
return query().firstPage().extract();
}
@Override @Nullable
public String getExtractHtml() {
return getExtract();
}
@Override @Nullable
public String getThumbnailUrl() {
if (query() == null || query().firstPage() == null) {
return null;
}
return query().firstPage().thumbUrl();
}
@Override @NonNull
public Namespace getNamespace() {
if (query() == null || query().firstPage() == null) {
return Namespace.MAIN;
}
return query().firstPage().namespace();
}
@NonNull @Override
public String getType() {
if (query() != null && query().firstPage() != null && query().firstPage().pageProps() != null
&& query().firstPage().pageProps().isDisambiguation()) {
return TYPE_DISAMBIGUATION;
}
return TYPE_STANDARD;
}
@Override public int getPageId() {
if (query() == null || query().firstPage() == null) {
return 0;
}
return query().firstPage().pageId();
}
}

View file

@ -0,0 +1,74 @@
package org.wikipedia.dataclient.okhttp;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
/**
* This is a subclass of InputStream that implements the available() method reliably enough
* to satisfy WebResourceResponses or other consumers like BufferedInputStream that depend
* on available() to return a meaningful value.
*
* The problem is that the InputStream provided by OkHttp's body().byteStream() returns zero
* when calling available() prior to making any read() calls, which means that it will break
* any consumers that wrap a BufferedInputStream onto this stream, or any other wrapper that
* relies on a consistent implementation of available().
*
* This is initialized with the original InputStream plus its total size, which must be known
* at the time of instantiation. You may then call the read() and skip() methods in the usual
* way, and then be able to call available() and get the number of bytes left to read.
*/
public class AvailableInputStream extends InputStream {
private InputStream stream;
private long available;
public AvailableInputStream(InputStream stream, long available) {
this.stream = stream;
this.available = available;
}
@Override public int read() throws IOException {
decreaseAvailable(1);
return stream.read();
}
@Override public int read(@NonNull byte[] b) throws IOException {
int ret = stream.read(b);
if (ret > 0) {
decreaseAvailable(ret);
}
return ret;
}
@Override public int read(@NonNull byte[] b, int off, int len) throws IOException {
int ret = stream.read(b, off, len);
if (ret > 0) {
decreaseAvailable(ret);
}
return ret;
}
@Override public long skip(long n) throws IOException {
long ret = stream.skip(n);
if (ret > 0) {
decreaseAvailable(ret);
}
return ret;
}
@Override public int available() throws IOException {
int ret = stream.available();
if (ret == 0 && available > 0) {
return (int) available;
}
return ret;
}
private void decreaseAvailable(long n) {
available -= n;
if (available < 0) {
available = 0;
}
}
}

View file

@ -0,0 +1,54 @@
package org.wikipedia.dataclient.okhttp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.ServiceError;
import org.wikipedia.dataclient.restbase.RbServiceError;
import org.wikipedia.util.log.L;
import java.io.IOException;
import okhttp3.Response;
public class HttpStatusException extends IOException {
private final int code;
private final String url;
@Nullable private ServiceError serviceError;
public HttpStatusException(@NonNull Response rsp) {
this.code = rsp.code();
this.url = rsp.request().url().uri().toString();
try {
if (rsp.body() != null && rsp.body().contentType() != null
&& rsp.body().contentType().toString().contains("json")) {
serviceError = RbServiceError.create(rsp.body().string());
}
} catch (Exception e) {
L.e(e);
}
}
public HttpStatusException(@Nullable ServiceError error) {
serviceError = error;
code = 0;
url = "";
}
public int code() {
return code;
}
public ServiceError serviceError() {
return serviceError;
}
@Override
public String getMessage() {
String str = "Code: " + Integer.toString(code) + ", URL: " + url;
if (serviceError != null) {
str += ", title: " + serviceError.getTitle() + ", detail: " + serviceError.getDetails();
}
return str;
}
}

View file

@ -0,0 +1,23 @@
package org.wikipedia.dataclient.okhttp
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
class TestStubInterceptor : Interceptor {
interface Callback {
@Throws(IOException::class)
fun getResponse(request: Interceptor.Chain): Response
}
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
return if (CALLBACK != null) {
CALLBACK!!.getResponse(chain)
} else chain.proceed(chain.request())
}
companion object {
var CALLBACK: Callback? = null
}
}

View file

@ -0,0 +1,16 @@
package org.wikipedia.dataclient.okhttp
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
class UnsuccessfulResponseInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val rsp = chain.proceed(chain.request())
if (rsp.isSuccessful) {
return rsp
}
throw HttpStatusException(rsp)
}
}

View file

@ -0,0 +1,23 @@
package org.wikipedia.dataclient.okhttp.util;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import okhttp3.HttpUrl;
public final class HttpUrlUtil {
private static final List<String> RESTBASE_SEGMENT_IDENTIFIERS = Arrays.asList("rest_v1", "v1");
public static boolean isRestBase(@NonNull HttpUrl url) {
return !Collections.disjoint(url.encodedPathSegments(), RESTBASE_SEGMENT_IDENTIFIERS);
}
public static boolean isMobileView(@NonNull HttpUrl url) {
return "mobileview".equals(url.queryParameter("action"));
}
private HttpUrlUtil() { }
}

View file

@ -0,0 +1,60 @@
package org.wikipedia.dataclient.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.WikiSite;
import io.reactivex.Observable;
import okhttp3.CacheControl;
import okhttp3.Request;
import retrofit2.Response;
/**
* Generic interface for Page content service.
* Usually we would use direct Retrofit Callbacks here but since we have two ways of
* getting to the data (MW API and RESTBase) we add this layer of indirection -- until we drop one.
*/
public interface PageClient {
/**
* Gets a page summary for a given title -- for link previews
*
* @param title the page title to be used including prefix
*/
@NonNull <T extends PageSummary> Observable<T> summary(@NonNull WikiSite wiki,
@NonNull String title,
@Nullable String referrerUrl);
/**
* Gets the lead section and initial metadata of a given title.
*
* @param title the page title with prefix if necessary
* @param leadThumbnailWidth one of the bucket widths for the lead image
*/
@NonNull <T extends PageLead> Observable<Response<T>> lead(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@Nullable String referrerUrl,
@NonNull String title,
int leadThumbnailWidth);
/**
* Gets the remaining sections of a given title.
*
* @param title the page title to be used including prefix
*/
@NonNull <T extends PageRemaining> Observable<Response<T>> sections(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title);
/**
* Gets the remaining sections request url of a given title.
*
* @param title the page title to be used including prefix
*/
@NonNull Request sectionsUrl(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title);
}

View file

@ -0,0 +1,26 @@
package org.wikipedia.dataclient.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.page.Page;
import org.wikipedia.page.PageTitle;
/**
* Gson POJI for loading the first stage of page content.
*/
public interface PageLead {
/** Note: before using this check that #hasError is false */
Page toPage(PageTitle title);
@NonNull String getLeadSectionContent();
@Nullable String getTitlePronunciationUrl();
@Nullable String getLeadImageUrl(int leadImageWidth);
@Nullable String getThumbUrl();
@Nullable String getDescription();
@Nullable Location getGeo();
}

View file

@ -0,0 +1,73 @@
package org.wikipedia.dataclient.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.Section;
import java.util.List;
/**
* The main properties of a page
*/
public interface PageLeadProperties {
int getId();
@NonNull Namespace getNamespace();
long getRevision();
@Nullable
String getLastModified();
int getLanguageCount();
@Nullable
String getDisplayTitle();
@Nullable
String getTitlePronunciationUrl();
@Nullable
Location getGeo();
@Nullable
String getRedirected();
@Nullable
String getNormalizedTitle();
@Nullable
String getWikiBaseItem();
@Nullable
String getDescriptionSource();
/**
* @return Nullable URL with no scheme. For example, foo.bar.com/ instead of
* http://foo.bar.com/.
*/
@Nullable
String getLeadImageUrl(int leadImageWidth);
@Nullable
String getThumbUrl();
@Nullable
String getLeadImageFileName();
@Nullable
String getFirstAllowedEditorRole();
boolean isEditable();
boolean isMainPage();
boolean isDisambiguation();
@NonNull List<Section> getSections();
}

View file

@ -0,0 +1,14 @@
package org.wikipedia.dataclient.page;
import androidx.annotation.NonNull;
import org.wikipedia.page.Section;
import java.util.List;
/**
* Gson POJI for loading remaining page content.
*/
public interface PageRemaining {
@NonNull List<Section> sections();
}

View file

@ -0,0 +1,26 @@
package org.wikipedia.dataclient.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.page.Namespace;
/**
* Represents a summary of a page, useful for page previews.
*/
public interface PageSummary {
String TYPE_STANDARD = "standard";
String TYPE_DISAMBIGUATION = "disambiguation";
String TYPE_MAIN_PAGE = "mainpage";
String TYPE_NO_EXTRACT = "no-extract";
@NonNull String getType();
@Nullable String getTitle();
@Nullable String getDisplayTitle();
@Nullable String getConvertedTitle();
@Nullable String getExtract();
@Nullable String getExtractHtml();
@Nullable String getThumbnailUrl();
@NonNull Namespace getNamespace();
int getPageId();
}

View file

@ -0,0 +1,23 @@
package org.wikipedia.dataclient.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.Set;
/** Protection settings for a page */
public class Protection {
@SuppressWarnings("MismatchedReadAndWriteOfArray") @NonNull private Set<String> edit = Collections.emptySet();
// TODO should send them all, but callers need to be updated, too, (future patch)
@Nullable
public String getFirstAllowedEditorRole() {
return edit.isEmpty() ? null : edit.iterator().next();
}
@NonNull
public Set<String> getEditRoles() {
return Collections.unmodifiableSet(edit);
}
}

View file

@ -0,0 +1,56 @@
package org.wikipedia.dataclient.restbase;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.json.annotations.Required;
import java.util.Map;
public class RbDefinition {
@Required @NonNull private Map<String, Usage[]> usagesByLang;
public RbDefinition(@NonNull Map<String, RbDefinition.Usage[]> usages) {
usagesByLang = usages;
}
@Nullable public Usage[] getUsagesForLang(String langCode) {
return usagesByLang.get(langCode);
}
public static class Usage {
@Required @NonNull private String partOfSpeech;
@Required @NonNull private Definition[] definitions;
public Usage(@NonNull String partOfSpeech, @NonNull Definition[] definitions) {
this.partOfSpeech = partOfSpeech;
this.definitions = definitions;
}
@NonNull public String getPartOfSpeech() {
return partOfSpeech;
}
@NonNull public Definition[] getDefinitions() {
return definitions;
}
}
public static class Definition {
@Required @NonNull private String definition;
@Nullable private String[] examples;
public Definition(@NonNull String definition, @Nullable String[] examples) {
this.definition = definition;
this.examples = examples;
}
@NonNull public String getDefinition() {
return definition;
}
@Nullable public String[] getExamples() {
return examples;
}
}
}

View file

@ -0,0 +1,33 @@
package org.wikipedia.dataclient.restbase;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import java.util.ArrayList;
import java.util.List;
public class RbRelatedPages {
@SuppressWarnings("unused") @Nullable private List<RbPageSummary> pages;
@Nullable
public List<RbPageSummary> getPages() {
return pages;
}
@NonNull
public List<RbPageSummary> getPages(int limit) {
List<RbPageSummary> list = new ArrayList<>();
if (getPages() != null) {
for (RbPageSummary page : getPages()) {
list.add(page);
if (limit == list.size()) {
break;
}
}
}
return list;
}
}

View file

@ -0,0 +1,35 @@
package org.wikipedia.dataclient.restbase;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.ServiceError;
import org.wikipedia.json.GsonUnmarshaller;
import org.wikipedia.model.BaseModel;
/**
* Gson POJO for a RESTBase API error.
*/
public class RbServiceError extends BaseModel implements ServiceError {
@SuppressWarnings("unused") private String type;
@SuppressWarnings("unused") private String title;
@SuppressWarnings("unused") private String detail;
@SuppressWarnings("unused") private String method;
@SuppressWarnings("unused") private String uri;
public static RbServiceError create(@NonNull String rspBody) {
return GsonUnmarshaller.unmarshal(RbServiceError.class, rspBody);
}
@Override
@NonNull
public String getTitle() {
return StringUtils.defaultString(title);
}
@Override
@NonNull
public String getDetails() {
return StringUtils.defaultString(detail);
}
}

View file

@ -0,0 +1,60 @@
package org.wikipedia.dataclient.restbase.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.page.PageClient;
import org.wikipedia.dataclient.page.PageSummary;
import io.reactivex.Observable;
import okhttp3.CacheControl;
import okhttp3.Request;
import retrofit2.Response;
// todo: consolidate with MwPageClient or just use the Services directly!
/**
* Retrofit web service client for RESTBase Nodejs API.
*/
public class RbPageClient implements PageClient {
// todo: RbPageSummary should specify an @Required annotation that throws a JsonParseException
// when the body is null rather than requiring all clients to check for a null body. There
// may be some abandoned demo patches that already have this functionality. It should be
// part of the Gson augmentation package and eventually cut into a separate lib. Repeat
// everywhere a Response.body() == null check occurs that throws
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<? extends PageSummary> summary(@NonNull WikiSite wiki, @NonNull String title, @Nullable String referrerUrl) {
return ServiceFactory.getRest(wiki).getSummary(referrerUrl, title);
}
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<Response<RbPageLead>> lead(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@Nullable String referrerUrl,
@NonNull String title,
int leadThumbnailWidth) {
return ServiceFactory.getRest(wiki).getLeadSection(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, referrerUrl, title);
}
@SuppressWarnings("unchecked")
@NonNull @Override public Observable<Response<RbPageRemaining>> sections(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title) {
return ServiceFactory.getRest(wiki).getRemainingSections(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, title);
}
@SuppressWarnings("unchecked")
@NonNull @Override public Request sectionsUrl(@NonNull WikiSite wiki,
@Nullable CacheControl cacheControl,
@Nullable String saveOfflineHeader,
@NonNull String title) {
return ServiceFactory.getRest(wiki).getRemainingSectionsUrl(cacheControl == null ? null : cacheControl.toString(),
saveOfflineHeader, title).request();
}
}

View file

@ -0,0 +1,268 @@
package org.wikipedia.dataclient.restbase.page;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.page.PageLead;
import org.wikipedia.dataclient.page.PageLeadProperties;
import org.wikipedia.dataclient.page.Protection;
import org.wikipedia.page.GeoTypeAdapter;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.Page;
import org.wikipedia.page.PageProperties;
import org.wikipedia.page.PageTitle;
import org.wikipedia.page.Section;
import org.wikipedia.util.UriUtil;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.wikipedia.dataclient.Service.PREFERRED_THUMB_SIZE;
/**
* Gson POJO for loading the first stage of page content.
*/
@SuppressWarnings("unused")
public class RbPageLead implements PageLead, PageLeadProperties {
private int ns;
private int id;
private long revision;
@Nullable private String lastmodified;
@Nullable private String displaytitle;
@Nullable private String redirected;
@Nullable private String normalizedtitle;
@Nullable @SerializedName("wikibase_item") private String wikiBaseItem;
@Nullable @SerializedName("pronunciation") private TitlePronunciation titlePronunciation;
@Nullable @JsonAdapter(GeoTypeAdapter.class) private Location geo;
private int languagecount;
private boolean editable;
private boolean mainpage;
private boolean disambiguation;
@Nullable private String description;
@Nullable @SerializedName("description_source") private String descriptionSource;
@Nullable private Image image;
@Nullable private Protection protection;
@Nullable private List<Section> sections;
/** Note: before using this check that #getMobileview != null */
@Override
public Page toPage(PageTitle title) {
return new Page(adjustPageTitle(title),
getSections(),
toPageProperties(),
true);
}
PageTitle adjustPageTitle(PageTitle title) {
if (redirected != null) {
// Handle redirects properly.
title = new PageTitle(redirected, title.getWikiSite(), title.getThumbUrl());
} else if (normalizedtitle != null) {
// We care about the normalized title only if we were not redirected
title = new PageTitle(normalizedtitle, title.getWikiSite(), title.getThumbUrl());
}
title.setDescription(description);
return title;
}
@Override
public String getLeadSectionContent() {
if (sections != null) {
return sections.get(0).getContent();
} else {
return "";
}
}
/** Converter */
private PageProperties toPageProperties() {
return new PageProperties(this);
}
@Override
public int getId() {
return id;
}
@NonNull @Override public Namespace getNamespace() {
return Namespace.of(ns);
}
@Override
public long getRevision() {
return revision;
}
@Override
@Nullable
public String getLastModified() {
return lastmodified;
}
@Override
@Nullable
public String getTitlePronunciationUrl() {
return titlePronunciation == null
? null
: UriUtil.resolveProtocolRelativeUrl(titlePronunciation.getUrl());
}
@Override
@Nullable
public Location getGeo() {
return geo;
}
@Override
public int getLanguageCount() {
return languagecount;
}
@Override
@Nullable
public String getDisplayTitle() {
return displaytitle;
}
@Override
@Nullable
public String getRedirected() {
return redirected;
}
@Override
@Nullable
public String getNormalizedTitle() {
return normalizedtitle;
}
@Override
@Nullable
public String getWikiBaseItem() {
return wikiBaseItem;
}
@Override
@Nullable
public String getDescription() {
return description;
}
@Override
@Nullable
public String getDescriptionSource() {
return descriptionSource;
}
@Override
@Nullable
public String getLeadImageUrl(int leadImageWidth) {
return image != null ? image.getUrl(leadImageWidth) : null;
}
@Override
@Nullable
public String getThumbUrl() {
return image != null ? image.getUrl(PREFERRED_THUMB_SIZE) : null;
}
@Override
@Nullable
public String getLeadImageFileName() {
return image != null ? image.getFileName() : null;
}
@Override
@Nullable
public String getFirstAllowedEditorRole() {
return protection != null ? protection.getFirstAllowedEditorRole() : null;
}
@Override
public boolean isEditable() {
return editable;
}
public Set<String> getEditRoles() {
return protection != null ? protection.getEditRoles() : Collections.emptySet();
}
@Override
public boolean isMainPage() {
return mainpage;
}
@Override
public boolean isDisambiguation() {
return disambiguation;
}
@Override @NonNull public List<Section> getSections() {
return sections == null ? Collections.emptyList() : sections;
}
/**
* For the lead image File: page name
*/
public static class TitlePronunciation {
@SuppressWarnings("unused,NullableProblems") @NonNull private String url;
@NonNull
public String getUrl() {
return url;
}
}
/**
* For the lead image File: page name
*/
public static class Image {
@SuppressWarnings("unused") @SerializedName("file") private String fileName;
@SuppressWarnings("unused") private ThumbUrls urls;
public String getFileName() {
return fileName;
}
@Nullable
public String getUrl(int width) {
return urls != null ? urls.get(width) : null;
}
}
/**
* For the lead image URLs
*/
public static class ThumbUrls {
private static final int SMALL = 320;
private static final int MEDIUM = 640;
private static final int LARGE = 800;
private static final int XL = 1024;
@SuppressWarnings("unused") @SerializedName("320") private String small;
@SuppressWarnings("unused") @SerializedName("640") private String medium;
@SuppressWarnings("unused") @SerializedName("800") private String large;
@SuppressWarnings("unused") @SerializedName("1024") private String xl;
@Nullable
public String get(int width) {
switch (width) {
case SMALL:
return small;
case MEDIUM:
return medium;
case LARGE:
return large;
case XL:
return xl;
default:
return null;
}
}
}
}

View file

@ -0,0 +1,24 @@
package org.wikipedia.dataclient.restbase.page;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.page.PageRemaining;
import org.wikipedia.page.Section;
import java.util.Collections;
import java.util.List;
/**
* Gson POJO for loading remaining page content.
*/
public class RbPageRemaining implements PageRemaining {
@SuppressWarnings("unused") @Nullable private List<Section> sections;
@NonNull @Override public List<Section> sections() {
if (sections == null) {
return Collections.emptyList();
}
return sections;
}
}

View file

@ -0,0 +1,136 @@
package org.wikipedia.dataclient.restbase.page;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.page.PageSummary;
import org.wikipedia.json.annotations.Required;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.PageTitle;
/**
* A standardized page summary object constructed by RESTBase, used for link previews and as the
* base class for various feed content (see the FeedPageSummary class).
*
* N.B.: The "title" field here sent by RESTBase is the *normalized* page title. However, in the
* FeedPageSummary subclass, "title" becomes the un-normalized, raw title, and the normalized title
* is sent as "normalizedtitle".
*/
@SuppressWarnings("unused")
public class RbPageSummary implements PageSummary {
@Nullable private String type;
@SuppressWarnings("NullableProblems") @Required @NonNull private String title;
@Nullable private String normalizedtitle;
@SuppressWarnings("NullableProblems") @NonNull private String displaytitle;
@Nullable private NamespaceContainer namespace;
@Nullable private String extract;
@Nullable @SerializedName("extract_html") private String extractHtml;
@Nullable private String description;
@Nullable private Thumbnail thumbnail;
@Nullable @SerializedName("originalimage") private Thumbnail originalImage;
@Nullable private String lang;
private int pageid;
@Nullable @SerializedName("wikibase_item") private String wikiBaseItem;
@Override @NonNull
public String getTitle() {
return title;
}
@Override @NonNull
public String getDisplayTitle() {
return displaytitle;
}
@Override @NonNull
public String getConvertedTitle() {
return title;
}
@Override @NonNull
public Namespace getNamespace() {
return namespace == null ? Namespace.MAIN : Namespace.of(namespace.id());
}
@Override @NonNull
public String getType() {
return TextUtils.isEmpty(type) ? TYPE_STANDARD : type;
}
@Override @Nullable
public String getExtract() {
return extract;
}
@Override @Nullable
public String getExtractHtml() {
return extractHtml;
}
@Override @Nullable
public String getThumbnailUrl() {
return thumbnail == null ? null : thumbnail.getUrl();
}
@Nullable
public String getDescription() {
return description;
}
@NonNull
public String getNormalizedTitle() {
return normalizedtitle == null ? title : normalizedtitle;
}
@Nullable
public String getOriginalImageUrl() {
return originalImage == null ? null : originalImage.getUrl();
}
@Nullable
public String getWikiBaseItem() {
return wikiBaseItem;
}
@NonNull
public PageTitle getPageTitle(@NonNull WikiSite wiki) {
return new PageTitle(getTitle(), wiki, getThumbnailUrl(), getDescription());
}
public int getPageId() {
return pageid;
}
public String getLang() {
return lang;
}
/**
* For the thumbnail URL of the page
*/
private static class Thumbnail {
@SuppressWarnings("unused") private String source;
public String getUrl() {
return source;
}
}
private static class NamespaceContainer {
@SuppressWarnings("unused") private int id;
@SuppressWarnings("unused") @Nullable private String text;
public int id() {
return id;
}
}
@Override public String toString() {
return getTitle();
}
}

View file

@ -0,0 +1,75 @@
package org.wikipedia.dataclient.retrofit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.IOException;
import retrofit2.Response;
/**
* This is RetrofitError converted to Retrofit 2
*/
public class RetrofitException extends RuntimeException {
public static RetrofitException httpError(Response<?> response) {
return httpError(response.raw().request().url().toString(), response);
}
public static RetrofitException httpError(String url, Response<?> response) {
String message = response.code() + " " + response.message();
return new RetrofitException(message, url, response.code(), Kind.HTTP, null);
}
public static RetrofitException httpError(@NonNull okhttp3.Response response) {
String message = response.code() + " " + response.message();
return new RetrofitException(message, response.request().url().toString(), response.code(), Kind.HTTP,
null);
}
public static RetrofitException networkError(IOException exception) {
return new RetrofitException(exception.getMessage(), null, null, Kind.NETWORK, exception);
}
public static RetrofitException unexpectedError(Throwable exception) {
return new RetrofitException(exception.getMessage(), null, null, Kind.UNEXPECTED, exception);
}
/** Identifies the event kind which triggered a {@link RetrofitException}. */
public enum Kind {
/** An {@link IOException} occurred while communicating to the server. */
NETWORK,
/** A non-200 HTTP status code was received from the server. */
HTTP,
/**
* An internal error occurred while attempting to execute a request. It is best practice to
* re-throw this exception so your application crashes.
*/
UNEXPECTED
}
private final String url;
@Nullable private final Integer code;
private final Kind kind;
RetrofitException(String message, String url, @Nullable Integer code, Kind kind, Throwable exception) {
super(message, exception);
this.url = url;
this.code = code;
this.kind = kind;
}
/** The request URL which produced the error. */
public String getUrl() {
return url;
}
/** HTTP status code. */
@Nullable public Integer getCode() {
return code;
}
/** The event kind which triggered this error. */
public Kind getKind() {
return kind;
}
}

View file

@ -0,0 +1,79 @@
package org.wikipedia.edit;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
public class Edit extends MwPostResponse {
@SuppressWarnings("unused,") @Nullable private Result edit;
@Nullable public Result edit() {
return edit;
}
boolean hasEditResult() {
return edit != null;
}
public class Result {
@SuppressWarnings("unused") @Nullable private String result;
@SuppressWarnings("unused") private int newrevid;
@SuppressWarnings("unused") @Nullable private Captcha captcha;
@SuppressWarnings("unused") @Nullable private String code;
@SuppressWarnings("unused") @Nullable private String info;
@SuppressWarnings("unused") @Nullable private String warning;
@SuppressWarnings("unused") @Nullable private String spamblacklist;
@Nullable String status() {
return result;
}
public int newRevId() {
return newrevid;
}
public boolean editSucceeded() {
return "Success".equals(result);
}
@Nullable String captchaId() {
return captcha == null ? null : captcha.id();
}
public boolean hasEditErrorCode() {
return code != null;
}
boolean hasCaptchaResponse() {
return captcha != null;
}
@Nullable public String code() {
return code;
}
@Nullable public String info() {
return info;
}
@Nullable public String warning() {
return warning;
}
@Nullable String spamblacklist() {
return spamblacklist;
}
boolean hasSpamBlacklistResponse() {
return spamblacklist != null;
}
}
private static class Captcha {
@SuppressWarnings("unused") @Nullable private String id;
@Nullable String id() {
return id;
}
}
}

View file

@ -0,0 +1,79 @@
package org.wikipedia.edit;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
public class EditAbuseFilterResult extends EditResult {
static final int TYPE_WARNING = 1;
static final int TYPE_ERROR = 2;
@Nullable private final String code;
@Nullable private final String info;
@Nullable private final String warning;
EditAbuseFilterResult(@Nullable String code, @Nullable String info, @Nullable String warning) {
super("Failure");
this.code = code;
this.info = info;
this.warning = warning;
}
private EditAbuseFilterResult(Parcel in) {
super(in);
code = in.readString();
info = in.readString();
warning = in.readString();
}
@Nullable public String getCode() {
return code;
}
@Nullable public String getInfo() {
return info;
}
@Nullable public String getWarning() {
return warning;
}
@Override public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(code);
dest.writeString(info);
dest.writeString(warning);
}
public int getType() {
if (code != null && code.startsWith("abusefilter-warning")) {
return TYPE_WARNING;
} else if (code != null && code.startsWith("abusefilter-disallowed")) {
return TYPE_ERROR;
} else if (info != null && info.startsWith("Hit AbuseFilter")) {
// This case is here because, unfortunately, an admin can create an abuse filter which
// emits an arbitrary error code over the API.
// TODO: More properly handle the case where the AbuseFilter throws an arbitrary error.
// Oh, and, you know, also fix the AbuseFilter API to not throw arbitrary error codes.
return TYPE_ERROR;
} else {
// We have no understanding of what kind of abuse filter response we got. It's safest
// to simply treat these as an error.
return TYPE_ERROR;
}
}
public static final Parcelable.Creator<EditAbuseFilterResult> CREATOR
= new Parcelable.Creator<EditAbuseFilterResult>() {
@Override
public EditAbuseFilterResult createFromParcel(Parcel in) {
return new EditAbuseFilterResult(in);
}
@Override
public EditAbuseFilterResult[] newArray(int size) {
return new EditAbuseFilterResult[size];
}
};
}

View file

@ -0,0 +1,72 @@
package org.wikipedia.edit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.wikipedia.captcha.CaptchaResult;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.PageTitle;
import java.io.IOException;
import retrofit2.Call;
import retrofit2.Response;
public class EditClient {
public interface Callback {
void success(@NonNull Call<Edit> call, @NonNull EditResult result);
void failure(@NonNull Call<Edit> call, @NonNull Throwable caught);
}
@SuppressWarnings("checkstyle:parameternumber")
public Call<Edit> request(@NonNull WikiSite wiki, @NonNull PageTitle title, int section,
@NonNull String text, @NonNull String token, @NonNull String summary,
@Nullable String baseTimeStamp, boolean loggedIn, @Nullable String captchaId,
@Nullable String captchaWord, @NonNull Callback cb) {
return request(ServiceFactory.get(wiki), title, section, text, token, summary,
baseTimeStamp, loggedIn, captchaId, captchaWord, cb);
}
@VisibleForTesting @SuppressWarnings("checkstyle:parameternumber")
Call<Edit> request(@NonNull Service service, @NonNull PageTitle title, int section,
@NonNull String text, @NonNull String token, @NonNull String summary,
@Nullable String baseTimeStamp, boolean loggedIn, @Nullable String captchaId,
@Nullable String captchaWord, @NonNull final Callback cb) {
Call<Edit> call = service.postEditSubmit(title.getPrefixedText(), section, summary, loggedIn ? "user" : null,
text, baseTimeStamp, token, captchaId, captchaWord);
call.enqueue(new retrofit2.Callback<Edit>() {
@Override
public void onResponse(@NonNull Call<Edit> call, @NonNull Response<Edit> response) {
if (response.body().hasEditResult()) {
handleEditResult(response.body().edit(), call, cb);
} else {
cb.failure(call, new IOException("An unknown error occurred."));
}
}
@Override
public void onFailure(@NonNull Call<Edit> call, @NonNull Throwable t) {
cb.failure(call, t);
}
});
return call;
}
private void handleEditResult(@NonNull Edit.Result result, @NonNull Call<Edit> call,
@NonNull Callback cb) {
if (result.editSucceeded()) {
cb.success(call, new EditSuccessResult(result.newRevId()));
} else if (result.hasEditErrorCode()) {
cb.success(call, new EditAbuseFilterResult(result.code(), result.info(), result.warning()));
} else if (result.hasSpamBlacklistResponse()) {
cb.success(call, new EditSpamBlacklistResult(result.spamblacklist()));
} else if (result.hasCaptchaResponse()) {
cb.success(call, new CaptchaResult(result.captchaId()));
} else {
cb.failure(call, new IOException("Received unrecognized edit response"));
}
}
}

View file

@ -0,0 +1,32 @@
package org.wikipedia.edit;
import android.os.Parcel;
import android.os.Parcelable;
import org.wikipedia.model.BaseModel;
public abstract class EditResult extends BaseModel implements Parcelable {
private final String result;
public EditResult(String result) {
this.result = result;
}
protected EditResult(Parcel in) {
this.result = in.readString();
}
public String getResult() {
return result;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(result);
}
}

View file

@ -0,0 +1,40 @@
package org.wikipedia.edit;
import android.os.Parcel;
import android.os.Parcelable;
public class EditSpamBlacklistResult extends EditResult {
private final String domain;
public EditSpamBlacklistResult(String domain) {
super("Failure");
this.domain = domain;
}
protected EditSpamBlacklistResult(Parcel in) {
super(in);
domain = in.readString();
}
public String getDomain() {
return domain;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(domain);
}
public static final Parcelable.Creator<EditSpamBlacklistResult> CREATOR
= new Parcelable.Creator<EditSpamBlacklistResult>() {
@Override
public EditSpamBlacklistResult createFromParcel(Parcel in) {
return new EditSpamBlacklistResult(in);
}
@Override
public EditSpamBlacklistResult[] newArray(int size) {
return new EditSpamBlacklistResult[size];
}
};
}

View file

@ -0,0 +1,34 @@
package org.wikipedia.edit;
import android.os.Parcel;
import android.os.Parcelable;
public class EditSuccessResult extends EditResult {
private final int revID;
public EditSuccessResult(int revID) {
super("Success");
this.revID = revID;
}
private EditSuccessResult(Parcel in) {
super(in);
revID = in.readInt();
}
public int getRevID() {
return revID;
}
public static final Parcelable.Creator<EditSuccessResult> CREATOR
= new Parcelable.Creator<EditSuccessResult>() {
@Override
public EditSuccessResult createFromParcel(Parcel in) {
return new EditSuccessResult(in);
}
@Override
public EditSuccessResult[] newArray(int size) {
return new EditSuccessResult[size];
}
};
}

View file

@ -0,0 +1,29 @@
package org.wikipedia.edit.preview;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
public class EditPreview extends MwPostResponse {
@SuppressWarnings("unused") @Nullable private Parse parse;
boolean hasPreviewResult() {
return parse != null;
}
@Nullable String result() {
return parse != null ? parse.text() : null;
}
private static class Parse {
@SuppressWarnings("unused,NullableProblems") @NonNull private String title;
@SuppressWarnings("unused") @SerializedName("pageid") private int pageId;
@SuppressWarnings("unused,NullableProblems") @NonNull private String text;
@NonNull String text() {
return text;
}
}
}

View file

@ -0,0 +1,46 @@
package org.wikipedia.feed.aggregated;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.feed.image.FeaturedImage;
import org.wikipedia.feed.mostread.MostReadArticles;
import org.wikipedia.feed.news.NewsItem;
import org.wikipedia.feed.onthisday.OnThisDay;
import java.util.List;
public class AggregatedFeedContent {
@SuppressWarnings("unused") @Nullable private RbPageSummary tfa;
@SuppressWarnings("unused") @Nullable private List<NewsItem> news;
@SuppressWarnings("unused") @SerializedName("mostread") @Nullable private MostReadArticles mostRead;
@SuppressWarnings("unused") @Nullable private FeaturedImage image;
@SuppressWarnings("unused") @Nullable private List<OnThisDay.Event> onthisday;
@Nullable
public List<OnThisDay.Event> onthisday() {
return onthisday;
}
@Nullable
public RbPageSummary tfa() {
return tfa;
}
@Nullable
List<NewsItem> news() {
return news;
}
@Nullable
MostReadArticles mostRead() {
return mostRead;
}
@Nullable
FeaturedImage potd() {
return image;
}
}

View file

@ -0,0 +1,164 @@
package org.wikipedia.feed.announcement;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.json.annotations.Required;
import org.wikipedia.model.BaseModel;
import org.wikipedia.util.DateUtil;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.defaultString;
public class Announcement extends BaseModel {
public static final String SURVEY = "survey";
public static final String FUNDRAISING = "fundraising";
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String id;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String type;
@SuppressWarnings("unused,NullableProblems") @SerializedName("start_time") @Required @NonNull private String startTime;
@SuppressWarnings("unused,NullableProblems") @SerializedName("end_time") @Required @NonNull private String endTime;
@SuppressWarnings("unused") @NonNull private List<String> platforms = Collections.emptyList();
@SuppressWarnings("unused") @NonNull private List<String> countries = Collections.emptyList();
@SuppressWarnings("unused") @SerializedName("caption_HTML") @Nullable private String footerCaption;
@SuppressWarnings("unused") @SerializedName("image_url") @Nullable private String imageUrl;
@SuppressWarnings("unused") @SerializedName("image_height") @Nullable private String imageHeight;
@SuppressWarnings("unused") @SerializedName("logged_in") @Nullable private Boolean loggedIn;
@SuppressWarnings("unused") @SerializedName("reading_list_sync_enabled") @Nullable private Boolean readingListSyncEnabled;
@SuppressWarnings("unused") @Nullable private Boolean beta;
@SuppressWarnings("unused") @SerializedName("min_version") @Nullable private String minVersion;
@SuppressWarnings("unused") @SerializedName("max_version") @Nullable private String maxVersion;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String text;
@SuppressWarnings("unused") @Nullable private Action action;
@SuppressWarnings("unused") @SerializedName("negative_text") @Nullable private String negativeText;
public Announcement() { }
public Announcement(@NonNull String id, @NonNull String text, @NonNull String imageUrl,
@NonNull Action action, @NonNull String negativeText) {
this.id = id;
this.text = text;
this.imageUrl = imageUrl;
this.action = action;
this.negativeText = negativeText;
}
@NonNull String id() {
return id;
}
@NonNull String type() {
return type;
}
@Nullable Date startTime() {
try {
return DateUtil.iso8601DateParse(startTime);
} catch (ParseException e) {
return null;
}
}
@Nullable Date endTime() {
try {
return DateUtil.iso8601DateParse(endTime);
} catch (ParseException e) {
return null;
}
}
@NonNull List<String> platforms() {
return platforms;
}
@NonNull List<String> countries() {
return countries;
}
@NonNull String text() {
return text;
}
boolean hasAction() {
return action != null;
}
@NonNull String actionTitle() {
return action != null ? action.title() : "";
}
@NonNull String actionUrl() {
return action != null ? action.url() : "";
}
boolean hasFooterCaption() {
return !TextUtils.isEmpty(footerCaption);
}
@NonNull String footerCaption() {
return defaultString(footerCaption);
}
boolean hasImageUrl() {
return !TextUtils.isEmpty(imageUrl);
}
@NonNull String imageUrl() {
return defaultString(imageUrl);
}
@NonNull String imageHeight() {
return defaultString(imageHeight);
}
@Nullable String negativeText() {
return negativeText;
}
@Nullable Boolean loggedIn() {
return loggedIn;
}
@Nullable Boolean readingListSyncEnabled() {
return readingListSyncEnabled;
}
@Nullable Boolean beta() {
return beta;
}
@Nullable String minVersion() {
return minVersion;
}
@Nullable String maxVersion() {
return maxVersion;
}
public static class Action {
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String title;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String url;
public Action(@NonNull String title, @NonNull String url) {
this.title = title;
this.url = url;
}
@NonNull String title() {
return title;
}
@NonNull String url() {
return url;
}
}
}

View file

@ -0,0 +1,21 @@
package org.wikipedia.feed.announcement;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import org.wikipedia.model.BaseModel;
import java.util.Collections;
import java.util.List;
public class AnnouncementList extends BaseModel {
@SuppressWarnings("unused") @SerializedName("announce") @NonNull private List<Announcement> items = Collections.emptyList();
@NonNull
List<Announcement> items() {
return items;
}
}

View file

@ -0,0 +1,41 @@
package org.wikipedia.feed.announcement;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class GeoIPCookie {
@NonNull private final String country;
@NonNull private final String region;
@NonNull private final String city;
@Nullable private final Location location;
GeoIPCookie(@NonNull String country, @NonNull String region, @NonNull String city, @Nullable Location location) {
this.country = country;
this.region = region;
this.city = city;
this.location = location;
}
@NonNull
public String country() {
return country;
}
@NonNull
public String region() {
return region;
}
@NonNull
public String city() {
return city;
}
@Nullable
public Location location() {
return location;
}
}

View file

@ -0,0 +1,60 @@
package org.wikipedia.feed.announcement;
import android.location.Location;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
/*
This currently supports the "v4" version of the GeoIP cookie.
For some info about the format and contents of the cookie:
https://phabricator.wikimedia.org/diffusion/ECNO/browse/master/resources/subscribing/ext.centralNotice.geoIP.js
*/
public final class GeoIPCookieUnmarshaller {
private static final String COOKIE_NAME = "GeoIP";
private enum Component {
COUNTRY, REGION, CITY, LATITUDE, LONGITUDE, VERSION
}
@NonNull
public static GeoIPCookie unmarshal() {
return unmarshal(SharedPreferenceCookieManager.getInstance().getCookieByName(COOKIE_NAME));
}
@VisibleForTesting
@NonNull
static GeoIPCookie unmarshal(@Nullable String cookie) throws IllegalArgumentException {
if (TextUtils.isEmpty(cookie)) {
throw new IllegalArgumentException("Cookie is empty.");
}
String[] components = cookie.split(":");
if (components.length < Component.values().length) {
throw new IllegalArgumentException("Cookie is malformed.");
} else if (!components[Component.VERSION.ordinal()].equals("v4")) {
throw new IllegalArgumentException("Incorrect cookie version.");
}
Location location = null;
if (!TextUtils.isEmpty(components[Component.LATITUDE.ordinal()])
&& !TextUtils.isEmpty(components[Component.LONGITUDE.ordinal()])) {
location = new Location("");
try {
location.setLatitude(Double.parseDouble(components[Component.LATITUDE.ordinal()]));
location.setLongitude(Double.parseDouble(components[Component.LONGITUDE.ordinal()]));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Location is malformed.");
}
}
return new GeoIPCookie(components[Component.COUNTRY.ordinal()],
components[Component.REGION.ordinal()],
components[Component.CITY.ordinal()],
location);
}
private GeoIPCookieUnmarshaller() {
}
}

View file

@ -0,0 +1,37 @@
package org.wikipedia.feed.configure;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import java.util.Collections;
import java.util.List;
@SuppressWarnings("unused")
public class FeedAvailability {
@SerializedName("todays_featured_article") private List<String> featuredArticle;
@SerializedName("most_read") private List<String> mostRead;
@SerializedName("picture_of_the_day") private List<String> featuredPicture;
@SerializedName("in_the_news") private List<String> news;
@SerializedName("on_this_day") private List<String> onThisDay;
@NonNull public List<String> featuredArticle() {
return featuredArticle != null ? featuredArticle : Collections.emptyList();
}
@NonNull public List<String> mostRead() {
return mostRead != null ? mostRead : Collections.emptyList();
}
@NonNull public List<String> featuredPicture() {
return featuredPicture != null ? featuredPicture : Collections.emptyList();
}
@NonNull public List<String> news() {
return news != null ? news : Collections.emptyList();
}
@NonNull public List<String> onThisDay() {
return onThisDay != null ? onThisDay : Collections.emptyList();
}
}

View file

@ -0,0 +1,34 @@
package org.wikipedia.feed.image;
import androidx.annotation.NonNull;
import org.wikipedia.gallery.GalleryItem;
import org.wikipedia.gallery.ImageInfo;
import org.wikipedia.json.PostProcessingTypeAdapter;
import org.wikipedia.json.annotations.Required;
public final class FeaturedImage extends GalleryItem implements PostProcessingTypeAdapter.PostProcessable {
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String title;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private ImageInfo image;
private int age;
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
@NonNull
public String title() {
return title;
}
@Override
public void postProcess() {
setTitle(title);
getOriginal().setSource(image.getSource());
}
}

View file

@ -0,0 +1,49 @@
package org.wikipedia.feed.model;
import androidx.annotation.NonNull;
import java.util.Calendar;
import static java.util.TimeZone.getTimeZone;
public class UtcDate {
@NonNull private Calendar cal;
@NonNull private String year;
@NonNull private String month;
@NonNull private String date;
public UtcDate(int age) {
this.cal = Calendar.getInstance(getTimeZone("UTC"));
cal.add(Calendar.DATE, -age);
this.year = Integer.toString(cal.get(Calendar.YEAR));
this.month = pad(Integer.toString(cal.get(Calendar.MONTH) + 1));
this.date = pad(Integer.toString(cal.get(Calendar.DATE)));
}
@NonNull
public Calendar baseCalendar() {
return cal;
}
@NonNull
public String year() {
return year;
}
@NonNull
public String month() {
return month;
}
@NonNull
public String date() {
return date;
}
private String pad(String value) {
if (value.length() == 1) {
return "0" + value;
}
return value;
}
}

View file

@ -0,0 +1,22 @@
package org.wikipedia.feed.mostread;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.json.annotations.Required;
import java.util.Date;
import java.util.List;
public final class MostReadArticles {
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private Date date;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private List<RbPageSummary> articles;
@NonNull public Date date() {
return date;
}
@NonNull public List<RbPageSummary> articles() {
return articles;
}
}

View file

@ -0,0 +1,56 @@
package org.wikipedia.feed.news;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.json.annotations.Required;
import java.util.Collections;
import java.util.List;
import static org.wikipedia.dataclient.Service.PREFERRED_THUMB_SIZE;
import static org.wikipedia.util.ImageUrlUtil.getUrlForSize;
public final class NewsItem {
@SuppressWarnings("unused") @Required @Nullable private String story;
@SuppressWarnings("unused") @Nullable private List<RbPageSummary> links
= Collections.emptyList();
@NonNull String story() {
return StringUtils.defaultString(story);
}
@NonNull public List<RbPageSummary> links() {
return links != null ? links : Collections.emptyList();
}
@Nullable public Uri thumb() {
Uri uri = getFirstImageUri(links());
return uri != null ? getUrlForSize(uri, PREFERRED_THUMB_SIZE) : null;
}
@Nullable Uri featureImage() {
return getFirstImageUri(links());
}
/**
* Iterate through the CardPageItems associated with the news story's links and return the first
* thumb URI found.
*/
@Nullable private Uri getFirstImageUri(List<RbPageSummary> links) {
for (RbPageSummary link : links) {
if (link == null) {
continue;
}
String thumbnail = link.getThumbnailUrl();
if (thumbnail != null) {
return Uri.parse(thumbnail);
}
}
return null;
}
}

View file

@ -0,0 +1,80 @@
package org.wikipedia.feed.onthisday;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.restbase.page.RbPageSummary;
import org.wikipedia.json.annotations.Required;
import org.wikipedia.util.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public class OnThisDay {
@SuppressWarnings("unused") @Nullable private List<Event> selected;
@SuppressWarnings("unused") @Nullable private List<Event> events;
@SuppressWarnings("unused") @Nullable private List<Event> births;
@SuppressWarnings("unused") @Nullable private List<Event> deaths;
@SuppressWarnings("unused") @Nullable private List<Event> holidays;
@NonNull public List<Event> selectedEvents() {
return selected != null ? selected : Collections.emptyList();
}
@NonNull public List<Event> events() {
ArrayList<Event> allEvents = new ArrayList<>();
if (events != null) {
allEvents.addAll(events);
}
if (births != null) {
allEvents.addAll(births);
}
if (deaths != null) {
allEvents.addAll(deaths);
}
if (holidays != null) {
allEvents.addAll(holidays);
}
Collections.sort(allEvents, (e1, e2) -> Integer.compare(e2.year(), e1.year()));
return allEvents;
}
public static class Event {
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private String text;
@SuppressWarnings("unused") private int year;
@SuppressWarnings("unused,NullableProblems") @Required @NonNull private List<RbPageSummary> pages;
@NonNull
public CharSequence text() {
List<String> pageTitles = new ArrayList<>();
for (RbPageSummary page : pages) {
pageTitles.add((StringUtil.fromHtml(StringUtils.defaultString(page.getNormalizedTitle()))).toString());
}
return StringUtil.boldenSubstrings(text, pageTitles);
}
public int year() {
return year;
}
@Nullable
public List<RbPageSummary> pages() {
Iterator iterator = pages.iterator();
while ((iterator.hasNext())) {
if (iterator.next() == null) {
iterator.remove();
}
}
return pages;
}
}
public void setSelected(@Nullable List<Event> selected) {
this.selected = selected;
}
}

View file

@ -0,0 +1,16 @@
package org.wikipedia.gallery;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
public class ArtistInfo extends TextInfo {
@SuppressWarnings("unused,NullableProblems") @Nullable private String name;
@SuppressWarnings("unused,NullableProblems") @Nullable @SerializedName("user_page") private String userPage;
@Nullable
public String getName() {
return name;
}
}

View file

@ -0,0 +1,102 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
@SuppressWarnings("unused")
public class ExtMetadata {
@SerializedName("DateTime") @Nullable private Values dateTime;
@SerializedName("ObjectName") @Nullable private Values objectName;
@SerializedName("CommonsMetadataExtension") @Nullable private Values commonsMetadataExtension;
@SerializedName("Categories") @Nullable private Values categories;
@SerializedName("Assessments") @Nullable private Values assessments;
@SerializedName("GPSLatitude") @Nullable private Values gpsLatitude;
@SerializedName("GPSLongitude") @Nullable private Values gpsLongitude;
@SerializedName("ImageDescription") @Nullable private Values imageDescription;
@SerializedName("DateTimeOriginal") @Nullable private Values dateTimeOriginal;
@SerializedName("Artist") @Nullable private Values artist;
@SerializedName("Credit") @Nullable private Values credit;
@SerializedName("Permission") @Nullable private Values permission;
@SerializedName("AuthorCount") @Nullable private Values authorCount;
@SerializedName("LicenseShortName") @Nullable private Values licenseShortName;
@SerializedName("UsageTerms") @Nullable private Values usageTerms;
@SerializedName("LicenseUrl") @Nullable private Values licenseUrl;
@SerializedName("AttributionRequired") @Nullable private Values attributionRequired;
@SerializedName("Copyrighted") @Nullable private Values copyrighted;
@SerializedName("Restrictions") @Nullable private Values restrictions;
@SerializedName("License") @Nullable private Values license;
@NonNull public String licenseShortName() {
return StringUtils.defaultString(licenseShortName == null ? null : licenseShortName.value());
}
@NonNull public String licenseUrl() {
return StringUtils.defaultString(licenseUrl == null ? null : licenseUrl.value());
}
@NonNull public String license() {
return StringUtils.defaultString(license == null ? null : license.value());
}
@NonNull public String imageDescription() {
return StringUtils.defaultString(imageDescription == null ? null : imageDescription.value());
}
@NonNull public String imageDescriptionSource() {
return StringUtils.defaultString(imageDescription == null ? null : imageDescription.source());
}
@NonNull public String objectName() {
return StringUtils.defaultString(objectName == null ? null : objectName.value());
}
@NonNull public String usageTerms() {
return StringUtils.defaultString(usageTerms == null ? null : usageTerms.value());
}
@NonNull public String dateTimeOriginal() {
return StringUtils.defaultString(dateTimeOriginal == null ? null : dateTimeOriginal.value());
}
@NonNull public String dateTime() {
return StringUtils.defaultString(dateTime == null ? null : dateTime.value());
}
@NonNull public String artist() {
return StringUtils.defaultString(artist == null ? null : artist.value());
}
@NonNull public String getCategories() {
return StringUtils.defaultString(categories == null ? null : categories.value());
}
@NonNull public String getGpsLatitude() {
return StringUtils.defaultString(gpsLatitude == null ? null : gpsLatitude.value());
}
@NonNull public String getGpsLongitude() {
return StringUtils.defaultString(gpsLongitude == null ? null : gpsLongitude.value());
}
@NonNull public String credit() {
return StringUtils.defaultString(credit == null ? null : credit.value());
}
public class Values {
@Nullable private String value;
@Nullable private String source;
@Nullable private String hidden;
@Nullable public String value() {
return value;
}
@Nullable public String source() {
return source;
}
}
}

View file

@ -0,0 +1,35 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class Gallery {
@SuppressWarnings("unused,NullableProblems") @Nullable private String revision;
@SuppressWarnings("unused,NullableProblems") @Nullable private String tid;
@SuppressWarnings("unused") @Nullable private List<GalleryItem> items;
@Nullable
public List<GalleryItem> getAllItems() {
return items;
}
@NonNull
public List<GalleryItem> getItems(@NonNull String... types) {
List<GalleryItem> list = new ArrayList<>();
if (items != null) {
for (GalleryItem item : items) {
if (item.isShowInGallery()) {
for (String type : types) {
if (item.getType().contains(type)) {
list.add(item);
}
}
}
}
}
return list;
}
}

View file

@ -0,0 +1,194 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.Service;
import org.wikipedia.util.ImageUrlUtil;
import org.wikipedia.util.StringUtil;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SuppressWarnings("unused")
public class GalleryItem implements Serializable {
public static final int PREFERRED_GALLERY_IMAGE_SIZE = 1280;
@SerializedName("section_id") private int sectionId;
@SuppressWarnings("NullableProblems") @NonNull private String type;
@Nullable @SerializedName("audio_type") private String audioType;
@Nullable private TextInfo caption;
private boolean showInGallery;
@SuppressWarnings("NullableProblems") @NonNull private Titles titles;
@Nullable private ImageInfo thumbnail;
@Nullable private ImageInfo original;
@Nullable private List<VideoInfo> sources;
@Nullable @SerializedName("file_page") private String filePage;
@Nullable private ArtistInfo artist;
private double duration;
@SuppressWarnings("NullableProblems") @NonNull private ImageLicense license;
@Nullable private TextInfo description;
@Nullable @SerializedName("wb_entity_id") private String entityId;
@Nullable @SerializedName("structured") private StructuredData structuredData;
public GalleryItem() {
}
public GalleryItem(@NonNull String title) {
this.type = "*/*";
this.titles = new Titles(title, StringUtil.addUnderscores(title), title);
this.original = new ImageInfo();
this.thumbnail = new ImageInfo();
this.description = new TextInfo();
this.license = new ImageLicense();
}
@NonNull
public String getType() {
return StringUtils.defaultString(type);
}
@NonNull
public String getAudioType() {
return StringUtils.defaultString(audioType);
}
@Nullable
public TextInfo getCaption() {
return caption;
}
public boolean isShowInGallery() {
return showInGallery;
}
@NonNull
public Titles getTitles() {
return titles;
}
protected void setTitle(@NonNull String title) {
titles = new Titles(title, StringUtil.addUnderscores(title), title);
}
@NonNull
public ImageInfo getThumbnail() {
if (thumbnail == null) {
thumbnail = new ImageInfo();
}
return thumbnail;
}
@NonNull
public String getThumbnailUrl() {
return getThumbnail().getSource();
}
@NonNull
public String getPreferredSizedImageUrl() {
return ImageUrlUtil.getUrlForPreferredSize(getThumbnailUrl(), PREFERRED_GALLERY_IMAGE_SIZE);
}
@NonNull
public ImageInfo getOriginal() {
if (original == null) {
original = new ImageInfo();
}
return original;
}
@Nullable
public List<VideoInfo> getSources() {
return sources;
}
@Nullable
public VideoInfo getOriginalVideoSource() {
// The getSources has different levels of source,
// should have an option that allows user to chose which quality to play
return sources == null || sources.size() == 0
? null : sources.get(sources.size() - 1);
}
public double getDuration() {
return duration;
}
@NonNull
public String getFilePage() {
// return the base url of Wiki Commons for WikiSite() if the file_page is null.
return filePage == null ? Service.COMMONS_URL : StringUtils.defaultString(filePage);
}
public void setFilePage(@NonNull String filePage) {
this.filePage = filePage;
}
@Nullable
public ArtistInfo getArtist() {
return artist;
}
public void setArtist(@Nullable ArtistInfo artist) {
this.artist = artist;
}
@NonNull
public ImageLicense getLicense() {
return license;
}
public void setLicense(@NonNull ImageLicense license) {
this.license = license;
}
@NonNull
public TextInfo getDescription() {
if (description == null) {
description = new TextInfo();
}
return description;
}
@NonNull
public Map<String, String> getStructuredCaptions() {
return (structuredData != null && structuredData.captions != null) ? structuredData.captions : Collections.emptyMap();
}
public static class Titles implements Serializable {
@Nullable private String canonical;
@Nullable private String normalized;
@Nullable private String display;
Titles(@NonNull String display, @NonNull String canonical, @NonNull String normalized) {
this.display = display;
this.canonical = canonical;
this.normalized = normalized;
}
@NonNull
public String getCanonical() {
return StringUtils.defaultString(canonical);
}
@NonNull
public String getNormalized() {
return StringUtils.defaultString(normalized);
}
@NonNull
public String getDisplay() {
return StringUtils.defaultString(display);
}
}
public static class StructuredData implements Serializable {
@Nullable private HashMap<String, String> captions;
}
}

View file

@ -0,0 +1,76 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
/**
* Gson POJO for a standard image info object as returned by the API ImageInfo module
*/
@SuppressWarnings("unused")
public class ImageInfo implements Serializable {
private int size;
private int width;
private int height;
@Nullable private String source;
@SerializedName("thumburl") @Nullable private String thumbUrl;
@SerializedName("thumbwidth") private int thumbWidth;
@SerializedName("thumbheight") private int thumbHeight;
@SerializedName("url") @Nullable private String originalUrl;
@SerializedName("descriptionurl") @Nullable private String descriptionUrl;
@SerializedName("descriptionshorturl") @Nullable private String descriptionShortUrl;
@SerializedName("mime") @Nullable private String mimeType;
@SerializedName("extmetadata")@Nullable private ExtMetadata metadata;
@Nullable private String user;
@Nullable private String timestamp;
@NonNull
public String getSource() {
return StringUtils.defaultString(source);
}
public void setSource(@Nullable String source) {
this.source = source;
}
public int getSize() {
return size;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
@NonNull public String getMimeType() {
return StringUtils.defaultString(mimeType, "*/*");
}
@NonNull public String getThumbUrl() {
return StringUtils.defaultString(thumbUrl);
}
@NonNull public String getOriginalUrl() {
return StringUtils.defaultString(originalUrl);
}
@NonNull public String getUser() {
return StringUtils.defaultString(user);
}
@NonNull public String getTimestamp() {
return StringUtils.defaultString(timestamp);
}
@Nullable public ExtMetadata getMetadata() {
return metadata;
}
}

View file

@ -0,0 +1,67 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.Locale;
import static org.apache.commons.lang3.StringUtils.defaultString;
public class ImageLicense implements Serializable {
private static final String CREATIVE_COMMONS_PREFIX = "cc";
private static final String PUBLIC_DOMAIN_PREFIX = "pd";
private static final String CC_BY_SA = "ccbysa";
@NonNull @SerializedName("type") private final String license;
@NonNull @SerializedName("code") private final String licenseShortName;
@NonNull @SerializedName("url") private final String licenseUrl;
public ImageLicense(@NonNull ExtMetadata metadata) {
this.license = metadata.license();
this.licenseShortName = metadata.licenseShortName();
this.licenseUrl = metadata.licenseUrl();
}
private ImageLicense(@NonNull String license, @NonNull String licenseShortName, @NonNull String licenseUrl) {
this.license = license;
this.licenseShortName = licenseShortName;
this.licenseUrl = licenseUrl;
}
public ImageLicense() {
this("", "", "");
}
@NonNull public String getLicenseName() {
return license;
}
@NonNull public String getLicenseShortName() {
return licenseShortName;
}
@NonNull public String getLicenseUrl() {
return licenseUrl;
}
public boolean isLicenseCC() {
return defaultString(license).toLowerCase(Locale.ENGLISH).startsWith(CREATIVE_COMMONS_PREFIX)
|| defaultString(licenseShortName).toLowerCase(Locale.ENGLISH).startsWith(CREATIVE_COMMONS_PREFIX);
}
public boolean isLicensePD() {
return defaultString(license).toLowerCase(Locale.ENGLISH).startsWith(PUBLIC_DOMAIN_PREFIX)
|| defaultString(licenseShortName).toLowerCase(Locale.ENGLISH).startsWith(PUBLIC_DOMAIN_PREFIX);
}
public boolean isLicenseCCBySa() {
return defaultString(license).toLowerCase(Locale.ENGLISH).replace("-", "").startsWith(CC_BY_SA)
|| defaultString(licenseShortName).toLowerCase(Locale.ENGLISH).replace("-", "").startsWith(CC_BY_SA);
}
public boolean hasLicenseInfo() {
return !(license.isEmpty() && licenseShortName.isEmpty() && licenseUrl.isEmpty());
}
}

View file

@ -0,0 +1,29 @@
package org.wikipedia.gallery;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
public class TextInfo implements Serializable {
@SuppressWarnings("unused,NullableProblems") @Nullable private String html;
@SuppressWarnings("unused,NullableProblems") @Nullable private String text;
@SuppressWarnings("unused,NullableProblems") @Nullable private String lang;
@NonNull
public String getHtml() {
return StringUtils.defaultString(html);
}
@NonNull
public String getText() {
return StringUtils.defaultString(text);
}
@NonNull
public String getLang() {
return StringUtils.defaultString(lang);
}
}

View file

@ -0,0 +1,16 @@
package org.wikipedia.gallery;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* Gson POJO for a standard video info object as returned by the API VideoInfo module
*/
public class VideoInfo extends ImageInfo {
@SuppressWarnings("unused") @Nullable private List<String> codecs;
@SuppressWarnings("unused,NullableProblems") @Nullable private String name;
@SuppressWarnings("unused,NullableProblems") @Nullable @SerializedName("short_name") private String shortName;
}

View file

@ -0,0 +1,53 @@
package org.wikipedia.json;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.WikiSite;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
public class CookieManagerTypeAdapter extends TypeAdapter<SharedPreferenceCookieManager> {
@Override public void write(JsonWriter out, SharedPreferenceCookieManager cookies) throws IOException {
Map<String, List<Cookie>> map = cookies.getCookieJar();
out.beginObject();
for (String key : map.keySet()) {
out.name(key).beginArray();
for (Cookie cookie : map.get(key)) {
out.value(cookie.toString());
}
out.endArray();
}
out.endObject();
}
@Override public SharedPreferenceCookieManager read(JsonReader in) throws IOException {
Map<String, List<Cookie>> map = new HashMap<>();
in.beginObject();
while (in.hasNext()) {
String key = in.nextName();
List<Cookie> list = new ArrayList<>();
map.put(key, list);
in.beginArray();
HttpUrl url = HttpUrl.parse(WikiSite.DEFAULT_SCHEME + "://" + key);
while (in.hasNext()) {
String str = in.nextString();
if (url != null) {
list.add(Cookie.parse(url, str));
}
}
in.endArray();
}
in.endObject();
return new SharedPreferenceCookieManager(map);
}
}

View file

@ -0,0 +1,18 @@
package org.wikipedia.json;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
public final class GsonMarshaller {
public static String marshal(@Nullable Object object) {
return marshal(GsonUtil.getDefaultGson(), object);
}
public static String marshal(@NonNull Gson gson, @Nullable Object object) {
return gson.toJson(object);
}
private GsonMarshaller() { }
}

View file

@ -0,0 +1,32 @@
package org.wikipedia.json;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
public final class GsonUnmarshaller {
/** @return Unmarshalled object. */
public static <T> T unmarshal(Class<T> clazz, @Nullable String json) {
return unmarshal(GsonUtil.getDefaultGson(), clazz, json);
}
/** @return Unmarshalled collection of objects. */
public static <T> T unmarshal(TypeToken<T> typeToken, @Nullable String json) {
return unmarshal(GsonUtil.getDefaultGson(), typeToken, json);
}
/** @return Unmarshalled object. */
public static <T> T unmarshal(@NonNull Gson gson, Class<T> clazz, @Nullable String json) {
return gson.fromJson(json, clazz);
}
/** @return Unmarshalled collection of objects. */
public static <T> T unmarshal(@NonNull Gson gson, TypeToken<T> typeToken, @Nullable String json) {
// From the manual: "Fairly hideous... Unfortunately, no way to get around this in Java".
return gson.fromJson(json, typeToken.getType());
}
private GsonUnmarshaller() { }
}

View file

@ -0,0 +1,38 @@
package org.wikipedia.json;
import android.net.Uri;
import androidx.annotation.VisibleForTesting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.page.Namespace;
public final class GsonUtil {
private static final String DATE_FORMAT = "MMM dd, yyyy HH:mm:ss";
private static final GsonBuilder DEFAULT_GSON_BUILDER = new GsonBuilder()
.setDateFormat(DATE_FORMAT)
.registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe())
.registerTypeHierarchyAdapter(Namespace.class, new NamespaceTypeAdapter().nullSafe())
.registerTypeAdapter(WikiSite.class, new WikiSiteTypeAdapter().nullSafe())
.registerTypeAdapter(SharedPreferenceCookieManager.class, new CookieManagerTypeAdapter().nullSafe())
.registerTypeAdapterFactory(new RequiredFieldsCheckOnReadTypeAdapterFactory())
.registerTypeAdapterFactory(new PostProcessingTypeAdapter());
private static final Gson DEFAULT_GSON = DEFAULT_GSON_BUILDER.create();
public static Gson getDefaultGson() {
return DEFAULT_GSON;
}
@VisibleForTesting
public static GsonBuilder getDefaultGsonBuilder() {
return DEFAULT_GSON_BUILDER;
}
private GsonUtil() { }
}

View file

@ -0,0 +1,29 @@
package org.wikipedia.json;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.wikipedia.page.Namespace;
import java.io.IOException;
public class NamespaceTypeAdapter extends TypeAdapter<Namespace> {
@Override
public void write(JsonWriter out, Namespace namespace) throws IOException {
out.value(namespace.code());
}
@Override
public Namespace read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.STRING) {
// Prior to 3210ce44, we marshaled Namespace as the name string of the enum, instead of
// the code number. This introduces a backwards-compatible check for the string value.
// TODO: remove after April 2017, when all older namespaces have been deserialized.
return Namespace.valueOf(in.nextString());
}
return Namespace.of(in.nextInt());
}
}

View file

@ -0,0 +1,34 @@
package org.wikipedia.json;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
public class PostProcessingTypeAdapter implements TypeAdapterFactory {
public interface PostProcessable {
void postProcess();
}
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
public T read(JsonReader in) throws IOException {
T obj = delegate.read(in);
if (obj instanceof PostProcessable) {
((PostProcessable)obj).postProcess();
}
return obj;
}
};
}
}

View file

@ -0,0 +1,94 @@
package org.wikipedia.json;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.wikipedia.json.annotations.Required;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Set;
/**
* TypeAdapterFactory that provides TypeAdapters that return null values for objects that are
* missing fields annotated with @Required.
*
* BEWARE: This means that a List or other Collection of objects that have @Required fields can
* contain null elements after deserialization!
*
* TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements
* annotation and another corresponding TypeAdapter(Factory).
*/
class RequiredFieldsCheckOnReadTypeAdapterFactory implements TypeAdapterFactory {
@Nullable @Override public final <T> TypeAdapter<T> create(@NonNull Gson gson, @NonNull TypeToken<T> typeToken) {
Class<?> rawType = typeToken.getRawType();
Set<Field> requiredFields = collectRequiredFields(rawType);
if (requiredFields.isEmpty()) {
return null;
}
setFieldsAccessible(requiredFields, true);
return new Adapter<>(gson.getDelegateAdapter(this, typeToken), requiredFields);
}
@NonNull private Set<Field> collectRequiredFields(@NonNull Class<?> clazz) {
Field[] fields = clazz.getDeclaredFields();
Set<Field> required = new ArraySet<>();
for (Field field : fields) {
if (field.isAnnotationPresent(Required.class)) {
required.add(field);
}
}
return Collections.unmodifiableSet(required);
}
private void setFieldsAccessible(Iterable<Field> fields, boolean accessible) {
for (Field field : fields) {
field.setAccessible(accessible);
}
}
private static final class Adapter<T> extends TypeAdapter<T> {
@NonNull private final TypeAdapter<T> delegate;
@NonNull private final Set<Field> requiredFields;
private Adapter(@NonNull TypeAdapter<T> delegate, @NonNull final Set<Field> requiredFields) {
this.delegate = delegate;
this.requiredFields = requiredFields;
}
@Override public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
@Override @Nullable public T read(JsonReader in) throws IOException {
T deserialized = delegate.read(in);
return allRequiredFieldsPresent(deserialized, requiredFields) ? deserialized : null;
}
private boolean allRequiredFieldsPresent(@NonNull T deserialized,
@NonNull Set<Field> required) {
for (Field field : required) {
try {
if (field.get(deserialized) == null) {
return false;
}
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new JsonParseException(e);
}
}
return true;
}
}
}

View file

@ -0,0 +1,22 @@
package org.wikipedia.json;
import android.net.Uri;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
public class UriTypeAdapter extends TypeAdapter<Uri> {
@Override
public void write(JsonWriter out, Uri value) throws IOException {
out.value(value.toString());
}
@Override
public Uri read(JsonReader in) throws IOException {
String url = in.nextString();
return Uri.parse(url);
}
}

View file

@ -0,0 +1,63 @@
package org.wikipedia.json;
import android.net.Uri;
import com.google.gson.JsonParseException;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.wikipedia.dataclient.WikiSite;
import java.io.IOException;
public class WikiSiteTypeAdapter extends TypeAdapter<WikiSite> {
private static final String DOMAIN = "domain";
private static final String LANGUAGE_CODE = "languageCode";
@Override public void write(JsonWriter out, WikiSite value) throws IOException {
out.beginObject();
out.name(DOMAIN);
out.value(value.url());
out.name(LANGUAGE_CODE);
out.value(value.languageCode());
out.endObject();
}
@Override public WikiSite read(JsonReader in) throws IOException {
// todo: legacy; remove in June 2018
if (in.peek() == JsonToken.STRING) {
return new WikiSite(Uri.parse(in.nextString()));
}
String domain = null;
String languageCode = null;
in.beginObject();
while (in.hasNext()) {
String field = in.nextName();
String val = in.nextString();
switch (field) {
case DOMAIN:
domain = val;
break;
case LANGUAGE_CODE:
languageCode = val;
break;
default: break;
}
}
in.endObject();
if (domain == null) {
throw new JsonParseException("Missing domain");
}
// todo: legacy; remove in June 2018
if (languageCode == null) {
return new WikiSite(domain);
}
return new WikiSite(domain, languageCode);
}
}

View file

@ -0,0 +1,21 @@
package org.wikipedia.json.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
/**
* Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return
* an instantiated object.
*
* E.g.: @NonNull @Required private String title;
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(FIELD)
public @interface Required {
}

View file

@ -0,0 +1,42 @@
package org.wikipedia.language;
import androidx.annotation.NonNull;
import java.util.Locale;
public final class AcceptLanguageUtil {
private static final float APP_LANGUAGE_QUALITY = .9f;
private static final float SYSTEM_LANGUAGE_QUALITY = .8f;
/**
* @return The value that should go in the Accept-Language header.
*/
@NonNull
public static String getAcceptLanguage(@NonNull String wikiLanguageCode,
@NonNull String appLanguageCode,
@NonNull String systemLanguageCode) {
String acceptLanguage = wikiLanguageCode;
acceptLanguage = appendToAcceptLanguage(acceptLanguage, appLanguageCode, APP_LANGUAGE_QUALITY);
acceptLanguage = appendToAcceptLanguage(acceptLanguage, systemLanguageCode, SYSTEM_LANGUAGE_QUALITY);
return acceptLanguage;
}
@NonNull
private static String appendToAcceptLanguage(@NonNull String acceptLanguage,
@NonNull String languageCode, float quality) {
// If accept-language already contains the language, just return accept-language.
if (acceptLanguage.contains(languageCode)) {
return acceptLanguage;
}
// If accept-language is empty, don't append. Just return the language.
if (acceptLanguage.isEmpty()) {
return languageCode;
}
// Accept-language is nonempty, append the language.
return String.format(Locale.ROOT, "%s,%s;q=%.1f", acceptLanguage, languageCode, quality);
}
private AcceptLanguageUtil() { }
}

View file

@ -0,0 +1,142 @@
package org.wikipedia.language;
import android.content.Context;
import android.content.res.Resources;
import android.text.TextUtils;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.R;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
/** Immutable look up table for all app supported languages. All article languages may not be
* present in this table as it is statically bundled with the app. */
public class AppLanguageLookUpTable {
public static final String SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans";
public static final String TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant";
public static final String CHINESE_CN_LANGUAGE_CODE = "zh-cn";
public static final String CHINESE_HK_LANGUAGE_CODE = "zh-hk";
public static final String CHINESE_MO_LANGUAGE_CODE = "zh-mo";
public static final String CHINESE_SG_LANGUAGE_CODE = "zh-sg";
public static final String CHINESE_TW_LANGUAGE_CODE = "zh-tw";
public static final String CHINESE_YUE_LANGUAGE_CODE = "zh-yue";
public static final String CHINESE_LANGUAGE_CODE = "zh";
public static final String NORWEGIAN_LEGACY_LANGUAGE_CODE = "no";
public static final String NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb";
public static final String TEST_LANGUAGE_CODE = "test";
public static final String FALLBACK_LANGUAGE_CODE = "en"; // Must exist in preference_language_keys.
@NonNull private final Resources resources;
// Language codes for all app supported languages in fixed order. The special code representing
// the dynamic system language is null.
@NonNull private SoftReference<List<String>> codesRef = new SoftReference<>(null);
// English names for all app supported languages in fixed order.
@NonNull private SoftReference<List<String>> canonicalNamesRef = new SoftReference<>(null);
// Native names for all app supported languages in fixed order.
@NonNull private SoftReference<List<String>> localizedNamesRef = new SoftReference<>(null);
public AppLanguageLookUpTable(@NonNull Context context) {
resources = context.getResources();
}
/**
* @return Nonnull immutable list. The special code representing the dynamic system language is
* null.
*/
@NonNull
public List<String> getCodes() {
List<String> codes = codesRef.get();
if (codes == null) {
codes = getStringList(R.array.preference_language_keys);
codesRef = new SoftReference<>(codes);
}
return codes;
}
@Nullable
public String getCanonicalName(@Nullable String code) {
String name = defaultIndex(getCanonicalNames(), indexOfCode(code), null);
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) {
if (code.equals(Locale.CHINESE.getLanguage())) {
name = Locale.CHINESE.getDisplayName(Locale.ENGLISH);
} else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) {
name = defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null);
}
}
return name;
}
@Nullable
public String getLocalizedName(@Nullable String code) {
String name = defaultIndex(getLocalizedNames(), indexOfCode(code), null);
if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) {
if (code.equals(Locale.CHINESE.getLanguage())) {
name = Locale.CHINESE.getDisplayName(Locale.CHINESE);
} else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) {
name = defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null);
}
}
return name;
}
public List<String> getCanonicalNames() {
List<String> names = canonicalNamesRef.get();
if (names == null) {
names = getStringList(R.array.preference_language_canonical_names);
canonicalNamesRef = new SoftReference<>(names);
}
return names;
}
public List<String> getLocalizedNames() {
List<String> names = localizedNamesRef.get();
if (names == null) {
names = getStringList(R.array.preference_language_local_names);
localizedNamesRef = new SoftReference<>(names);
}
return names;
}
public boolean isSupportedCode(@Nullable String code) {
return getCodes().contains(code);
}
private <T> T defaultIndex(List<T> list, int index, T defaultValue) {
return inBounds(list, index) ? list.get(index) : defaultValue;
}
/**
* Searches #codes for the specified language code and returns the index for use in
* #canonicalNames and #localizedNames.
*
* @param code The language code to search for. The special code representing the dynamic system
* language is null.
* @return The index of the language code or -1 if the code is not supported.
*/
private int indexOfCode(@Nullable String code) {
return getCodes().indexOf(code);
}
/** @return Nonnull immutable list. */
@NonNull
private List<String> getStringList(int id) {
return Arrays.asList(getStringArray(id));
}
private boolean inBounds(List<?> list, int index) {
return index >= 0 && index < list.size();
}
public String[] getStringArray(@ArrayRes int id) {
return resources.getStringArray(id);
}
}

View file

@ -0,0 +1,248 @@
package org.wikipedia.login;
import android.annotation.SuppressLint;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.ServiceFactory;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.dataclient.mwapi.ListUserResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.mwapi.MwServiceError;
import org.wikipedia.util.log.L;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* Responsible for making login related requests to the server.
*/
public class LoginClient {
@Nullable private Call<MwQueryResponse> tokenCall;
@Nullable private Call<LoginResponse> loginCall;
public interface LoginCallback {
void success(@NonNull LoginResult result);
void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token);
void passwordResetPrompt(@Nullable String token);
void error(@NonNull Throwable caught);
}
public void request(@NonNull final WikiSite wiki, @NonNull final String userName,
@NonNull final String password, @NonNull final LoginCallback cb) {
cancel();
tokenCall = ServiceFactory.get(wiki).getLoginToken();
tokenCall.enqueue(new Callback<MwQueryResponse>() {
@Override public void onResponse(@NonNull Call<MwQueryResponse> call,
@NonNull Response<MwQueryResponse> response) {
login(wiki, userName, password, null, null, response.body().query().loginToken(), cb);
}
@Override
public void onFailure(@NonNull Call<MwQueryResponse> call, @NonNull Throwable caught) {
if (call.isCanceled()) {
return;
}
cb.error(caught);
}
});
}
public void login(@NonNull final WikiSite wiki, @NonNull final String userName, @NonNull final String password,
@Nullable final String retypedPassword, @Nullable final String twoFactorCode,
@Nullable final String loginToken, @NonNull final LoginCallback cb) {
loginCall = TextUtils.isEmpty(twoFactorCode) && TextUtils.isEmpty(retypedPassword)
? ServiceFactory.get(wiki).postLogIn(userName, password, loginToken, Service.WIKIPEDIA_URL)
: ServiceFactory.get(wiki).postLogIn(userName, password, retypedPassword, twoFactorCode, loginToken, true);
loginCall.enqueue(new Callback<LoginResponse>() {
@Override
public void onResponse(@NonNull Call<LoginResponse> call, @NonNull Response<LoginResponse> response) {
LoginResponse loginResponse = response.body();
LoginResult loginResult = loginResponse.toLoginResult(wiki, password);
if (loginResult != null) {
if (loginResult.pass() && !TextUtils.isEmpty(loginResult.getUserName())) {
// The server could do some transformations on user names, e.g. on some
// wikis is uppercases the first letter.
String actualUserName = loginResult.getUserName();
getExtendedInfo(wiki, actualUserName, loginResult, cb);
} else if ("UI".equals(loginResult.getStatus())) {
if (loginResult instanceof LoginOAuthResult) {
cb.twoFactorPrompt(new LoginFailedException(loginResult.getMessage()), loginToken);
} else if (loginResult instanceof LoginResetPasswordResult) {
cb.passwordResetPrompt(loginToken);
} else {
cb.error(new LoginFailedException(loginResult.getMessage()));
}
} else {
cb.error(new LoginFailedException(loginResult.getMessage()));
}
} else {
cb.error(new IOException("Login failed. Unexpected response."));
}
}
@Override
public void onFailure(@NonNull Call<LoginResponse> call, @NonNull Throwable t) {
if (call.isCanceled()) {
return;
}
cb.error(t);
}
});
}
public void loginBlocking(@NonNull final WikiSite wiki, @NonNull final String userName,
@NonNull final String password, @Nullable final String twoFactorCode) throws Throwable {
Response<MwQueryResponse> tokenResponse = ServiceFactory.get(wiki).getLoginToken().execute();
if (tokenResponse.body() == null || TextUtils.isEmpty(tokenResponse.body().query().loginToken())) {
throw new IOException("Unexpected response when getting login token.");
}
String loginToken = tokenResponse.body().query().loginToken();
Call<LoginResponse> tempLoginCall = StringUtils.defaultIfEmpty(twoFactorCode, "").isEmpty()
? ServiceFactory.get(wiki).postLogIn(userName, password, loginToken, Service.WIKIPEDIA_URL)
: ServiceFactory.get(wiki).postLogIn(userName, password, null, twoFactorCode, loginToken, true);
Response<LoginResponse> response = tempLoginCall.execute();
LoginResponse loginResponse = response.body();
if (loginResponse == null) {
throw new IOException("Unexpected response when logging in.");
}
LoginResult loginResult = loginResponse.toLoginResult(wiki, password);
if (loginResult == null) {
throw new IOException("Unexpected response when logging in.");
}
if ("UI".equals(loginResult.getStatus())) {
if (loginResult instanceof LoginOAuthResult) {
// TODO: Find a better way to boil up the warning about 2FA
throw new LoginFailedException(loginResult.getMessage());
} else {
throw new LoginFailedException(loginResult.getMessage());
}
} else if (!loginResult.pass() || TextUtils.isEmpty(loginResult.getUserName())) {
throw new LoginFailedException(loginResult.getMessage());
}
}
@SuppressLint("CheckResult")
private void getExtendedInfo(@NonNull final WikiSite wiki, @NonNull String userName,
@NonNull final LoginResult loginResult, @NonNull final LoginCallback cb) {
ServiceFactory.get(wiki).getUserInfo(userName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(response -> {
ListUserResponse user = response.query().getUserResponse(userName);
int id = response.query().userInfo().id();
loginResult.setUserId(id);
loginResult.setGroups(user.getGroups());
cb.success(loginResult);
L.v("Found user ID " + id + " for " + wiki.subdomain());
}, caught -> {
L.e("Login succeeded but getting group information failed. " + caught);
cb.error(caught);
});
}
public void cancel() {
cancelTokenRequest();
cancelLogin();
}
private void cancelTokenRequest() {
if (tokenCall == null) {
return;
}
tokenCall.cancel();
tokenCall = null;
}
private void cancelLogin() {
if (loginCall == null) {
return;
}
loginCall.cancel();
loginCall = null;
}
public static final class LoginResponse {
@SuppressWarnings("unused") @SerializedName("error") @Nullable
private MwServiceError error;
@SuppressWarnings("unused") @SerializedName("clientlogin") @Nullable
private ClientLogin clientLogin;
@Nullable public MwServiceError getError() {
return error;
}
@Nullable LoginResult toLoginResult(@NonNull WikiSite site, @NonNull String password) {
return clientLogin != null ? clientLogin.toLoginResult(site, password) : null;
}
private static class ClientLogin {
@SuppressWarnings("unused,NullableProblems") @NonNull private String status;
@SuppressWarnings("unused") @Nullable private List<Request> requests;
@SuppressWarnings("unused") @Nullable private String message;
@SuppressWarnings("unused") @SerializedName("username") @Nullable private String userName;
LoginResult toLoginResult(@NonNull WikiSite site, @NonNull String password) {
String userMessage = message;
if ("UI".equals(status)) {
if (requests != null) {
for (Request req : requests) {
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest".equals(req.id())) {
return new LoginOAuthResult(site, status, userName, password, message);
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest".equals(req.id())) {
return new LoginResetPasswordResult(site, status, userName, password, message);
}
}
}
} else if (!"PASS".equals(status) && !"FAIL".equals(status)) {
//TODO: String resource -- Looks like needed for others in this class too
userMessage = "An unknown error occurred.";
}
return new LoginResult(site, status, userName, password, userMessage);
}
}
private static class Request {
@SuppressWarnings("unused") @Nullable private String id;
//@SuppressWarnings("unused") @Nullable private JsonObject metadata;
@SuppressWarnings("unused") @Nullable private String required;
@SuppressWarnings("unused") @Nullable private String provider;
@SuppressWarnings("unused") @Nullable private String account;
@SuppressWarnings("unused") @Nullable private Map<String, RequestField> fields;
@Nullable String id() {
return id;
}
}
private static class RequestField {
@SuppressWarnings("unused") @Nullable private String type;
@SuppressWarnings("unused") @Nullable private String label;
@SuppressWarnings("unused") @Nullable private String help;
}
}
public static class LoginFailedException extends Throwable {
public LoginFailedException(String message) {
super(message);
}
}
}

View file

@ -0,0 +1,14 @@
package org.wikipedia.login;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.WikiSite;
public class LoginOAuthResult extends LoginResult {
public LoginOAuthResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
@Nullable String password, @Nullable String message) {
super(site, status, userName, password, message);
}
}

View file

@ -0,0 +1,13 @@
package org.wikipedia.login;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.WikiSite;
public class LoginResetPasswordResult extends LoginResult {
public LoginResetPasswordResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
@Nullable String password, @Nullable String message) {
super(site, status, userName, password, message);
}
}

View file

@ -0,0 +1,73 @@
package org.wikipedia.login;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.wikipedia.dataclient.WikiSite;
import java.util.Collections;
import java.util.Set;
public class LoginResult {
@NonNull private final WikiSite site;
@NonNull private final String status;
@Nullable private final String userName;
@Nullable private final String password;
@Nullable private final String message;
private int userId;
@NonNull private Set<String> groups = Collections.emptySet();
public LoginResult(@NonNull WikiSite site, @NonNull String status, @Nullable String userName,
@Nullable String password, @Nullable String message) {
this.site = site;
this.status = status;
this.userName = userName;
this.password = password;
this.message = message;
}
@NonNull public WikiSite getSite() {
return site;
}
@NonNull public String getStatus() {
return status;
}
public boolean pass() {
return "PASS".equals(status);
}
public boolean fail() {
return "FAIL".equals(status);
}
@Nullable public String getUserName() {
return userName;
}
@Nullable public String getPassword() {
return password;
}
@Nullable public String getMessage() {
return message;
}
public void setUserId(int id) {
this.userId = id;
}
public int getUserId() {
return userId;
}
public void setGroups(@NonNull Set<String> groups) {
this.groups = groups;
}
@NonNull public Set<String> getGroups() {
return groups;
}
}

View file

@ -0,0 +1,19 @@
package org.wikipedia.model
import org.apache.commons.lang3.builder.EqualsBuilder
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.apache.commons.lang3.builder.ToStringBuilder
abstract class BaseModel {
override fun toString(): String {
return ToStringBuilder.reflectionToString(this)
}
override fun hashCode(): Int {
return HashCodeBuilder.reflectionHashCode(this)
}
override fun equals(other: Any?): Boolean {
return EqualsBuilder.reflectionEquals(this, other)
}
}

View file

@ -0,0 +1,5 @@
package org.wikipedia.model
interface CodeEnum<T> {
fun enumeration(code: Int): T
}

Some files were not shown because too many files have changed in this diff Show more