package thaw.plugins.index; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.OutputStream; import java.util.Observer; import java.util.Observable; import java.util.Vector; /* Swing */ import javax.swing.JPanel; import javax.swing.JLabel; import javax.swing.JComboBox; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JButton; import javax.swing.BorderFactory; import javax.swing.JOptionPane; import java.awt.BorderLayout; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; /* DOM */ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.DOMImplementation; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Text; /* SAX */ import org.xml.sax.*; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.SAXParser; /* SQL */ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /* Thaw */ import thaw.core.Config; import thaw.core.Logger; import thaw.core.MainWindow; /* used for warning popups */ import thaw.core.I18n; import thaw.fcp.FreenetURIHelper; import thaw.fcp.FCPTransferQuery; import thaw.fcp.FCPClientGet; import thaw.fcp.FCPClientPut; import thaw.fcp.FCPQueueManager; import thaw.plugins.Hsqldb; import thaw.plugins.signatures.Identity; /** * Will use, from the configuration: * 'userNickname' as the author name */ public class Comment extends Observable implements Observer, ActionListener { public final static int MAX_SIZE = 16384; private Identity author; private String comment; private Index index; private Hsqldb db; private int rev; private boolean newComment = false; private boolean valid = false; /* needed to check the signature */ private String sig; private Comment() { newComment = false; } /** * @param index parent index * @param rev revision of the comment (-1) if not inserted at the moment * @param comment comment inside the comment ... :) */ public Comment(Hsqldb db, Index index, int rev, Identity author, String comment) { this.db = db; this.author = author; this.comment = comment; this.index = index; this.rev = rev; newComment = false; } private CommentTab tab; private JComboBox trust; private JButton changeBlackListState; private boolean blackListed; public JPanel getPanel(CommentTab tab) { this.tab= tab; blackListed = isBlackListed(); boolean hasPrivateKey = (index.getPrivateKey() != null); boolean isPrivateKeyPublished = index.publishPrivateKey(); /** * we don't display if it is blacklisted and we don't have the private key */ if (blackListed && !hasPrivateKey) return null; JPanel panel = new JPanel(new BorderLayout(10, 10)); JTextArea text; if (!blackListed) text = new JTextArea(comment.trim()); else text = new JTextArea(I18n.getMessage("thaw.plugin.index.comment.moderated")); text.setLineWrap(true); text.setWrapStyleWord(true); panel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "--- "+author.toString()+" ---", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, new java.awt.Font("Dialog", java.awt.Font.BOLD, 14) )); JLabel sigLabel = new JLabel(I18n.getMessage("thaw.plugin.signature.trustLevel.trustLevel")+ " : "); JTextField sigLevel = new JTextField(author.getTrustLevelStr() + (author.isDup() ? " - " + I18n.getMessage("thaw.plugin.signature.duplicata") : "")); sigLevel.setForeground(author.getTrustLevelColor()); sigLevel.setEditable(false); sigLevel.setBackground(panel.getBackground()); JPanel sigPanel = new JPanel(new BorderLayout()); sigPanel.add(sigLabel, BorderLayout.WEST); sigPanel.add(sigLevel, BorderLayout.CENTER); Vector trustLevels = new Vector(); for (int i = 0 ; i < Identity.trustLevelInt.length ; i++) { if (Identity.trustLevelInt[i] < 100) trustLevels.add(Identity.trustLevelStr[i]); } trust = new JComboBox(trustLevels); trust.setSelectedItem(author.getTrustLevelStr()); trust.addActionListener(this); JPanel trustPanel = new JPanel(new BorderLayout(5, 5)); if (author.getPrivateKey() == null) { trustPanel.add(trust, BorderLayout.CENTER); } JPanel bottomRightPanel = new JPanel(new BorderLayout(5, 5)); bottomRightPanel.add(trustPanel, BorderLayout.CENTER); if ( (hasPrivateKey && (author.getPrivateKey() == null || blackListed) ) && (!isPrivateKeyPublished) ) { changeBlackListState = new JButton(blackListed ? I18n.getMessage("thaw.plugin.index.comment.unmoderate") : I18n.getMessage("thaw.plugin.index.comment.moderate")); changeBlackListState.addActionListener(this); bottomRightPanel.add(changeBlackListState, BorderLayout.EAST); } JPanel topPanel = new JPanel(new BorderLayout()); topPanel.add(sigPanel, BorderLayout.WEST); topPanel.add(new JLabel(""), BorderLayout.CENTER); topPanel.add(bottomRightPanel, BorderLayout.EAST); text.setEditable(false); text.setBackground(panel.getBackground()); panel.add(text, BorderLayout.CENTER); panel.add(topPanel, BorderLayout.NORTH); return panel; } public boolean mustBeIgnored(Config config) { if (author == null) /* fix nextgens weird case */ return true; return author.mustBeIgnored(config); } public boolean isBlackListed() { return isBlackListed(db, index.getId(), rev); } public void unBlackList() { try { synchronized(db.dbLock) { PreparedStatement st = db.getConnection().prepareStatement("DELETE FROM indexCommentBlackList WHERE rev = ? AND indexId = ?"); st.setInt(1, rev); st.setInt(2, index.getId()); st.execute(); st.close(); } } catch(SQLException e) { Logger.error(this, "Unable to un-blacklist comment because: "+e.toString()); } } public void blackList() { try { synchronized(db.dbLock) { PreparedStatement st = db.getConnection().prepareStatement("INSERT INTO indexCommentBlackList (rev, indexId) VALUES (?, ?)"); st.setInt(1, rev); st.setInt(2, index.getId()); st.execute(); st.close(); } } catch(SQLException e) { Logger.error(this, "Unable to blacklist comment because: "+e.toString()); } } /** * Only index owner(s) must be able to see black listed comments */ public static boolean isBlackListed(Hsqldb db, int indexId, int rev) { try { synchronized(db.dbLock) { PreparedStatement st; st = db.getConnection().prepareStatement("SELECT id FROM indexCommentBlackList WHERE indexId = ? AND rev = ? LIMIT 1"); st.setInt(1, indexId); st.setInt(2, rev); ResultSet set = st.executeQuery(); boolean b= set.next(); st.close(); return b; } } catch(SQLException e) { Logger.error(db, "thaw.plugins.index.Comment : Error while checking if the message is in the blacklist :"+e.toString()); } return false; } public void actionPerformed(ActionEvent e) { if (e.getSource() == trust) { if (author == null) return; author.setTrustLevel((String)trust.getSelectedItem()); tab.updateCommentList(); return; } if (e.getSource() == changeBlackListState) { if (blackListed) { unBlackList(); } else { blackList(); } tab.updateCommentList(); return; } } /** * Will write it in a temporary file */ public java.io.File writeCommentToFile() { java.io.File outputFile; try { outputFile = java.io.File.createTempFile("thaw-", "-comment.xml"); outputFile.deleteOnExit(); } catch(java.io.IOException e) { Logger.error(new Comment(), "Unable to write comment in a temporary file because: "+e.toString()); return null; } OutputStream out; try { out = new FileOutputStream(outputFile); } catch(java.io.FileNotFoundException e) { Logger.error(new Comment(), "File not found exception ?!"); return null; } StreamResult streamResult; streamResult = new StreamResult(out); Document xmlDoc; final DocumentBuilderFactory xmlFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder xmlBuilder; try { xmlBuilder = xmlFactory.newDocumentBuilder(); } catch(final javax.xml.parsers.ParserConfigurationException e) { Logger.error(new Comment(), "Unable to generate the comment xml file because : "+e.toString()); return null; } final DOMImplementation impl = xmlBuilder.getDOMImplementation(); xmlDoc = impl.createDocument(null, "comment", null); final Element rootEl = xmlDoc.getDocumentElement(); /** START FILLING THE XML TREE HERE **/ Element authorTag = xmlDoc.createElement("author"); Element textTag = xmlDoc.createElement("text"); Text authorTxt = xmlDoc.createTextNode(author.getNick()); Text textTxt = xmlDoc.createTextNode(comment); authorTag.appendChild(authorTxt); textTag.appendChild(textTxt); Element signatureTag = xmlDoc.createElement("signature"); Element sigTag = xmlDoc.createElement("sig"); String sig = author.sign(index.getCommentPublicKey()+"-"+ author.getNick()+"-"+ comment); Text sigTxt = xmlDoc.createTextNode(sig); sigTag.appendChild(sigTxt); Element publicKeyTag = xmlDoc.createElement("publicKey"); Text publicKeyTxt = xmlDoc.createTextNode(author.getPublicKey()); publicKeyTag.appendChild(publicKeyTxt); signatureTag.appendChild(sigTag); signatureTag.appendChild(publicKeyTag); rootEl.appendChild(authorTag); rootEl.appendChild(textTag); rootEl.appendChild(signatureTag); /** GENERATE THE FILE **/ /* Serialization */ final DOMSource domSource = new DOMSource(xmlDoc); final TransformerFactory transformFactory = TransformerFactory.newInstance(); Transformer serializer; try { serializer = transformFactory.newTransformer(); } catch(final javax.xml.transform.TransformerConfigurationException e) { Logger.error(new Comment(), "Unable to write comment in an XML file because: "+e.toString()); return null; } serializer.setOutputProperty(OutputKeys.ENCODING,"UTF-8"); serializer.setOutputProperty(OutputKeys.INDENT,"yes"); /* final step */ try { serializer.transform(domSource, streamResult); } catch(final javax.xml.transform.TransformerException e) { Logger.error(new Comment(), "Unable to save comment in an XML file (2) because: "+e.toString()); return null; } return outputFile; } private FCPQueueManager queueManager; private MainWindow mainWindow; /** * @param privateKey must be an SSK without anything useless */ public boolean insertComment(FCPQueueManager queueManager, MainWindow mainWindow) { String privateKey = index.getCommentPrivateKey(); this.queueManager = queueManager; this.mainWindow = mainWindow; java.io.File xmlFile = writeCommentToFile(); if (xmlFile == null) return false; FCPClientPut put = new FCPClientPut(xmlFile, FCPClientPut.KEY_TYPE_SSK, 0, /* rev : as we insert as USK => EDONTCARE */ "comment", /* filename (not really used anymore) */ FreenetURIHelper.convertSSKtoUSK(privateKey)+"/", /* the convertion fonction forget the '/' */ 2, /* priority */ false, /* global */ FCPClientPut.PERSISTENCE_FOREVER, /* persistence */ true, /* doCompress */ -1); /* compression codec */ put.addObserver(this); return queueManager.addQueryToTheRunningQueue(put); } protected class CommentHandler extends DefaultHandler { public CommentHandler() { } /** * @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator) */ public void setDocumentLocator(Locator value) { } /** * Called when parsing is started * @see org.xml.sax.ContentHandler#startDocument() */ public void startDocument() throws SAXException { } /** * Called when starting to parse in a specific name space * @param prefix name space prefix * @param URI name space URI * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String) */ public void startPrefixMapping(String prefix, String URI) throws SAXException { /* \_o< */ } /** * @param prefix name space prefix * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String) */ public void endPrefixMapping(String prefix) throws SAXException { /* \_o< */ } private boolean authorTag; private boolean textTag; private boolean publicKeyTag; private boolean sigTag; /* needed to create / get the corresponding identity */ private String authorTxt; private String publicKey; /** * Called when the parsed find an opening tag * @param localName local tag name * @param rawName rawName (the one used here) * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) */ public void startElement(String nameSpaceURI, String localName, String rawName, Attributes attrs) throws SAXException { if (rawName == null) { rawName = localName; } if (rawName == null) return; if ("author".equals(rawName)) authorTag = true; if ("text".equals(rawName)) textTag = true; if ("publicKey".equals(rawName)) publicKeyTag = true; if ("sig".equals(rawName)) sigTag = true; } /** * Called when a closing tag is met * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String) */ public void endElement(String nameSpaceURI, String localName, String rawName) throws SAXException { if (rawName == null) { rawName = localName; } if (rawName == null) return; if ("author".equals(rawName)) authorTag = false; if ("text".equals(rawName)) textTag = false; if ("publicKey".equals(rawName)) publicKeyTag = false; if ("sig".equals(rawName)) sigTag = false; } /** * Called when a text between two tag is met * @param ch text * @param start position * @param end position * @see org.xml.sax.ContentHandler#characters(char[], int, int) */ public void characters(char[] ch, int start, int end) throws SAXException { String txt = new String(ch, start, end); if (authorTag) authorTxt = txt; if (textTag) comment = txt; if (publicKeyTag) publicKey = txt; if (sigTag) sig = txt; } public void ignorableWhitespace(char[] ch, int start, int end) throws SAXException { } public void processingInstruction(String target, String data) throws SAXException { } /** * @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String) */ public void skippedEntity(String arg0) throws SAXException { } /** * Called when parsing is finished * @see org.xml.sax.ContentHandler#endDocument() */ public void endDocument() throws SAXException { valid = false; try { if (comment != null && authorTxt != null && publicKey != null && sig != null && index != null) { author = Identity.getIdentity(db, authorTxt, publicKey); if (author == null) { Logger.warning(this, "Can't find the identity in the DB ?! WTF ?!"); valid = false; return; } valid = author.check(index.getCommentPublicKey()+"-"+ author.getNick()+"-"+ comment, sig); if (!valid) { Logger.notice(this, "Signature validation failed !"); } } else { Logger.notice(this, "Signature validation failed ! (missing elements)"); valid = false; } } catch(Exception e) { /* we must not failed ! */ Logger.error(this, "Error while checking signature: "+e.toString()); e.printStackTrace(); valid = false; } } } /** * @return false if already in the bdd or if there is any error */ public boolean parseComment(java.io.File xmlFile) { newComment = false; Logger.info(this, "Parsing comment : "+index.getCommentPublicKey() + " : "+Integer.toString(rev)); FileInputStream in; try { in = new FileInputStream(xmlFile); } catch(final java.io.FileNotFoundException e) { Logger.error(this, "Unable to load XML: FileNotFoundException ('"+xmlFile.getPath()+"') ! : "+e.toString()); return false; } CommentHandler handler = new CommentHandler(); try { SAXParserFactory factory = SAXParserFactory.newInstance(); // Parse the input SAXParser saxParser = factory.newSAXParser(); saxParser.parse(in, handler); } catch(javax.xml.parsers.ParserConfigurationException e) { Logger.error(this, "Error (1) while parsing index: "+e.toString()); } catch(org.xml.sax.SAXException e) { Logger.error(this, "Error (2) while parsing index: "+e.toString()); } catch(java.io.IOException e) { Logger.error(this, "Error (3) while parsing index: "+e.toString()); } if (comment != null && author != null && valid) { Logger.info(this, "Parsing done"); try { synchronized(db.dbLock) { if (existsInTheBdd()) { Logger.debug(this, "Comment already in db"); newComment = false; valid = false; return false; } Logger.info(this, "New comment !"); newComment = true; PreparedStatement st; st = db.getConnection().prepareStatement("INSERT INTO indexComments "+ "(authorId, text, rev, indexId, sig) "+ "VALUES (?, ?, ?, ?, ?)"); st.setInt(1, author.getId()); st.setString(2, comment); st.setInt(3, rev); st.setInt(4, index.getId()); st.setString(5, sig); st.execute(); st.close(); return true; } } catch(SQLException e) { Logger.error(this, "Unable to add comment in the db because: "+e.toString()); } } else Logger.notice(this, "Parsing failed !"); return false; } /** * On freenet */ public boolean exists() { return (comment != null && author != null); } public boolean isValid() { return valid; } /** * r and s must be set * You have to do the synchronized(db.dbLock) ! */ private boolean existsInTheBdd() { if (sig == null) { Logger.notice(this, "No sig, can't say if it's already in the bdd"); return true; } try { PreparedStatement st; st = db.getConnection().prepareStatement("SELECT id FROM indexComments "+ "WHERE sig = ?"); st.setString(1, sig); ResultSet set = st.executeQuery(); boolean b = (set.next()); st.close(); return b; } catch(SQLException e) { Logger.error(this, "Unable to check if the comment is already in the bdd, because: "+e.toString()); } return true; } public boolean isNew() { return newComment; } public boolean fetchComment(FCPQueueManager queueManager) { newComment = false; this.queueManager = queueManager; String publicKey = index.getCommentPublicKey(); /* should be an SSK */ if (publicKey == null) return false; publicKey += "comment-"+Integer.toString(rev)+"/comment.xml"; FCPClientGet get = new FCPClientGet(publicKey, 2 /* priority */, FCPClientGet.PERSISTENCE_UNTIL_DISCONNECT /* persistence */, false /* global queue */, 0 /* maxretries */, System.getProperty("java.io.tmpdir"), MAX_SIZE, true /* no DDA */); get.addObserver(this); return get.start(queueManager); } public void update(Observable o, Object param) { if (o instanceof FCPTransferQuery) { FCPTransferQuery query = (FCPTransferQuery)o; if (query.isFinished()) { query.deleteObserver(this); query.stop(queueManager); queueManager.remove(query); } if (o instanceof FCPClientPut) { FCPClientPut put = (FCPClientPut)o; if (put.isFinished() && put.isSuccessful()) { if (put.stop(queueManager)) queueManager.remove(put); /* because the PersistentPut message sent by the node problably made it * added to the queueManager by the QueueLoader*/ } else if (put.isFinished() && !put.isSuccessful()) { int ret = JOptionPane.showOptionDialog(mainWindow.getMainFrame(), I18n.getMessage("thaw.plugin.index.comment.failed"), I18n.getMessage("thaw.error.title"), JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, null, null); if (ret == JOptionPane.YES_OPTION) { /* we stop */ if (put.stop(queueManager)) queueManager.remove(put); /* and we restart */ insertComment(queueManager, mainWindow); } } } if (o instanceof FCPClientGet) { FCPClientGet get = (FCPClientGet)o; if (get.isFinished() && get.isSuccessful()) { parseComment(new java.io.File(get.getPath())); } } FCPTransferQuery q = ((FCPTransferQuery)o); if (q.isFinished() && q.isSuccessful()) { java.io.File file = new java.io.File(q.getPath()); file.delete(); } if (q.isFinished()) { setChanged(); notifyObservers(); } } } }