/* * Created on Sep 02, 2005 * * This file is part of susidns project, see http://susi.i2p/ * * Copyright (C) 2005 <susi23@mail.i2p> * * 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 * * $Revision: 1.2 $ */ package i2p.susi.dns; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedList; import java.util.Locale; import java.util.Map; import java.util.Properties; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.util.SecureFileOutputStream; public class AddressbookBean extends BaseBean { protected String book, filter, search, hostname, destination; protected int beginIndex, endIndex; private Properties addressbook; private int trClass; protected final LinkedList<String> deletionMarks; protected static final Comparator<AddressBean> sorter; private static final int DISPLAY_SIZE = 50; static { sorter = new AddressByNameSorter(); } public String getSearch() { return search; } public void setSearch(String search) { this.search = DataHelper.stripHTML(search).trim(); // XSS; } public boolean isHasFilter() { return filter != null && filter.length() > 0; } public void setTrClass(int trClass) { this.trClass = trClass; } public int getTrClass() { trClass = 1 - trClass; return trClass; } public boolean isIsEmpty() { return ! isNotEmpty(); } public boolean isNotEmpty() { return addressbook != null && !addressbook.isEmpty(); } public AddressbookBean() { super(); deletionMarks = new LinkedList<String>(); beginIndex = 0; endIndex = DISPLAY_SIZE - 1; } public String getFileName() { loadConfig(); String filename = properties.getProperty( getBook() + "_addressbook" ); // clean up the ../ with getCanonicalPath() File path = new File(addressbookDir(), filename); try { return path.getCanonicalPath(); } catch (IOException ioe) {} return filename; } public String getDisplayName() { return getFileName(); } protected AddressBean[] entries; public AddressBean[] getEntries() { return entries; } /** * This always returns a valid book, non-null. */ public String getBook() { if( book == null || ( !book.equalsIgnoreCase( "master" ) && !book.equalsIgnoreCase( "router" ) && !book.equalsIgnoreCase( "private" ) && !book.equalsIgnoreCase( "published" ))) book = "router"; return book; } public void setBook(String book) { this.book = DataHelper.stripHTML(book); // XSS } /** Load addressbook and apply filter, returning messages about this. */ public String getLoadBookMessages() { // Config and addressbook now loaded here, hence not needed in getMessages() loadConfig(); addressbook = new Properties(); String message = ""; FileInputStream fis = null; try { fis = new FileInputStream( getFileName() ); addressbook.load( fis ); LinkedList<AddressBean> list = new LinkedList<AddressBean>(); for( Map.Entry<Object, Object> entry : addressbook.entrySet() ) { String name = (String) entry.getKey(); String destination = (String) entry.getValue(); if( filter != null && filter.length() > 0 ) { if( filter.compareTo( "0-9" ) == 0 ) { char first = name.charAt(0); if( first < '0' || first > '9' ) continue; } else if( ! name.toLowerCase(Locale.US).startsWith( filter.toLowerCase(Locale.US) ) ) { continue; } } if( search != null && search.length() > 0 ) { if( name.indexOf( search ) == -1 ) { continue; } } list.addLast( new AddressBean( name, destination ) ); } AddressBean array[] = list.toArray(new AddressBean[list.size()]); Arrays.sort( array, sorter ); entries = array; message = generateLoadMessage(); } catch (IOException e) { warn(e); } finally { if (fis != null) try { fis.close(); } catch (IOException ioe) {} } if( message.length() > 0 ) message = "<p>" + message + "</p>"; return message; } /** * Format a message about filtered addressbook size, and the number of displayed entries * addressbook.jsp catches the case where the whole book is empty. */ protected String generateLoadMessage() { String message; String filterArg = ""; int resultCount = resultSize(); if( filter != null && filter.length() > 0 ) { if( search != null && search.length() > 0 ) message = ngettext("One result for search within filtered list.", "{0} results for search within filtered list.", resultCount); else message = ngettext("Filtered list contains 1 entry.", "Filtered list contains {0} entries.", resultCount); filterArg = "&filter=" + filter; } else if( search != null && search.length() > 0 ) { message = ngettext("One result for search.", "{0} results for search.", resultCount); } else { if (resultCount <= 0) // covered in jsp //message = _t("This addressbook is empty."); message = ""; else message = ngettext("Address book contains 1 entry.", "Address book contains {0} entries.", resultCount); } if (resultCount <= 0) { // nothing to display } else if (getBeginInt() == 0 && getEndInt() == resultCount - 1) { // nothing to display } else { if (getBeginInt() > 0) { int newBegin = Math.max(0, getBeginInt() - DISPLAY_SIZE); int newEnd = Math.max(0, getBeginInt() - 1); message += " <a href=\"addressbook?book=" + getBook() + filterArg + "&begin=" + newBegin + "&end=" + newEnd + "\">" + (newBegin+1) + '-' + (newEnd+1) + "</a> | "; } message += ' ' + _t("Showing {0} of {1}", "" + (getBeginInt()+1) + '-' + (getEndInt()+1), Integer.valueOf(resultCount)); if (getEndInt() < resultCount - 1) { int newBegin = Math.min(resultCount - 1, getEndInt() + 1); int newEnd = Math.min(resultCount, getEndInt() + DISPLAY_SIZE); message += " | <a href=\"addressbook?book=" + getBook() + filterArg + "&begin=" + newBegin + "&end=" + newEnd + "\">" + (newBegin+1) + '-' + (newEnd+1) + "</a>"; } } return message; } /** Perform actions, returning messages about this. */ public String getMessages() { // Loading config and addressbook moved into getLoadBookMessages() String message = ""; if( action != null ) { if (_context.getBooleanProperty(PROP_PW_ENABLE) || (serial != null && serial.equals(lastSerial))) { boolean changed = false; if (action.equals(_t("Add")) || action.equals(_t("Replace"))) { if( addressbook != null && hostname != null && destination != null ) { try { // throws IAE with translated message String host = AddressBean.toASCII(hostname); String displayHost = host.equals(hostname) ? hostname : hostname + " (" + host + ')'; String oldDest = (String) addressbook.get(host); if (destination.equals(oldDest)) { message = _t("Host name {0} is already in address book, unchanged.", displayHost); } else if (oldDest != null && !action.equals(_t("Replace"))) { message = _t("Host name {0} is already in address book with a different destination. Click \"Replace\" to overwrite.", displayHost); } else { boolean valid = true; try { // just to check validity new Destination(destination); } catch (DataFormatException dfe) { valid = false; } if (valid) { addressbook.put( host, destination ); changed = true; if (oldDest == null) message = _t("Destination added for {0}.", displayHost); else message = _t("Destination changed for {0}.", displayHost); if (!host.endsWith(".i2p")) message += "<br>" + _t("Warning - host name does not end with \".i2p\""); // clear form hostname = null; destination = null; } else { message = _t("Invalid Base 64 destination."); } } } catch (IllegalArgumentException iae) { message = iae.getMessage(); if (message == null) message = _t("Invalid host name \"{0}\".", hostname); } } else { message = _t("Please enter a host name and destination"); } // clear search when adding search = null; } else if (action.equals(_t("Delete Selected")) || action.equals(_t("Delete Entry"))) { String name = null; int deleted = 0; for (String n : deletionMarks) { addressbook.remove(n); String uni = AddressBean.toUnicode(n); String displayHost = uni.equals(n) ? n : uni + " (" + n + ')'; if (deleted++ == 0) { changed = true; name = displayHost; } } if( changed ) { if (deleted == 1) message = _t("Destination {0} deleted.", name); else // parameter will always be >= 2 message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted); } else { message = _t("No entries selected to delete."); } if (action.equals(_t("Delete Entry"))) search = null; } else if (action.equals(_t("Add Alternate"))) { // button won't be in UI message = "Unsupported"; } if( changed ) { try { save(); message += "<br>" + _t("Address book saved."); } catch (IOException e) { warn(e); message += "<br>" + _t("ERROR: Could not write addressbook file."); } } } else { message = _t("Invalid form submission, probably because you used the \"back\" or \"reload\" button on your browser. Please resubmit.") + ' ' + _t("If the problem persists, verify that you have cookies enabled in your browser."); } } action = null; if( message.length() > 0 ) message = "<p class=\"messages\">" + message + "</p>"; return message; } private void save() throws IOException { String filename = properties.getProperty( getBook() + "_addressbook" ); FileOutputStream fos = null; try { fos = new SecureFileOutputStream(new File(addressbookDir(), filename)); addressbook.store( fos, null ); } finally { if (fos != null) { try { fos.close(); } catch (IOException ioe) {} } } } public String getFilter() { return filter; } /**** public boolean isMaster() { return getBook().equalsIgnoreCase("master"); } public boolean isRouter() { return getBook().equalsIgnoreCase("router"); } public boolean isPublished() { return getBook().equalsIgnoreCase("published"); } public boolean isPrivate() { return getBook().equalsIgnoreCase("private"); } ****/ /** * Because the following from addressbook.jsp fails parsing in the new EL: * javax.el.ELException: Failed to parse the expression * Can't figure out why, so just replace it with book.validBook: * <c:if test="${book.master || book.router || book.published || book.private}"> * * This always returns true anyway, because getBook() always * returns a valid book. * * @return true * @since 0.9.28 */ public boolean isValidBook() { String s = getBook().toLowerCase(Locale.US); return s.equals("router") || s.equals("master") || s.equals("published") || s.equals("private"); } public void setFilter(String filter) { if( filter != null && ( filter.length() == 0 || filter.equalsIgnoreCase("none"))) { filter = null; search = null; } this.filter = DataHelper.stripHTML(filter); // XSS } public String getDestination() { return destination; } public void setDestination(String destination) { this.destination = DataHelper.stripHTML(destination).trim(); // XSS } public String getHostname() { return hostname; } public void setResetDeletionMarks( String dummy ) { deletionMarks.clear(); } public void setMarkedForDeletion( String name ) { deletionMarks.addLast( DataHelper.stripHTML(name) ); // XSS } public void setHostname(String hostname) { this.hostname = DataHelper.stripHTML(hostname).trim(); // XSS } protected int getBeginInt() { return Math.max(0, Math.min(resultSize() - 1, beginIndex)); } public String getBegin() { return "" + getBeginInt(); } /** * @return beginning index into results * @since 0.8.7 */ public String getResultBegin() { return isPrefiltered() ? "0" : Integer.toString(getBeginInt()); } public void setBegin(String s) { try { beginIndex = Integer.parseInt(s); } catch (NumberFormatException nfe) {} } protected int getEndInt() { return Math.max(0, Math.max(getBeginInt(), Math.min(resultSize() - 1, endIndex))); } public String getEnd() { return "" + getEndInt(); } /** * @return ending index into results * @since 0.8.7 */ public String getResultEnd() { return Integer.toString(isPrefiltered() ? resultSize() - 1 : getEndInt()); } public void setEnd(String s) { try { endIndex = Integer.parseInt(s); } catch (NumberFormatException nfe) {} } /** * Does the entries map contain only the lookup result, * or must we index into it? * @since 0.8.7 */ protected boolean isPrefiltered() { return false; } /** * @return the size of the lookup result * @since 0.8.7 */ protected int resultSize() { return entries.length; } /** * @return the total size of the address book * @since 0.8.7 */ protected int totalSize() { return entries.length; } }