/* * Copyright (C) 2010 Nullbyte <http://nullbyte.eu> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.liato.bankdroid.banking.banks.nordea; import com.liato.bankdroid.Helpers; import com.liato.bankdroid.banking.Account; import com.liato.bankdroid.banking.Bank; import com.liato.bankdroid.banking.Transaction; import com.liato.bankdroid.banking.exceptions.BankChoiceException; import com.liato.bankdroid.banking.exceptions.BankException; import com.liato.bankdroid.banking.exceptions.LoginException; import com.liato.bankdroid.legacy.R; import com.liato.bankdroid.provider.IBankTypes; import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; import android.content.Context; import android.text.Html; import android.text.InputType; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import eu.nullbyte.android.urllib.Urllib; public class Nordea extends Bank { private static final String NAME = "Nordea"; private static final String BASE_URL = "https://internetbanken.privat.nordea.se/nsp/"; private static final String LOGIN_URL = BASE_URL + "login"; private static final int BANKTYPE_ID = IBankTypes.NORDEA; private static final int INPUT_TYPE_USERNAME = InputType.TYPE_CLASS_PHONE; private static final int INPUT_TYPE_PASSWORD = InputType.TYPE_CLASS_PHONE; private static final String INPUT_HINT_USERNAME = "ÅÅÅÅMMDDXXXX"; private static final int MAX_TRANSACTIONS = 50; private Pattern reSimpleLoginLink = Pattern.compile( "href=\"(login\\?" + "(?=[^\"]*usecase=commonlogin)" + "(?=[^\"]*command=commonlogintabcommand)" + "(?=[^\"]*guid=([\\w]*))" + "(?=[^\"]*fpid=([\\w]*))" + "(?=[^\"]*commonlogintab=2)" + "(?=[^\"]*hash=([\\w]*))" + "[^\"]*)", Pattern.CASE_INSENSITIVE ); private Pattern reLoginFormContents = Pattern.compile( "<form[^>]+id=\"commonlogin\"[^>]*>(.*?)</form>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private Pattern reNonTextInputField = Pattern.compile( "<input(?=[^>]+type=\"((?!text)[^\"]*)\")(?=[^>]+name=\"([^\"]+)\")(?=[^>]+value=\"([^\"]+)\")", Pattern.CASE_INSENSITIVE); private Pattern reNonTelInputField = Pattern.compile( "<input(?=[^>]+type=\"((?!tel)[^\"]*)\")(?=[^>]+name=\"([^\"]+)\")(?=[^>]+value=\"([^\"]+)\")", Pattern.CASE_INSENSITIVE); // Link to home/overview - PageType.ENTRY private Pattern reHomeLink = Pattern.compile( "href=\"(core[^\"#]*)#?\"" + // The actual url (trim the '#') "[^>]*>" + "[^<]*" + "<img[^>]+id=\"home\"" // Identificator ); private Pattern reTransactionFormContents = Pattern.compile( "<form(?=[^>]+id=\"accountTransactions\")(?=[^>]+action=\"([^\"]*)\").*?>(.*?)</form>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private Pattern reAccountLink = Pattern.compile( "href=\"(core\\?" + "(?=[^\"]*usecase=accountsoverview)" + "(?=[^\"]*command=getcurrenttransactions)" + "(?=[^\"]*currentaccountsoverviewtable=([\\d]+))" + "[^\"]*)[^>]*>" + // End of link attributes "(.*?)" + // Link contents - account name "</a>" + ".*?" + // fast forward "([*\\d]+)" + // censured account number (account identifier) ".*?" + // fast forward "<td.*?>(.*?)</td>", // account balance Pattern.DOTALL ); private Pattern reTransaction = Pattern.compile( "(\\d{4}-\\d{2}-\\d{2})[\n\r <].*?<td.*?>(.*?)</td>.*?<td.*?>.*?</td>.*?<td.*?>([\\s\\d+,.-]*)", Pattern.DOTALL); private Pattern reCurrency = Pattern.compile( "Saldo:.*?[\\d\\.,-]+[\\s]*</td>[\\s]*<td[^>]*>([^<]*)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); // The link to go to the credit cards overview page private Pattern reCreditCardsLink = Pattern.compile("<a href=\"([^\"#]*)#?\">Kort<"); // Link to specific credit card private Pattern reCreditCardLink = Pattern.compile( "href=\"" + "(" + // Start group 1: link url "engine\\?" + "(?=[^\"]*usecase=viewallcards)" + "(?=[^\"]*command=gettransactionscredit)" + // debit cards have "debit" - but we don't need those "[^\"#]*" + // Rest of link url ")" + // End group 1 "[^>]*>" + // Rest of link attributes "(.*?)" + // Group 2: Link contents - Credit card type (Eg. "Nordea Gold") "</a>" + ".*?" + // Fast forward "\\*+(\\d+)" + // Group 3: Censured credit card number (account identifier) ".*?" + // Fast forward "<td.*?>([^<]*)</td>" + // Group 4: Expire date **/** ".*?" + // Fast forward "<td.*?>([^<]*)</td>", // Group 5: Account balance Pattern.DOTALL ); // Credit card transaction entry private Pattern reCreditCardTransaction = Pattern.compile( "(\\d{4}-\\d{2}-\\d{2})</a>" + // Group 1: Transaction date "[^<]*</td>" + // End date col "[^<]*<td[^>]*>" + // Start transaction name col "\\s*([^<]*)\\s*</td>" + // Group 2: (trimmed) Transaction name "[^<]*<td[^>]*>" + // Start recipient name col (same as transaction name?) "[^<]*</td>" + // Transaction name "[^<]*<td[^>]*>" + // Start transaction native amount/currency col "\\s*([^<]*)\\s*</td>" + // Group 3: Transaction native amount/currency (Empty when SEK) "[^<]*<td[^>]*>" + // Start amount col "\\s*([\\d,.-]+)", // Group 4: Transaction amount Pattern.DOTALL ); // Credit card currency private Pattern reCreditCardCurrency = Pattern.compile( "<th[^>]*>Belopp\\s([^<]+)</th>" ); // The link to go to the loans overview page private Pattern reLoansLink = Pattern.compile("<a href=\"([^\"#]*)#?\">Lån<"); // Link to specific loan private Pattern reLoanLink = Pattern.compile( "href=\"" + "(" + // Start group 1: link url "engine\\?" + "(?=[^\"]*usecase=loansoverview)" + "(?=[^\"]*command=get_loan_details_command)" + "[^\"#]*" + // Rest of link url ")" + // End group 1 "#?" + // Trim off a padded # "[^>]*>" + // Rest of link attributes "(.*?)" + // Group 2: Link contents - Loan type (Eg. "Bolån") "</a>" + ".*?" + // Fast forward "\\*+(\\d+)" + // Group 3: Censured loan number (account identifier) ".*?" + // Fast forward "(\\d{4}-\\d{2}-\\d{2})" + // Group 4: "Transaction date" - Latest interest payment date ".*?" + // Fast forward "([\\d\\.,]+)", // Group 5: Loan amount Pattern.DOTALL ); private Pattern reAccountSelect = Pattern.compile( "<select[^>]+name=\"transactionaccount\"[^>]*>(.*?)</select>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private Pattern reAccountOption = Pattern.compile( "<option[^>]+value=\"([\\d]+)\"[^>]*>.*?(\\*{12}\\d{4})", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); // Nordea generates unique urls on each page load and serves content from session info, // so we need to find new links in lastResponse after each page load. private String lastResponse; private int currentPageType; public Nordea(Context context) { super(context, R.drawable.logo_nordea); super.url = BASE_URL; super.inputTypeUsername = INPUT_TYPE_USERNAME; super.inputTypePassword = INPUT_TYPE_PASSWORD; super.inputHintUsername = INPUT_HINT_USERNAME; } @Override public int getBanktypeId() { return BANKTYPE_ID; } @Override public String getName() { return NAME; } public Nordea(String username, String password, Context context) throws BankException, LoginException, BankChoiceException, IOException { this(context); this.update(username, password); } @Override protected LoginPackage preLogin() throws BankException, IOException { urlopen = new Urllib(context); Matcher matcher; // Find "simple login" link this.lastResponse = urlopen.open(LOGIN_URL); this.currentPageType = PageType.LOGIN; matcher = reSimpleLoginLink.matcher(this.lastResponse); if (!matcher.find()) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " login link."); } // Visit login link String link = BASE_URL + matcher.group(1); this.lastResponse = urlopen.open(link); this.currentPageType = PageType.SIMPLE_LOGIN; matcher = reLoginFormContents.matcher(this.lastResponse); if (!matcher.find()) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " login form."); } // Extract hidden fields String formContents = matcher.group(1); matcher = reNonTelInputField.matcher(formContents); if (!matcher.find()) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " login fields."); } matcher.reset(); List<NameValuePair> postData = new ArrayList<NameValuePair>(); while (matcher.find()) { String name = matcher.group(2); String value = matcher.group(3); // The non-mobile page requires javascript, so we'd best pretend we have it if ("JAVASCRIPT_DETECTED".equals(name)) { value = "true"; } postData.add(new BasicNameValuePair(name, value)); } // Login information postData.add(new BasicNameValuePair("userid", getUsername())); postData.add(new BasicNameValuePair("pin", getPassword())); // Submit button is not contained within the form and thus cannot (should not) be found with the InputField matcher postData.add(new BasicNameValuePair("commonlogin$loginLight", "Logga in")); return new LoginPackage(urlopen, postData, this.lastResponse, LOGIN_URL); } @Override public Urllib login() throws LoginException, BankException, IOException { LoginPackage lp = preLogin(); this.lastResponse = urlopen.open(lp.getLoginTarget(), lp.getPostData()); this.currentPageType = PageType.ENTRY; if (this.lastResponse.contains("fel uppgifter")) { throw new LoginException(res.getText(R.string.invalid_username_password).toString()); } return urlopen; } @Override public void update() throws BankException, LoginException, BankChoiceException, IOException { super.update(); if (getUsername().isEmpty() || getPassword().isEmpty()) { throw new LoginException(res.getText(R.string.invalid_username_password).toString()); } // This puts us at PageType.ENTRY urlopen = login(); String loanName; // Add regular accounts Matcher matcher = reAccountLink.matcher(this.lastResponse); while (matcher.find()) { accounts.add(new Account( // Account name Html.fromHtml(matcher.group(3)).toString().trim(), // Balance Helpers.parseBalance(Html.fromHtml(matcher.group(5)).toString()), // Account identifier - half censured account number: "************1234" Html.fromHtml(matcher.group(4)).toString().trim() )); } // TODO: Code for funds goToPage(PageType.CREDIT_CARDS); matcher = reCreditCardLink.matcher(this.lastResponse); // Add credit cards while (matcher.find()) { accounts.add(new Account( // Account/Credit card name matcher.group(2), // Balance (not available through simple login) Helpers.parseBalance(matcher.group(5)), // Account/Credit card identifier "c:" + matcher.group(3), -1L, Account.CCARD )); } goToPage(PageType.LOANS); matcher = reLoanLink.matcher(this.lastResponse); // Add loans while (matcher.find()) { loanName = matcher.group(2) + ' ' + matcher.group(3); accounts.add(new Account( loanName, Helpers.parseBalance(matcher.group(5)), "l:" + matcher.group(3).trim(), -1L, Account.LOANS )); } if (accounts.isEmpty()) { throw new BankException(res.getText(R.string.no_accounts_found).toString()); } super.updateComplete(); // Demo account to use with screenshots //accounts.add(new Account("Personkonto", Helpers.parseBalance("7953.37"), "1")); //accounts.add(new Account("Kapitalkonto", Helpers.parseBalance("28936.08"), "0")); } @Override public void updateTransactions(Account account, Urllib urlopen) throws LoginException, BankException, IOException { super.updateTransactions(account, urlopen); int accType = account.getType(); switch (accType) { case Account.REGULAR: updateRegularTransactions(account, urlopen); break; case Account.CCARD: updateCreditTransactions(account, urlopen); break; default: break; } } private void goToPage(int pageType) throws IOException { // Convenience method for going to an overview page Matcher matcher; String link; switch (pageType) { case PageType.ENTRY: // Find home link matcher = reHomeLink.matcher(this.lastResponse); break; case PageType.LOANS: // Find loans link matcher = reLoansLink.matcher(this.lastResponse); break; case PageType.CREDIT_CARDS: // Get credit cards link matcher = reCreditCardsLink.matcher(this.lastResponse); break; default: return; } // Find link to page if (matcher.find()) { link = matcher.group(1); // Go to page this.lastResponse = urlopen.open(BASE_URL + link); this.currentPageType = pageType; } } public void updateRegularTransactions(Account account, Urllib urlopen) throws LoginException, BankException, IOException { // If we're on the entry page it's easy to just find the link to the account and navigate to it, // If we're already on a transaction page we use the account-switching form instead of going // back to the entry page and starting over. This saves us 1 request. Matcher matcher; String link = null; List<NameValuePair> postData = new ArrayList<NameValuePair>(); if (this.currentPageType != PageType.ENTRY && this.currentPageType != PageType.TRANSACTIONS) { goToPage(PageType.ENTRY); } if (currentPageType == PageType.ENTRY) { // Find the link to the transaction page for this account matcher = reAccountLink.matcher(this.lastResponse); while (matcher.find()) { if (Html.fromHtml(matcher.group(4)).toString().trim().equals(account.getId())) { link = matcher.group(1); break; } } if (link == null) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " transactions link."); } } else if (currentPageType == PageType.TRANSACTIONS) { // Find the account dropdown form matcher = reTransactionFormContents.matcher(this.lastResponse); if (!matcher.find()) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " account form."); } link = matcher.group(1); matcher = reNonTextInputField.matcher(matcher.group(2)); if (!matcher.find()) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " input fields."); } matcher.reset(); // Input fields while (matcher.find()) { // For some odd reason, it does not like us sending the submit button... So don't. if (!matcher.group(1).equals("submit")) { postData.add(new BasicNameValuePair(matcher.group(2), matcher.group(3))); } } postData.add(new BasicNameValuePair("transactionPeriod", "0")); // Account id matcher = reAccountSelect.matcher(this.lastResponse); if (!matcher.find()) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " account selection."); } // Find account to switch to in dropdown matcher = reAccountOption.matcher(matcher.group(1)); String id = null; while (matcher.find()) { if (matcher.group(2).equals(account.getId())) { id = matcher.group(1); break; } } if (id == null) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " account id."); } postData.add(new BasicNameValuePair("transactionaccount", id)); } else { throw new BankException("This should never happen. If it does: Grats, you broke it."); } // URL established. Either we have a simple URL parsed from ENTRY-page or a base URL + // a populated postData variable. This works with both. this.lastResponse = urlopen.open(BASE_URL + link, postData); this.currentPageType = PageType.TRANSACTIONS; // Match up transactions for this account matcher = reTransaction.matcher(this.lastResponse); ArrayList<Transaction> transactions = new ArrayList<Transaction>(); while (matcher.find() && transactions.size() < MAX_TRANSACTIONS) { String date = Html.fromHtml(matcher.group(1)).toString().trim(); String text = Html.fromHtml(matcher.group(2)).toString().trim(); BigDecimal amount = Helpers.parseBalance(matcher.group(3)); Transaction transaction = new Transaction(date, text, amount); transactions.add(transaction); } // Add the transactions to this account account.setTransactions(transactions); // Set currency for this account matcher = reCurrency.matcher(this.lastResponse); if (matcher.find()) { account.setCurrency(Html.fromHtml(matcher.group(1)).toString().trim()); } } public void updateCreditTransactions(Account account, Urllib urlopen) throws LoginException, BankException, IOException { Matcher matcher; String link = null; ArrayList<Transaction> transactions = new ArrayList<Transaction>(); if (this.currentPageType != PageType.CREDIT_CARDS) { goToPage(PageType.CREDIT_CARDS); } // Find the link to the transaction page for this credit card matcher = reCreditCardLink.matcher(this.lastResponse); while (matcher.find()) { if (("c:" + matcher.group(3)).equals(account.getId())) { link = matcher.group(1); break; } } if (link == null) { throw new BankException( res.getText(R.string.unable_to_find).toString() + " transactions link."); } this.lastResponse = urlopen.open(BASE_URL + link); this.currentPageType = PageType.CREDIT_CARD_TRANSACTIONS; matcher = reCreditCardTransaction.matcher(this.lastResponse); while (matcher.find() && transactions.size() < MAX_TRANSACTIONS) { String date = matcher.group(1); String text = matcher.group(2); BigDecimal amount = Helpers.parseBalance(matcher.group(4)); Transaction transaction = new Transaction(date, text, amount); transactions.add(transaction); } // Add the transactions to this account account.setTransactions(transactions); // Set currency for this account matcher = reCreditCardCurrency.matcher(this.lastResponse); if (matcher.find()) { account.setCurrency(Html.fromHtml(matcher.group(1)).toString().trim()); } } private static class PageType { public static final int LOGIN = 0; public static final int SIMPLE_LOGIN = 1; public static final int ENTRY = 2; public static final int TRANSACTIONS = 3; public static final int LOANS = 4; public static final int CREDIT_CARDS = 5; public static final int CREDIT_CARD_TRANSACTIONS = 6; } }