Skip to content

Commit

Permalink
Temporary accounts. (#4914)
Browse files Browse the repository at this point in the history
* Fix usage of non-default / beta base URL for MediaWiki.

* Initial steps towards IP masking.

* Reduce a bit.

* Reduce a bit.

* Clean up.

* Introduce dialog shown upon receiving temp account.

* String resources for Temporary Accounts feature.

* Add resources.

* More formatting.

* Correctly wire up NavTabDialog.

* Get temp account expiry, and wire into Settings/Logout.

* Just a couple more.

* Further wire in expiry.

* Require full account for suggested edits.

* Wire up into editing activity.

* Wire into Talk activity.

* Show dialog upon editing.

* Update icons.

* Wire into Talk activity.

* Update logic for syncing reading lists.

* Fix.

* Use icons in dialogs.

* Treat anon token as acceptable for temp accounts.

* Add proper user assertion for edits.

* Pass into Metrics performer data.

* Rename for clarity.

* Update Edit History with further temp account designs.

* Design tweaks in Talk activity.

* Design updates for popup dialog when editing.

* Sequentialize snackbars.

* Reset temp-account dialog state when logging out.

* Add new edit count type.

* Fix error

* Make item not clickable.

* Correctly wire up login/createaccount from linkmovementmethod.

* Remove unnecessary movement method.

* Handle external link correctly.

* Thread into description/caption edit snackbar sequence.

* Finish wiring up API call to tell if temp accounts are enabled.

* Use home wiki for unread notifications.

* Unused import

---------

Co-authored-by: Cooltey Feng <[email protected]>
  • Loading branch information
dbrant and cooltey authored Oct 15, 2024
1 parent 262b6da commit 55f6631
Show file tree
Hide file tree
Showing 48 changed files with 692 additions and 278 deletions.
2 changes: 2 additions & 0 deletions app/src/main/java/org/wikipedia/WikipediaApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ class WikipediaApp : Application() {
AccountUtil.removeAccount()
Prefs.isPushNotificationTokenSubscribed = false
Prefs.pushNotificationTokenOld = ""
Prefs.tempAccountWelcomeShown = false
Prefs.tempAccountDialogShown = false

val token = ServiceFactory.get(wikiSite).getToken().query!!.csrfToken()
WikipediaFirebaseMessagingService.unsubscribePushToken(token!!, Prefs.pushNotificationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ open class MetricsEvent {
AccountUtil.hashCode(),
AccountUtil.userName,
AccountUtil.isLoggedIn,
null,
AccountUtil.isTemporaryAccount,
EventPlatformClient.AssociationController.sessionId,
EventPlatformClient.AssociationController.pageViewId,
AccountUtil.groups,
Expand Down
21 changes: 18 additions & 3 deletions app/src/main/java/org/wikipedia/auth/AccountUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ package org.wikipedia.auth
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.app.Activity
import android.os.Build
import androidx.core.os.bundleOf
import org.wikipedia.R
import org.wikipedia.WikipediaApp
import org.wikipedia.dataclient.SharedPreferenceCookieManager
import org.wikipedia.json.JsonUtil
import org.wikipedia.login.LoginResult
import org.wikipedia.settings.Prefs
import org.wikipedia.util.FeedbackUtil
import org.wikipedia.util.UriUtil
import org.wikipedia.util.log.L.d
import org.wikipedia.util.log.L.logRemoteErrorIfProd
import java.util.*
import java.util.concurrent.TimeUnit

object AccountUtil {
private const val CENTRALAUTH_USER_COOKIE_NAME = "centralauth_User"
Expand All @@ -32,13 +36,13 @@ object AccountUtil {
}

val isLoggedIn: Boolean
get() = account() != null
get() = account() != null || isTemporaryAccount

val isTemporaryAccount: Boolean
get() = account() == null && getUserNameFromCookie().isNotEmpty()

val userName: String
get() = account()?.name.orEmpty()
get() = account()?.name ?: getUserNameFromCookie()

val password: String?
get() {
Expand All @@ -47,7 +51,7 @@ object AccountUtil {
}

val assertUser: String?
get() = if (isLoggedIn) "user" else null
get() = if (isLoggedIn && !isTemporaryAccount) "user" else null

var groups: Set<String>
get() {
Expand Down Expand Up @@ -102,6 +106,17 @@ object AccountUtil {
return SharedPreferenceCookieManager.instance.getCookieExpiryByName(CENTRALAUTH_USER_COOKIE_NAME)
}

fun maybeShowTempAccountWelcome(activity: Activity) {
if (!Prefs.tempAccountWelcomeShown && isTemporaryAccount) {
Prefs.tempAccountWelcomeShown = true
Prefs.tempAccountDialogShown = false

val expiryDays = TimeUnit.MILLISECONDS.toDays(getUserNameExpiryFromCookie() - System.currentTimeMillis()).toInt()
FeedbackUtil.showMessage(activity, activity.resources.getQuantityString(R.plurals.temp_account_created,
expiryDays, userName, expiryDays))
}
}

fun isUserNameTemporary(userName: String): Boolean {
return userName.startsWith("~")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.util.Patterns
import android.view.KeyEvent
import android.view.View
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
Expand All @@ -21,6 +22,7 @@ import org.wikipedia.R
import org.wikipedia.WikipediaApp
import org.wikipedia.activity.BaseActivity
import org.wikipedia.analytics.eventplatform.CreateAccountEvent
import org.wikipedia.auth.AccountUtil
import org.wikipedia.captcha.CaptchaHandler
import org.wikipedia.captcha.CaptchaResult
import org.wikipedia.databinding.ActivityCreateAccountBinding
Expand Down Expand Up @@ -60,6 +62,14 @@ class CreateAccountActivity : BaseActivity() {
if (savedInstanceState == null) {
createAccountEvent.logStart()
}

if (AccountUtil.isTemporaryAccount) {
binding.footerContainer.tempAccountInfoContainer.isVisible = true
binding.footerContainer.tempAccountInfoText.text = StringUtil.fromHtml(getString(R.string.temp_account_login_status, AccountUtil.userName))
} else {
binding.footerContainer.tempAccountInfoContainer.isVisible = false
}

// Set default result to failed, so we can override if it did not
setResult(RESULT_ACCOUNT_NOT_CREATED)
lifecycleScope.launch {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/org/wikipedia/csrf/CsrfTokenClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ object CsrfTokenClient {
L.e(e)
lastError = e
}
if (token.isEmpty() || (AccountUtil.isLoggedIn && token == ANON_TOKEN)) {
if (token.isEmpty() || (AccountUtil.isLoggedIn && !AccountUtil.isTemporaryAccount && token == ANON_TOKEN)) {
continue
}
break
}
if (token.isEmpty() || (AccountUtil.isLoggedIn && token == ANON_TOKEN)) {
if (token.isEmpty() || (AccountUtil.isLoggedIn && !AccountUtil.isTemporaryAccount && token == ANON_TOKEN)) {
if (token == ANON_TOKEN) {
bailWithLogout()
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/org/wikipedia/dataclient/Service.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ interface Service {
@GET(MW_API_PREFIX + "action=query&prop=info|description|pageimages&inprop=varianttitles|displaytitle&redirects=1&pithumbsize=" + PREFERRED_THUMB_SIZE)
suspend fun getInfoByPageIdsOrTitles(@Query("pageids") pageIds: String? = null, @Query("titles") titles: String? = null): MwQueryResponse

@GET(MW_API_PREFIX + "action=query")
@GET(MW_API_PREFIX + "action=query&meta=siteinfo&siprop=general|autocreatetempuser")
suspend fun getPageIds(@Query("titles") titles: String): MwQueryResponse

@GET(MW_API_PREFIX + "action=query&prop=imageinfo&iiprop=timestamp|user|url|mime|extmetadata&iiurlwidth=" + PREFERRED_THUMB_SIZE)
Expand Down Expand Up @@ -366,7 +366,7 @@ interface Service {

// ------- Editing -------

@GET(MW_API_PREFIX + "action=query&prop=revisions|info&rvslots=main&rvprop=content|timestamp|ids&rvlimit=1&converttitles=&intestactions=edit&intestactionsdetail=full&inprop=editintro")
@GET(MW_API_PREFIX + "action=query&prop=revisions|info&meta=siteinfo&siprop=general|autocreatetempuser&rvslots=main&rvprop=content|timestamp|ids&rvlimit=1&converttitles=&intestactions=edit&intestactionsdetail=full&inprop=editintro")
suspend fun getWikiTextForSectionWithInfo(
@Query("titles") title: String,
@Query("rvsection") section: Int?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class MwQueryPage {
@SerialName("revid") val revId: Long = 0
@SerialName("parentid") val parentRevId: Long = 0
@SerialName("anon") val isAnon = false
@SerialName("temp") val isTemp = false
@SerialName("timestamp") val timeStamp: String = ""
val size = 0
val user: String = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class MwQueryResult {
@SerialName("unreadnotificationpages") val unreadNotificationWikis: Map<String, UnreadNotificationWikiItem>? = null
@SerialName("authmanagerinfo") private val amInfo: MwAuthManagerInfo? = null
@SerialName("general") val siteInfo: SiteInfo? = null
@SerialName("autocreatetempuser") val autoCreateTempUser: SiteInfo.AutoCreateTempUser? = null
@SerialName("recentchanges") val recentChanges: List<RecentChange>? = null
@SerialName("usercontribs") val userContributions: List<UserContribution> = emptyList()
@SerialName("allusers") val allUsers: List<UserInfo>? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class EditCount {

companion object {
const val EDIT_TYPE_ANONYMOUS = "anonymous"
const val EDIT_TYPE_TEMPORARY = "temporary"
const val EDIT_TYPE_BOT = "bot"
const val EDIT_TYPE_EDITORS = "editors"
const val EDIT_TYPE_EDITS = "edits"
Expand Down
77 changes: 61 additions & 16 deletions app/src/main/java/org/wikipedia/edit/EditSectionActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.os.Bundle
import android.os.Handler
import android.text.TextUtils
import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
Expand All @@ -17,7 +18,6 @@ import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.net.toUri
import androidx.core.os.postDelayed
import androidx.core.view.isGone
import androidx.core.view.isVisible
Expand All @@ -36,7 +36,7 @@ import org.wikipedia.activity.BaseActivity
import org.wikipedia.analytics.eventplatform.BreadCrumbLogEvent
import org.wikipedia.analytics.eventplatform.EditAttemptStepEvent
import org.wikipedia.analytics.eventplatform.ImageRecommendationsEvent
import org.wikipedia.auth.AccountUtil.isLoggedIn
import org.wikipedia.auth.AccountUtil
import org.wikipedia.captcha.CaptchaHandler
import org.wikipedia.captcha.CaptchaResult
import org.wikipedia.databinding.ActivityEditSectionBinding
Expand Down Expand Up @@ -94,6 +94,7 @@ class EditSectionActivity : BaseActivity(), ThemeChooserDialog.Callback, EditPre
private val requestLogin = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == LoginActivity.RESULT_LOGIN_SUCCESS) {
updateEditLicenseText()
invalidateOptionsMenu()
FeedbackUtil.showMessage(this, R.string.login_success_toast)
}
}
Expand Down Expand Up @@ -228,10 +229,12 @@ class EditSectionActivity : BaseActivity(), ThemeChooserDialog.Callback, EditPre
displaySectionText()
maybeShowEditSourceDialog()
invalidateOptionsMenu()
if (Prefs.autoShowEditNotices) {
showEditNotices()
} else {
maybeShowEditNoticesTooltip()
if (!maybeShowTempAccountDialog()) {
if (Prefs.autoShowEditNotices) {
showEditNotices()
} else {
maybeShowEditNoticesTooltip()
}
}
it.data?.let { error ->
FeedbackUtil.showError(this@EditSectionActivity, MwException(error), viewModel.pageTitle.wikiSite)
Expand Down Expand Up @@ -313,17 +316,10 @@ class EditSectionActivity : BaseActivity(), ThemeChooserDialog.Callback, EditPre

private fun updateEditLicenseText() {
val editLicenseText = ActivityCompat.requireViewById<TextView>(this, R.id.licenseText)
editLicenseText.text = StringUtil.fromHtml(getString(if (isLoggedIn) R.string.edit_save_action_license_logged_in else R.string.edit_save_action_license_anon,
editLicenseText.text = StringUtil.fromHtml(getString(R.string.edit_save_action_license_logged_in,
getString(R.string.terms_of_use_url),
getString(R.string.cc_by_sa_4_url)))
editLicenseText.movementMethod = LinkMovementMethodExt { url: String ->
if (url == "https://#login") {
val loginIntent = LoginActivity.newIntent(this@EditSectionActivity, LoginActivity.SOURCE_EDIT)
requestLogin.launch(loginIntent)
} else {
UriUtil.handleExternalLink(this@EditSectionActivity, url.toUri())
}
}
editLicenseText.movementMethod = LinkMovementMethod.getInstance()
}

private fun doSave() {
Expand Down Expand Up @@ -517,14 +513,23 @@ class EditSectionActivity : BaseActivity(), ThemeChooserDialog.Callback, EditPre
showEditNotices()
true
}
R.id.menu_temp_account -> {
maybeShowTempAccountDialog(true)
true
}
else -> super.onOptionsItemSelected(item)
}
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_edit_section, menu)
val item = menu.findItem(R.id.menu_save_section)

menu.findItem(R.id.menu_temp_account).apply {
isVisible = !AccountUtil.isLoggedIn || AccountUtil.isTemporaryAccount
setIcon(if (AccountUtil.isTemporaryAccount) R.drawable.ic_temp_account else R.drawable.ic_anon_account)
}

val item = menu.findItem(R.id.menu_save_section)
supportActionBar?.elevation = if (editPreviewFragment.isActive) 0f else DimenUtil.dpToPx(4f)
menu.findItem(R.id.menu_edit_notices).isVisible = viewModel.editNotices.isNotEmpty() && !editPreviewFragment.isActive
menu.findItem(R.id.menu_edit_theme).isVisible = !editPreviewFragment.isActive
Expand Down Expand Up @@ -764,6 +769,46 @@ class EditSectionActivity : BaseActivity(), ThemeChooserDialog.Callback, EditPre
}
}

private fun maybeShowTempAccountDialog(fromToolbar: Boolean = false): Boolean {
if (fromToolbar || (!Prefs.tempAccountDialogShown && (!AccountUtil.isLoggedIn || AccountUtil.isTemporaryAccount))) {
val dialog = MaterialAlertDialogBuilder(this, R.style.AlertDialogTheme_Icon_NegativeInactive)
.setIcon(if (AccountUtil.isTemporaryAccount) R.drawable.ic_temp_account else R.drawable.ic_anon_account)
.setTitle(if (AccountUtil.isTemporaryAccount) R.string.temp_account_using_title else R.string.temp_account_not_logged_in)
.setMessage(StringUtil.fromHtml(if (AccountUtil.isTemporaryAccount) getString(R.string.temp_account_temp_dialog_body, AccountUtil.userName)
else getString(if (viewModel.tempAccountsEnabled) R.string.temp_account_anon_dialog_body else R.string.temp_account_anon_ip_dialog_body, getString(R.string.temp_accounts_help_url))))
.setPositiveButton(getString(if (fromToolbar) R.string.temp_account_dialog_ok else R.string.create_account_button)) { dialog, _ ->
dialog.dismiss()
if (!fromToolbar) {
launchLogin()
}
}
.setNegativeButton(getString(if (fromToolbar) R.string.create_account_login else R.string.temp_account_dialog_ok)) { dialog, _ ->
dialog.dismiss()
if (fromToolbar) {
launchLogin()
}
}
.show()
dialog.window?.let {
it.decorView.findViewById<TextView>(android.R.id.message)?.movementMethod = LinkMovementMethodExt { link ->
if (link.contains("#login") || link.contains("#createaccount")) {
launchLogin(link.contains("#createaccount"))
} else {
UriUtil.handleExternalLink(this, Uri.parse(link))
}
dialog.dismiss()
}
}
Prefs.tempAccountDialogShown = true
return true
}
return false
}

private fun launchLogin(createAccountFirst: Boolean = true) {
requestLogin.launch(LoginActivity.newIntent(this, LoginActivity.SOURCE_EDIT, createAccountFirst))
}

private fun startInsertImageFlow() {
val addImageTitle = intent.parcelableExtra<PageTitle>(InsertMediaActivity.EXTRA_IMAGE_TITLE)!!
val addImageSource = intent.getStringExtra(InsertMediaActivity.EXTRA_IMAGE_SOURCE)!!
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/org/wikipedia/edit/EditSectionViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class EditSectionViewModel(bundle: Bundle) : ViewModel() {
var textToHighlight = bundle.getString(EditSectionActivity.EXTRA_HIGHLIGHT_TEXT)
var sectionWikitext: String? = null
var sectionWikitextOriginal: String? = null
var tempAccountsEnabled = true
var editingAllowed = false
val editNotices = mutableListOf<String>()

Expand Down Expand Up @@ -60,6 +61,8 @@ class EditSectionViewModel(bundle: Bundle) : ViewModel() {

val infoResponse = ServiceFactory.get(pageTitle.wikiSite).getWikiTextForSectionWithInfo(pageTitle.prefixedText, if (sectionID >= 0) sectionID else null)

tempAccountsEnabled = infoResponse.query?.autoCreateTempUser?.enabled == true

infoResponse.query?.firstPage()?.let { firstPage ->
val rev = firstPage.revisions.first()

Expand Down
Loading

0 comments on commit 55f6631

Please sign in to comment.