mirror of
https://github.com/nextcloud/android.git
synced 2024-11-27 09:39:25 +03:00
AuthenticatorActivity updated to test existance through SAML-based-federated-authenticated entry point
This commit is contained in:
parent
0713ba613d
commit
5a65ff53c2
5 changed files with 129 additions and 78 deletions
|
@ -5,6 +5,6 @@
|
|||
|
||||
<!-- Flags to setup the authentication methods available in the app -->
|
||||
<string name="auth_method_oauth2">off</string>
|
||||
<string name="auth_method_saml_web_sso">off</string>
|
||||
<string name="auth_method_saml_web_sso">on</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -32,6 +32,7 @@ public class AccountUtils {
|
|||
public static final String WEBDAV_PATH_2_0 = "/files/webdav.php";
|
||||
public static final String WEBDAV_PATH_4_0 = "/remote.php/webdav";
|
||||
private static final String ODAV_PATH = "/remote.php/odav";
|
||||
private static final String SAML_SSO_PATH = "/ocShibAuth";
|
||||
public static final String CARDDAV_PATH_2_0 = "/apps/contacts/carddav.php";
|
||||
public static final String CARDDAV_PATH_4_0 = "/remote/carddav.php";
|
||||
public static final String STATUS_PATH = "/status.php";
|
||||
|
@ -115,11 +116,41 @@ public class AccountUtils {
|
|||
* @param version version of owncloud
|
||||
* @return webdav path for given OC version, null if OC version unknown
|
||||
*/
|
||||
public static String getWebdavPath(OwnCloudVersion version, boolean supportsOAuth) {
|
||||
public static String getWebdavPath(OwnCloudVersion version, boolean supportsOAuth, boolean supportsSamlSso) {
|
||||
if (version != null) {
|
||||
if (supportsOAuth) {
|
||||
return ODAV_PATH;
|
||||
}
|
||||
if (supportsSamlSso) {
|
||||
return SAML_SSO_PATH;
|
||||
}
|
||||
if (version.compareTo(OwnCloudVersion.owncloud_v4) >= 0)
|
||||
return WEBDAV_PATH_4_0;
|
||||
if (version.compareTo(OwnCloudVersion.owncloud_v3) >= 0
|
||||
|| version.compareTo(OwnCloudVersion.owncloud_v2) >= 0)
|
||||
return WEBDAV_PATH_2_0;
|
||||
if (version.compareTo(OwnCloudVersion.owncloud_v1) >= 0)
|
||||
return WEBDAV_PATH_1_2;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proper URL path to access the WebDAV interface of an ownCloud server,
|
||||
* according to its version and the authorization method used.
|
||||
*
|
||||
* @param version Version of ownCloud server.
|
||||
* @param authTokenType Authorization token type, matching some of the AUTH_TOKEN_TYPE_* constants in {@link AccountAuthenticator}.
|
||||
* @return WebDAV path for given OC version and authorization method, null if OC version is unknown.
|
||||
*/
|
||||
public static String getWebdavPath(OwnCloudVersion version, String authTokenType) {
|
||||
if (version != null) {
|
||||
if (AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN.equals(authTokenType)) {
|
||||
return ODAV_PATH;
|
||||
}
|
||||
if (AccountAuthenticator.AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE.equals(authTokenType)) {
|
||||
return SAML_SSO_PATH;
|
||||
}
|
||||
if (version.compareTo(OwnCloudVersion.owncloud_v4) >= 0)
|
||||
return WEBDAV_PATH_4_0;
|
||||
if (version.compareTo(OwnCloudVersion.owncloud_v3) >= 0
|
||||
|
@ -143,8 +174,9 @@ public class AccountUtils {
|
|||
String baseurl = ama.getUserData(account, AccountAuthenticator.KEY_OC_BASE_URL);
|
||||
String strver = ama.getUserData(account, AccountAuthenticator.KEY_OC_VERSION);
|
||||
boolean supportsOAuth = (ama.getUserData(account, AccountAuthenticator.KEY_SUPPORTS_OAUTH2) != null);
|
||||
boolean supportsSamlSso = (ama.getUserData(account, AccountAuthenticator.KEY_SUPPORTS_SAML_WEB_SSO) != null);
|
||||
OwnCloudVersion ver = new OwnCloudVersion(strver);
|
||||
String webdavpath = getWebdavPath(ver, supportsOAuth);
|
||||
String webdavpath = getWebdavPath(ver, supportsOAuth, supportsSamlSso);
|
||||
|
||||
if (baseurl == null || webdavpath == null)
|
||||
throw new AccountNotFoundException(account, "Account not found", null);
|
||||
|
|
|
@ -96,7 +96,6 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
private static final String KEY_SERVER_STATUS_ICON = "SERVER_STATUS_ICON";
|
||||
private static final String KEY_IS_SSL_CONN = "IS_SSL_CONN";
|
||||
private static final String KEY_PASSWORD_VISIBLE = "PASSWORD_VISIBLE";
|
||||
private static final String KEY_AUTH_METHOD = "AUTH_METHOD";
|
||||
private static final String KEY_AUTH_STATUS_TEXT = "AUTH_STATUS_TEXT";
|
||||
private static final String KEY_AUTH_STATUS_ICON = "AUTH_STATUS_ICON";
|
||||
private static final String KEY_REFRESH_BUTTON_ENABLED = "KEY_REFRESH_BUTTON_ENABLED";
|
||||
|
@ -105,10 +104,6 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
private static final String AUTH_OFF = "off";
|
||||
private static final String AUTH_OPTIONAL = "optional";
|
||||
|
||||
private static final int AUTH_METHOD_BASIC_HTTP = 0;
|
||||
private static final int AUTH_METHOD_OAUTH2 = 1;
|
||||
private static final int AUTH_METHOD_SAML_WEB_SSO = 2;
|
||||
|
||||
private static final int DIALOG_LOGIN_PROGRESS = 0;
|
||||
private static final int DIALOG_SSL_VALIDATOR = 1;
|
||||
private static final int DIALOG_CERT_NOT_SAVED = 2;
|
||||
|
@ -142,7 +137,7 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
private boolean mHostUrlInputEnabled;
|
||||
private View mRefreshButton;
|
||||
|
||||
private int mCurrentAuthorizationMethod;
|
||||
private String mCurrentAuthTokenType;
|
||||
|
||||
private EditText mUsernameInput;
|
||||
private EditText mPasswordInput;
|
||||
|
@ -230,7 +225,7 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
mHostUrlInput.setText(mHostBaseUrl);
|
||||
}
|
||||
initAuthorizationMethod(); // checks intent and setup.xml to determine mCurrentAuthorizationMethod
|
||||
mOAuth2Check.setChecked(mCurrentAuthorizationMethod == AUTH_METHOD_OAUTH2);
|
||||
mOAuth2Check.setChecked(mCurrentAuthTokenType == AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN);
|
||||
mJustCreated = true;
|
||||
|
||||
if (mAction == ACTION_UPDATE_TOKEN || !mHostUrlInputEnabled) {
|
||||
|
@ -259,7 +254,7 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
|
||||
// account data, if updating
|
||||
mAccount = savedInstanceState.getParcelable(KEY_ACCOUNT);
|
||||
mCurrentAuthorizationMethod = savedInstanceState.getInt(KEY_AUTH_METHOD, AUTH_METHOD_BASIC_HTTP);
|
||||
mCurrentAuthTokenType = savedInstanceState.getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE, AccountAuthenticator.AUTH_TOKEN_TYPE_PASSWORD);
|
||||
|
||||
// check if server check was interrupted by a configuration change
|
||||
if (savedInstanceState.getBoolean(KEY_SERVER_CHECK_IN_PROGRESS, false)) {
|
||||
|
@ -289,7 +284,8 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
if (mServerIsChecked && !mServerIsValid && refreshButtonEnabled) showRefreshButton();
|
||||
mOkButton.setEnabled(mServerIsValid); // state not automatically recovered in configuration changes
|
||||
|
||||
if (mCurrentAuthorizationMethod == AUTH_METHOD_SAML_WEB_SSO || !AUTH_OPTIONAL.equals(getString(R.string.auth_method_oauth2))) {
|
||||
if (mCurrentAuthTokenType == AccountAuthenticator.AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE ||
|
||||
!AUTH_OPTIONAL.equals(getString(R.string.auth_method_oauth2))) {
|
||||
mOAuth2Check.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
|
@ -331,33 +327,31 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
boolean oAuthRequired = false;
|
||||
boolean samlWebSsoRequired = false;
|
||||
|
||||
String tokenType = getIntent().getExtras().getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE);
|
||||
mCurrentAuthTokenType = getIntent().getExtras().getString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE);
|
||||
mAccount = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT);
|
||||
|
||||
if (tokenType != null) {
|
||||
/// use the authentication method requested by caller
|
||||
oAuthRequired = AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN.equals(tokenType);
|
||||
samlWebSsoRequired = AccountAuthenticator.AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE.equals(tokenType);
|
||||
|
||||
} else if (mAccount != null) {
|
||||
/// same authentication method than the one used to create the account to update
|
||||
oAuthRequired = (mAccountMgr.getUserData(mAccount, AccountAuthenticator.KEY_SUPPORTS_OAUTH2) != null);
|
||||
samlWebSsoRequired = (mAccountMgr.getUserData(mAccount, AccountAuthenticator.KEY_SUPPORTS_SAML_WEB_SSO) != null);
|
||||
|
||||
} else {
|
||||
/// use the one set in setup.xml
|
||||
oAuthRequired = AUTH_ON.equals(getString(R.string.auth_method_oauth2));
|
||||
samlWebSsoRequired = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso));
|
||||
}
|
||||
// TODO could be a good moment to validate the received token type, if not null
|
||||
|
||||
if (oAuthRequired) {
|
||||
mCurrentAuthorizationMethod = AUTH_METHOD_OAUTH2;
|
||||
} else if (samlWebSsoRequired) {
|
||||
mCurrentAuthorizationMethod = AUTH_METHOD_SAML_WEB_SSO;
|
||||
} else {
|
||||
mCurrentAuthorizationMethod = AUTH_METHOD_BASIC_HTTP;
|
||||
if (mCurrentAuthTokenType == null) {
|
||||
if (mAccount != null) {
|
||||
/// same authentication method than the one used to create the account to update
|
||||
oAuthRequired = (mAccountMgr.getUserData(mAccount, AccountAuthenticator.KEY_SUPPORTS_OAUTH2) != null);
|
||||
samlWebSsoRequired = (mAccountMgr.getUserData(mAccount, AccountAuthenticator.KEY_SUPPORTS_SAML_WEB_SSO) != null);
|
||||
|
||||
} else {
|
||||
/// use the one set in setup.xml
|
||||
oAuthRequired = AUTH_ON.equals(getString(R.string.auth_method_oauth2));
|
||||
samlWebSsoRequired = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso));
|
||||
}
|
||||
if (oAuthRequired) {
|
||||
mCurrentAuthTokenType = AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN;
|
||||
} else if (samlWebSsoRequired) {
|
||||
mCurrentAuthTokenType = AccountAuthenticator.AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE;
|
||||
} else {
|
||||
mCurrentAuthTokenType = AccountAuthenticator.AUTH_TOKEN_TYPE_PASSWORD;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (mAccount != null) {
|
||||
String userName = mAccount.name.substring(0, mAccount.name.lastIndexOf('@'));
|
||||
mUsernameInput.setText(userName);
|
||||
|
@ -397,7 +391,7 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
if (mAccount != null) {
|
||||
outState.putParcelable(KEY_ACCOUNT, mAccount);
|
||||
}
|
||||
outState.putInt(KEY_AUTH_METHOD, mCurrentAuthorizationMethod);
|
||||
outState.putString(AccountAuthenticator.KEY_AUTH_TOKEN_TYPE, mCurrentAuthTokenType);
|
||||
|
||||
// refresh button enabled
|
||||
outState.putBoolean(KEY_REFRESH_BUTTON_ENABLED, (mRefreshButton.getVisibility() == View.VISIBLE));
|
||||
|
@ -626,9 +620,10 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
return;
|
||||
}
|
||||
|
||||
if (mOAuth2Check.isChecked()) {
|
||||
if (AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN.equals(mCurrentAuthTokenType)) {
|
||||
startOauthorization();
|
||||
|
||||
} else if (AccountAuthenticator.AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE.equals(mCurrentAuthTokenType)) {
|
||||
startSamlBasedFederatedSingleSignOnAuthorization();
|
||||
} else {
|
||||
checkBasicAuthorization();
|
||||
}
|
||||
|
@ -641,7 +636,7 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
*/
|
||||
private void checkBasicAuthorization() {
|
||||
/// get the path to the root folder through WebDAV from the version server
|
||||
String webdav_path = AccountUtils.getWebdavPath(mDiscoveredVersion, false);
|
||||
String webdav_path = AccountUtils.getWebdavPath(mDiscoveredVersion, mCurrentAuthTokenType);
|
||||
|
||||
/// get basic credentials entered by user
|
||||
String username = mUsernameInput.getText().toString();
|
||||
|
@ -684,6 +679,20 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts the Web Single Sign On flow to get access to the root folder
|
||||
* in the server.
|
||||
*/
|
||||
private void startSamlBasedFederatedSingleSignOnAuthorization() {
|
||||
/// get the path to the root folder through WebDAV from the version server
|
||||
String webdav_path = AccountUtils.getWebdavPath(mDiscoveredVersion, mCurrentAuthTokenType);
|
||||
|
||||
/// test credentials accessing the root folder
|
||||
mAuthCheckOperation = new ExistenceCheckOperation("", this, false);
|
||||
WebdavClient client = OwnCloudClientUtils.createOwnCloudClient(Uri.parse(mHostBaseUrl + webdav_path), this);
|
||||
mOperationThread = mAuthCheckOperation.execute(client, this, mHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method invoked when a RemoteOperation executed by this Activity finishes.
|
||||
*
|
||||
|
@ -699,8 +708,12 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
onGetOAuthAccessTokenFinish((OAuth2GetAccessToken)operation, result);
|
||||
|
||||
} else if (operation instanceof ExistenceCheckOperation) {
|
||||
onAuthorizationCheckFinish((ExistenceCheckOperation)operation, result);
|
||||
|
||||
if (AccountAuthenticator.AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE.equals(mCurrentAuthTokenType)) {
|
||||
Toast.makeText(this, result.getLogMessage(), Toast.LENGTH_LONG).show();
|
||||
|
||||
} else {
|
||||
onAuthorizationCheckFinish((ExistenceCheckOperation)operation, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -952,7 +965,7 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
// NOTHING TO DO ; can't find out what situation that leads to the exception in this code, but user logs signal that it happens
|
||||
}
|
||||
|
||||
String webdav_path = AccountUtils.getWebdavPath(mDiscoveredVersion, true);
|
||||
String webdav_path = AccountUtils.getWebdavPath(mDiscoveredVersion, mCurrentAuthTokenType);
|
||||
if (result.isSuccess() && webdav_path != null) {
|
||||
/// be gentle with the user
|
||||
showDialog(DIALOG_LOGIN_PROGRESS);
|
||||
|
@ -1305,9 +1318,9 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
public void onCheckClick(View view) {
|
||||
CheckBox oAuth2Check = (CheckBox)view;
|
||||
if (oAuth2Check.isChecked()) {
|
||||
mCurrentAuthorizationMethod = AUTH_METHOD_OAUTH2;
|
||||
mCurrentAuthTokenType = AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN;
|
||||
} else {
|
||||
mCurrentAuthorizationMethod = AUTH_METHOD_BASIC_HTTP;
|
||||
mCurrentAuthTokenType = AccountAuthenticator.AUTH_TOKEN_TYPE_PASSWORD;
|
||||
}
|
||||
adaptViewAccordingToAuthenticationMethod();
|
||||
}
|
||||
|
@ -1318,37 +1331,33 @@ implements OnRemoteOperationListener, OnSslValidatorListener, OnFocusChangeList
|
|||
* the current authorization method.
|
||||
*/
|
||||
private void adaptViewAccordingToAuthenticationMethod () {
|
||||
switch (mCurrentAuthorizationMethod) {
|
||||
case AUTH_METHOD_OAUTH2:
|
||||
// OAuth 2 authorization
|
||||
mOAuthAuthEndpointText.setVisibility(View.VISIBLE);
|
||||
mOAuthTokenEndpointText.setVisibility(View.VISIBLE);
|
||||
mUsernameInput.setVisibility(View.GONE);
|
||||
mPasswordInput.setVisibility(View.GONE);
|
||||
mAccountNameInput.setVisibility(View.GONE);
|
||||
mWebSsoView.setVisibility(View.GONE);
|
||||
break;
|
||||
|
||||
case AUTH_METHOD_SAML_WEB_SSO:
|
||||
// SAML-based web Single Sign On
|
||||
mOAuthAuthEndpointText.setVisibility(View.GONE);
|
||||
mOAuthTokenEndpointText.setVisibility(View.GONE);
|
||||
mUsernameInput.setVisibility(View.GONE);
|
||||
mPasswordInput.setVisibility(View.GONE);
|
||||
mAccountNameInput.setVisibility(View.VISIBLE);
|
||||
mWebSsoView.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case AUTH_METHOD_BASIC_HTTP:
|
||||
default:
|
||||
// basic HTTP authorization
|
||||
mOAuthAuthEndpointText.setVisibility(View.GONE);
|
||||
mOAuthTokenEndpointText.setVisibility(View.GONE);
|
||||
mUsernameInput.setVisibility(View.VISIBLE);
|
||||
mPasswordInput.setVisibility(View.VISIBLE);
|
||||
mAccountNameInput.setVisibility(View.GONE);
|
||||
mWebSsoView.setVisibility(View.GONE);
|
||||
}
|
||||
if (AccountAuthenticator.AUTH_TOKEN_TYPE_ACCESS_TOKEN.equals(mCurrentAuthTokenType)) {
|
||||
// OAuth 2 authorization
|
||||
mOAuthAuthEndpointText.setVisibility(View.VISIBLE);
|
||||
mOAuthTokenEndpointText.setVisibility(View.VISIBLE);
|
||||
mUsernameInput.setVisibility(View.GONE);
|
||||
mPasswordInput.setVisibility(View.GONE);
|
||||
mAccountNameInput.setVisibility(View.GONE);
|
||||
mWebSsoView.setVisibility(View.GONE);
|
||||
|
||||
} else if (AccountAuthenticator.AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE.equals(mCurrentAuthTokenType)) {
|
||||
// SAML-based web Single Sign On
|
||||
mOAuthAuthEndpointText.setVisibility(View.GONE);
|
||||
mOAuthTokenEndpointText.setVisibility(View.GONE);
|
||||
mUsernameInput.setVisibility(View.GONE);
|
||||
mPasswordInput.setVisibility(View.GONE);
|
||||
mAccountNameInput.setVisibility(View.VISIBLE);
|
||||
mWebSsoView.setVisibility(View.VISIBLE);
|
||||
|
||||
} else {
|
||||
// basic HTTP authorization
|
||||
mOAuthAuthEndpointText.setVisibility(View.GONE);
|
||||
mOAuthTokenEndpointText.setVisibility(View.GONE);
|
||||
mUsernameInput.setVisibility(View.VISIBLE);
|
||||
mPasswordInput.setVisibility(View.VISIBLE);
|
||||
mAccountNameInput.setVisibility(View.GONE);
|
||||
mWebSsoView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -50,6 +50,7 @@ import com.actionbarsherlock.view.MenuItem;
|
|||
import com.owncloud.android.authentication.AccountAuthenticator;
|
||||
import com.owncloud.android.authentication.AuthenticatorActivity;
|
||||
import com.owncloud.android.authentication.AccountUtils;
|
||||
import com.owncloud.android.ui.activity.FileActivity.AccountCreationCallback;
|
||||
import com.owncloud.android.Log_OC;
|
||||
|
||||
import com.owncloud.android.R;
|
||||
|
@ -133,11 +134,20 @@ public class AccountSelectActivity extends SherlockListActivity implements
|
|||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
if (item.getItemId() == R.id.createAccount) {
|
||||
Intent intent = new Intent(
|
||||
/*Intent intent = new Intent(
|
||||
android.provider.Settings.ACTION_ADD_ACCOUNT);
|
||||
intent.putExtra("authorities",
|
||||
new String[] { AccountAuthenticator.AUTHORITY });
|
||||
startActivity(intent);
|
||||
startActivity(intent);*/
|
||||
AccountManager am = AccountManager.get(getApplicationContext());
|
||||
am.addAccount(AccountAuthenticator.ACCOUNT_TYPE,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
this,
|
||||
null,
|
||||
null);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -174,7 +174,7 @@ public abstract class FileActivity extends SherlockFragmentActivity {
|
|||
private void createFirstAccount() {
|
||||
AccountManager am = AccountManager.get(getApplicationContext());
|
||||
am.addAccount(AccountAuthenticator.ACCOUNT_TYPE,
|
||||
AccountAuthenticator.AUTH_TOKEN_TYPE_PASSWORD,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
this,
|
||||
|
|
Loading…
Reference in a new issue