/************************************************************************************************** * Copyright (c) 2014 Dennis Fischer. * * All rights reserved. This program and the accompanying materials * * are made available under the terms of the GNU Public License v3.0+ * * which accompanies this distribution, and is available at * * http://www.gnu.org/licenses/gpl.html * * * * Contributors: Dennis Fischer * **************************************************************************************************/ package de.chaosfisch.uploader.gui.controller; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.inject.Inject; import com.google.inject.name.Named; import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.JsonNode; import com.mashape.unirest.http.Unirest; import com.sun.webpane.webkit.dom.HTMLButtonElementImpl; import com.sun.webpane.webkit.dom.HTMLInputElementImpl; import de.chaosfisch.google.GDATAConfig; import de.chaosfisch.google.account.Account; import de.chaosfisch.google.account.IAccountService; import de.chaosfisch.google.http.PersistentCookieStore; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBoxBuilder; import javafx.scene.layout.VBox; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import java.io.UnsupportedEncodingException; import java.net.CookieHandler; import java.net.CookieManager; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; import java.util.regex.Matcher; import java.util.regex.Pattern; public class AccountAddDialogController extends UndecoratedDialogController { @Inject @Named("i18n-resources") private ResourceBundle resources; @FXML private URL location; @FXML private TextField code; @FXML private Label codeCount; @FXML private Button continueButton; @FXML private ProgressIndicator loading; @FXML private Button loginButton; @FXML private PasswordField password; @FXML private GridPane step1; @FXML private GridPane step2; @FXML private VBox step3; @FXML private TextField username; @Override @FXML public void closeDialog(final ActionEvent actionEvent) { account = null; super.closeDialog(actionEvent); } @FXML void continueSecondFactor(final ActionEvent event) { final Document document = webView.getEngine().getDocument(); final HTMLInputElementImpl persistentCookie = (HTMLInputElementImpl) document.getElementById("PersistentCookie"); if (null != persistentCookie) { persistentCookie.setChecked(true); } ((HTMLInputElementImpl) document.getElementById("smsUserPin")).setValue(code.getText()); ((HTMLInputElementImpl) document.getElementById("smsVerifyPin")).click(); step2.setVisible(false); loading.setVisible(true); } @FXML void onLogin(final ActionEvent event) { step1.setVisible(false); loading.setVisible(true); final PersistentCookieStore persistentCookieStore = new PersistentCookieStore(); if (null != account) { persistentCookieStore.setSerializeableCookies(account.getSerializeableCookies()); } final CookieManager cmrCookieMan = new CookieManager(persistentCookieStore, null); CookieHandler.setDefault(cmrCookieMan); if (webView.getEngine().getLocation().contains("accounts.google.com/ServiceLoginAuth")) { webView.getEngine().load("http://www.youtube.com/my_videos"); } if (initialized) { return; } webView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() { @Override public void changed(final ObservableValue<? extends Worker.State> observableValue, final Worker.State state, final Worker.State state2) { LOGGER.info("Browser at {}", webView.getEngine().getLocation()); if (Worker.State.SUCCEEDED == state2) { step1.setVisible(false); loading.setVisible(true); final String location = webView.getEngine().getLocation(); if (location.contains("accounts.google.com/ServiceLoginAuth")) { loading.setVisible(false); step1.setVisible(true); } else if (location.contains("accounts.google.com/ServiceLogin")) { handleStep1(); } else if (location.contains("accounts.google.com/SecondFactor")) { loading.setVisible(false); step2.setVisible(true); codeCount.setText(String.format(resources.getString("accountDialog.code.text"), ++count)); } else if (location.contains("youtube.com/my_videos")) { webView.getEngine().load("https://www.youtube.com/channel_switcher?next=%2F"); } else if (location.contains("youtube.com/channel_switcher") || location.contains("accounts.google.com/b/0/DelegateAccountSelector")) { handleStep3(); } else if (location.contains("accounts.google.com/o/oauth2/auth")) { System.out.println("Was: " + location); handleStep4(); } else if (location.contains("youtube.com/signin?") && location.contains("action_prompt_identity=true")) { webView.getEngine().load("https://www.youtube.com/channel_switcher?next=%2F"); } else if (location.endsWith("youtube.com/")) { copyCookies = new ArrayList<>(persistentCookieStore.getSerializeableCookies()); persistentCookieStore.removeAll(); startOAuthFlow(webView.getEngine()); } } } }); webView.getEngine().titleProperty().addListener(new ChangeListener<String>() { @Override public void changed(final ObservableValue<? extends String> observableValue, final String oldTitle, final String newTitle) { if (null != newTitle) { final Matcher matcher = OAUTH_TITLE_PATTERN.matcher(newTitle); if (matcher.matches()) { try { final Account modifiedAccount = null != account ? account : new Account(); modifiedAccount.setRefreshToken(accountService.getRefreshToken(matcher.group(1))); modifiedAccount.setSerializeableCookies(copyCookies); final HttpResponse<JsonNode> response = Unirest.get(USERINFO_URL) .header("Authorization", accountService.getAuthentication(modifiedAccount).getHeader()) .asJson(); modifiedAccount.setName(response.getBody().getObject().getString("email")); if (!accountService.verifyAccount(modifiedAccount)) { startOAuthFlow(webView.getEngine()); } else { if (null != account) { accountService.update(modifiedAccount); } else { accountService.insert(modifiedAccount); } account = null; closeDialog(null); } } catch (final Exception e) { LOGGER.error("Authentication exception", e); } } } } }); webView.getEngine().load("http://www.youtube.com/my_videos"); initialized = true; } @FXML void initialize() { assert null != code : "fx:id=\"code\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != codeCount : "fx:id=\"codeCount\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != continueButton : "fx:id=\"continueButton\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != loading : "fx:id=\"loading\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != loginButton : "fx:id=\"loginButton\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != password : "fx:id=\"password\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != step1 : "fx:id=\"step1\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != step2 : "fx:id=\"step2\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != step3 : "fx:id=\"step3\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != username : "fx:id=\"username\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; assert null != view : "fx:id=\"view\" was not injected: check your FXML file 'AccountAddDialog.fxml'."; /* NEEDED FOR DEBUG */ Stage stage = new Stage(); Scene scene = new Scene(null); scene.setRoot(webView); stage.setScene(scene); stage.show(); /**/ } private final IAccountService accountService; private final WebView webView = new WebView(); private static final int WAIT_TIME = 2000; private String selectedOption; private boolean initialized; private Account account; private int count; @Inject public AccountAddDialogController(final IAccountService accountService) { this.accountService = accountService; } private static final Pattern OAUTH_TITLE_PATTERN = Pattern.compile("Success code=(.*)"); private static final String[] SCOPES = {"https://www.googleapis.com/auth/youtube", "https://www.googleapis.com/auth/youtube.readonly", "https://www.googleapis.com/auth/youtube.upload", "https://www.googleapis.com/auth/youtubepartner", "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"}; private static final String OAUTH_URL = "https://accounts.google" + "" + ".com/o/oauth2/auth?access_type=offline&scope=%s&redirect_uri=%s&response_type=%s&client_id=%s"; private static final String USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo"; private static final Logger LOGGER = LoggerFactory.getLogger(AccountAddController.class); private List<PersistentCookieStore.SerializableCookie> copyCookies; public void initAuth(final Account account) { this.account = account; } private void startOAuthFlow(final WebEngine webEngine) { try { final Joiner joiner = Joiner.on(" ").skipNulls(); final String scope = URLEncoder.encode(joiner.join(SCOPES), Charsets.UTF_8.toString()); webEngine.load(String.format(OAUTH_URL, scope, GDATAConfig.REDIRECT_URI, "code", GDATAConfig.CLIENT_ID)); } catch (final UnsupportedEncodingException ignored) { } } private void handleStep1() { final Document document = webView.getEngine().getDocument(); ((HTMLInputElementImpl) document.getElementById("Email")).setValue(username.getText()); ((HTMLInputElementImpl) document.getElementById("Passwd")).setValue(password.getText()); ((HTMLInputElementImpl) document.getElementById("signIn")).click(); } private void handleStep3() { final WebEngine engine = webView.getEngine(); if (null != selectedOption) { System.out.println("Found: " + selectedOption); step3.setVisible(false); loading.setVisible(true); final int itemCount = (int) engine.executeScript( "document.evaluate('//*[@id=\"account-list\"]', document, null, XPathResult.ANY_TYPE, null).iterateNext().getElementsByTagName(\"li\")" + ".length"); String foundUrl = null; for (int i = 0; i < itemCount; i++) { final String url = (String) engine.executeScript( "document.evaluate('//*[@id=\"account-list\"]/li[" + (i + 1) + "]/a', document, null, XPathResult.ANY_TYPE, null).iterateNext().href"); if (url.contains(selectedOption)) { foundUrl = url; break; } if (url.contains("pageId=none")) { foundUrl = url; } } if (null == foundUrl) { throw new RuntimeException("Failed finding correct url."); } engine.load(foundUrl); return; } final int length = (int) engine.executeScript("document.getElementsByClassName(\"channel-switcher-button\").length"); if (1 < length) { loading.setVisible(false); step3.setVisible(true); } else { webView.getEngine().load("https://www.youtube.com"); } for (int i = 1; i < length; i++) { final String name = (String) engine.executeScript( "document.evaluate('//*[@id=\"ytcc-existing-channels\"]/li[" + (i + 1) + "]/div/a/span/div/div[2]/div[1]', document, null, " + "XPathResult.ANY_TYPE, null).iterateNext().innerHTML.trim()"); final String image = (String) engine.executeScript( "document.evaluate('//*[@id=\"ytcc-existing-channels\"]/li[" + (i + 1) + "]/div/a/span/div/div[1]/span/span/span/img', document, null, " + "XPathResult.ANY_TYPE, null).iterateNext().src"); final String url = (String) engine.executeScript( "document.evaluate('//*[@id=\"ytcc-existing-channels\"]/li[" + (i + 1) + "]/div/a', document, null, XPathResult.ANY_TYPE, " + "null).iterateNext().href"); step3.getChildren().add(HBoxBuilder.create().alignment(Pos.CENTER_LEFT) .children(new ImageView(new Image(image)), new Label(name)) .onMouseClicked(new EventHandler<MouseEvent>() { @Override public void handle(final MouseEvent mouseEvent) { step3.setVisible(false); loading.setVisible(true); final int pageIdStartIndex = url.indexOf("pageid=") + 7; final int pageIdEndIndex = -1 == url.indexOf("&", pageIdStartIndex) ? url.length() : url.indexOf("&", pageIdStartIndex); selectedOption = !url.contains("pageid=") ? "none" : url.substring(pageIdStartIndex, pageIdEndIndex); System.out.println(selectedOption); engine.load(url); } }) .onMouseEntered(new EventHandler<MouseEvent>() { @Override public void handle(final MouseEvent mouseEvent) { ((Node) mouseEvent.getSource()).setCursor(Cursor.HAND); } }) .onMouseExited(new EventHandler<MouseEvent>() { @Override public void handle(final MouseEvent mouseEvent) { ((Node) mouseEvent.getSource()).setCursor(Cursor.DEFAULT); } }) .build()); } } private void handleStep4() { final Thread thread = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(WAIT_TIME); } catch (final InterruptedException ignored) { } Platform.runLater(new Runnable() { @Override public void run() { ((HTMLButtonElementImpl) webView.getEngine().getDocument().getElementById("submit_approve_access")).click(); } }); } }, "Auth-Handle-Step4"); thread.setDaemon(true); thread.start(); } }