/**
* Copyright (c) 2005-2017, KoLmafia development team
* http://kolmafia.sourceforge.net/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* [1] Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* [2] Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* [3] Neither the name "KoLmafia" nor the names of its contributors may
* be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package net.sourceforge.kolmafia.swingui;
import java.awt.Component;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JToolBar;
import javax.swing.ScrollPaneConstants;
import net.java.dev.spellcast.utilities.JComponentUtilities;
import net.sourceforge.kolmafia.KoLAdventure;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.RequestEditorKit;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.StaticEntity;
import net.sourceforge.kolmafia.chat.ChatManager;
import net.sourceforge.kolmafia.persistence.AdventureDatabase;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.CharPaneRequest;
import net.sourceforge.kolmafia.request.FightRequest;
import net.sourceforge.kolmafia.request.GenericRequest;
import net.sourceforge.kolmafia.swingui.button.ThreadedButton;
import net.sourceforge.kolmafia.swingui.listener.HyperlinkAdapter;
import net.sourceforge.kolmafia.swingui.widget.AutoHighlightTextField;
import net.sourceforge.kolmafia.swingui.widget.RequestPane;
import net.sourceforge.kolmafia.utilities.FileUtilities;
import net.sourceforge.kolmafia.utilities.StringUtilities;
import net.sourceforge.kolmafia.webui.RelayLoader;
public class RequestFrame
extends GenericFrame
{
private static final int HISTORY_LIMIT = 4;
private static final Pattern IMAGE_PATTERN =
Pattern.compile( "images\\.kingdomofloathing\\.com/[^\\s\"\'>]+" );
private static final Pattern TOID_PATTERN = Pattern.compile( "toid=(\\d+)" );
private static final Pattern BOOKSHELF_PATTERN =
Pattern.compile( "onClick=\"location.href='(.*?)';\"", Pattern.DOTALL );
private static final ArrayList sideBarFrames = new ArrayList();
private int locationIndex = 0;
private final ArrayList history = new ArrayList();
private final ArrayList shownHTML = new ArrayList();
private String currentLocation;
public RequestPane sideDisplay;
public RequestPane mainDisplay;
private final AutoHighlightTextField locationField = new AutoHighlightTextField();
public RequestFrame()
{
this( "Mini-Browser" );
this.displayRequest( new GenericRequest( "main.php" ) );
}
public RequestFrame( final String title )
{
super( title );
this.mainDisplay = new RequestPane();
this.mainDisplay.addHyperlinkListener( new RequestHyperlinkAdapter() );
JScrollPane mainScroller =
new JScrollPane(
this.mainDisplay, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER );
// Game text descriptions and player searches should not add
// extra requests to the server by having a side panel.
this.constructSideBar( mainScroller );
this.getToolbar();
}
@Override
public boolean showInWindowMenu()
{
return false;
}
private void constructSideBar( final JScrollPane mainScroller )
{
if ( !this.hasSideBar() )
{
JComponentUtilities.setComponentSize( mainScroller, 400, 300 );
this.setCenterComponent( mainScroller );
return;
}
RequestFrame.sideBarFrames.add( this );
this.sideDisplay = new RequestPane();
this.sideDisplay.addHyperlinkListener( new RequestHyperlinkAdapter() );
JScrollPane sideScroller =
new JScrollPane(
this.sideDisplay, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER );
JComponentUtilities.setComponentSize( sideScroller, 150, 450 );
JSplitPane horizontalSplit = new JSplitPane( JSplitPane.HORIZONTAL_SPLIT, true, sideScroller, mainScroller );
horizontalSplit.setOneTouchExpandable( true );
JComponentUtilities.setComponentSize( horizontalSplit, 600, 450 );
this.setCenterComponent( horizontalSplit );
RequestFrame.refreshStatus();
}
@Override
public JToolBar getToolbar()
{
JToolBar toolbarPanel = super.getToolbar( true );
// Add toolbar pieces so that people can quickly
// go to locations they like.
toolbarPanel.add( new ThreadedButton( JComponentUtilities.getImage( "back.gif" ), new BackRunnable() ) );
toolbarPanel.add( new ThreadedButton( JComponentUtilities.getImage( "forward.gif" ), new ForwardRunnable() ) );
toolbarPanel.add( new ThreadedButton( JComponentUtilities.getImage( "home.gif" ), new HomeRunnable() ) );
toolbarPanel.add( new ThreadedButton( JComponentUtilities.getImage( "reload.gif" ), new ReloadRunnable() ) );
toolbarPanel.add( new JToolBar.Separator() );
toolbarPanel.add( this.locationField );
toolbarPanel.add( new JToolBar.Separator() );
ThreadedButton goButton = new ThreadedButton( "Go", new GoRunnable() );
this.locationField.addKeyListener( goButton );
toolbarPanel.add( goButton );
return toolbarPanel;
}
@Override
public Component getCenterComponent()
{
return this.getFramePanel();
}
/**
* Returns whether or not this request frame has a side bar. This is used to ensure that bookmarks correctly use a
* new frame if this frame does not have one.
*/
public boolean hasSideBar()
{
return true;
}
public String getCurrentLocation()
{
return this.currentLocation;
}
/**
* Utility method which refreshes the current frame with data contained in the given request. If the request has not
* yet been run, it will be run before the data is display in this frame.
*/
public void refresh( final GenericRequest request )
{
if ( !this.isVisible() && !this.appearsInTab() )
{
this.setVisible( true );
}
this.displayRequest( request );
}
/**
* Utility method which converts the given text into a form which can be displayed properly in a
* <code>RequestPane</code>. This method is necessary primarily due to the bad HTML which is used but can still
* be properly rendered by post-3.2 browsers.
*/
protected String getDisplayHTML( final String responseText )
{
return RequestFrame.getDisplayHTML( this.currentLocation, responseText, true );
}
private static String getDisplayHTML( final String location, final String responseText, boolean logIt )
{
if ( responseText == null || responseText.length() == 0 )
{
return "";
}
logIt &= RequestLogger.isDebugging();
if ( logIt )
{
RequestLogger.updateDebugLog( "Rendering hypertext..." );
}
String displayHTML = RequestEditorKit.getFeatureRichHTML( location, responseText, false );
// Switch all the <BR> tags that are not understood
// by the default Java browser to an understood form,
// and remove all <HR> tags.
displayHTML = KoLConstants.SCRIPT_PATTERN.matcher( displayHTML ).replaceAll( "" );
displayHTML = KoLConstants.STYLE_PATTERN.matcher( displayHTML ).replaceAll( "" );
displayHTML = KoLConstants.COMMENT_PATTERN.matcher( displayHTML ).replaceAll( "" );
displayHTML = KoLConstants.LINE_BREAK_PATTERN.matcher( displayHTML ).replaceAll( "" );
displayHTML = displayHTML.replaceAll( "<[Bb][Rr]( ?/)?>", "<br>" );
displayHTML = displayHTML.replaceAll( "<[Hh][Rr].*?>", "<br>" );
// The default Java browser doesn't display blank lines correctly
displayHTML = displayHTML.replaceAll( "<br><br>", "<br> <br>" );
// Fix all the tables which decide to put a row end,
// but no row beginning.
displayHTML = displayHTML.replaceAll( "</tr><td", "</tr><tr><td" );
displayHTML = displayHTML.replaceAll( "</tr><table", "</tr></table><table" );
// Fix all the super-small font displays used in the
// various KoL panes.
displayHTML = displayHTML.replaceAll( "font-size: .8em;", "" );
displayHTML = displayHTML.replaceAll( "<font size=[12]>", "" );
displayHTML = displayHTML.replaceAll( " class=small", "" );
displayHTML = displayHTML.replaceAll( " class=tiny", "" );
// This is to replace all the rows with a black background
// because they are not properly rendered.
displayHTML =
displayHTML.replaceAll(
"<td valign=center><table[^>]*?><tr><td([^>]*?) bgcolor=black([^>]*?)>.*?</table></td>", "" );
displayHTML = displayHTML.replaceAll( "<tr[^>]*?><td[^>]*bgcolor=\'?\"?black(.*?)</tr>", "" );
displayHTML = displayHTML.replaceAll( "<table[^>]*title=.*?</table>", "" );
// The default browser doesn't understand the table directive
// style="border: 1px solid black"; turn it into a simple "border=1"
displayHTML = displayHTML.replaceAll( "style=\"border: 1px solid black\"", "border=1" );
// turn: <form...><td...>...</td></form>
// into: <td...><form...>...</form></td>
displayHTML = displayHTML.replaceAll( "(<form[^>]*>)((<input[^>]*>)*)?(<td[^>]*>)", "$4$1$2" );
displayHTML = displayHTML.replaceAll( "</td></form>", "</form></td>" );
// KoL also has really crazy nested Javascript links, and
// since the default browser doesn't recognize these, be
// sure to convert them to standard <A> tags linking to
// the correct document.
displayHTML = displayHTML.replaceAll( "<a[^>]*?\\((?<!discardconf\\()[\'\"](.*?)[\'\"].*?>", "<a href=\"$1\">" );
displayHTML =
displayHTML.replaceAll(
"<img([^>]*?) onClick=\'window.open\\(\"(.*?)\".*?\'(.*?)>", "<a href=\"$2\"><img$1 $3 border=0></a>" );
// The search form for viewing players has an </html>
// tag appearing right after </style>, which may confuse
// the HTML parser.
displayHTML = displayHTML.replaceAll( "</style></html>", "</style>" );
// Image links are mangled a little bit because they use
// Javascript now -- fix them.
displayHTML =
displayHTML.replaceAll(
"<img([^>]*?) onClick=\'descitem\\((\\d+)\\);\'>",
"<a href=\"desc_item.php?whichitem=$2\"><img$1 border=0></a>" );
// The last thing to worry about is the problems in
// specific pages.
// The first of these is the familiar page, where the
// first "Take this one with you" link does not work.
displayHTML =
displayHTML.replaceFirst( "<input class=button type=submit value=\"Take this one with you\">", "" );
// The second of these is the betting page. Here, the
// problem is an "onClick" in the input field, if the
// Hagnk option is available.
if ( displayHTML.indexOf( "whichbet" ) != -1 )
{
// Since the introduction of MMG bots, bets are usually
// placed and taken instantaneously. Therefore, the
// search form is extraneous.
displayHTML = displayHTML.replaceAll( "<center><b>Search.*?<center>", "<center>" );
// Also, placing a bet is awkward through the KoLmafia
// interface. Remove this capability.
displayHTML = displayHTML.replaceAll( "<center><b>Add.*?</form><br>", "<br>" );
// Checkboxes were a safety which were added server-side,
// but they do not really help anything and Java is not
// very good at rendering them -- remove it.
displayHTML = displayHTML.replaceFirst( "\\(confirm\\)", "" );
displayHTML =
displayHTML.replaceAll(
"<input type=checkbox name=confirm>", "<input type=hidden name=confirm value=on>" );
// In order to avoid the problem of having two submits,
// which confuses the built-in Java parser, remove one
// of the buttons and leave the one that makes sense.
if ( KoLCharacter.canInteract() )
{
displayHTML =
displayHTML.replaceAll(
"whichbet value='(\\d+)'><input type=hidden name=from value=0>.*?</td><td><input type=hidden",
"whichbet value='$1'><input type=hidden name=from value=0><input class=button type=submit value=\"On Hand\"><input type=hidden" );
}
else
{
displayHTML =
displayHTML.replaceAll(
"whichbet value='(\\d+)'><input type=hidden name=from value=0>.*?</td><td><input type=hidden",
"whichbet value='$1'><input type=hidden name=from value=1><input class=button type=submit value=\"In Hagnk's\"><input type=hidden" );
}
}
// The third of these is the outfit managing page,
// which requires that the form for the table be
// on the outside of the table.
if ( displayHTML.indexOf( "action=account_manageoutfits.php" ) != -1 )
{
// turn: <center><table><form>...</center></td></tr></form></table>
// into: <form><center><table>...</td></tr></table></center></form>
displayHTML = displayHTML.replaceAll( "<center>(<table[^>]*>)(<form[^>]*>)", "$2<center>$1" );
displayHTML =
displayHTML.replaceAll( "</center></td></tr></form></table>", "</td></tr></table></center></form>" );
}
// The fourth of these is the fight page, which is
// totally mixed up -- in addition to basic modifications,
// also resort the combat item list.
if ( displayHTML.indexOf( "action=fight.php" ) != -1 )
{
displayHTML = displayHTML.replaceAll( "<form(.*?)<tr><td([^>]*)>", "<tr><td$2><form$1" );
displayHTML = displayHTML.replaceAll( "</td></tr></form>", "</form></td></tr>" );
// The following all appear when the WOWbar is active
// and are useless without Javascript.
displayHTML = displayHTML.replaceAll( "<img.*?id='dragged'>", "" );
displayHTML = displayHTML.replaceAll( "<div class=contextmenu.*?</div>", "");
displayHTML = displayHTML.replaceAll( "<div id=topbar>?.*?</div>", "");
displayHTML = displayHTML.replaceAll( "<div id='fightform' class='hideform'>.*?</div>(<p><center>You win the fight!)", "$1" );
}
// The library bookshelf has some secretive Javascript
// which needs to be removed.
displayHTML = RequestFrame.BOOKSHELF_PATTERN.matcher( displayHTML ).replaceAll( "href=\"$1\"" );
if ( logIt )
{
// Print it to the debug log for reference purposes.
RequestLogger.updateDebugLog( displayHTML );
}
// All HTML is now properly rendered! Return compiled string.
return displayHTML;
}
/**
* Utility method which displays the given request.
*/
public void displayRequest( GenericRequest request )
{
if ( this.mainDisplay == null || request == null )
{
return;
}
if ( request instanceof FightRequest )
{
request = new GenericRequest( request.getURLString() );
request.responseText = FightRequest.lastResponseText;
}
this.currentLocation = request.getURLString();
if ( request.responseText == null || request.responseText.length() == 0 )
{
RequestThread.runInParallel( new DisplayRequestRunnable( request ) );
}
else
{
this.showHTML( request.responseText );
}
}
private class DisplayRequestRunnable
implements Runnable
{
private GenericRequest request;
public DisplayRequestRunnable( GenericRequest request )
{
this.request = request;
}
public void run()
{
// New prevention mechanism: tell the requests that there
// will be no synchronization.
boolean original = Preferences.getBoolean( "showAllRequests" );
Preferences.setBoolean( "showAllRequests", false );
this.request.run();
Preferences.setBoolean( "showAllRequests", original );
// If this resulted in a redirect, then update the display
// to indicate that you were redirected and the display
// cannot be shown in the minibrowser.
if ( this.request.responseText == null || this.request.responseText.length() == 0 )
{
RequestFrame.this.mainDisplay.setText( "" );
return;
}
RequestFrame.this.showHTML( this.request.responseText );
}
}
public void showHTML( String responseText )
{
// Function exactly like a history in a normal browser -
// if you open a new frame after going back, all the ones
// in the future get removed.
responseText = this.getDisplayHTML( responseText );
String location = this.currentLocation;
this.history.add( location );
this.shownHTML.add( responseText );
if ( this.history.size() > RequestFrame.HISTORY_LIMIT )
{
this.history.remove( 0 );
this.shownHTML.remove( 0 );
}
location = location.substring( location.lastIndexOf( "/" ) + 1 );
this.locationField.setText( location );
this.locationIndex = RequestFrame.this.shownHTML.size() - 1;
Matcher imageMatcher = RequestFrame.IMAGE_PATTERN.matcher( responseText );
while ( imageMatcher.find() )
{
FileUtilities.downloadImage( "http://" + imageMatcher.group() );
}
this.mainDisplay.setText( responseText );
}
public static final void refreshStatus()
{
String displayHTML = RequestFrame.getDisplayHTML( "charpane.php", CharPaneRequest.getLastResponse(), false );
for ( int i = 0; i < RequestFrame.sideBarFrames.size(); ++i )
{
RequestFrame current = (RequestFrame) RequestFrame.sideBarFrames.get( i );
if ( current.sideDisplay == null )
{
continue;
}
try
{
current.sideDisplay.setText( displayHTML );
}
catch ( Exception e )
{
// This should not happen. Therefore, print
// a stack trace for debug purposes.
StaticEntity.printStackTrace( e );
}
}
}
public boolean containsText( final String search )
{
return this.mainDisplay.getText().indexOf( search ) != -1;
}
@Override
public void dispose()
{
this.history.clear();
this.shownHTML.clear();
super.dispose();
}
@Override
public boolean shouldAddStatusBar()
{
return false;
}
private class HomeRunnable
implements Runnable
{
public void run()
{
RequestFrame.this.refresh( new GenericRequest( "main.php" ) );
}
}
private class BackRunnable
implements Runnable
{
public void run()
{
if ( RequestFrame.this.locationIndex > 0 )
{
--RequestFrame.this.locationIndex;
RequestFrame.this.mainDisplay.setText( (String) RequestFrame.this.shownHTML.get( RequestFrame.this.locationIndex ) );
RequestFrame.this.locationField.setText( (String) RequestFrame.this.history.get( RequestFrame.this.locationIndex ) );
}
}
}
private class ForwardRunnable
implements Runnable
{
public void run()
{
if ( RequestFrame.this.locationIndex + 1 < RequestFrame.this.shownHTML.size() )
{
++RequestFrame.this.locationIndex;
RequestFrame.this.mainDisplay.setText( (String) RequestFrame.this.shownHTML.get( RequestFrame.this.locationIndex ) );
RequestFrame.this.locationField.setText( (String) RequestFrame.this.history.get( RequestFrame.this.locationIndex ) );
}
}
}
private class ReloadRunnable
implements Runnable
{
public void run()
{
if ( RequestFrame.this.currentLocation == null )
{
return;
}
RequestFrame.this.refresh( new GenericRequest( RequestFrame.this.currentLocation ) );
}
}
private class GoRunnable
implements Runnable
{
public void run()
{
KoLAdventure adventure = AdventureDatabase.getAdventure( RequestFrame.this.locationField.getText() );
GenericRequest request =
RequestEditorKit.extractRequest( adventure == null ? RequestFrame.this.locationField.getText() : adventure.getRequest().getURLString() );
RequestFrame.this.refresh( request );
}
}
public class RequestHyperlinkAdapter
extends HyperlinkAdapter
{
@Override
public void handleInternalLink( final String location )
{
if ( location.equals( "lchat.php" ) )
{
ChatManager.initialize();
}
else if ( location.startsWith( "makeoffer.php" ) || location.startsWith( "counteroffer.php" ) )
{
RelayLoader.openSystemBrowser( location );
}
else if ( location.startsWith( "sendmessage.php" ) || location.startsWith( "town_sendgift.php" ) )
{
// Attempts to send a message should open up
// KoLmafia's built-in message sender.
Matcher idMatcher = RequestFrame.TOID_PATTERN.matcher( location );
String[] parameters = new String[]
{ idMatcher.find() ? idMatcher.group( 1 ) : ""
};
GenericFrame.createDisplay( SendMessageFrame.class, parameters );
}
else if ( location.startsWith( "search" ) || location.startsWith( "desc" ) || location.startsWith( "static" ) || location.startsWith( "show" ) )
{
DescriptionFrame.showLocation( location );
return;
}
else
{
RequestFrame.this.gotoLink( location );
}
}
}
public void gotoLink( final String location )
{
this.refresh( RequestEditorKit.extractRequest( location ) );
}
}