001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.dev.shell;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019: import com.google.gwt.core.ext.UnableToCompleteException;
020: import com.google.gwt.dev.util.Util;
021:
022: import org.eclipse.swt.SWT;
023: import org.eclipse.swt.browser.Browser;
024: import org.eclipse.swt.browser.LocationEvent;
025: import org.eclipse.swt.browser.LocationListener;
026: import org.eclipse.swt.browser.OpenWindowListener;
027: import org.eclipse.swt.browser.StatusTextEvent;
028: import org.eclipse.swt.browser.StatusTextListener;
029: import org.eclipse.swt.browser.TitleEvent;
030: import org.eclipse.swt.browser.TitleListener;
031: import org.eclipse.swt.browser.WindowEvent;
032: import org.eclipse.swt.events.DisposeEvent;
033: import org.eclipse.swt.events.DisposeListener;
034: import org.eclipse.swt.events.FocusEvent;
035: import org.eclipse.swt.events.FocusListener;
036: import org.eclipse.swt.events.KeyEvent;
037: import org.eclipse.swt.events.KeyListener;
038: import org.eclipse.swt.events.SelectionAdapter;
039: import org.eclipse.swt.events.SelectionEvent;
040: import org.eclipse.swt.events.SelectionListener;
041: import org.eclipse.swt.graphics.Color;
042: import org.eclipse.swt.graphics.Cursor;
043: import org.eclipse.swt.layout.GridData;
044: import org.eclipse.swt.layout.GridLayout;
045: import org.eclipse.swt.program.Program;
046: import org.eclipse.swt.widgets.Button;
047: import org.eclipse.swt.widgets.Composite;
048: import org.eclipse.swt.widgets.Label;
049: import org.eclipse.swt.widgets.MessageBox;
050: import org.eclipse.swt.widgets.Shell;
051: import org.eclipse.swt.widgets.Text;
052: import org.eclipse.swt.widgets.ToolItem;
053:
054: import java.io.File;
055: import java.io.IOException;
056: import java.util.HashMap;
057: import java.util.HashSet;
058: import java.util.Map;
059: import java.util.Set;
060:
061: /**
062: * Represents an individual browser window and all of its controls.
063: */
064: public abstract class BrowserWidget extends Composite {
065:
066: private class Toolbar extends HeaderBarBase implements
067: SelectionListener {
068: private final ToolItem backButton;
069:
070: private final ToolItem forwardButton;
071:
072: private final ToolItem openWebModeButton;
073:
074: private final ToolItem refreshButton;
075: private final ToolItem stopButton;
076:
077: public Toolbar(Composite parent) {
078: super (parent);
079:
080: backButton = newItem("back.gif", " &Back ",
081: "Go back one state");
082: backButton.addSelectionListener(this );
083:
084: forwardButton = newItem("forward.gif", "&Forward",
085: "Go forward one state");
086: forwardButton.addSelectionListener(this );
087:
088: refreshButton = newItem("refresh.gif", " &Refresh ",
089: "Reload the page");
090: refreshButton.addSelectionListener(this );
091:
092: stopButton = newItem("stop.gif", " &Stop ",
093: "Stop loading the page");
094: stopButton.addSelectionListener(this );
095:
096: newSeparator();
097:
098: openWebModeButton = newItem("new-web-mode-window.gif",
099: "&Compile/Browse",
100: "Compiles and opens the current URL in the system browser");
101: openWebModeButton.addSelectionListener(this );
102: openWebModeButton.setEnabled(false);
103: }
104:
105: public void widgetDefaultSelected(SelectionEvent e) {
106: }
107:
108: public void widgetSelected(SelectionEvent evt) {
109: if (evt.widget == backButton) {
110: browser.back();
111: } else if (evt.widget == forwardButton) {
112: browser.forward();
113: } else if (evt.widget == refreshButton) {
114: // we have to clean up old module spaces here b/c we don't get a
115: // location changed event
116:
117: // lastHostPageLocation = null;
118: browser.refresh();
119: } else if (evt.widget == stopButton) {
120: browser.stop();
121: } else if (evt.widget == openWebModeButton) {
122: // first, compile
123: Set<String> keySet = new HashSet<String>();
124: for (Map.Entry<?, ModuleSpace> entry : loadedModules
125: .entrySet()) {
126: ModuleSpace module = entry.getValue();
127: keySet.add(module.getModuleName());
128: }
129: String[] moduleNames = Util.toStringArray(keySet);
130: if (moduleNames.length == 0) {
131: // A latent problem with a module.
132: //
133: openWebModeButton.setEnabled(false);
134: return;
135: }
136: try {
137: Cursor waitCursor = getDisplay().getSystemCursor(
138: SWT.CURSOR_WAIT);
139: getShell().setCursor(waitCursor);
140: getHost().compile(moduleNames);
141: } catch (UnableToCompleteException e) {
142: // Already logged by callee.
143: //
144: MessageBox msgBox = new MessageBox(getShell(),
145: SWT.OK | SWT.ICON_ERROR);
146: msgBox.setText("Compilation Failed");
147: msgBox
148: .setMessage("Compilation failed. Please see the log in the development shell for details.");
149: msgBox.open();
150: return;
151: } finally {
152: // Restore the cursor.
153: //
154: Cursor normalCursor = getDisplay().getSystemCursor(
155: SWT.CURSOR_ARROW);
156: getShell().setCursor(normalCursor);
157: }
158:
159: String locationText = location.getText();
160:
161: launchExternalBrowser(logger, locationText);
162: }
163: }
164: }
165:
166: static void launchExternalBrowser(TreeLogger logger, String location) {
167:
168: // check GWT_EXTERNAL_BROWSER first, it overrides everything else
169: LowLevel.init();
170: String browserCmd = LowLevel.getEnv("GWT_EXTERNAL_BROWSER");
171: if (browserCmd != null) {
172: browserCmd += " " + location;
173: try {
174: Runtime.getRuntime().exec(browserCmd);
175: return;
176: } catch (IOException e) {
177: logger.log(TreeLogger.ERROR,
178: "Error launching GWT_EXTERNAL_BROWSER executable '"
179: + browserCmd + "'", e);
180: return;
181: }
182: }
183:
184: // legacy: gwt.browser.default
185: browserCmd = System.getProperty("gwt.browser.default");
186: if (browserCmd != null) {
187: browserCmd += " " + location;
188: try {
189: Runtime.getRuntime().exec(browserCmd);
190: return;
191: } catch (IOException e) {
192: logger.log(TreeLogger.ERROR,
193: "Error launching gwt.browser.default executable '"
194: + browserCmd + "'", e);
195: return;
196: }
197: }
198:
199: // Programmatically try to find something that can handle html files
200: Program browserProgram = Program.findProgram("html");
201: if (browserProgram != null) {
202: if (browserProgram.execute(location)) {
203: return;
204: } else {
205: logger.log(TreeLogger.ERROR,
206: "Error launching external HTML program '"
207: + browserProgram.getName() + "'", null);
208: return;
209: }
210: }
211:
212: // We're out of options, so fail.
213: logger.log(TreeLogger.ERROR,
214: "Unable to find a default external web browser", null);
215:
216: logger
217: .log(
218: TreeLogger.WARN,
219: "Try setting the environment variable "
220: + "GWT_EXTERNAL_BROWSER to your web browser executable before "
221: + "launching the GWT shell", null);
222: }
223:
224: protected Browser browser;
225:
226: private Color bgColor = new Color(null, 239, 237, 216);
227:
228: private Button goButton;
229:
230: private final BrowserWidgetHost host;
231:
232: private final Map<Object, ModuleSpace> loadedModules = new HashMap<Object, ModuleSpace>();
233:
234: private Text location;
235:
236: private final TreeLogger logger;
237:
238: private Label statusBar;
239:
240: private Toolbar toolbar;
241:
242: public BrowserWidget(Composite parent, BrowserWidgetHost host) {
243: super (parent, SWT.NONE);
244:
245: this .host = host;
246: logger = this .host.getLogger();
247:
248: bgColor = new Color(null, 239, 237, 216);
249:
250: toolbar = new Toolbar(this );
251: Composite secondBar = buildLocationBar(this );
252:
253: browser = new Browser(this , SWT.NONE);
254:
255: {
256: statusBar = new Label(this , SWT.BORDER | SWT.SHADOW_IN);
257: statusBar.setBackground(bgColor);
258: GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
259: gridData.verticalAlignment = GridData.CENTER;
260: gridData.verticalIndent = 0;
261: gridData.horizontalIndent = 0;
262: statusBar.setLayoutData(gridData);
263: }
264:
265: GridLayout layout = new GridLayout();
266: layout.numColumns = 1;
267: layout.verticalSpacing = 1;
268: layout.marginWidth = 0;
269: layout.marginHeight = 0;
270: setLayout(layout);
271:
272: toolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
273: secondBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
274:
275: GridData data = new GridData(GridData.FILL_BOTH);
276: data.grabExcessVerticalSpace = true;
277: data.grabExcessHorizontalSpace = true;
278: browser.setLayoutData(data);
279:
280: // Hook up all appropriate event listeners.
281: //
282: hookBrowserListeners();
283: }
284:
285: /**
286: * Gets the browser object wrapped by this window.
287: */
288: public Browser getBrowser() {
289: return browser;
290: }
291:
292: public BrowserWidgetHost getHost() {
293: return host;
294: }
295:
296: public abstract String getUserAgent();
297:
298: /**
299: * Go to a given url, possibly rewriting it if it can be served from any
300: * project's public directory.
301: */
302: public void go(String target) {
303: String url = host.normalizeURL(target);
304: browser.setUrl(url);
305: }
306:
307: public void onFirstShown() {
308: String baseUrl = host.normalizeURL("/");
309: setLocationText(baseUrl);
310: location.setFocus();
311: location.setSelection(baseUrl.length());
312: location.addFocusListener(new FocusListener() {
313: public void focusGained(FocusEvent e) {
314: int length = location.getText().length();
315: location.setSelection(length, length);
316: }
317:
318: public void focusLost(FocusEvent e) {
319: }
320: });
321: }
322:
323: /**
324: * Initializes and attaches module space to this browser widget. Called by
325: * subclasses in response to calls from JavaScript.
326: *
327: * @param space ModuleSpace instance to initialize
328: */
329: protected final void attachModuleSpace(ModuleSpace space)
330: throws UnableToCompleteException {
331: Object key = space.getKey();
332: loadedModules.put(key, space);
333:
334: logger.log(TreeLogger.SPAM, "Loading module "
335: + space.getModuleName() + " (id " + key.toString()
336: + ")", null);
337:
338: // Let the space do its thing.
339: //
340: space.onLoad(logger);
341:
342: // Enable the compile button since we successfully loaded.
343: //
344: toolbar.openWebModeButton.setEnabled(true);
345: }
346:
347: /**
348: * Unload one or more modules. If key is null, emulate old behavior by
349: * unloading all loaded modules.
350: *
351: * @param key unique key to identify module to unload or null for all
352: */
353: protected void doUnload(Object key) {
354: if (key == null) {
355: // BEGIN BACKWARD COMPATIBILITY
356: // remove all modules
357: for (Map.Entry<?, ModuleSpace> entry : loadedModules
358: .entrySet()) {
359: unloadModule(entry.getValue());
360: }
361: loadedModules.clear();
362: // END BACKWARD COMPATIBILITY
363: } else {
364: ModuleSpace moduleSpace = loadedModules.get(key);
365: if (moduleSpace != null) {
366: // If the module failed to load at all, it may not be in the map.
367: unloadModule(moduleSpace);
368: loadedModules.remove(key);
369: }
370: }
371: if (loadedModules.isEmpty()) {
372: if (!toolbar.openWebModeButton.isDisposed()) {
373: // Disable the compile button.
374: //
375: toolbar.openWebModeButton.setEnabled(false);
376: }
377: }
378: }
379:
380: /**
381: * Unload the specified module.
382: *
383: * @param moduleSpace a ModuleSpace instance to unload.
384: */
385: protected void unloadModule(ModuleSpace moduleSpace) {
386: String moduleName = moduleSpace.getModuleName();
387: Object key = moduleSpace.getKey();
388: moduleSpace.dispose();
389: logger.log(TreeLogger.SPAM, "Unloading module " + moduleName
390: + " (id " + key.toString() + ")", null);
391: }
392:
393: private Composite buildLocationBar(Composite parent) {
394: Color white = new Color(null, 255, 255, 255);
395:
396: Composite bar = new Composite(parent, SWT.BORDER);
397: bar.setBackground(white);
398:
399: location = new Text(bar, SWT.FLAT);
400:
401: goButton = new Button(bar, SWT.NONE);
402: goButton.setBackground(bgColor);
403: goButton.setText("Go");
404: goButton.setImage(LowLevel.loadImage("go.gif"));
405:
406: GridLayout layout = new GridLayout();
407: layout.numColumns = 2;
408: layout.marginWidth = layout.marginHeight = 0;
409: layout.marginLeft = 2;
410: layout.verticalSpacing = layout.horizontalSpacing = 0;
411: bar.setLayout(layout);
412:
413: GridData data = new GridData(GridData.FILL_HORIZONTAL);
414: data.grabExcessHorizontalSpace = true;
415: data.verticalAlignment = GridData.CENTER;
416: location.setLayoutData(data);
417:
418: return bar;
419: }
420:
421: /**
422: * Hooks up all necessary event listeners.
423: */
424: private void hookBrowserListeners() {
425:
426: this .addDisposeListener(new DisposeListener() {
427: public void widgetDisposed(DisposeEvent e) {
428: bgColor.dispose();
429: }
430: });
431:
432: goButton.addSelectionListener(new SelectionAdapter() {
433: @Override
434: public void widgetSelected(SelectionEvent e) {
435: go(location.getText());
436: }
437: });
438:
439: // Hook up the return key in the location bar.
440: //
441: location.addKeyListener(new KeyListener() {
442: public void keyPressed(KeyEvent e) {
443: if (e.character == '\r') {
444: go(location.getText());
445: }
446: }
447:
448: public void keyReleased(KeyEvent e) {
449: }
450: });
451:
452: // Tie the status label to the browser's status.
453: //
454: browser.addStatusTextListener(new StatusTextListener() {
455: public void changed(StatusTextEvent evt) {
456: // Add a little space so it doesn't look so crowded.
457: statusBar.setText(" " + evt.text);
458: }
459: });
460:
461: browser.addTitleListener(new TitleListener() {
462: public void changed(TitleEvent evt) {
463: browser.getShell().setText(evt.title);
464: }
465: });
466:
467: // Tie the location text box to the browser's location.
468: //
469: browser.addLocationListener(new LocationListener() {
470:
471: public void changed(LocationEvent evt) {
472: if (evt.top) {
473: setLocationText(evt.location);
474: }
475: }
476:
477: public void changing(LocationEvent evt) {
478: String whitelistRuleFound = null;
479: String blacklistRuleFound = null;
480: if (evt.location.indexOf(":") == -1) {
481: evt.location = "file://" + evt.location;
482: }
483: String url = evt.location;
484: evt.doit = false;
485:
486: // Ensure that the request is 'safe', meaning it targets the user's
487: // local machine or a host that has been whitelisted.
488: //
489: if (BrowserWidgetHostChecker.isAlwaysWhitelisted(url)) {
490: // if the URL is 'always whitelisted', i.e. localhost
491: // we load the page without regard to blacklisting
492: evt.doit = true;
493: return;
494: }
495: whitelistRuleFound = BrowserWidgetHostChecker
496: .matchWhitelisted(url);
497: blacklistRuleFound = BrowserWidgetHostChecker
498: .matchBlacklisted(url);
499:
500: // If a host is blacklisted and whitelisted, disallow
501: evt.doit = whitelistRuleFound != null
502: && blacklistRuleFound == null;
503: // We need these if we show a dialog box, so we declare them here and
504: // initialize them inside the dialog box case before we change the
505: // [in]valid hosts
506: // no opinion either way
507: if (whitelistRuleFound == null
508: && blacklistRuleFound == null) {
509: if (DialogBase
510: .confirmAction(
511: (Shell) getParent(),
512: "Browsing to remote sites is a security risk! A malicious site could\r\n"
513: + "execute Java code though this browser window. Only click \"Yes\" if you\r\n"
514: + "are sure you trust the remote site. See the log for details and\r\n"
515: + "configuration instructions.\r\n"
516: + "\r\n"
517: + "\r\n"
518: + "Allow access to '"
519: + url
520: + "' for the rest of this session?\r\n",
521: "Security Warning")) {
522: evt.doit = true;
523: BrowserWidgetHostChecker.whitelistURL(url);
524: } else {
525: evt.doit = false;
526: BrowserWidgetHostChecker.blacklistURL(url);
527: }
528: }
529:
530: // Check for file system.
531: //
532: if (!evt.doit) {
533: // Rip off the query string part. When launching files directly from
534: // the filesystem, the existence of a query string when doing the
535: // lookup below causes problems (e.g. we don't want to look up a file
536: // called "C:\www\myapp.html?gwt.hybrid").
537: //
538: int lastQues = url.lastIndexOf('?');
539: int lastSlash = url
540: .lastIndexOf(File.pathSeparatorChar);
541: if (lastQues != -1 && lastQues > lastSlash) {
542: url = url.substring(0, lastQues);
543: }
544:
545: // If any part of the path exists, it is at least a valid attempt.
546: // This avoids the misleading security message when a file simply
547: // cannot be found.
548: //
549: if (!url.startsWith("http:")
550: && !url.startsWith("https:")) {
551: File file = new File(url);
552: while (file != null) {
553: if (file.exists()) {
554: evt.doit = true;
555: break;
556: } else {
557: String msg = "Cannot find file '"
558: + file.getAbsolutePath() + "'";
559: TreeLogger branch = logger.branch(
560: TreeLogger.ERROR, msg, null);
561: if ("gwt-hosted.html"
562: .equalsIgnoreCase(file
563: .getName())) {
564: branch
565: .log(
566: TreeLogger.ERROR,
567: "If you want to open compiled output within this hosted browser, add '?gwt.hybrid' to the end of the URL",
568: null);
569: }
570: }
571: file = file.getParentFile();
572: }
573: }
574: }
575: // if it wasn't whitelisted or we were blocked we want to say something
576: if (whitelistRuleFound == null || !evt.doit) {
577: // Restore the URL.
578: String typeStr = "untrusted";
579: if (blacklistRuleFound != null) {
580: typeStr = "blocked";
581: }
582: TreeLogger header;
583: TreeLogger.Type msgType = TreeLogger.ERROR;
584: if (!evt.doit) {
585: header = logger.branch(msgType,
586: "Unable to visit " + typeStr
587: + " URL: '" + url, null);
588: } else {
589: msgType = TreeLogger.WARN;
590: header = logger.branch(TreeLogger.WARN,
591: "Confirmation was required to visit "
592: + typeStr + " URL: '" + url,
593: null);
594: }
595: if (blacklistRuleFound == null) {
596: BrowserWidgetHostChecker.notifyUntrustedHost(
597: url, header, msgType);
598: } else {
599: BrowserWidgetHostChecker.notifyBlacklistedHost(
600: blacklistRuleFound, url, header,
601: msgType);
602: }
603: setLocationText(browser.getUrl());
604: }
605: }
606:
607: });
608:
609: // Handle new window requests.
610: //
611: browser.addOpenWindowListener(new OpenWindowListener() {
612: public void open(WindowEvent event) {
613: try {
614: event.browser = host.openNewBrowserWindow()
615: .getBrowser();
616: event.browser.getShell().open();
617: } catch (UnableToCompleteException e) {
618: logger.log(TreeLogger.ERROR,
619: "Unable to open new browser window", e);
620: }
621: }
622: });
623: }
624:
625: private void setLocationText(String text) {
626: location.setText(text);
627: int length = text.length();
628: location.setSelection(length, length);
629: }
630: }
|