Manitou-Mail logo title

Source file: src/body_view.cpp

/* Copyright (C) 2004-2012 Daniel Verite

   This file is part of Manitou-Mail (see http://www.manitou-mail.org)

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License version 2 as
   published by the Free Software Foundation.

   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.
*/

#include "main.h"
#include "body_view.h"
#include "mailheader.h"
#include "attachment.h"
#include "db.h"
#include "xface/xface.h"

#include <QDebug>
#include <QKeyEvent>
#include <QPainter>
#include <QTextEdit>
#include <QWebPage>
#include <QWebFrame>
#include <QTimer>

body_view::body_view(QWidget* parent) : QWebView(parent)
{
  m_pmsg=NULL;
  m_loaded=false;

  page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
  m_netman = new network_manager(this);
  page()->setNetworkAccessManager(m_netman);
  connect(m_netman, SIGNAL(external_contents_requested()), this,
	  SLOT(ask_perm_for_contents()));

  QWebSettings* settings=page()->settings();
  settings->setAttribute(QWebSettings::PrivateBrowsingEnabled, true);
  settings->setAttribute(QWebSettings::JavascriptEnabled, false);
  settings->setAttribute(QWebSettings::JavaEnabled, false);
  settings->setAttribute(QWebSettings::PluginsEnabled, false);
  settings->setAttribute(QWebSettings::LinksIncludedInFocusChain, false);
  
  set_body_style();
}

body_view::~body_view()
{
  delete m_netman;
}


void
body_view::set_mail_item(mail_msg* p)
{
  m_pmsg=p;
  m_netman->m_pmsg=p;
}

QSize
body_view::sizeHint() const
{
  return QSize(400,200);
}

void
body_view::display(const QString& html_body)
{
  m_netman->m_ext_download_permission_asked=false;
  m_netman->m_ext_download_permitted=false;
  m_html_text = html_body;
  m_loaded=false;
  setHtml(html_body, QUrl("/"));
}

void
body_view::force_style_sheet()
{
#if QT_VERSION<0x040600
  /* we use a different dummy URL each time to avoid webkit cache
     (and we don't use QWebSettings::setObjectCacheCapacities since it appears
     to have adverse side effects) */
  static int counter;
  
  page()->settings()->setUserStyleSheetUrl(QString("style://manitou-%1").arg(++counter));
#endif
}

void
body_view::set_body_style()
{
  static const char* default_style =
    "span.searchword-manitou { background-color:yellow;}\n";
  QString body_style=QString("body { margin: 5px; padding: 0px; background-color: #ffffff; %1}\n").arg(m_font_css);
  QString header_style=QString("div#manitou-header { text-align:left; color:#000000; background-color: #ffffff; %1}\n").arg(m_font_css);
#if QT_VERSION<0x040600
  m_netman->m_body_style=default_style + body_style + header_style;
  force_style_sheet();
#else
  QString style = default_style + body_style + header_style;
  QByteArray b = style.toAscii().toBase64();
  page()->settings()->setUserStyleSheetUrl(QUrl(QString("data:text/css;charset=utf-8;base64,")+QString(b)));  
#endif
}

void
body_view::set_font(const QFont& font)
{
  QString s;
  if (!font.family().isEmpty()) {
    s.append("font-family:\"" + font.family() + "\";");
  }
  if (font.pointSize()>1) {
    s.append(QString("font-size:%1 pt;").arg(font.pointSize()));
  }
  else if (font.pixelSize()>1) {
    s.append(QString("font-size:%1 px;").arg(font.pixelSize()));
  }
  if (font.bold()) {
    s.append("font-weight: bold;");
  }
  if (font.style()==QFont::StyleItalic) {
    s.append("font-style: italic;");
  }
  else if (font.style()==QFont::StyleOblique) {
    s.append("font-style: oblique;");
  }
  m_font_css=s;
  set_body_style();
}

void
body_view::redisplay()
{
  m_loaded=false;
  setHtml(m_html_text, QUrl("/"));
}

void
body_view::copy()
{
  triggerPageAction(QWebPage::Copy);
}

void
body_view::set_wrap(bool on)
{
  Q_UNUSED(on);
  // for text parts, we'll probably need to implement wrapmode by HTML-styling the contents
#ifndef TODO_WEBKIT
  if (on) {
    setWordWrapMode(QTextOption::WordWrap);
    setLineWrapMode(QTextEdit::WidgetWidth);
  }
  else {
    setLineWrapMode(QTextEdit::NoWrap);
    setWordWrapMode(QTextOption::NoWrap);
  }
#endif
}


//static
void
body_view::rich_to_plain(QString& s)
{
  QTextEdit tmp; 
  /* hack: remove <br /> or the conversion to plaintext will insert
     question marks at line break positions. <p> on the other hand
     is correctly replaced by '\n' */
  s.replace("<br />", "<p>");
  tmp.setText(s);
  s = tmp.toPlainText();
}

void
body_view::contextMenuEvent(QContextMenuEvent* e)
{
  Q_UNUSED(e);
  emit popup_body_context_menu();
}

void
body_view::clear()
{
  setHtml("<html><body></body></html>");
}

void
body_view::clear_selection()
{
  page()->settings()->setAttribute(QWebSettings::JavascriptEnabled, true);
  page()->mainFrame()->evaluateJavaScript("document.execCommand('unselect');");
  page()->settings()->setAttribute(QWebSettings::JavascriptEnabled, false);
}

void
body_view::prepend_body_fragment(const QString& fragment)
{
  page()->settings()->setAttribute(QWebSettings::JavascriptEnabled, true);
  QString s = fragment;
  s.replace("'", "\\'");
  QString js = QString("try {var b=document.getElementsByTagName('body')[0]; var p=document.createElement('div'); p.innerHTML='%1'; p.id='manitou-header'; b.insertBefore(p, b.firstChild); body.style.cssText=''; 1;} catch(e) { e; }").arg(s);
  QVariant v = page()->mainFrame()->evaluateJavaScript(js);
  page()->settings()->setAttribute(QWebSettings::JavascriptEnabled, false);
}

void
body_view::highlight_terms(const std::list<searched_text>& lst)
{
  std::list<searched_text>::const_iterator it = lst.begin();
  if (it==lst.end())
    return;

  // JS code based on http://www.kryogenix.org/code/browser/searchhi/searchhi.js
  const char* jscript="searchhi = {"
"  highlightWord: function(node,word) {"
"    if (node.hasChildNodes) {"
"	    for (var hi_cn=0;hi_cn<node.childNodes.length;hi_cn++) {"
"		    searchhi.highlightWord(node.childNodes[hi_cn],word);"
"	    }"
"    }"
"    if (node.nodeType == 3) {"
"	    tempNodeVal = node.nodeValue.toLowerCase();"
"	    tempWordVal = word.toLowerCase();"
"	    if (tempNodeVal.indexOf(tempWordVal) != -1) {"
"		var pn = node.parentNode;"
"	        if (pn.className != 'searchword-manitou') {"
"		    var nv = node.nodeValue;"
"		    var ni = tempNodeVal.indexOf(tempWordVal);"
"		    var before = document.createTextNode(nv.substr(0,ni));"
"		    var docWordVal = nv.substr(ni,word.length);"
"		    var after = document.createTextNode(nv.substr(ni+word.length));"
"		    var hiwordtext = document.createTextNode(docWordVal);"
"		    var hiword = document.createElement('span');"
"		    hiword.className = 'searchword-manitou';"
"		    hiword.appendChild(hiwordtext);"
"		    pn.insertBefore(before,node);"
"		    pn.insertBefore(hiword,node);"
"		    pn.insertBefore(after,node);"
"		    pn.removeChild(node);"
"	        }"
"	    }"
"      }"
" return 1;"
"  }"
"};"
    ;

  page()->settings()->setAttribute(QWebSettings::JavascriptEnabled, true);
  QVariant v=page()->mainFrame()->evaluateJavaScript(jscript);

  // TODO: be case-sensitive if it->m_is_cs is set
  while (it != lst.end()) {
    QString s = it->m_text;
    s.replace("'", "\\'");
    QString jsearch = QString("searchhi.highlightWord(document.getElementsByTagName('body')[0],'%1');").arg(s);
    v=page()->mainFrame()->evaluateJavaScript(jsearch);
    ++it;
  }
  page()->settings()->setAttribute(QWebSettings::JavascriptEnabled, false);
}

void
body_view::authorize_external_contents(bool b)
{
  m_netman->m_ext_download_permitted=b;
}

void
body_view::ask_perm_for_contents()
{
  // transmit signal upwards
  emit external_contents_requested();
}


internal_img_network_reply::internal_img_network_reply(const QNetworkRequest& req, const QString& encoded_img, int type, QObject* parent) : QNetworkReply(parent)
{
  /* internal_img_network_reply is modeled after:
     http://qt.gitorious.org/qt-labs/graphics-dojo/blobs/master/url-rendering/main.cpp
  */
  position=0;
  setRequest(req);
  if (type==1) { // Face
    m_buffer = QByteArray::fromBase64(encoded_img.toAscii().constData());
  }
  else { // X-Face
    QImage qi;
    QString s;
    xface_to_xpm(encoded_img.toAscii().constData(), s);
    if (qi.loadFromData((const uchar*)s.toAscii().constData(), s.length(), "XPM")) {
      QBuffer b(&m_buffer);
      qi.save(&b, "PNG");
    }
  }
  setOperation(QNetworkAccessManager::GetOperation);
  setHeader(QNetworkRequest::ContentTypeHeader, "image/png");
  setHeader(QNetworkRequest::ContentLengthHeader, m_buffer.size());
  open(ReadOnly);
  setUrl(req.url());
  QTimer::singleShot(0, this, SLOT(go()));
}

qint64
internal_img_network_reply::readData(char* data, qint64 size)
{
  qint64 r=qMin(size, (qint64)(m_buffer.size()-position));
  memcpy(data, m_buffer.constData()+position, r);
  position += r;
  return r;
}

qint64
internal_img_network_reply::bytesAvailable() const
{
  return m_buffer.size() - position + QNetworkReply::bytesAvailable();
}

bool
internal_img_network_reply::seek(qint64 pos)
{
  if (pos<0 || pos>=m_buffer.size())
    return false;
  position=pos;
  return true;
}

void
internal_img_network_reply::go()
{
  position=0;
  emit readyRead();
  emit finished();
}


internal_style_network_reply::internal_style_network_reply(const QNetworkRequest& req, const QString& style, QObject* parent) : QNetworkReply(parent)
{
  setRequest(req);
  setOperation(QNetworkAccessManager::GetOperation);
  m_buf.setData(style.toLocal8Bit());
  open(QIODevice::ReadOnly);
  m_buf.open(QIODevice::ReadOnly);
  QTimer::singleShot(0, this, SLOT(go()));
}

qint64
internal_style_network_reply::readData(char* data, qint64 size)
{
  qint64 r=m_buf.read(data, size);
  if (r>0) {
    data[r]=0;
  }
  if (r<=0)
    emit finished();
  else
    emit readyRead();
  return r;
}

qint64
internal_style_network_reply::bytesAvailable() const
{
  return m_buf.size()+QNetworkReply::bytesAvailable();
}

void
internal_style_network_reply::abort()
{
}

void
internal_style_network_reply::go()
{
  
  setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); 
  setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, "OK");
  emit metaDataChanged(); 
  emit readyRead();
}


QNetworkReply*
network_manager::empty_network_reply(Operation op, const QNetworkRequest& req)
{
  QNetworkRequest req2 = req;
  req2.setUrl(QUrl());	// empty URL to avoid accessing external contents
  return QNetworkAccessManager::createRequest(op, req2, NULL);
}

/* outgoingData is always 0 for Get and Head requests */
QNetworkReply*
network_manager::createRequest(Operation op, const QNetworkRequest& req, QIODevice* outgoingData)
{
  DBG_PRINTF(5, "createRequest for %s", req.url().toString().toLocal8Bit().constData());
  if (op!=GetOperation) {
    // only GET is currently supported, see if HEAD should be
    return empty_network_reply(op, req);
  }
  const QUrl& url = req.url();
  // the request refers to attached contents
  if (req.url().scheme() == "cid") {
    // refers to a MIME part that should be attached
    if (m_pmsg) {
      attachment* a = m_pmsg->attachments().get_by_content_id(req.url().path());
      if (a!=NULL) {
	attachment_network_reply* reply = a->network_reply(req, this);
	connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
		this, SLOT(download_error(QNetworkReply::NetworkError)));
	connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
		this, SLOT(download_progress(qint64,qint64)));
	connect(reply, SIGNAL(finished()), this, SLOT(download_finished()));
	return reply;
      }
      
    }
    return empty_network_reply(op, req);
  }
  else if (url.scheme()=="manitou" && (url.authority()=="xface" || url.authority()=="face")) {
    if (url.hasQueryItem("id") && url.hasQueryItem("o")) {
      QString headers = m_pmsg->get_headers();
      bool id_ok, o_ok;
      uint id = url.queryItemValue("id").toUInt(&id_ok);
      int offset = url.queryItemValue("o").toInt(&o_ok);
      if (id_ok && o_ok && id == m_pmsg->get_id()) {
	int lf_pos = headers.indexOf('\n', offset);
	QString ascii_line;
	if (lf_pos>0) {
	  ascii_line = headers.mid(offset, lf_pos-offset);
	}
	else {
	  ascii_line = headers.mid(offset);
	}
	ascii_line.replace(" ", "");
	int type = url.authority()=="face" ? 1:2;
	return new internal_img_network_reply(req, ascii_line, type, this);
      }
    }
    return empty_network_reply(op, req);
  }
  else if (req.url().scheme()=="style") { // internal scheme for styling contents
    return new internal_style_network_reply(req, m_body_style, this);
  }
  // the request refers to external contents
  if (m_ext_download_permitted) {
    //        qDebug() << "op accepted for " << req.url().toString();
    return QNetworkAccessManager::createRequest(op, req, outgoingData);
  }
  else {
    if (!m_ext_download_permission_asked) {
      // let know that contents were skipped so that the user can be
      // presented with the choice to fetch them or not
      emit external_contents_requested();
      m_ext_download_permission_asked=true;
    }
    return empty_network_reply(op, req);
  }
}

void
network_manager::download_finished()
{
  DBG_PRINTF(5, "download_finished\n");
}

void
network_manager::download_error(QNetworkReply::NetworkError err)
{
  DBG_PRINTF(2, "download_error: %d\n", (int)err);
}
void
network_manager::download_progress(qint64 received, qint64 total)
{
  DBG_PRINTF(5, "download_progress(%ld / %ld)\n", received, total);
}

HTML source code generated by GNU Source-Highlight plus some custom post-processing

List of all available source files