Revision [14430]
This is an old revision of BookmarkManager made by BrianKoontz on 2006-06-01 14:48:38.
Bookmark Manager
An integrated bookmark manager in the spirit of de.lirio.us.Design ideas
- Allow both user-specific and site-wide bookmark access (other than those tagged as "private")
- Allow tagging of objects other than URIs (such as pages)
- Implement as both a handler and an action
Progress reports
- Beta test announcement: Well, after hacking around with Scuttle, I decided it was much too complex for what I had in mind. Nils suggested another project, Freetag, that seems to provide the basic functionality needed without all of the formatting/presentation overhead. I have a beta up and running here, and would appreciate your comments and suggestions! Keep in mind this is still quite rough around the edges, but I was aiming more towards proof-of-concept. I think something like this would be a cool addition (as a 100% modular plugin) to Wikka...
- Progress is going well. Add/edit/delete capabilities have been added. Two Perl scripts (see below) have been provided that allow you to export your bookmarks from de.lirio.us and import them into BookmarkManager. Beta currently running under Wikka 1.1.6.2-alpha. A page of 25 links (out of 200+) and about 100 tags renders in approximately 1.1 seconds.
TODO
Indicates issues that have been addressed- This code is most likely not yet safe for a production environment! I've worked to secure all data passed in from GET/POST requests, as well as from Wikka itself in some cases. I'm hoping a few more folks have time to look over the code for any obvious security weaknesses.
- User tags should be displayed as raw tags.
- As a general consideration it would be nice to provide this functionality as a handler.
- it would be nice to have a link for the add-form near list yours.
- how about something like my tags / all tags? List all/list yours now provides this functionality
- what would be usefull: If I tag a link with the name of an existing wiki-page, this link could show up at the end of the page.
- Need to fix logic so "your" tags only display "your" links (also, identify tag cloud as "all" or "yours")
- Optional logic to display tags in a right-justified vertical list instead of a cloud
- Paging, user prefs
- Tag pages as well as links
- Please put the wikka_freetag class in a file like Wikka.freetag.class.php in the new \libs directory.
- $freetag_options -> why not use the params provided by wikka ($this->mysql_host, etc.)?
- Create bookmarklets such as these (borrowed from de.lirio.us):
post bookmarklet: javascript:location.href='http://de.lirio.us/rubric/post?uri='+escape(location.href)+'&title='+encodeURIComponent(document.title)+'&when_done=go_back' post bookmarklet (with popup): javascript:void(open('http://de.lirio.us/rubric/post?uri='+escape(location.href)+'&title='+encodeURIComponent(document.title)+'&when_done=close','Rubric','toolbar=no,width=700,height=325,scrollbars'));
- Provide edit/delete functionality
- Search functionality
- Restricting spam. Some ideas: Option to allow only registered users to set bookmarks; supplying a random number for each add request to preclude calling the add action from a script
Installation
- Install a copy of Freetag in 3rdparty/plugins Check for freetag library
include_once('Wikka.freetag.class.php');
$freetag_options = array(
'db_user' => 'root',
'db_pass' => ,
'db_host' => 'localhost',
'db_name' => 'freetag',
'PCONNECT' => true,
);
$freetag = new wikka_freetag($freetag_options);
function wikka_id_to_tagger_id ($wikka_id, $obj, $freetag_options) {
if(!isset($wikka_id)) {
return NULL;
}
$wikka_id = mysql_real_escape_string($wikka_id);
$res = $obj->LoadSingle("select tagger_id from ".$freetag_options['db_name'].".freetag_wikka_id_map where wikka_id = '".$wikka_id."';");
$tagger_id = $res['tagger_id'];
if(!$tagger_id) {
$obj->Query("insert ".$freetag_options['db_name'].".freetag_wikka_id_map set "."wikka_id = '".$wikka_id."';");
$res = $obj->LoadSingle("select last_insert_id();");
$tagger_id = $res['last_insert_id()'];
}
return $tagger_id;
}
function output_tag_cloud($freetag,$tag_page_url,$tagger_id=NULL,$header=NULL) {
Output tag cloud
print "<div class='floatr'>";
if($header) {
print $header."<br/>";
}
print $freetag->get_tag_cloud_html(100,10,20,'px','cloud_tag',$tag_page_url,$tagger_id);
print "</div>";
print "<div class=\"clear\"> </div>";
}
function count_objects($wikka_id, $obj, $freetag_options) {
if(isset($wikka_id)) {
$wikka_sql = "AND wikka_id = '".mysql_real_escape_string($wikka_id)."'";
} else {
$wikka_sql = "AND private = 0";
}
$res = $obj->LoadSingle("select COUNT(*) as count from ".$freetag_options['db_name'].".freetag_bookmarks where 1 ".$wikka_sql.";");
$count = $res['count'];
return $count;
}
function count_tagged_objects($wikka_id, $tag, $obj, $freetag_options, $freetag) {
$num_objs = count_objects($wikka_id, $obj, $freetag_options);
$tagger_id = wikka_id_to_tagger_id($wikka_id, $obj, $freetag_options);
$ids = $freetag->get_most_recent_objects($tagger_id, $tag, 0, $num_objs);
return count($ids);
}
$wikka_id = $this->GetUserName();
$tagger_id = wikka_id_to_tagger_id($wikka_id, $this, $freetag_options);
$all_obj_count = count_objects(NULL, $this, $freetag_options);
$your_obj_count = count_objects($wikka_id, $this, $freetag_options);
echo "<h1>BookmarkTest</h1>";
echo "<a href=\.$this->href()."&action=list_all"."\">List all (".$all_obj_count.") | ".
".$this->href()."&action=list_yours"."\">List yours</a> (".$your_obj_count.") | ".
"<a href=\.$this->href()."&action=add"."\">Add
"; if(!isset($_REQUEST['action'])) { // Display all tags at start of session $this->Redirect($this->href()."&action=list_all"); } // Add new entry to DB if(isset($_REQUEST['action']) && ($_REQUEST['action']=="add")) { if(trim($_REQUEST['tags']) != ) {
"; if(!isset($_REQUEST['action'])) { // Display all tags at start of session $this->Redirect($this->href()."&action=list_all"); } // Add new entry to DB if(isset($_REQUEST['action']) && ($_REQUEST['action']=="add")) { if(trim($_REQUEST['tags']) != ) {
Private bookmark?
$private = 0;
if(strpos(trim($_REQUEST['tags']), "@private") ! false)
false)
$private = 1;
$this->Query("insert ".$freetag_options['db_name'].".freetag_bookmarks set ".
"title = '".mysql_real_escape_string($_REQUEST['title'])."', ".
"uri = '".mysql_real_escape_string($this->cleanUrl($_REQUEST['uri']))."', ".
"wikka_id = '".mysql_real_escape_string($wikka_id)."', ".
"private = '".$private."', ".
"description = '".mysql_real_escape_string($_REQUEST['desc'])."';");
$last_id = $this->LoadSingle("select last_insert_id();");
$freetag->tag_object($tagger_id, $last_id['last_insert_id()'], $_REQUEST['tags']);
$this->Redirect($this->href()."&action=list_yours");
} else {
Display add form
?>
<input type="hidden" name="action" value="add" />
<table>
<tr>
<td align="right">Title:</td>
<td><input name="title" size="40"/></td>
</tr>
<tr>
<td align="right">URI:
<td><input name="uri" size="40"/></td>
</tr>
<tr>
<td align="right">Description:</td>
<td><input name="desc" size="40"/></td>
</tr>
<tr>
<td align="right"></td>
<td><?php echo $this->Format("Use blank space between tags, include \"@private\" for private bookmark"); ?></td>
</tr>
<tr>
<td align="right">Tags:</td>
<td><input name="tags" size="40"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Add" size="40" /></td>
</tr>
</table>
<?php
}
}
Edit entry
if(isset($_REQUEST['action']) && ($_REQUEST['action']
"edit") && (isset($_REQUEST['object_id']))) {
$object_id = $_REQUEST['object_id'];
$obj = $this->LoadSingle("select bookmark_id,title,uri,description,wikka_id from ".$freetag_options['db_name'].".freetag_bookmarks where bookmark_id = ".$object_id.";");
Is the logged-in user the owner?
if($wikka_id != $obj['wikka_id']) {
$this->Redirect($this->href()."&action=list_all");
}
if(isset($_REQUEST['modify']) && trim($_REQUEST['tags']) != ) {
// Delete all tags, then re-tag
$freetag->delete_all_object_tags_for_user($tagger_id,$object_id);
// Private bookmark?
$private = 0;
if(strpos(trim($_REQUEST['tags']), "@private") !== false)
$private = 1;
$this->Query("update ".$freetag_options['db_name'].".freetag_bookmarks set ".
"title = '".mysql_real_escape_string($_REQUEST['title'])."', ".
"uri = '".mysql_real_escape_string($this->cleanUrl($_REQUEST['uri']))."', ".
"wikka_id = '".mysql_real_escape_string($wikka_id)."', ".
"private = '".$private."', ".
"description = '".mysql_real_escape_string($_REQUEST['desc'])."' ".
"where bookmark_id = ".mysql_real_escape_string($object_id)." ".
"limit 1;");
$freetag->tag_object($tagger_id, $obj['bookmark_id'], $_REQUEST['tags']);
$this->Redirect($this->href()."&action=list_yours");
}
// Display add form
print($this->FormOpen());
?>
;
Title: | |
URI: | |
Description: | |
Format("Use blank space between tags, include \"@private\" for private bookmark"); ?> | |
Tags: | get_tags_on_object($object_id); $taglist =
foreach($tags as $idx=>$res) {
$taglist .= trim($res['raw_tag'])." ";
}
?>
<td><input name="tags" size="40" value="<?php print $taglist?>"/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Edit" size="40" /></td>
</tr>
</table>
<?php
print($this->FormClose());
}
Delete entry
if(isset($_REQUEST['action']) && $_REQUEST['action']"delete") {
"delete") {
$object_id = $_REQUEST['object_id'];
Is the logged-in user the owner?
if($wikka_id != $obj['wikka_id']) {
$this->Redirect($this->href()."&action=list_all");
}
Delete all tags first...
$freetag->delete_all_object_tags_for_user($tagger_id,$object_id);
...then delete object
$this->Query("delete from ".$freetag_options['db_name'].".freetag_bookmarks where bookmark_id = ".mysql_real_escape_string($object_id)." limit 1;");
$this->Redirect($this->href()."&action=list_yours");
}
List all bookmarks (ordered by most recent)
if(isset($_REQUEST['action']) && ($_REQUEST['action']
"list_all")) {
$offset = 0;
if(isset($_REQUEST['offset'])) {
$offset = $_REQUEST['offset'];
}
$limit = 25;
if(isset($_REQUEST['limit'])) {
$limit = $_REQUEST['limit'];
}
Output tag cloud
$url_opts = $this->href()."&action=list_all&tag=";
output_tag_cloud($freetag,$url_opts,NULL,"All tags:");
$ids = ;
$tag = ;
if(isset($_REQUEST['tag']) && $_REQUEST['tag'] != ) {
$ids = $freetag->get_most_recent_objects(NULL,$_REQUEST['tag'],$offset,$limit);
$tag = $_REQUEST['tag'];
} else {
$ids = $freetag->get_most_recent_objects(NULL,NULL,$offset,$limit);
}
foreach($ids as $key=>$val) {
$tags = $freetag->get_tags_on_object($val['object_id']);
$obj = $this->LoadSingle("select title,uri,description,wikka_id from ".$freetag_options['db_name'].".freetag_bookmarks where bookmark_id = ".$val['object_id']." and private = 0;");
if(!isset($obj)) {
continue;
}
print ".$obj['uri']."\">".$obj['title']."</a><br/>";
print "<div class=\"commentsheader\"><b>".$obj['description']."</b>";
print "<br/>";
$taglist = ;
foreach($tags as $idx=>$res) {
$taglist .= trim($res['tag'])." ";
}
print $taglist."by ".$obj['wikka_id']." (created: ".$val['tagged_on'].")";
print " ";
}
// Display pagination links
$obj_count = 0;
$tag_url = ;
if($tag != ) {
$obj_count = count_tagged_objects(NULL, $tag, $this, $freetag_options, $freetag);
$tag_url = "&tag=".$tag;
} else {
$obj_count = count_objects(NULL, $this, $freetag_options);
}
$prev_offset = $offset - $limit;
$prev_url = ;
if($prev_offset < 0) {
$prev_offset = 0;
}
if($offset > 0) {
$prev_url = "<a href=\.$this->href()."&action=list_all&offset=".$prev_offset."&limit=".$limit.$tag_url."\"><<";
}
$next_offset = $offset + $limit;
$next_url = ;
if($next_offset < $obj_count - 1) {
$next_url = "<a href=\.$this->href()."&action=list_all&offset=".$next_offset."&limit=".$limit.$tag_url."\">>>";
}
print "
"; print "
";
print $prev_url." ".$next_url;
print "
";
}
// List user bookmarks (ordered by most recent)
if(isset($_REQUEST['action']) && ($_REQUEST['action']=="list_yours")) {
$offset = 0;
if(isset($_REQUEST['offset'])) {
$offset = $_REQUEST['offset'];
}
$limit = 25;
if(isset($_REQUEST['limit'])) {
$limit = $_REQUEST['limit'];
}
// Output tag cloud
$url_opts = $this->href()."&action=list_yours&tag=";
output_tag_cloud($freetag,$url_opts,$tagger_id,"Your tags:");
$ids = ;
$tag = ;
if(isset($_REQUEST['tag']) && $_REQUEST['tag'] != ) {
$ids = $freetag->get_most_recent_objects($tagger_id, $_REQUEST['tag'], $offset, $limit);
$tag = $_REQUEST['tag'];
} else {
$ids = $freetag->get_most_recent_objects($tagger_id, NULL, $offset, $limit);
}
foreach($ids as $key=>$val) {
$tags = $freetag->get_tags_on_object($val['object_id']);
$obj = $this->LoadSingle("select title,uri,description,wikka_id from ".$freetag_options['db_name'].".freetag_bookmarks where bookmark_id = ".$val['object_id'].";");
print "<a href=\.$obj['uri']."\">".$obj['title']."
"; print "
"; print "
".$obj['description']."";
print "
.$this->href()."&action=edit&object_id=".$val['object_id']."\">edit</a>";
print "|";
print "<a href=\.$this->href()."&action=delete&object_id=".$val['object_id']."\">delete";
print "
"; $taglist = ;
"; $taglist = ;
foreach($tags as $idx=>$res) {
$taglist .= trim($res['raw_tag'])." ";
}
print $taglist."by ".$obj['wikka_id']." (created: ".$val['tagged_on'].")</div>";
print "<div class=\"clear\"> </div>";
}
Display pagination links
$obj_count = 0;
$tag_url = ;
if($tag != ) {
$obj_count = count_tagged_objects($wikka_id, $tag, $this, $freetag_options, $freetag);
$tag_url = "&tag=".$tag;
} else {
$obj_count = count_objects($wikka_id, $this, $freetag_options);
}
$prev_offset = $offset - $limit;
}
$next_offset = $offset + $limit;
}
print "<p><p>";
print "<div class=\"center\">";
print $prev_url." ".$next_url;
print "</div>";
}
?>- Save the following file as ##actions/Wikka.freetag.class.php## (needs to be moved to libs/ eventually): **Wikka.freetag.class.php**
<?php
Check for freetag library
$freetag_lib = '3rdparty/plugins/freetag/freetag.class.php';
if(!is_file($freetag_lib)) {
print("<br/><br/><div class=\"error\">".$this->Format("Can't find $freetag_lib!")."<br/><br/>\n");
die();
}
include_once($freetag_lib);
class wikka_freetag extends freetag {
function wikka_freetag($freetag_options) {
parent::freetag($freetag_options);
}
function get_most_recent_objects($tagger_id = NULL, $tag = NULL, $offset = 0, $limit = 25) {
$db = $this->db;
if(isset($tagger_id)) {
$tagger_sql = "AND tagger_id = $tagger_id";
} else {
$tagger_sql = ;
}
$prefix = $this->_table_prefix;
$sql = ;
if(!$tag) {
$sql = "SELECT DISTINCT object_id, tagged_on FROM
${prefix}freetagged_objects
WHERE 1
$tagger_sql
ORDER BY tagged_on DESC
LIMIT $offset, $limit ";
} else {
$tag = $db->qstr($tag, get_magic_quotes_gpc());
$sql = "SELECT DISTINCT object_id, tagged_on
FROM ${prefix}freetagged_objects INNER JOIN ${prefix}freetags ON (tag_id = id)
WHERE tag = $tag
$tagger_sql
ORDER BY tagged_on DESC
LIMIT $offset, $limit ";
}
$rs = $db->Execute($sql) or die("Syntax Error: $sql");
$retarr = array();
while(!$rs->EOF) {
$retarr[] = array(
'object_id' => $rs->fields['object_id'],
'tagged_on' => $rs->fields['tagged_on']
);
$rs->MoveNext();
}
return $retarr;
}
}
- Adjust the DB connection parameters for your own installation - (Optional) After line 918 in ##freetag.class.php##, add the following line to prevent @private tags from cluttering up the landscape:
foreach ($tag_list as $tag => $qty) {
Add the following line:
if(strpos($tag, "private") ! false) continue;
false) continue;
$size = $min_font_size + ($qty - $min_qty) * $step;
- Add the ""{{bookmarks}}"" handler code to a new page ==Other stuff== - The two Perl scripts that follow can be used to import bookmarks from de.lirio.us. Use your browser to save a page of bookmarks as an HTML file (you might have to save multiple pages; that's OK, the script can handle it). Export the HTML data into text format:
parseDelirious.pl file1.html file2.html file3.html > myLinks.txt
Change the ##$base_url##, ##$wikiname##, and ##$password## global vars in ##importDelirious.pl##, then run against the file created in the previous step:
importDelirious.pl myLinks.txt
##parseDelirious.pl##
#! /usr/bin/perl
#
# $Id: parseDelirious.pl,v 1.2 2006/05/30 03:27:21 brian Exp brian $
#
# parseDelirious.pl - Parses a de.lirio.us screen dump (as saved by
# Firefox)
#
#
require HTML::TreeBuilder;
foreach my $filename(@ARGV) {
my $tree = HTML::TreeBuilder->new;
$tree->parse_file($filename);
$tree->elementify();
@nodes = $tree->look_down("class","xfolkentry");
foreach $node(@nodes) {
$_ = $node->look_down("class","uri");
$link = $_->extract_links();
$title = $link->[0]->[1]->as_text();
print "Title: $title\n";
print "URI: $link->[0]->[0]\n";
# Get description, if any
$_=$node->look_down("class","extended");
print "Desc: ";
if($_) {
$desc = $_->as_text();
print "$desc";
}
print "\n";
# A de.lirio.us export quirk prevents some tags from
# displaying; default these to "@private" for later review
@_ = $node->look_down("class","tag");
print "Tags: ";
if($#_ < 0) {
print "\@private";
}
foreach $tagnode(@_) {
$link = $tagnode->extract_links();
$tag = $link->[0]->[1]->as_text();
print "$tag ";
}
print "\n\n\n";
}
$tree = $tree->delete;
}##importDelirious.pl##
#! /usr/bin/perl
#
# $Id: importDelirious.pl,v 1.3 2006/05/31 04:43:19 brian Exp brian $
#
# importDelirious.pl -- Imports file created by parseDelirious.pl
#
# Usage: importDelirious.pl exportFile
#
#
require LWP::UserAgent;
require HTTP::Cookies;
#Global#
$base_url = "http://some.url.com/wiki/";
$bookmark_page = "Bookmarks";
$wikiname = "YourName";
$password = "yourpassword";
$ua = LWP::UserAgent->new;
$cookie_jar = HTTP::Cookies->new(file => "lwpcookies.txt",
autosave => 1);
# Login
$login_url = $base_url."wikka.php?wakka=UserSettings";
my $req = HTTP::Request->new(POST=>"$login_url");
$req->content_type('application/x-www-form-urlencoded');
$req->content("name=$wikiname&password=$password&action=login&wakka=UserSettings");
my $res = $ua->request($req);
$cookie_jar->extract_cookies($res);
# Import
open(IN, "<$ARGV[0]")
die "Can't open $ARGV[0] for reading!"; # Set autoflush so progress is displayed my $oldfh = select(STDOUT); $| = 1; select($oldfh); print "Importing..."; while(<IN>) {
print ".";
next until /Title: /;
chomp;
$title = (split(": ",$_))[1];
$_ = <IN>;
chomp;
$uri = (split(": ",$_))[1];
$_ = <IN>;
chomp;
$desc = (split(": ",$_))[1];
$_ = <IN>;
chomp;
$tags = (split(": ",$_))[1];
$add_url = $base_url."wikka.php?wakka=".$bookmark_page."&action=add";
$req = HTTP::Request->new(POST=>"$add_url");
$req->content_type('application/x-www-form-urlencoded');
$req->content("title=$title&uri=$uri&desc=$desc&tags=$tags");
$ua->request($req);
}print "done!\n"; %% Category CategoryDevelopmentDiscussion |