/* * Created on Jul 19, 2006 10:16:26 PM * Copyright (C) 2006 Aelitis, All Rights Reserved. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * AELITIS, SAS au capital de 46,603.30 euros * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France. */ package com.aelitis.azureus.ui.swt.browser; import java.net.*; import java.util.*; import java.util.List; import org.eclipse.swt.SWT; import org.eclipse.swt.browser.*; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.program.Program; import org.eclipse.swt.widgets.*; import org.gudy.azureus2.core3.util.*; import org.gudy.azureus2.plugins.utils.StaticUtilities; import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader; import org.gudy.azureus2.pluginsimpl.local.PluginInitializer; import org.gudy.azureus2.ui.swt.Utils; import org.gudy.azureus2.ui.swt.components.shell.ShellFactory; import org.gudy.azureus2.ui.swt.mainwindow.TorrentOpener; import org.gudy.azureus2.ui.swt.shells.MessageBoxShell; import com.aelitis.azureus.core.messenger.ClientMessageContextImpl; import com.aelitis.azureus.core.messenger.browser.listeners.BrowserMessageListener; import com.aelitis.azureus.core.vuzefile.VuzeFile; import com.aelitis.azureus.core.vuzefile.VuzeFileHandler; import com.aelitis.azureus.ui.swt.browser.msg.MessageDispatcherSWT; import com.aelitis.azureus.util.ConstantsVuze; import com.aelitis.azureus.util.JSONUtils; import com.aelitis.azureus.util.UrlFilter; /** * Manages the context for a single SWT {@link Browser} component, * including listeners and messages. * * @author dharkness * @created Jul 19, 2006 */ public class BrowserContext extends ClientMessageContextImpl implements DisposeListener { private static final String CONTEXT_KEY = "BrowserContext"; private static final String KEY_ENABLE_MENU = "browser.menu.enable"; private Browser browser; private Display display; private boolean pageLoading = false; private long pageLoadingStart = 0; private long pageLoadingEnd = 0; private String lastValidURL = null; private final boolean forceVisibleAfterLoad; private TimerEventPeriodic checkURLEvent; private Control widgetWaitIndicator; private MessageDispatcherSWT messageDispatcherSWT; protected boolean wiggleBrowser = Utils.isCarbon; private torrentURLHandler torrentURLHandler; private List loadingListeners = Collections.EMPTY_LIST; private long pageLoadTime; private long contentNetworkID = ConstantsVuze.DEFAULT_CONTENT_NETWORK_ID; private AEMonitor mon_listJS = new AEMonitor("listJS"); private List<String> listJS = new ArrayList<String>(1); private boolean allowPopups = true; /** * Creates a context and registers the given browser. * * @param id unique identifier of this context * @param browser the browser to be registered */ public BrowserContext( String _id, Browser _browser, Control _widgetWaitingIndicator, boolean _forceVisibleAfterLoad ) { super( _id, null ); browser = _browser; forceVisibleAfterLoad = _forceVisibleAfterLoad; widgetWaitIndicator = _widgetWaitingIndicator; // System.out.println( "Registered browser context: id=" + getID()); messageDispatcherSWT = new MessageDispatcherSWT(this); setMessageDispatcher( messageDispatcherSWT ); final TimerEventPerformer showBrowersPerformer = new TimerEventPerformer() { public void perform(TimerEvent event) { if (browser != null && !browser.isDisposed()) { Utils.execSWTThread(new AERunnable() { public void runSupport() { if (forceVisibleAfterLoad && browser != null && !browser.isDisposed() && !browser.isVisible()) { browser.setVisible(true); } } }); } } }; final TimerEventPerformer hideIndicatorPerformer = new TimerEventPerformer() { public void perform(TimerEvent event) { setPageLoading(false, browser.getUrl()); if (widgetWaitIndicator != null && !widgetWaitIndicator.isDisposed()) { Utils.execSWTThread(new AERunnable() { public void runSupport() { if (widgetWaitIndicator != null && !widgetWaitIndicator.isDisposed()) { widgetWaitIndicator.setVisible(false); } } }); } } }; final TimerEventPerformer checkURLEventPerformer = new TimerEventPerformer() { public void perform(TimerEvent event) { if (browser != null && !browser.isDisposed()) { Utils.execSWTThreadLater(0, new AERunnable() { public void runSupport() { if (browser != null && !browser.isDisposed()) { browser.execute("try { " + "tuxLocString = document.location.toString();" + "if (tuxLocString.indexOf('res://') == 0) {" + " document.title = 'err: ' + tuxLocString;" + "} else {" + " tuxTitleString = document.title.toString();" + " if (tuxTitleString.indexOf('408 ') == 0 || tuxTitleString.indexOf('503 ') == 0 || tuxTitleString.indexOf('500 ') == 0) " + " { document.title = 'err: ' + tuxTitleString; } " + "}" + "} catch (e) { }"); } } }); } } }; if (forceVisibleAfterLoad) { browser.setVisible(false); } setPageLoading(false, browser.getUrl()); if (widgetWaitIndicator != null && !widgetWaitIndicator.isDisposed()) { widgetWaitIndicator.setVisible(false); } browser.addTitleListener(new TitleListener() { public void changed(TitleEvent event) { /* * The browser might have been disposed already by the time this method is called */ if (browser.isDisposed() || browser.getShell().isDisposed()) { return; } if (!browser.isVisible()) { SimpleTimer.addEvent("Show Browser", System.currentTimeMillis() + 700, showBrowersPerformer); } if (event.title.startsWith("err: ")) { fillWithRetry(event.title, "err in title"); } } }); browser.addProgressListener(new ProgressListener() { public void changed(ProgressEvent event) { //int pct = event.total == 0 ? 0 : 100 * event.current / event.total; //System.out.println(pct + "%/" + event.current + "/" + event.total); } public void completed(ProgressEvent event) { /* * The browser might have been disposed already by the time this method is called */ if (browser.isDisposed() || browser.getShell().isDisposed()) { return; } checkURLEventPerformer.perform(null); if (forceVisibleAfterLoad && !browser.isVisible()) { browser.setVisible(true); } browser.execute("try { if (azureusClientWelcome) { azureusClientWelcome('" + ConstantsVuze.AZID + "'," + "{ 'azv':'" + org.gudy.azureus2.core3.util.Constants.AZUREUS_VERSION + "', 'browser-id':'" + getID() + "' }" + ");} } catch (e) { }"); if (org.gudy.azureus2.core3.util.Constants.isCVSVersion() || System.getProperty("debug.https", null) != null) { if (browser.getUrl().indexOf("https") == 0) { browser.execute("try { o = document.getElementsByTagName('body'); if (o) o[0].style.borderTop = '2px dotted #3b3b3b'; } catch (e) {}"); } } if (wiggleBrowser ) { Shell shell = browser.getShell(); Point size = shell.getSize(); size.x -= 1; size.y -= 1; shell.setSize(size); size.x += 1; size.y += 1; shell.setSize(size); } } }); checkURLEvent = SimpleTimer.addPeriodicEvent("checkURL", 10000, checkURLEventPerformer); browser.addOpenWindowListener(new OpenWindowListener() { public void open(WindowEvent event) { if (browser.isDisposed() || browser.getShell().isDisposed()) { return; } event.required = true; if (browser.getUrl().contains("js.debug=1")) { Shell shell = ShellFactory.createMainShell(SWT.SHELL_TRIM); shell.setLayout(new FillLayout()); Browser subBrowser = new Browser(shell, Utils.getInitialBrowserStyle(SWT.NONE)); shell.open(); event.browser = subBrowser; } else { final Browser subBrowser = new Browser(browser, Utils.getInitialBrowserStyle(SWT.NONE)); subBrowser.addLocationListener(new LocationListener() { public void changed(LocationEvent arg0) { // TODO Auto-generated method stub } public void changing(LocationEvent event) { event.doit = false; boolean wasAskCom = browser.getUrl().toLowerCase().startsWith( "http://www.ask.com"); if (wasAskCom) { Program.launch(event.location); } else if (allowPopups() && !UrlFilter.getInstance().urlIsBlocked(event.location) && (event.location.startsWith("http://") || event.location.startsWith("https://"))) { debug("open sub browser: " + event.location); Program.launch(event.location); } else { debug("blocked open sub browser: " + event.location); } Utils.execSWTThreadLater(0, new AERunnable() { public void runSupport() { subBrowser.dispose(); } }); } }); event.browser = subBrowser; } } }); browser.addLocationListener(new LocationListener() { private TimerEvent timerevent; public void changed(LocationEvent event) { if (browser.isDisposed() || browser.getShell().isDisposed()) { return; } debug("browser.changed " + event.location); if (timerevent != null) { timerevent.cancel(); } checkURLEventPerformer.perform(null); setPageLoading(false, event.top ? event.location : null); if (widgetWaitIndicator != null && !widgetWaitIndicator.isDisposed()) { widgetWaitIndicator.setVisible(false); } // event.top is only filled on changed event (not changing!) if (!event.top) { return; } String location = event.location.toLowerCase(); boolean isWebURL = location.startsWith("http://") || location.startsWith("https://"); if (!isWebURL) { if (event.location.startsWith("res://")) { fillWithRetry(event.location, "top changed"); return; } // we don't get a changed state on non URLs (mailto, javascript, etc) } if (UrlFilter.getInstance().isWhitelisted(event.location)) { lastValidURL = event.location; } //System.out.println("cd" + event.location); } public void changing(LocationEvent event) { // event.top is always false. changed event has it set though.. debug("browser.changing " + event.location + " from " + browser.getUrl() + ";" + event.top); /* * The browser might have been disposed already by the time this method is called */ if (browser.isDisposed() || browser.getShell().isDisposed()) { return; } String event_location = event.location; //Utils.openMessageBox(Utils.findAnyShell(), SWT.OK, "Location Changing", "Navigating to " + event_location ); if (event_location.startsWith("javascript") && event_location.indexOf("back()") > 0) { if (browser.isBackEnabled()) { browser.back(); } else if (lastValidURL != null) { fillWithRetry(event_location, "back"); } return; } String lowerLocation = event_location.toLowerCase(); boolean isOurURI = lowerLocation.startsWith("magnet:") || lowerLocation.startsWith("vuze:") || lowerLocation.startsWith("bc:") || lowerLocation.startsWith("bctp:") || lowerLocation.startsWith("dht:"); if (isOurURI) { event.doit = false; TorrentOpener.openTorrent(event_location); return; } boolean isWebURL = lowerLocation.startsWith("http://") || lowerLocation.startsWith("https://"); if (!isWebURL) { // we don't get a changed state on non URLs (mailto, javascript, etc) return; } boolean blocked = UrlFilter.getInstance().urlIsBlocked(event_location); if (!allowPopups()) { if (blocked) { return; } String curURL = browser.getUrl().toLowerCase(); boolean isPageLoadingOrRecent = isPageLoading() || (pageLoadingEnd > 0 && pageLoadingEnd + 500 > SystemTime.getCurrentTime()) || event_location.contains(".admonkey."); boolean wasGoogleSearch = curURL.startsWith( "http://www.google.com/#q") || curURL.startsWith("http://www.google.com/search") || curURL.contains("vuzesearch=1"); boolean isGoogleSearch = event_location.startsWith("http://www.google.com/#q") || (event_location.startsWith("http://www.google.com/search")) || event_location.contains("vuzesearch=1"); if (wasGoogleSearch && !isGoogleSearch && !curURL.equalsIgnoreCase(event_location) && !event_location.equals("about:blank") && !isPageLoadingOrRecent) { event.doit = false; String[] contentTypes = getContentTypes(event_location, ((Browser)event.widget).getUrl()); boolean isTorrent = false; for (String s : contentTypes) { if ( s != null ){ if ( s.indexOf("torrent") != -1 ) { isTorrent = true; } } } if (!isTorrent || !openTorrent(event)) { Utils.launch(event.location); } return; } boolean wasVuzeSearch = curURL.contains("vuzesearch=1") && !curURL.equalsIgnoreCase(event_location) && !event_location.equals("about:blank"); if (wasVuzeSearch && !isPageLoadingOrRecent) { event.doit = false; Utils.launch(event.location); return; } } if (blocked) { event.doit = false; new MessageBoxShell(SWT.OK, "URL blocked", "Tried to open " + event_location + " but it's blocked").open(null); browser.back(); } else { if (UrlFilter.getInstance().isWhitelisted(event_location)) { lastValidURL = event_location; } setPageLoading(true, event.location); if(event.top) { if (widgetWaitIndicator != null && !widgetWaitIndicator.isDisposed()) { widgetWaitIndicator.setVisible(true); } // Backup in case changed(..) is never called timerevent = SimpleTimer.addEvent("Hide Indicator", System.currentTimeMillis() + 20000, hideIndicatorPerformer); } else { boolean isTorrent = false; boolean isVuzeFile = false; //Try to catch .torrent files // URLs ending in "?torrent" on Amazon S3's Simple Storage Service // return an auto-generated a torrent based on the url, but only on // GET. HEAD will fail, so we have to trap and assume if(event_location.endsWith(".torrent") || event_location.endsWith("?torrent")) { isTorrent = true; } else { //If it's not obviously a web page boolean can_rpc = UrlFilter.getInstance().urlCanRPC(event_location); boolean test_for_torrent = !can_rpc && event_location.indexOf(".htm") == -1; boolean test_for_vuze = can_rpc && ( event_location.endsWith( ".xml" ) || event_location.endsWith( ".vuze" )); if ( test_for_torrent || test_for_vuze ){ String[] contentTypes = getContentTypes(event_location, ((Browser)event.widget).getUrl()); for (String s : contentTypes) { if ( s != null ){ if ( test_for_torrent && s.indexOf("torrent") != -1 ) { isTorrent = true; } if ( test_for_vuze && s.indexOf("vuze") != -1 ) { isVuzeFile = true; } } } //System.out.println( "Test for t/v: " + event_location + " -> " + isTorrent + "/" + isVuzeFile ); } } if ( isTorrent ){ openTorrent(event); }else if ( isVuzeFile ){ event.doit = false; setPageLoading(false, event.location); try { String referer_str = null; try{ referer_str = new URL(((Browser)event.widget).getUrl()).toExternalForm(); }catch( Throwable e ){ } Map headers = UrlUtils.getBrowserHeaders( referer_str ); String cookies = (String) ((Browser)event.widget).getData("current-cookies"); if ( cookies != null ){ headers.put("Cookie", cookies); } ResourceDownloader rd = StaticUtilities.getResourceDownloaderFactory().create( new URL( event_location )); VuzeFileHandler vfh = VuzeFileHandler.getSingleton(); VuzeFile vf = vfh.loadVuzeFile( rd.download()); if ( vf == null ){ event.doit = true; setPageLoading(true, event.location); }else{ vfh.handleFiles( new VuzeFile[]{ vf }, 0 ); } }catch( Throwable e ){ e.printStackTrace(); } } } } } }); browser.setData(CONTEXT_KEY, this); browser.addDisposeListener(this); // enable right-click context menu only if system property is set final boolean enableMenu = System.getProperty(KEY_ENABLE_MENU, "0").equals( "1"); browser.addListener(SWT.MenuDetect, new Listener() { // @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event) public void handleEvent(Event event) { event.doit = enableMenu; } }); messageDispatcherSWT.registerBrowser(browser); this.display = browser.getDisplay(); } protected boolean openTorrent(LocationEvent event) { event.doit = false; setPageLoading(false, event.location); try { String referer_str = null; try{ referer_str = new URL(((Browser)event.widget).getUrl()).toExternalForm(); }catch( Throwable e ){ } final Map headers = UrlUtils.getBrowserHeaders( referer_str ); String cookies = (String) ((Browser)event.widget).getData("current-cookies"); if (cookies != null ){ headers.put("Cookie", cookies); } final String url = event.location; if ( torrentURLHandler != null ){ try{ torrentURLHandler.handleTorrentURL(url); }catch( Throwable e ){ Debug.printStackTrace(e); } } Utils.getOffOfSWTThread(new AERunnable() { public void runSupport() { try { PluginInitializer.getDefaultInterface().getDownloadManager().addDownload( new URL(url), headers ); } catch (Exception e) { Debug.out(e); } } }); return true; }catch( Throwable e ){ Debug.out(e); return false; } } protected String[] getContentTypes(String event_location, String _referer) { try { //See what the content type is URL url = new URL(event_location); URLConnection conn = url.openConnection(); // we're only trying to get the content type so just use head ((HttpURLConnection) conn).setRequestMethod("HEAD"); String referer_str = null; try { URL referer = new URL(_referer); if (referer != null) { referer_str = referer.toExternalForm(); } } catch (Throwable e) { } UrlUtils.setBrowserHeaders(conn, referer_str); UrlUtils.connectWithTimeouts(conn, 1500, 5000); String contentType = conn.getContentType(); String contentDisposition = conn.getHeaderField("Content-Disposition"); // There's a bug in the ":3" server where a HEAD followed by a GET on the // same content results in a corrupt GET reply String server = conn.getHeaderField("Server"); if ("application/x-bittorrent".equals(contentType) && ":3".equals(server)) { Thread.sleep(6000); } return new String[] { contentType, contentDisposition }; } catch (Throwable e) { } return new String[0]; } /** * @param b * @param url * * @since 3.1.1.1 */ protected void setPageLoading(boolean b, String url) { //System.out.println("SPL: " + b + ";" + url); // we may get multiple "load done"s (from each frame) which we don't // want to skip if (b && pageLoading) { return; } mon_listJS.enter(); try { pageLoading = b; if (pageLoading) { pageLoadingStart = SystemTime.getCurrentTime(); pageLoadTime = -1; } else if (pageLoadingStart > 0 && url != null) { pageLoadingEnd = SystemTime.getCurrentTime(); pageLoadTime = pageLoadingEnd - pageLoadingStart; executeInBrowser("clientSetLoadTime(" + pageLoadTime + ");"); pageLoadingStart = 0; } if (!pageLoading && listJS.size() > 0) { debug(listJS.size() + " javascripts queued. Executing now.."); for (String js : listJS) { executeInBrowser(js); } listJS.clear(); } } finally { mon_listJS.exit(); } Object[] listeners = loadingListeners.toArray(); for (int i = 0; i < listeners.length; i++) { loadingListener l = (loadingListener) listeners[i]; l.browserLoadingChanged(b, url); } } public void setTorrentURLHandler( torrentURLHandler handler) { torrentURLHandler = handler; } public void fillWithRetry(String s, String s2) { Color bg = browser.getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); Color fg = browser.getDisplay().getSystemColor(SWT.COLOR_WIDGET_FOREGROUND); browser.setText("<html><body style='overflow:auto; font-family: verdana; font-size: 10pt' bgcolor=#" + Utils.toColorHexString(bg) + " text=#" + Utils.toColorHexString(fg) + ">" + "<br>Sorry, there was a problem loading this page.<br> " + "Please check if your internet connection is working and click <a href='" + lastValidURL + "' style=\"color: rgb(100, 155, 255); \">retry</a> to continue." + "<div style='word-wrap: break-word'><font size=1 color=#" + Utils.toColorHexString(bg) + ">" + s + "<br><br>" + s2 + "</font></div>" + "</body></html>"); } private void deregisterBrowser() { if (browser == null) { throw new IllegalStateException("Context " + getID() + " doesn't have a registered browser"); } // System.out.println( "Unregistered browser context: id=" + getID()); if (!browser.isDisposed()) { browser.setData(CONTEXT_KEY, null); browser.removeDisposeListener(this); messageDispatcherSWT.deregisterBrowser(browser); } browser = null; if (checkURLEvent != null && !checkURLEvent.isCancelled()) { checkURLEvent.cancel(); checkURLEvent = null; } } /** * Accesses the context associated with the given browser. * * @param browser holds the context in its application data map * @return the browser's context or <code>null</code> if there is none */ public static BrowserContext getContext(Browser browser) { Object data = browser.getData(CONTEXT_KEY); if (data != null && !(data instanceof BrowserContext)) { Debug.out("Data in Browser with key " + CONTEXT_KEY + " is not a BrowserContext"); return null; } return (BrowserContext) data; } public void addMessageListener(BrowserMessageListener listener) { messageDispatcherSWT.addListener(listener); } public Object getBrowserData(String key) { return browser.getData(key); } public void setBrowserData(String key, Object value) { browser.setData(key, value); } public boolean sendBrowserMessage(String key, String op) { return sendBrowserMessage(key, op, (Map) null); } public boolean sendBrowserMessage(String key, String op, Map params) { StringBuffer msg = new StringBuffer(); msg.append("az.msg.dispatch('").append(key).append("', '").append(op).append( "'"); if (params != null) { msg.append(", ").append(JSONUtils.encodeToJSON(params)); } msg.append(")"); return executeInBrowser(msg.toString()); } public boolean sendBrowserMessage(String key, String op, Collection params) { StringBuffer msg = new StringBuffer(); msg.append("az.msg.dispatch('").append(key).append("', '").append(op).append( "'"); if (params != null) { msg.append(", ").append(JSONUtils.encodeToJSON(params)); } msg.append(")"); return executeInBrowser(msg.toString()); } protected boolean maySend(String key, String op, Map params) { return !pageLoading; } public boolean executeInBrowser(final String javascript) { mon_listJS.enter(); try { if (!mayExecute(javascript)) { listJS.add(javascript); return false; } } finally { mon_listJS.exit(); } if (display == null || display.isDisposed()) { debug("CANNOT: browser.execute( " + getShortJavascript(javascript) + " )"); return false; } // swallow errors silently final String reallyExecute = "try { " + javascript + " } catch ( e ) { }"; Utils.execSWTThread(new AERunnable() { public void runSupport() { if (browser == null || browser.isDisposed()) { debug("CANNOT: browser.execute( " + getShortJavascript(javascript) + " )"); } else if (!browser.execute(reallyExecute)) { debug("FAILED: browser.execute( " + getShortJavascript(javascript) + " )"); } else { debug("SUCCESS: browser.execute( " + getShortJavascript(javascript) + " )"); } } }); return true; } protected boolean mayExecute(String javascript) { return !pageLoading; } public void widgetDisposed(DisposeEvent event) { if (event.widget == browser) { deregisterBrowser(); } } private String getShortJavascript(String javascript) { if (javascript.length() < (256 + 3 + 256)) { return javascript; } StringBuffer result = new StringBuffer(); result.append(javascript.substring(0, 256)); result.append("..."); result.append(javascript.substring(javascript.length() - 256)); return result.toString(); } public void setWiggleBrowser(boolean wiggleBrowser) { this.wiggleBrowser = wiggleBrowser; } public boolean isPageLoading() { return pageLoading; } public void addListener(loadingListener l) { if (loadingListeners == Collections.EMPTY_LIST) { loadingListeners = new ArrayList(1); } loadingListeners.add(l); } public static interface loadingListener { public void browserLoadingChanged(boolean loading, String url); } public long getContentNetworkID() { return contentNetworkID; } public void setContentNetworkID(long contentNetworkID) { this.contentNetworkID = contentNetworkID; } public void setAllowPopups(boolean allowPopups) { this.allowPopups = allowPopups; } public boolean allowPopups() { return allowPopups; } }