note preview
authorChristian Weiske <cweiske@cweiske.de>
Thu, 23 Oct 2014 21:07:01 +0000 (23:07 +0200)
committerChristian Weiske <cweiske@cweiske.de>
Thu, 23 Oct 2014 21:07:01 +0000 (23:07 +0200)
13 files changed:
appinfo/database.xml
appinfo/routes.php
controller/guicontroller.php
grauphel.css
lib/converter/html.php [new file with mode: 0644]
lib/notestorage.php
templates/gui-note.php [new file with mode: 0644]
templates/tag.php
tests/Lib/Converter/HtmlTest.php [new file with mode: 0644]
tests/data/formattest.html [new file with mode: 0644]
tests/data/formattest.tomboynotecontent [new file with mode: 0644]
tests/data/xss.html [new file with mode: 0644]
tests/data/xss.tomboynotecontent [new file with mode: 0644]

index 7419ee794ee14f3825f6acd6098c32cd6aaf41a8..4738d60c433b491cf6ee853124d3bab5833d6d60 100755 (executable)
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" ?>
 <database>
 <?xml version="1.0" encoding="UTF-8" ?>
 <database>
+ <!-- http://www.wiltonhotel.com/_ext/pear/docs/MDB2/docs/xml_schema_documentation.html -->
         <name>*dbname*</name>
         <create>true</create>
         <overwrite>false</overwrite>
         <name>*dbname*</name>
         <create>true</create>
         <overwrite>false</overwrite>
index 19a4b930092d9107cfab4e09eb255c9454916e29..ddfc90b861dfdd8494260cf48c20ddb3c362d6b2 100644 (file)
@@ -68,6 +68,11 @@ $application->registerRoutes(
                 'name' => 'gui#tag',
                 'verb' => 'GET',
             ),
                 'name' => 'gui#tag',
                 'verb' => 'GET',
             ),
+            array(
+                'url'  => '/note/{guid}',
+                'name' => 'gui#note',
+                'verb' => 'GET',
+            ),
             array(
                 'url'  => '/tokens',
                 'name' => 'gui#tokens',
             array(
                 'url'  => '/tokens',
                 'name' => 'gui#tokens',
index 6f0a15c821dba9139fc62beb025c9ae7e9c932fb..97a7f10c9289f2622b6e77eabc531ca882744583 100644 (file)
@@ -17,6 +17,7 @@ use \OCP\AppFramework\Controller;
 use \OCP\AppFramework\Http\TemplateResponse;
 use \OCA\Grauphel\Lib\Client;
 use \OCA\Grauphel\Lib\TokenStorage;
 use \OCP\AppFramework\Http\TemplateResponse;
 use \OCA\Grauphel\Lib\Client;
 use \OCA\Grauphel\Lib\TokenStorage;
+use \OCA\Grauphel\Lib\Response\ErrorResponse;
 
 /**
  * Owncloud frontend
 
 /**
  * Owncloud frontend
@@ -72,6 +73,47 @@ class GuiController extends Controller
         return $res;
     }
 
         return $res;
     }
 
+    /**
+     * Show contents of a note
+     *
+     * @NoAdminRequired
+     * @NoCSRFRequired
+     */
+    public function note($guid)
+    {
+        $res = new TemplateResponse('grauphel', 'gui-note');
+
+        $note = $this->getNotes()->load($guid, false);
+        if ($note === null) {
+            return new ErrorResponse('Note does not exist');
+        }
+
+        $converter = new \OCA\Grauphel\Lib\Converter\Html();
+        $converter->internalLinkHandler = array($this, 'noteLinkHandler');
+        $res->setParams(
+            array(
+                'note' => $note,
+                'note-content' => $converter->convert(
+                    $note->{'note-content'}
+                ),
+            )
+        );
+
+        $this->addNavigation($res);
+        return $res;
+    }
+
+    public function noteLinkHandler($noteTitle)
+    {
+        $guid = $this->getNotes()->loadGuidByTitle($noteTitle);
+        if ($guid === null) {
+            return '#';
+        }
+        return $this->urlGen->linkToRoute(
+            'grauphel.gui.note', array('guid' => $guid)
+        );
+    }
+
     /**
      * Show all notes of a tag
      *
     /**
      * Show all notes of a tag
      *
@@ -81,6 +123,12 @@ class GuiController extends Controller
     public function tag($rawtag)
     {
         $notes = $this->getNotes()->loadNotesOverview(null, $rawtag);
     public function tag($rawtag)
     {
         $notes = $this->getNotes()->loadNotesOverview(null, $rawtag);
+        usort(
+            $notes,
+            function($noteA, $noteB) {
+                return strcmp($noteA['title'], $noteB['title']);
+            }
+        );
 
         $res = new TemplateResponse('grauphel', 'tag');
         $res->setParams(
 
         $res = new TemplateResponse('grauphel', 'tag');
         $res->setParams(
index 6ba1cde0432139af86490d160a93e1118e87dd68..1ad5a2d4c0d4ad07cf30adb0942f6d47ad480bfa 100644 (file)
@@ -135,6 +135,9 @@ a.action.delete, table.table form button.action.delete {
 a.action {
     line-height: 30px;
 }
 a.action {
     line-height: 30px;
 }
+a.cellclick {
+    display: block;
+}
 table.table form {
     display: inline;
 }
 table.table form {
     display: inline;
 }
@@ -142,3 +145,30 @@ table.table form button.action {
     border: none;
     background-color: transparent;
 }
     border: none;
     background-color: transparent;
 }
+
+.muted {
+    color: #999;
+}
+
+.note-content {
+    padding-top: 2ex;
+}
+
+.note-content .strikethrough {
+    text-decoration: line-through;
+}
+.note-content .highlight {
+    background-color: yellow;
+}
+.note-content .small {
+    font-size: 80%;
+}
+.note-content .large {
+    font-size: 150%;
+}
+.note-content .huge {
+    font-size: 200%;
+}
+.note-content a {
+    text-decoration: underline;
+}
\ No newline at end of file
diff --git a/lib/converter/html.php b/lib/converter/html.php
new file mode 100644 (file)
index 0000000..1723a5b
--- /dev/null
@@ -0,0 +1,197 @@
+<?php
+/**
+ * Part of grauphel
+ *
+ * PHP version 5
+ *
+ * @category  Tools
+ * @package   Grauphel
+ * @author    Christian Weiske <cweiske@cweiske.de>
+ * @copyright 2014 Christian Weiske
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
+ * @link      http://cweiske.de/grauphel.htm
+ */
+namespace OCA\Grauphel\Lib\Converter;
+use \XMLReader;
+
+/**
+ * Convert Tomboy note XML to HTML
+ *
+ * Tomboy already ships with a converter:
+ * https://git.gnome.org/browse/tomboy/tree/Tomboy/Addins/ExportToHtml/ExportToHtml.xsl
+ * We cannot use it since we want nice callbacks, and we do not want to rely
+ * on the PHP XSL extension, and we have to fix the links.
+ *
+ * @category  Tools
+ * @package   Grauphel
+ * @author    Christian Weiske <cweiske@cweiske.de>
+ * @copyright 2014 Christian Weiske
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
+ * @version   Release: @package_version@
+ * @link      http://cweiske.de/grauphel.htm
+ */
+class Html
+{
+    protected static $tagMap = array(
+        'list'      => 'ul',
+        'list-item' => 'li',
+        'bold'      => 'b',
+        'italic'    => 'i',
+        'monospace' => 'tt',
+    );
+
+    protected static $styleClassMap = array(
+        'strikethrough' => 'strikethrough',
+        'highlight'  => 'highlight',
+        'size:small' => 'small',
+        'size:large' => 'large',
+        'size:huge'  => 'huge',
+    );
+
+    public $internalLinkHandler;
+
+
+
+    public function __construct()
+    {
+        $this->internalLinkHandler = array($this, 'internalLinkHandler');
+    }
+
+    /**
+     * Converts the tomboy note XML into HTML
+     *
+     * @param string $xmlContent Tomboy note content
+     *
+     * @return string HTML
+     */
+    public function convert($xmlContent)
+    {
+        if (strpos($xmlContent, '</link:internal><link:internal>') !== false) {
+            $xmlContent = $this->fixNastyLinks($xmlContent);
+        }
+
+        $html = '';
+        $reader = new XMLReader();
+        $reader->xml(
+            '<?xml version="1.0" encoding="utf-8"?>' . "\n"
+            . '<content xmlns:size="size" xmlns:link="link">'
+            . $xmlContent
+            . '</content>'
+        );
+
+        $withinLink = false;
+        $store = &$html;
+        while ($reader->read()) {
+            switch ($reader->nodeType) {
+            case XMLReader::ELEMENT:
+                //echo $reader->name . "\n";
+                if (isset(static::$tagMap[$reader->name])) {
+                    $store .= '<' . static::$tagMap[$reader->name] . '>';
+                } else if (isset(static::$styleClassMap[$reader->name])) {
+                    $store .= '<span class="'
+                        . static::$styleClassMap[$reader->name]
+                        . '">';
+                } else if (substr($reader->name, 0, 5) == 'link:') {
+                    $withinLink = true;
+                    $linkText    = '';
+                    $store       = &$linkText;
+                }
+                break;
+            case XMLReader::END_ELEMENT:
+                if (isset(static::$tagMap[$reader->name])) {
+                    $store .= '</' . static::$tagMap[$reader->name] . '>';
+                } else if (isset(static::$styleClassMap[$reader->name])) {
+                    $store .= '</span>';
+                } else if (substr($reader->name, 0, 5) == 'link:') {
+                    $withinLink = false;
+                    $store      = &$html;
+                    $linkUrl = htmlspecialchars_decode(strip_tags($linkText));
+                    if ($reader->name == 'link:internal') {
+                        $linkUrl = call_user_func($this->internalLinkHandler, $linkUrl);
+                    } else {
+                        $linkUrl = $this->fixLinkUrl($linkUrl);
+                    }
+                    $store .= '<a href="' . htmlspecialchars($linkUrl) . '">'
+                        . $linkText
+                        . '</a>';
+                }
+                break;
+            case XMLReader::TEXT:
+            case XMLReader::SIGNIFICANT_WHITESPACE:
+                $store .= nl2br(htmlspecialchars($reader->value));
+                break;
+            default:
+                throw new \Exception(
+                    'Unsupported XML node type: ' . $reader->nodeType
+                );
+            }
+        }
+
+        $html = str_replace("</ul><br />\n", "</ul>\n", $html);
+
+        return $html;
+    }
+
+    /**
+     * Fixes external URLs without a protocol
+     *
+     * @param string $linkUrl URL to fix
+     *
+     * @return string Fixed URL
+     */
+    protected function fixLinkUrl($linkUrl)
+    {
+        if ($linkUrl{0} == '/') {
+            //Unix file path
+            $linkUrl = 'file://' . $linkUrl;
+        }
+        return $linkUrl;
+    }
+
+    /**
+     * Dummy internal link handler that simply adds ".htm" to the note title
+     *
+     * @param string $linkUrl Title of page that is linked
+     *
+     * @return string URL to link to
+     */
+    public function internalLinkHandler($linkUrl)
+    {
+        return $linkUrl . '.htm';
+    }
+
+    /**
+     * Re-arranges the XML of formatted links to that clean link tags can
+     * be generated.
+     *
+     * Tomboy 1.15.2 allows link formatting, and the resulting XML is a
+     * mess of multiple(!) link tags that are within or around other formatting
+     * tags.
+     *
+     * This method tries to re-arrange the links so that only a single link tag
+     * appears with all the formatting inside.
+     * 
+     * @param string $xmlContent Tomboy note content
+     *
+     * @return string XML content, with re-arranged link tags.
+     */
+    protected function fixNastyLinks($xmlContent)
+    {
+        preg_match_all(
+            '#(?:<.*>)?<link:internal>.+</link:internal><link:internal>.+</link:internal>#U',
+            $xmlContent,
+            $matches
+        );
+
+        foreach ($matches[0] as $nastyLink) {
+            $cleaner = str_replace('</link:internal><link:internal>', '', $nastyLink);
+            $cleaner = preg_replace('#<([a-z]+)><(link:internal)>#U', '<\2><\1>', $cleaner);
+            $cleaner = preg_replace('#</(link:internal)></([a-z]+)>#U', '</\2></\1>', $cleaner);
+            $cleaner = str_replace('</link:internal><link:internal>', '', $cleaner);
+            $xmlContent = str_replace($nastyLink, $cleaner, $xmlContent);
+        }
+
+        return $xmlContent;
+    }
+}
+?>
index fb6803029f780b8a00b31c8f6b54685d40daf435..7ecf049af02a21fb3e351201c65e4ebcc91d5995 100644 (file)
@@ -245,6 +245,28 @@ class NoteStorage
         return $this->noteFromRow($row);
     }
 
         return $this->noteFromRow($row);
     }
 
+    /**
+     * Load a GUID of a note by the note title
+     *
+     * @param string  $title Note title
+     *
+     * @return string GUID, NULL if note could not be found
+     */
+    public function loadGuidByTitle($title)
+    {
+        $row = \OC_DB::executeAudited(
+            'SELECT note_guid FROM `*PREFIX*grauphel_notes`'
+            . ' WHERE `note_user` = ? AND `note_title` = ?',
+            array($this->username, $title)
+        )->fetchRow();
+
+        if ($row === false) {
+            return null;
+        }
+
+        return $row['note_guid'];
+    }
+
     /**
      * Save a note into storage.
      *
     /**
      * Save a note into storage.
      *
diff --git a/templates/gui-note.php b/templates/gui-note.php
new file mode 100644 (file)
index 0000000..a7e0587
--- /dev/null
@@ -0,0 +1,17 @@
+<link rel="stylesheet" href="<?php p(OCP\Util::linkTo('grauphel','grauphel.css')); ?>" type="text/css"/>
+
+<?php /** @var $l OC_L10N */ ?>
+<?php $_['appNavigation']->printPage(); ?>
+
+<script type="text/javascript" src="<?php p(OCP\Util::linkTo('grauphel','js/grauphel.js')); ?>"></script>
+
+<div id="app-content" class="content">
+ <h1><?php p($_['note']->title); ?></h1>
+ <p class="muted">
+  Last modified:
+  <?php p(\OCP\Util::formatDate(strtotime($_['note']->{'last-change-date'}))); ?>
+ </p>
+ <div class="note-content">
+  <?php echo $_['note-content']; ?>
+ </div>
+</div>
index 68fa8131ea893ceeaec686bf5b969d020655e920..8c7eecc2a4f8b0b98613ddc4b251b8b8b0361f43 100644 (file)
@@ -3,14 +3,28 @@
 <?php /** @var $l OC_L10N */ ?>
 <?php $_['appNavigation']->printPage(); ?>
 
 <?php /** @var $l OC_L10N */ ?>
 <?php $_['appNavigation']->printPage(); ?>
 
-<div id="app-content" class="content">
+<div id="app-content" class="list">
   <h1>Notebook: <?php p($_['tag']); ?></h1>
   <h1>Notebook: <?php p($_['tag']); ?></h1>
-  <p class="error">
-     Work in progress: You can't do anything here.
-  </p>
-  <ul>
+
+  <table class="table" id="grauphel-notes">
+   <thead>
+    <tr>
+     <th>Title</th>
+     <th>Last change</th>
+    </tr>
+   </thead>
+   <tbody>
+
     <?php foreach ($_['notes'] as $note) { ?>
     <?php foreach ($_['notes'] as $note) { ?>
-      <li data-id="<?php p($note['guid']); ?>"><a href="#"><?php p($note['title']); ?></a></li>
+     <tr id="note-<?php p($note['guid']); ?>">
+      <td>
+       <a class="cellclick" href="<?php p(OCP\Util::linkToRoute('grauphel.gui.note', array('guid' => $note['guid']))); ?>"><?php p($note['title']); ?></a>
+      </td>
+      <td>
+      </td>
+     </tr>
     <?php } ?>
     <?php } ?>
-  </ul>
+
+   </tbody>
+  </table>
 </div>
 </div>
diff --git a/tests/Lib/Converter/HtmlTest.php b/tests/Lib/Converter/HtmlTest.php
new file mode 100644 (file)
index 0000000..0a66ee4
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+require_once __DIR__ . '/../../../lib/converter/html.php';
+
+class Lib_Converter_HtmlTest extends PHPUnit_Framework_TestCase
+{
+    public function testConvert()
+    {
+        $input = file_get_contents(__DIR__ . '/../../data/formattest.tomboynotecontent');
+
+        $converter = new OCA\Grauphel\Lib\Converter\Html();
+        $output = $converter->convert($input);
+        $this->assertEquals(
+            file_get_contents(__DIR__ . '/../../data/formattest.html'),
+            $output
+        );
+    }
+
+    public function testXSS()
+    {
+        $input = file_get_contents(__DIR__ . '/../../data/xss.tomboynotecontent');
+
+        $converter = new OCA\Grauphel\Lib\Converter\Html();
+        $output = $converter->convert($input);
+        $this->assertEquals(
+            file_get_contents(__DIR__ . '/../../data/xss.html'),
+            $output
+        );
+    }
+}
+?>
\ No newline at end of file
diff --git a/tests/data/formattest.html b/tests/data/formattest.html
new file mode 100644 (file)
index 0000000..5bc3709
--- /dev/null
@@ -0,0 +1,36 @@
+Eine Zeile Text.<br />
+Zeilenumbruch.<br />
+Noch ein Zeilenumbruch.<br />
+<br />
+Eine Leerzeile obendrüber.<br />
+<br />
+Jetzt kommt eine Liste:<br />
+<ul><li>eins<br />
+</li><li>zwei<br />
+<ul><li>zwei-eins<br />
+<ul><li>zwei-eins-eins<br />
+</li><li>zwei-eins-zwei<br />
+</li></ul></li><li>zwei-zwei<br />
+</li></ul></li><li>drei<br />
+<ul><li>drei-eins<br />
+<ul><li>drei-eins-eins<br />
+<ul><li>drei-eins-eins-eins</li></ul></li></ul></li></ul></li></ul>
+Formatierungen:<br />
+<b>Fetter Text</b><br />
+<i>Kursiver Text</i><br />
+<span class="strikethrough">Durchgestrichener Text</span><br />
+<span class="highlight">Hervorgehobener Text</span><br />
+<tt>Feste Breite</tt><br />
+<br />
+Schriftgrößen:<br />
+<span class="small">klein</span><br />
+normal<br />
+<span class="large">groß</span><br />
+<span class="huge">riesig</span><br />
+<br />
+Links:<br />
+<ul><li><a href="Test.htm">Test</a><br />
+</li><li><a href="http://cweiske.de/?foo#bar">http://cweiske.de/?foo#bar</a><br />
+</li><li><a href="file:///home/cweiske/fam.jpg">/home/cweiske/fam.jpg</a></li></ul>
+<br />
+Ende.<br />
diff --git a/tests/data/formattest.tomboynotecontent b/tests/data/formattest.tomboynotecontent
new file mode 100644 (file)
index 0000000..8f0b017
--- /dev/null
@@ -0,0 +1,36 @@
+Eine Zeile Text.
+Zeilenumbruch.
+Noch ein Zeilenumbruch.
+
+Eine Leerzeile obendrüber.
+
+Jetzt kommt eine Liste:
+<list><list-item dir="ltr">eins
+</list-item><list-item dir="ltr">zwei
+<list><list-item dir="ltr">zwei-eins
+<list><list-item dir="ltr">zwei-eins-eins
+</list-item><list-item dir="ltr">zwei-eins-zwei
+</list-item></list></list-item><list-item dir="ltr">zwei-zwei
+</list-item></list></list-item><list-item dir="ltr">drei
+<list><list-item dir="ltr">drei-eins
+<list><list-item dir="ltr">drei-eins-eins
+<list><list-item dir="ltr">drei-eins-eins-eins</list-item></list></list-item></list></list-item></list></list-item></list>
+Formatierungen:
+<bold>Fetter Text</bold>
+<italic>Kursiver Text</italic>
+<strikethrough>Durchgestrichener Text</strikethrough>
+<highlight>Hervorgehobener Text</highlight>
+<monospace>Feste Breite</monospace>
+
+Schriftgrößen:
+<size:small>klein</size:small>
+normal
+<size:large>groß</size:large>
+<size:huge>riesig</size:huge>
+
+Links:
+<list><list-item dir="ltr"><link:internal>Test</link:internal>
+</list-item><list-item dir="ltr"><link:url>http://cweiske.de/?foo#bar</link:url>
+</list-item><list-item dir="ltr"><link:url>/home/cweiske/fam.jpg</link:url></list-item></list>
+
+Ende.
diff --git a/tests/data/xss.html b/tests/data/xss.html
new file mode 100644 (file)
index 0000000..1908ce2
--- /dev/null
@@ -0,0 +1,11 @@
+&lt;h1&gt;Hallo!&lt;/h1&gt;<br />
+Was ist denn &lt;a href=&quot;foo&quot;&gt;bar<br />
+<br />
+Link mit fett drin: <a href="Formattest.htm">Format<b>te</b>st</a>.<br />
+<ul><li>Liste<br />
+</li><li>ein <b>bisschen</b> fett<br />
+</li><li>mo<tt>nospac</tt>ed<br />
+</li><li>&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</li></ul>
+<br />
+Link zum <a href="Formattest.htm"><span class="highlight">Form</span>a<tt>tt</tt>e<span class="huge">st</span></a>.<br />
+Link mit <a href="Sonder&quot;&gt;zeichen.htm">Sonder&quot;&gt;zeichen</a>.<br />
diff --git a/tests/data/xss.tomboynotecontent b/tests/data/xss.tomboynotecontent
new file mode 100644 (file)
index 0000000..6a86314
--- /dev/null
@@ -0,0 +1,11 @@
+&lt;h1&gt;Hallo!&lt;/h1&gt;
+Was ist denn &lt;a href="foo"&gt;bar
+
+Link mit fett drin: <link:internal>Format<bold>te</bold></link:internal><link:internal>st</link:internal>.
+<list><list-item dir="ltr">Liste
+</list-item><list-item dir="ltr">ein <bold>bisschen</bold> fett
+</list-item><list-item dir="ltr">mo<monospace>nospac</monospace>ed
+</list-item><list-item dir="ltr">&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</list-item></list>
+
+Link zum <highlight><link:internal>Form</link:internal></highlight><link:internal>a<monospace>tt</monospace></link:internal><link:internal>e<size:huge>st</size:huge></link:internal>.
+Link mit <link:internal>Sonder"&gt;zeichen</link:internal>.