]> 91.132.146.200 Git - insipid.git/commitdiff
import
authorluke <luke@neuro-tech.net>
Thu, 4 Sep 2008 01:06:10 +0000 (21:06 -0400)
committerluke <luke@neuro-tech.net>
Thu, 4 Sep 2008 01:06:10 +0000 (21:06 -0400)
25 files changed:
AUTHORS [new file with mode: 0755]
COPYING [new file with mode: 0755]
ChangeLog [new file with mode: 0755]
README [changed mode: 0644->0755]
TODO [new file with mode: 0755]
help.html [new file with mode: 0755]
htaccess [new file with mode: 0755]
insipid-config.cgi.example [new file with mode: 0755]
insipid.cgi [new file with mode: 0755]
insipid.css [new file with mode: 0755]
lib/Insipid/Bookmarks.pm [new file with mode: 0755]
lib/Insipid/Config.pm [new file with mode: 0755]
lib/Insipid/Database.pm [new file with mode: 0755]
lib/Insipid/JSON.pm [new file with mode: 0755]
lib/Insipid/LinkExtractor.pm [new file with mode: 0755]
lib/Insipid/Main.pm [new file with mode: 0755]
lib/Insipid/RSS.pm [new file with mode: 0755]
lib/Insipid/Schemas.pm [new file with mode: 0755]
lib/Insipid/Sessions.pm [new file with mode: 0755]
lib/Insipid/Snapshots.pm [new file with mode: 0755]
lib/Insipid/Tags.pm [new file with mode: 0755]
lib/Insipid/Util.pm [new file with mode: 0755]
tools/export.pl [new file with mode: 0755]
tools/import.pl [new file with mode: 0755]
tools/mozilla.pl [new file with mode: 0755]

diff --git a/AUTHORS b/AUTHORS
new file mode 100755 (executable)
index 0000000..0e34ef1
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,12 @@
+Luke Reeves <luke@neuro-tech.net>\r
+\r
+Contributors:\r
+Manuel de la Torre <mdltorre@gmail.com>\r
+Artur Marques <am@arturmarques.com>\r
+Danny Miles <dannymiles2@gmail.com>\r
+Henning Sprang <henning@sprang.de>\r
+Paul Boin <paul@boin.org>\r
+Jutta Horstmann <jh@weltraumsofa.de>\r
+Stephen Patterson <steve@patter.mine.nu>\r
+Thom Baxter <k386@pmlf.net>\r
+Robin Ward <robin.ward@gmail.com>\r
diff --git a/COPYING b/COPYING
new file mode 100755 (executable)
index 0000000..daef9ef
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,341 @@
+                   GNU GENERAL PUBLIC LICENSE\r
+                      Version 2, June 1991\r
+\r
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.\r
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+ Everyone is permitted to copy and distribute verbatim copies\r
+ of this license document, but changing it is not allowed.\r
+\r
+                           Preamble\r
+\r
+  The licenses for most software are designed to take away your\r
+freedom to share and change it.  By contrast, the GNU General Public\r
+License is intended to guarantee your freedom to share and change free\r
+software--to make sure the software is free for all its users.  This\r
+General Public License applies to most of the Free Software\r
+Foundation's software and to any other program whose authors commit to\r
+using it.  (Some other Free Software Foundation software is covered by\r
+the GNU Library General Public License instead.)  You can apply it to\r
+your programs, too.\r
+\r
+  When we speak of free software, we are referring to freedom, not\r
+price.  Our General Public Licenses are designed to make sure that you\r
+have the freedom to distribute copies of free software (and charge for\r
+this service if you wish), that you receive source code or can get it\r
+if you want it, that you can change the software or use pieces of it\r
+in new free programs; and that you know you can do these things.\r
+\r
+  To protect your rights, we need to make restrictions that forbid\r
+anyone to deny you these rights or to ask you to surrender the rights.\r
+These restrictions translate to certain responsibilities for you if you\r
+distribute copies of the software, or if you modify it.\r
+\r
+  For example, if you distribute copies of such a program, whether\r
+gratis or for a fee, you must give the recipients all the rights that\r
+you have.  You must make sure that they, too, receive or can get the\r
+source code.  And you must show them these terms so they know their\r
+rights.\r
+\r
+  We protect your rights with two steps: (1) copyright the software, and\r
+(2) offer you this license which gives you legal permission to copy,\r
+distribute and/or modify the software.\r
+\r
+  Also, for each author's protection and ours, we want to make certain\r
+that everyone understands that there is no warranty for this free\r
+software.  If the software is modified by someone else and passed on, we\r
+want its recipients to know that what they have is not the original, so\r
+that any problems introduced by others will not reflect on the original\r
+authors' reputations.\r
+\r
+  Finally, any free program is threatened constantly by software\r
+patents.  We wish to avoid the danger that redistributors of a free\r
+program will individually obtain patent licenses, in effect making the\r
+program proprietary.  To prevent this, we have made it clear that any\r
+patent must be licensed for everyone's free use or not licensed at all.\r
+\r
+  The precise terms and conditions for copying, distribution and\r
+modification follow.\r
+\f\r
+                   GNU GENERAL PUBLIC LICENSE\r
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\r
+\r
+  0. This License applies to any program or other work which contains\r
+a notice placed by the copyright holder saying it may be distributed\r
+under the terms of this General Public License.  The "Program", below,\r
+refers to any such program or work, and a "work based on the Program"\r
+means either the Program or any derivative work under copyright law:\r
+that is to say, a work containing the Program or a portion of it,\r
+either verbatim or with modifications and/or translated into another\r
+language.  (Hereinafter, translation is included without limitation in\r
+the term "modification".)  Each licensee is addressed as "you".\r
+\r
+Activities other than copying, distribution and modification are not\r
+covered by this License; they are outside its scope.  The act of\r
+running the Program is not restricted, and the output from the Program\r
+is covered only if its contents constitute a work based on the\r
+Program (independent of having been made by running the Program).\r
+Whether that is true depends on what the Program does.\r
+\r
+  1. You may copy and distribute verbatim copies of the Program's\r
+source code as you receive it, in any medium, provided that you\r
+conspicuously and appropriately publish on each copy an appropriate\r
+copyright notice and disclaimer of warranty; keep intact all the\r
+notices that refer to this License and to the absence of any warranty;\r
+and give any other recipients of the Program a copy of this License\r
+along with the Program.\r
+\r
+You may charge a fee for the physical act of transferring a copy, and\r
+you may at your option offer warranty protection in exchange for a fee.\r
+\r
+  2. You may modify your copy or copies of the Program or any portion\r
+of it, thus forming a work based on the Program, and copy and\r
+distribute such modifications or work under the terms of Section 1\r
+above, provided that you also meet all of these conditions:\r
+\r
+    a) You must cause the modified files to carry prominent notices\r
+    stating that you changed the files and the date of any change.\r
+\r
+    b) You must cause any work that you distribute or publish, that in\r
+    whole or in part contains or is derived from the Program or any\r
+    part thereof, to be licensed as a whole at no charge to all third\r
+    parties under the terms of this License.\r
+\r
+    c) If the modified program normally reads commands interactively\r
+    when run, you must cause it, when started running for such\r
+    interactive use in the most ordinary way, to print or display an\r
+    announcement including an appropriate copyright notice and a\r
+    notice that there is no warranty (or else, saying that you provide\r
+    a warranty) and that users may redistribute the program under\r
+    these conditions, and telling the user how to view a copy of this\r
+    License.  (Exception: if the Program itself is interactive but\r
+    does not normally print such an announcement, your work based on\r
+    the Program is not required to print an announcement.)\r
+\f\r
+These requirements apply to the modified work as a whole.  If\r
+identifiable sections of that work are not derived from the Program,\r
+and can be reasonably considered independent and separate works in\r
+themselves, then this License, and its terms, do not apply to those\r
+sections when you distribute them as separate works.  But when you\r
+distribute the same sections as part of a whole which is a work based\r
+on the Program, the distribution of the whole must be on the terms of\r
+this License, whose permissions for other licensees extend to the\r
+entire whole, and thus to each and every part regardless of who wrote it.\r
+\r
+Thus, it is not the intent of this section to claim rights or contest\r
+your rights to work written entirely by you; rather, the intent is to\r
+exercise the right to control the distribution of derivative or\r
+collective works based on the Program.\r
+\r
+In addition, mere aggregation of another work not based on the Program\r
+with the Program (or with a work based on the Program) on a volume of\r
+a storage or distribution medium does not bring the other work under\r
+the scope of this License.\r
+\r
+  3. You may copy and distribute the Program (or a work based on it,\r
+under Section 2) in object code or executable form under the terms of\r
+Sections 1 and 2 above provided that you also do one of the following:\r
+\r
+    a) Accompany it with the complete corresponding machine-readable\r
+    source code, which must be distributed under the terms of Sections\r
+    1 and 2 above on a medium customarily used for software interchange; or,\r
+\r
+    b) Accompany it with a written offer, valid for at least three\r
+    years, to give any third party, for a charge no more than your\r
+    cost of physically performing source distribution, a complete\r
+    machine-readable copy of the corresponding source code, to be\r
+    distributed under the terms of Sections 1 and 2 above on a medium\r
+    customarily used for software interchange; or,\r
+\r
+    c) Accompany it with the information you received as to the offer\r
+    to distribute corresponding source code.  (This alternative is\r
+    allowed only for noncommercial distribution and only if you\r
+    received the program in object code or executable form with such\r
+    an offer, in accord with Subsection b above.)\r
+\r
+The source code for a work means the preferred form of the work for\r
+making modifications to it.  For an executable work, complete source\r
+code means all the source code for all modules it contains, plus any\r
+associated interface definition files, plus the scripts used to\r
+control compilation and installation of the executable.  However, as a\r
+special exception, the source code distributed need not include\r
+anything that is normally distributed (in either source or binary\r
+form) with the major components (compiler, kernel, and so on) of the\r
+operating system on which the executable runs, unless that component\r
+itself accompanies the executable.\r
+\r
+If distribution of executable or object code is made by offering\r
+access to copy from a designated place, then offering equivalent\r
+access to copy the source code from the same place counts as\r
+distribution of the source code, even though third parties are not\r
+compelled to copy the source along with the object code.\r
+\f\r
+  4. You may not copy, modify, sublicense, or distribute the Program\r
+except as expressly provided under this License.  Any attempt\r
+otherwise to copy, modify, sublicense or distribute the Program is\r
+void, and will automatically terminate your rights under this License.\r
+However, parties who have received copies, or rights, from you under\r
+this License will not have their licenses terminated so long as such\r
+parties remain in full compliance.\r
+\r
+  5. You are not required to accept this License, since you have not\r
+signed it.  However, nothing else grants you permission to modify or\r
+distribute the Program or its derivative works.  These actions are\r
+prohibited by law if you do not accept this License.  Therefore, by\r
+modifying or distributing the Program (or any work based on the\r
+Program), you indicate your acceptance of this License to do so, and\r
+all its terms and conditions for copying, distributing or modifying\r
+the Program or works based on it.\r
+\r
+  6. Each time you redistribute the Program (or any work based on the\r
+Program), the recipient automatically receives a license from the\r
+original licensor to copy, distribute or modify the Program subject to\r
+these terms and conditions.  You may not impose any further\r
+restrictions on the recipients' exercise of the rights granted herein.\r
+You are not responsible for enforcing compliance by third parties to\r
+this License.\r
+\r
+  7. If, as a consequence of a court judgment or allegation of patent\r
+infringement or for any other reason (not limited to patent issues),\r
+conditions are imposed on you (whether by court order, agreement or\r
+otherwise) that contradict the conditions of this License, they do not\r
+excuse you from the conditions of this License.  If you cannot\r
+distribute so as to satisfy simultaneously your obligations under this\r
+License and any other pertinent obligations, then as a consequence you\r
+may not distribute the Program at all.  For example, if a patent\r
+license would not permit royalty-free redistribution of the Program by\r
+all those who receive copies directly or indirectly through you, then\r
+the only way you could satisfy both it and this License would be to\r
+refrain entirely from distribution of the Program.\r
+\r
+If any portion of this section is held invalid or unenforceable under\r
+any particular circumstance, the balance of the section is intended to\r
+apply and the section as a whole is intended to apply in other\r
+circumstances.\r
+\r
+It is not the purpose of this section to induce you to infringe any\r
+patents or other property right claims or to contest validity of any\r
+such claims; this section has the sole purpose of protecting the\r
+integrity of the free software distribution system, which is\r
+implemented by public license practices.  Many people have made\r
+generous contributions to the wide range of software distributed\r
+through that system in reliance on consistent application of that\r
+system; it is up to the author/donor to decide if he or she is willing\r
+to distribute software through any other system and a licensee cannot\r
+impose that choice.\r
+\r
+This section is intended to make thoroughly clear what is believed to\r
+be a consequence of the rest of this License.\r
+\f\r
+  8. If the distribution and/or use of the Program is restricted in\r
+certain countries either by patents or by copyrighted interfaces, the\r
+original copyright holder who places the Program under this License\r
+may add an explicit geographical distribution limitation excluding\r
+those countries, so that distribution is permitted only in or among\r
+countries not thus excluded.  In such case, this License incorporates\r
+the limitation as if written in the body of this License.\r
+\r
+  9. The Free Software Foundation may publish revised and/or new versions\r
+of the General Public License from time to time.  Such new versions will\r
+be similar in spirit to the present version, but may differ in detail to\r
+address new problems or concerns.\r
+\r
+Each version is given a distinguishing version number.  If the Program\r
+specifies a version number of this License which applies to it and "any\r
+later version", you have the option of following the terms and conditions\r
+either of that version or of any later version published by the Free\r
+Software Foundation.  If the Program does not specify a version number of\r
+this License, you may choose any version ever published by the Free Software\r
+Foundation.\r
+\r
+  10. If you wish to incorporate parts of the Program into other free\r
+programs whose distribution conditions are different, write to the author\r
+to ask for permission.  For software which is copyrighted by the Free\r
+Software Foundation, write to the Free Software Foundation; we sometimes\r
+make exceptions for this.  Our decision will be guided by the two goals\r
+of preserving the free status of all derivatives of our free software and\r
+of promoting the sharing and reuse of software generally.\r
+\r
+                           NO WARRANTY\r
+\r
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\r
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\r
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\r
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\r
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\r
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\r
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\r
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\r
+REPAIR OR CORRECTION.\r
+\r
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\r
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\r
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\r
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\r
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\r
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\r
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\r
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\r
+POSSIBILITY OF SUCH DAMAGES.\r
+\r
+                    END OF TERMS AND CONDITIONS\r
+\f\r
+           How to Apply These Terms to Your New Programs\r
+\r
+  If you develop a new program, and you want it to be of the greatest\r
+possible use to the public, the best way to achieve this is to make it\r
+free software which everyone can redistribute and change under these terms.\r
+\r
+  To do so, attach the following notices to the program.  It is safest\r
+to attach them to the start of each source file to most effectively\r
+convey the exclusion of warranty; and each file should have at least\r
+the "copyright" line and a pointer to where the full notice is found.\r
+\r
+    <one line to give the program's name and a brief idea of what it does.>\r
+    Copyright (C) <year>  <name of author>\r
+\r
+    This program is free software; you can redistribute it and/or modify\r
+    it under the terms of the GNU General Public License as published by\r
+    the Free Software Foundation; either version 2 of the License, or\r
+    (at your option) any later version.\r
+\r
+    This program is distributed in the hope that it will be useful,\r
+    but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+    GNU General Public License for more details.\r
+\r
+    You should have received a copy of the GNU General Public License\r
+    along with this program; if not, write to the Free Software\r
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+\r
+\r
+Also add information on how to contact you by electronic and paper mail.\r
+\r
+If the program is interactive, make it output a short notice like this\r
+when it starts in an interactive mode:\r
+\r
+    Gnomovision version 69, Copyright (C) year name of author\r
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\r
+    This is free software, and you are welcome to redistribute it\r
+    under certain conditions; type `show c' for details.\r
+\r
+The hypothetical commands `show w' and `show c' should show the appropriate\r
+parts of the General Public License.  Of course, the commands you use may\r
+be called something other than `show w' and `show c'; they could even be\r
+mouse-clicks or menu items--whatever suits your program.\r
+\r
+You should also get your employer (if you work as a programmer) or your\r
+school, if any, to sign a "copyright disclaimer" for the program, if\r
+necessary.  Here is a sample; alter the names:\r
+\r
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\r
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.\r
+\r
+  <signature of Ty Coon>, 1 April 1989\r
+  Ty Coon, President of Vice\r
+\r
+This General Public License does not permit incorporating your program into\r
+proprietary programs.  If your program is a subroutine library, you may\r
+consider it more useful to permit linking proprietary applications with the\r
+library.  If this is what you want to do, use the GNU Library General\r
+Public License instead of this License.\r
+\r
diff --git a/ChangeLog b/ChangeLog
new file mode 100755 (executable)
index 0000000..6a2c187
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,100 @@
+Mon Mar 27 18:59 EST 2006  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Added an option for disabling mod_rewrite support\r
+       * Added a prefix option for database tables\r
+\r
+Tue Mar 21 19:34 EST 2006  Luke Reeves <luke@neuro-tech.net>\r
+\r
+       * Fix the export system\r
+\r
+Tue Feb 21 12:30 EST 2006  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Fix up database connectivity for newer PostgreSQL \r
+         libraries\r
+       * Incorporate patch from Paul Boin that allows importing\r
+         of regular del.icio.us backups (as opposed to only\r
+         supporting exports as before)\r
+\r
+Mon Nov 21 12:41 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Fixed some of the cleanup process for snapshot deletion\r
+       * Added a "Fetch Related" function, so that the snapshot\r
+         tool can grab related objects\r
+       * Work on the tag tools - delete and rename tags          \r
+\r
+Fri Nov 04 09:00 EST 2005  Luke Reeves <luke@neuro-tech.net>\r
+\r
+       * Database fix (thanks to Jutta Horstmann)\r
+       * Remove export and import to server-side files, since the \r
+         standalone tools can easily accomplish that\r
+\r
+Fri Sep 23 07:35 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Enable taint mode for all the modules/scripts.\r
+\r
+Thu Sep 22 12:15 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Remove dependency on App::Config, since all it does is\r
+         read a simple text file.\r
+       * Remove dependency on CGI::Session in lieu of a built-in\r
+         session management system\r
+       * Fixed up database tables\r
+\r
+Mon Sep 19 20:37 EST 2005  Luke Reeves <luke@neuro-tech.net>\r
+\r
+       * Add the ability to drill-down into more details on cached\r
+         objects.\r
+       * Fix up the standalone import and export tools.\r
+       * Show the correct total size of snapshots in the snapshot\r
+         view.\r
+\r
+Fri Sep 16 21:02 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Move tools to a separate directory\r
+       * Add a standalone export script.\r
+       * Fix bugs in the export concerning character encoding.\r
+\r
+Fri Sep 16 12:30 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Fix a couple possible installation issues on PostgreSQL\r
+         systems.\r
+\r
+Mon Sep 05 22:28 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+       \r
+       * Major code cleanup (splitting the main CGI file into multiple,\r
+         more maintainable ones inside of the lib/Insipid directory)\r
+       * Bugfix for adding duplicate items not being flagged as such\r
+\r
+Tue Aug 09 22:05 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Added proxy support for fetching pages\r
+       * Fix blob size for MySQL tables\r
+       * Added a toggle to not redirect when a mark is added\r
+       * Added a toggle to snapshot a page when bookmarked\r
+       * Support passing the Referer header when snapshotting a page.\r
+       * Added support for If-Modified-Since requests to cached objects.\r
+       * Added a Last-Modified header for cache objects.\r
+\r
+Thu Jul 21 21:35 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Fix a bunch of errors concerning PostgreSQL and the snapshots,\r
+         as well as other bug fixes.\r
+\r
+Sun Jul 17 15:19 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+       \r
+       * Added the page cache feature, so a user can take snapshots of pages\r
+         that have been bookmarked.\r
+\r
+Tue Jul 05 21:58 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Added addition of tags (like Documentation + Java) to get\r
+         intersections of bookmarks.\r
+\r
+Fri May 27 11:05 JST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Added postgresql backend support (submitted by Stephen Patterson)\r
+\r
+Mon Apr 25 21:00 EST 2005  Luke Reeves  <luke@neuro-tech.net>\r
+\r
+       * Add a search box, with the option of showing it to non-logged in\r
+         users.\r
diff --git a/README b/README
old mode 100644 (file)
new mode 100755 (executable)
index e69de29..27f0ddc
--- a/README
+++ b/README
@@ -0,0 +1,41 @@
+Insipid is a web-based bookmark manager similar to the Delicious service.\r
+\r
+Rename the insipid-config.cgi.example to insipid-config.cgi and edit it,\r
+replacing the example values with those that match your system.  Make sure \r
+that the line "dbtype" is set:\r
+\r
+# For MySQL:\r
+dbtype = mysql\r
+\r
+# For PostgreSQL:\r
+dbtype = Pg\r
+\r
+Create a user and password for the database if you don't have one assigned\r
+to you by your adiministrator or ISP.  Doing this is beyond the scope of this\r
+readme file - refer to the MySQL or PostgreSQL server documentation for more\r
+help.\r
+\r
+Rename the htaccess file to .htaccess and add a "RewriteBase" clause to the \r
+top, such as:\r
+\r
+       RewriteBase /~luke/\r
+\r
+You can optionally add "insipid.cgi" to the list of files in the \r
+DirectoryIndex line (for example, "DirectoryIndex index.cgi insipid.cgi").\r
+\r
+The non-standard Perl packages required for use (with their Debian package\r
+names) are:\r
+\r
+       Module          Debian Package\r
+       --------------  ------------------------------------\r
+       Date::Format    libtimedate-perl\r
+       XML::Parser     libxml-parser-perl\r
+       XML::Writer     libxml-writer-perl\r
+\r
+Once the configuration is finished, access the insipid.cgi through your\r
+web browser at the URL you've configured and the database should be \r
+automatically configured.\r
+\r
+If you ever upgrade Insipid, the database and what not will be \r
+automatically updated upon access.\r
+\r
diff --git a/TODO b/TODO
new file mode 100755 (executable)
index 0000000..b9e9c37
--- /dev/null
+++ b/TODO
@@ -0,0 +1,25 @@
+For 1.0\r
+==========================================================================\r
+- Quote marks screw up adding a bookmark\r
+- Move htaccess management into the configuration page\r
+- Fix the text/html content type going that's snapshotted\r
+- Check for duplicate page cache objects before insert (due to all the\r
+  warnings that appear in the log)\r
+- Fix the missing tag rename and delete functionality\r
+\r
+\r
+Everything else\r
+==========================================================================\r
+- Optionally use LWP to automatically import Delicious marks\r
+- Link checker\r
+- Pageable snapshot list\r
+- An inbox of links that people submit, either through a public interface \r
+  or XML-RPC (Rest of course)\r
+- A way to publish links to friends via their exposed RPC interface\r
+- Add a new column to the bookmark table for a "Via" field (which can be \r
+  filled in automatically when a mark is pushed and accepted)\r
+- Add - (subtractive) filtering\r
+- Share the iteration code between export and display\r
+- More configurable options (e.g., max RSS entries, max tags displayed, etc)\r
+- Order and categories of options\r
+\r
diff --git a/help.html b/help.html
new file mode 100755 (executable)
index 0000000..8095486
--- /dev/null
+++ b/help.html
@@ -0,0 +1,65 @@
+<!--\r
+   Copyright (C) 2006 Luke Reeves\r
+  \r
+   This program is free software; you can redistribute it and/or modify\r
+   it under the terms of the GNU General Public License as published by\r
+   the Free Software Foundation; either version 2 of the License, or\r
+   (at your option) any later version.\r
+   \r
+   This program is distributed in the hope that it will be useful,\r
+   but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+   GNU General Public License for more details.\r
+  \r
+   You should have received a copy of the GNU General Public License\r
+   along with this program; if not, write to the Free Software\r
+   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+   USA\r
+-->\r
+\r
+<html>\r
+<head>\r
+    <title>Insipid Help</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\r
+    <link rel="stylesheet" href="insipid.css" type="text/css" title="Standard">\r
+</head>\r
+\r
+<body>\r
+\r
+<br><div class="bodyTitle" align="center">Insipid Help</div>\r
+<br />\r
+<!-- /bookmark list title -->\r
+\r
+<!-- bookmark list -->\r
+<table class="bookmarklist">\r
+<tr><td>\r
+\r
+<p><div class="bodyTitle">Adding Bookmarks</div><br>\r
+<div class="bodytext">Ensure that you are logged into the system (the login button is located at the top right of the window) and click the "add" link from the toolbar, this will bring you to the add form.<br> <br>\r
+Enter in the URL of the site you wish to add, the title you want the URL to be displayed under, a brief description of the link (if you want, this isnt necessary) and the tags - separated by  spaces - that you want this bookmark to be classified under, leave this blank if you are unsure what you want to classify the bookmark as, you can always update and change this later. If you want this link to be viewable by anyone who visits your site, leave the "Public" checkbox checked. If you want this bookmark only viewable by you, uncheck the box<br><br>\r
+Click the "Add" button at the bottom of the form, and this bookmark will be added to your list\r
+</div></p>\r
+\r
+<p><div class="bodyTitle">Using Tags</div><br>\r
+<div class="bodytext">Tags are the way you organize and sort your bookmarks in Insipid, they are essentially keywords used to group similar links together. Tags can be any single word that you choose and you can use as many tags as you want for each link. Each time you add a link with a new tag it will automatically be added to the Tag List table on the left hand side of the page.\r
+</div></p>\r
+\r
+<p><div class="bodyTitle">Using Bookmarklets</div><br>\r
+<div class="bodytext">A bookmarklet is essentially a shortcut to adding a new bookmark and a great tool for quickly building up your links. To start usinga bookmarklet follow these steps:\r
+<ol>\r
+<li>Login to Insipid</li>\r
+<li>Click on "Bookmarklet", this will bring you to a page with a single link "Post this page"</li>\r
+<li>Right click on this link, and click "bookmark this link" in Moz/Firefox or "Add to Favourites" in IE (shame on you for using IE!).</li>\r
+<li>Thats it!</li>\r
+</ol>\r
+Now whenever you are on a page that you want to add to Insipid, simply select this bookmark and you will automagically be taken to Insipid's add bookmark page, the URL and Title already filled in for you. Add your description and tags, decide whether to make this a private link, click add and you are done. After you click add you will be taken back to the page you just bookmarked.\r
+</div></p>\r
+\r
+<p><div class="bodyTitle">RSS Feeds</div><br>\r
+<div class="bodytext">To subscribe to a specific users list of bookmarks, add the XML page to whichever feed reader you are using. To find out more about <a href="http://www.whatisrss.com/">RSS click here</a>\r
+</div></p>\r
+\r
+</td></tr>\r
+</table>\r
+<!-- /bookmarklist -->\r
+\r
+</body></html>\r
diff --git a/htaccess b/htaccess
new file mode 100755 (executable)
index 0000000..334569c
--- /dev/null
+++ b/htaccess
@@ -0,0 +1,14 @@
+RewriteEngine On\r
+RewriteBase /yourwebdirectory\r
+RewriteRule ^bookmarks/(.*) insipid.cgi?tag=$1 [QSA]\r
+RewriteRule ^bookmarks insipid.cgi [QSA]\r
+RewriteRule ^feeds/bookmarks/(.*) insipid.cgi?op=rss&tag=$1 [QSA]\r
+RewriteRule ^feeds/bookmarks insipid.cgi?op=rss [QSA]\r
+RewriteRule ^snapshot/(.*) insipid.cgi?op=viewsnapshot&md5=$1 [QSA]\r
+\r
+RewriteRule ^feeds/json/tags/(.*) insipid.cgi?op=json_tags&tag=$1 [QSA]\r
+RewriteRule ^feeds/json/tags insipid.cgi?op=json_tags [QSA]\r
+\r
+RewriteRule ^feeds/json/posts/(.*) insipid.cgi?op=json_posts&tag=$1 [QSA]\r
+RewriteRule ^feeds/json/posts insipid.cgi?op=json_posts [QSA]\r
+\r
diff --git a/insipid-config.cgi.example b/insipid-config.cgi.example
new file mode 100755 (executable)
index 0000000..8227a7d
--- /dev/null
@@ -0,0 +1,22 @@
+# This is the database configuration for Insipid.  The "dbtype" parameter\r
+# can be either "mysql" for the MySQL database or "Pg" for PostgreSQL.\r
+dbname = insipid_luke\r
+dbuser = luke\r
+dbpass = dbpass\r
+dbtype = mysql\r
+\r
+# If you'd like the Insipid database tables to use a prefix, uncomment\r
+# this line.\r
+#dbprefix = insipid_\r
+\r
+# The base URI to your installation.\r
+pagepath = /Luke\r
+\r
+# If your webserver is not running on port 80 and it doesn't properly set\r
+# the SERVER_PORT variable, set it here.\r
+#server_port = 8080\r
+\r
+# The login name and password for using the Insipid interface.\r
+username = luke\r
+userpass = yourpassword\r
+\r
diff --git a/insipid.cgi b/insipid.cgi
new file mode 100755 (executable)
index 0000000..413476e
--- /dev/null
@@ -0,0 +1,77 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+use warnings;\r
+use strict;\r
+\r
+# This stub checks for libraries and what not and then calls the main program.\r
+push(@INC, "lib");\r
+\r
+if(!-e "insipid-config.cgi") {\r
+       # TODO: Better error message here.\r
+       show_error("Configuration file missing", "The \"insipid-config.cgi\"" .\r
+               " file could not be found.");\r
+}\r
+\r
+\r
+eval {\r
+       require Insipid::Main;\r
+       Insipid::Main::main();\r
+};\r
+\r
+if($@) {\r
+       my $errstr = $@;        \r
+       if($errstr =~ /Can\'t locate (.*) in/) {\r
+               show_error("Couldn't find the module \"$1\".", "You may want to " .\r
+                       "<a href=\"http://search.cpan.org/\">search CPAN</a> " .\r
+                       "for the module or check the " .\r
+                       "<a href=\"http://www.neuro-tech.net/insipid/\">" .\r
+                       "Insipid</a> homepage for more information.");\r
+       } elsif (\r
+               ($errstr =~ /Couldn\'t acquire lock on id/) ||\r
+               ($errstr =~ /doesn\'t exist/) ||\r
+               ($errstr =~ /relation \".*\" does not exist/) ) {\r
+               # This means that a database connection was established but the\r
+               # tables were not found.\r
+               eval {\r
+                       require Insipid::Database;\r
+                       Insipid::Database::install();\r
+               };\r
+\r
+               if($@) {\r
+                       show_error("Database error", "There was a problem " . \r
+                       "creating the database tables required by Insipid.");\r
+               }\r
+       } else {\r
+               print "Content-Type: text/plain\r\n\r\nError: $@\r\n";\r
+       }\r
+}\r
+\r
+sub show_error {\r
+       my ($subject, $body) = (@_);\r
+       print "Content-Type: text/html\r\n\r\n";\r
+       print "<html><head><title>Insipid Error</title></head>";\r
+       print "<body>";\r
+       print "<h2>$subject</h2>";\r
+       print "<p><font size=\"+1\"></font></p>";\r
+       print "<p><font size=\"+1\">$body";\r
+       print "</font></p></body></html>";\r
+       exit;\r
+}\r
diff --git a/insipid.css b/insipid.css
new file mode 100755 (executable)
index 0000000..d6d509c
--- /dev/null
@@ -0,0 +1,178 @@
+/**\r
+ * Copyright (C) 2006 Luke Reeves\r
+ *\r
+ * This program is free software; you can redistribute it and/or modify\r
+ * it under the terms of the GNU General Public License as published by\r
+ * the Free Software Foundation; either version 2 of the License, or\r
+ * (at your option) any later version.\r
+ *\r
+ * This program is distributed in the hope that it will be useful,\r
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+ * GNU General Public License for more details.\r
+ *\r
+ * You should have received a copy of the GNU General Public License\r
+ * along with this program; if not, write to the Free Software\r
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+ * USA\r
+ */\r
+\r
+* {\r
+       font-family:Arial, Helvetica, sans-serif;\r
+}\r
+\r
+#leftside {\r
+       float:left;\r
+       padding-right:20;\r
+       padding-left:20;\r
+       padding-top:20;\r
+}\r
+\r
+.bodyContent {\r
+       margin-right: 15px;\r
+       margin-top: 10px;\r
+}\r
+\r
+.title {\r
+       padding-left:20px;\r
+       padding-top:10px;\r
+       color: blue;\r
+       font-size:medium;\r
+       font-weight:bold;\r
+       background-color: #CCCCCC;\r
+}\r
+\r
+.tagsummarytable{\r
+       border-color:#000000;\r
+       border-style:solid;\r
+       -moz-box-sizing: border-box;\r
+       -moz-border-radius:8px;\r
+       border-width: mid;\r
+       font-size: small;\r
+       margin-bottom: 15px;\r
+}\r
+\r
+.tagtabletext{\r
+       font-family:Arial, Helvetica, sans-serif;\r
+       text-decoration: none;\r
+       color:#000000;\r
+       \r
+}\r
+\r
+.bodytext{\r
+       font-family:Arial, Helvetica, sans-serif;\r
+       color: black; \r
+}\r
+\r
+/* Used for "Most Recent Bookmarks" */\r
+.bodyTitle{\r
+       color: black; \r
+       font-weight: bold;\r
+}\r
+\r
+.inline_title{\r
+       font-family:Arial, Helvetica, sans-serif;\r
+       color:#FFFFFF;\r
+       background:#000000;\r
+}\r
+\r
+.search {\r
+       padding-left: 10px;\r
+       text-align: left;\r
+       background-color: #CCCCCC;\r
+}\r
+\r
+.toolbar {\r
+       text-align: right;\r
+       padding-right: 10px;\r
+       background-color: #CCCCCC;\r
+       font-size: small;\r
+}\r
+h2 {\r
+       font-size: medium;\r
+       padding-top: 10px;\r
+}\r
+\r
+h3 {\r
+       font-size: medium;\r
+       text-align: center;\r
+       padding-top: 10px;\r
+}\r
+\r
+a.tools { \r
+       font-family:Arial, Helvetica, sans-serif;\r
+       color: black; \r
+       font-weight: bold;\r
+       line-height:2;\r
+}\r
+\r
+a.tools:visited {\r
+       color: black;\r
+}\r
+\r
+a.tools:hover { \r
+       font-family:Arial, Helvetica, sans-serif;\r
+       color: black; \r
+       font-weight: bold;\r
+       background:white;\r
+       text-decoration: none;\r
+       line-height:2;\r
+}\r
+\r
+\r
+.formtext { \r
+       font-family:Arial, Helvetica, sans-serif;\r
+       text-decoration: none; \r
+}\r
+\r
+\r
+A { \r
+       font-family:Arial, Helvetica, sans-serif;\r
+       text-decoration: none; \r
+}\r
+\r
+A:hover { \r
+       font-family:Arial, Helvetica, sans-serif;\r
+       text-decoration: underline \r
+}\r
+\r
+A:visited { \r
+       font-family:Arial, Helvetica, sans-serif;\r
+       text-decoration: none;\r
+       color:blue;\r
+}\r
+\r
+.error {\r
+       font-family:Arial, Helvetica, sans-serif;\r
+       margin-top: 40px;\r
+       margin-bottom: 10px;\r
+       color: red;\r
+}\r
+\r
+.bookmarkOperations {\r
+       font-family: Arial, Helvetica, sans-serif;\r
+       font-size: small;\r
+       margin-left: 20px;\r
+       color: #777777;\r
+}\r
+\r
+li{    color:#8888ff; }\r
+.bookmarklistitem{padding-bottom:8;}\r
+\r
+.bookmarklist{\r
+       font-size: medium;\r
+       margin-left: 20px;\r
+       border-width:mid;\r
+       border-color:#CCCCCC;\r
+       border-style:dotted;\r
+       padding-right:10px;\r
+}\r
+\r
+A.bookmarkTag:link { color: #8888ff; }\r
+A.bookmarkTag:visited { color: #8888ff; }\r
+A.bookmarkTag:hover { color: #8888ff; }\r
+\r
+A.bookmarkOp:link {    color: #800000; }\r
+A.bookmarkOp:visited { color: #800000; }\r
+A.bookmarkOp:hover { color: #800000; }\r
+\r
diff --git a/lib/Insipid/Bookmarks.pm b/lib/Insipid/Bookmarks.pm
new file mode 100755 (executable)
index 0000000..750e750
--- /dev/null
@@ -0,0 +1,214 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Bookmarks;\r
+\r
+use strict;\r
+use warnings;\r
+\r
+use vars qw(@ISA @EXPORT @EXPORT_OK $icount $duplicates);\r
+use Insipid::Config;\r
+use Insipid::Database;\r
+use Insipid::Schemas;\r
+use Insipid::Sessions;\r
+use Insipid::Tags;\r
+use Insipid::Util;\r
+use DBI qw/:sql_types/;;\r
+use Date::Format;\r
+use Date::Parse;\r
+use CGI qw/:standard/;\r
+use CGI::Carp qw(fatalsToBrowser);\r
+use Digest::MD5 qw(md5 md5_hex);\r
+\r
+require Exporter;\r
+\r
+@ISA = qw(Exporter);\r
+\r
+@EXPORT = qw(\r
+add_bookmark\r
+export_bookmarks\r
+get_bookmark_id_by_url\r
+$icount\r
+$duplicates\r
+);\r
+\r
+sub get_bookmark_id_by_url {\r
+       my ($url) = (@_);\r
+       my $sql = "select $tbl_bookmarks.id from $tbl_bookmarks \r
+                       where ($tbl_bookmarks.url = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($url);\r
+\r
+       my @r = $sth->fetchrow_array;\r
+       return $r[0];\r
+}\r
+\r
+sub add_bookmark {\r
+       my ($url, $title, $description, $access_level, $epoch, $tags) = (@_);\r
+       my ($sql, $sth);\r
+\r
+       if(logged_in() ne 1) {\r
+               push(@errors, 'You have to be logged in to perform ' .\r
+                       'that operation.');\r
+               return;\r
+       }\r
+\r
+       my $md5 = md5_hex($url);\r
+\r
+       # Check for duplicate\r
+       $sql = "select title from $tbl_bookmarks where (md5 = ?)";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute($md5);\r
+       \r
+       if($sth->rows ne 0) {\r
+               $duplicates++;\r
+               return;\r
+       }\r
+\r
+       $sql = "INSERT INTO $tbl_bookmarks \r
+               (url, md5, title, description, access_level, date) \r
+               VALUES (?, ?, ?, ?, ?, ?)";\r
+\r
+       if($epoch eq 0) { $epoch = time; }\r
+       $sth = $dbh->prepare($sql);\r
+\r
+       $sth->execute($url, $md5, $title, $description, $access_level, $epoch)\r
+               or die $DBI::errstr;\r
+       \r
+       $icount++;\r
+\r
+       set_tags(get_bookmark_id_by_url($url), $tags);\r
+}\r
+\r
+sub export_bookmarks {\r
+       my ($writer) = (@_);\r
+\r
+       my ($sql, $sth, $last_id);\r
+\r
+       $writer->startTag("posts");\r
+\r
+       $sql = "select \r
+                 $tbl_bookmarks.id, $tbl_bookmarks.title, \r
+                 $tbl_bookmarks.date, $tbl_bookmarks.access_level, \r
+                 $tbl_bookmarks.url, $tbl_tags.name\r
+               from $tbl_bookmarks\r
+               left join $tbl_bookmark_tags on\r
+                 ($tbl_bookmarks.id = $tbl_bookmark_tags.bookmark_id)\r
+               left join $tbl_tags on\r
+                 ($tbl_bookmark_tags.tag_id = $tbl_tags.id)";\r
+\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+\r
+       my ($url, $title);\r
+       my $tags = "";\r
+\r
+       my %last;\r
+       $last_id = -1;\r
+       my $current = 0;\r
+       my $max = $sth->rows;\r
+\r
+       # There HAS to be a better way to do this horrible looping for tags.\r
+       while(my $hr = $sth->fetchrow_hashref) {\r
+               $current++;\r
+\r
+               # For the first bookmark\r
+               if($last_id eq -1) {\r
+                 $last_id = $hr->{'id'};\r
+                 $last{title} = $hr->{'title'};\r
+                 $last{url} = $hr->{'url'};\r
+                 $last{tags} = "";\r
+                 $last{timestamp} = $hr->{'date'};\r
+                 $last{access_level} = $hr->{'access_level'};\r
+               } \r
+               \r
+               #if(($hr->{'id'} ne $last_id) || ($current eq $max)) {\r
+               if($hr->{'id'} ne $last_id) {\r
+                 # the id changed, so show the last mark.\r
+                 #my $url = sanitize_html($last{'url'});\r
+                 my $url = $last{'url'};\r
+                 my $title = $last{'title'};\r
+                 #my $title = sanitize_html($last{'title'});\r
+                 #$title =~ s/"/&quot;/gi;\r
+                 if(defined($last{tags})) {\r
+                       if($last{tags} eq "") {\r
+                               $last{tags} = "system:unfiled"; \r
+                       }\r
+                 } else {\r
+                       $last{tags} = "system:unfiled";\r
+                 }\r
+                 \r
+                 if($last{url} ne "") {\r
+                       my $tstr = time2str("%Y-%m-%dT%TZ", $last{timestamp}, "GMT");\r
+                       $writer->emptyTag('post',\r
+                               'access_level' => $last{access_level},\r
+                               'href' => $url,\r
+                               'description' => $title,\r
+                               'tag' => $last{tags},\r
+                               'time' => $tstr);\r
+                 }\r
+\r
+                 # Swap the new one in.\r
+                 $last_id = $hr->{'id'};\r
+                 $last{title} = $hr->{'title'};\r
+                 $last{url} = $hr->{'url'};\r
+                 $last{tags} = $hr->{'name'};\r
+                 $last{timestamp} = $hr->{'date'};\r
+                 $last{access_level} = $hr->{'access_level'};\r
+               } else {\r
+                 # Add tag to the current bookmark\r
+                 if($hr->{'name'}) {\r
+                         $last{tags} = "$last{tags} $hr->{'name'}";\r
+                 }\r
+               }\r
+       }\r
+       \r
+       if($last{'url'}) {\r
+               #$url = sanitize_html($last{'url'});\r
+               #$title = sanitize_html($last{'title'});\r
+               #$title =~ s/"/&quot;/gi;\r
+               \r
+               $url = $last{'url'};\r
+               $title = $last{'title'};\r
+               \r
+               if(defined($last{tags})) {\r
+                       if($last{tags} eq "") {\r
+                               $last{tags} = "system:unfiled"; \r
+                       }\r
+               } else {\r
+                       $last{tags} = "system:unfiled";\r
+               }\r
+                 \r
+               if($last{url} ne "") {\r
+                       my $tstr = time2str("%Y-%m-%dT%TZ", $last{timestamp}, "GMT");\r
+                       $writer->emptyTag('post',\r
+                               'access_level' => $last{access_level},\r
+                               'href' => $url,\r
+                               'description' => $title,\r
+                               'tag' => $last{tags},\r
+                               'time' => $tstr);\r
+               }\r
+       }\r
+\r
+       $writer->endTag("posts");\r
+}\r
+\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/Config.pm b/lib/Insipid/Config.pm
new file mode 100755 (executable)
index 0000000..213a57d
--- /dev/null
@@ -0,0 +1,103 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Config;\r
+\r
+use strict;\r
+use warnings;\r
+\r
+use CGI qw/:standard/;\r
+\r
+use vars qw(@ISA @EXPORT $pagepath $site_url $tag_url $feed_url $full_url \r
+       $tbl_authentication $tbl_bookmarks $tbl_tags $tbl_options\r
+       $tbl_bookmark_tags $tbl_pagecache $tbl_pagecache_references\r
+       @errors);\r
+@ISA = qw(Exporter);\r
+@EXPORT = qw(getconfig $pagepath $site_url $tag_url $feed_url \r
+               $tbl_authentication $tbl_bookmarks $tbl_tags\r
+               $tbl_options $tbl_bookmark_tags $tbl_pagecache\r
+               $tbl_pagecache_references $full_url @errors);\r
+\r
+my $config_file;\r
+my %config;\r
+\r
+if(-e "insipid-config.cgi") { $config_file = "insipid-config.cgi"; }\r
+if(-e "../insipid-config.cgi") { $config_file = "../insipid-config.cgi"; }\r
+\r
+# Read basic database and user configuration\r
+open (CFG, $config_file);\r
+while(my $line = <CFG>) {\r
+       if($line =~ /^[^#]/) {\r
+               $line =~ /(.*?)\s*=\s*(.*?)\s/;\r
+               if(defined($1)) {\r
+                       if(defined($2)) {\r
+                               $config{$1} = $2;\r
+                       } else {\r
+                               $config{$1} = "";\r
+                       }\r
+               }\r
+       }\r
+}\r
+close(CFG);\r
+\r
+sub getconfig {\r
+       my ($key) = (@_);\r
+       return $config{$key};\r
+}\r
+\r
+my $prefix = $ENV{'HTTPS'} ? 'https://' : 'http://';\r
+my $port = '';\r
+if(defined($ENV{'SERVER_PORT'})) {\r
+       if($ENV{'SERVER_PORT'} ne '80') {\r
+               $port = ':' . $ENV{'SERVER_PORT'};\r
+       }\r
+}\r
+\r
+# Override the port from the configuration file if available\r
+if(defined(getconfig('server_port'))) {\r
+       if(getconfig('server_port') ne '') {\r
+               $port = ':' . getconfig('server_port');\r
+       }\r
+}\r
+\r
+$site_url = $prefix . virtual_host() . $port . getconfig('pagepath');\r
+\r
+my $dbprefix;\r
+if(defined(getconfig('dbprefix'))) {\r
+       $dbprefix = getconfig('dbprefix');\r
+} else {\r
+       $dbprefix = '';\r
+}\r
+\r
+# Table names.\r
+$tbl_authentication            = $dbprefix . 'authentication';\r
+$tbl_bookmarks                 = $dbprefix . 'bookmarks';\r
+$tbl_tags                      = $dbprefix . 'tags';\r
+$tbl_bookmark_tags             = $dbprefix . 'bookmark_tags';\r
+$tbl_options                   = $dbprefix . 'options';\r
+$tbl_pagecache                 = $dbprefix . 'pagecache';\r
+$tbl_pagecache_references      = $dbprefix . 'pagecache_references';\r
+\r
+\r
+\r
+1;\r
+\r
+__END__\r
+\r
diff --git a/lib/Insipid/Database.pm b/lib/Insipid/Database.pm
new file mode 100755 (executable)
index 0000000..2db1007
--- /dev/null
@@ -0,0 +1,184 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Database;\r
+\r
+use strict;\r
+use warnings;\r
+\r
+use Insipid::Config;\r
+use Insipid::Schemas;\r
+\r
+use DBI qw/:sql_types/;;\r
+use vars qw($version);\r
+\r
+use Exporter ();\r
+our (@ISA, @EXPORT);\r
+       \r
+@ISA = qw(Exporter);\r
+@EXPORT = qw($dbname $dbuser $dbpass $dsn $dbh $dbtype get_option \r
+       install $version $tag_url $feed_url $full_url $snapshot_url\r
+       export_options $dbprefix);\r
+       \r
+our ($dsn, $dbh, $dbname, $dbuser, $dbpass, $snapshot_url,\r
+       $dbtype, $tag_url, $feed_url, $full_url, $dbprefix);\r
+\r
+$dbname = getconfig('dbname');\r
+$dbuser = getconfig('dbuser');\r
+$dbpass = getconfig('dbpass');\r
+\r
+if(defined(getconfig('dbtype'))) {\r
+       $dbtype = getconfig('dbtype');\r
+} else {\r
+       $dbtype = 'mysql';\r
+}\r
+\r
+$dsn = "DBI:$dbtype:dbname=$dbname;host=localhost";\r
+$dbh = DBI->connect($dsn, $dbuser, $dbpass, { 'RaiseError' => 0}) or die $DBI::errstr;\r
+\r
+my %options;\r
+\r
+my $sql = "select name, value from $tbl_options";\r
+my $sth = $dbh->prepare($sql);\r
+$sth->execute() or die $DBI::errstr;\r
+\r
+while(my $hr = $sth->fetchrow_hashref) {\r
+       $options{$hr->{'name'}} = $hr->{'value'};\r
+}\r
+\r
+if(need_upgrade() eq 1) {\r
+       dbupgrade();\r
+}\r
+\r
+sub export_options {\r
+       my ($writer) = (@_);\r
+       my ($sth);\r
+       \r
+       $writer->startTag('options');\r
+       $sth = $dbh->prepare("select name, value from $tbl_options");\r
+       $sth->execute();\r
+       while(my $row = $sth->fetchrow_hashref) {\r
+               if($row->{name} ne 'version') {\r
+                       $writer->emptyTag('option', \r
+                               'name' => $row->{name},\r
+                               'value' => $row->{value});\r
+               }\r
+       }\r
+       \r
+       $writer->endTag('options');\r
+}\r
+\r
+sub dbupgrade {\r
+       print STDERR "Upgrading Insipid database...\n";\r
+\r
+       my $sql = "update $tbl_options set value = ? where (name = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($version, 'version');\r
+\r
+       $sql = "insert into $tbl_options(name, value, description) \r
+                       values(?, ?, ?)";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute('version', $version, 'Internal Insipid version');\r
+       $sth->execute('use_rewrite', 'yes', 'Use mod_rewrite - disable this if you do not want to use mod_rewrite.');\r
+\r
+       # Delete the old sessions table\r
+       $sql = 'drop table sessions';\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+\r
+       # Create the new session table if it's not there.\r
+       $sql = "create table $tbl_authentication (\r
+                       session_id varchar(32),\r
+                       create_time int,\r
+                       primary key(session_id))";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+       if($dbh->errstr) {\r
+               print STDERR $dbh->errstr;\r
+       }\r
+       \r
+       return;\r
+}\r
+\r
+# Check if we need an upgrade\r
+sub need_upgrade {\r
+       if(!defined($options{version})) { return 1; }\r
+\r
+       if($options{version} ne $version) {\r
+               return 1;\r
+       } else {\r
+               return 0;\r
+       }\r
+}\r
+\r
+# Functions\r
+\r
+sub get_option {\r
+       my ($name) = (@_);\r
+       return $options{$name};\r
+}\r
+\r
+sub install {\r
+       my ($sth, @creates);\r
+       \r
+       print "Content-Type: text/html\r\n\r\n";\r
+       print "<html><head><title>Insipid Installation</title></head><body>";\r
+\r
+       print "<p>Creating tables...";\r
+\r
+       if($dbtype eq 'mysql') {\r
+               @creates = split(/\;/, $createMySQL);\r
+       } else {\r
+               @creates = split(/\;/, $createPostgres);\r
+       }\r
+\r
+       foreach(@creates) {\r
+               my $sql = $_;\r
+               if(length($sql) > 2) {\r
+                       $sth = $dbh->prepare($sql);\r
+                       $sth->execute() or print "<br />Error executing \"$sql\" - $DBI::errstr<br />";\r
+               }\r
+       }\r
+       print " done!</p>";\r
+\r
+       print "<p>Insipid's database has been installed.  You can reload this " .\r
+               "page to start using Insipid.</p>";\r
+       \r
+       print "</body></html>";\r
+\r
+}\r
+\r
+# This configures the URLs in the application to support mod_rewrite or\r
+# a webserver sans mod_rewrite.\r
+if(get_option('use_rewrite') eq 'yes') {\r
+       $tag_url        = $site_url . '/bookmarks/';\r
+       $feed_url       = $site_url . '/feeds/bookmarks';\r
+       $full_url       = $site_url . '/bookmarks';\r
+       $snapshot_url   = $site_url . '/snapshot/';\r
+} else {\r
+       $tag_url        = 'insipid.cgi?tag=';\r
+       $feed_url       = $site_url . '/insipid.cgi?op=rss&tag=';\r
+       $full_url       = $site_url . '/insipid.cgi';\r
+       $snapshot_url   = 'insipid.cgi?op=viewsnapshot&md5=';\r
+}\r
+\r
+\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/JSON.pm b/lib/Insipid/JSON.pm
new file mode 100755 (executable)
index 0000000..2f11583
--- /dev/null
@@ -0,0 +1,427 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::JSON;\r
+\r
+use strict;\r
+use warnings;\r
+\r
+use vars qw(@ISA @EXPORT @EXPORT_OK);\r
+use CGI qw/:standard/;\r
+use CGI::Carp qw(fatalsToBrowser);\r
+use Insipid::Config;\r
+use Insipid::Database;\r
+use Insipid::Sessions;\r
+use Date::Format;\r
+use Date::Parse;\r
+\r
+require Exporter;\r
+\r
+@ISA = qw(Exporter);\r
+\r
+@EXPORT = qw(\r
+send_json_tags\r
+send_json_posts\r
+);\r
+\r
+my $query = '';\r
+my $last_page = 0;\r
+\r
+\r
+\r
+sub send_json_tags {\r
+       my ($sql, $sth);\r
+       \r
+       # Building up JSON structure before parsing the data:\r
+       my ($json_prefix,$json_suffix);\r
+       # limiting url_param('callback')  to a reasonable length (100): \r
+       if((defined(url_param('callback'))) && (length(url_param('callback')) < 100)){\r
+               $json_prefix = ''.url_param('callback').'({';\r
+               $json_suffix = '})';\r
+               }  elsif(url_param('raw') eq 1) {\r
+                               $json_prefix = '{';\r
+                               $json_suffix = '}';\r
+                       } else {\r
+                                       $json_prefix = 'if(typeof(Insipid) == \'undefined\') Insipid = {}; Insipid.tags = {';\r
+                                       $json_suffix = '}';\r
+                               }\r
+       #limiting tags count, only if url_param('count') is a valid integer:\r
+       my $limit ;\r
+       if (url_param('count') =~ /^[+-]?\d+$/) {\r
+               $limit = ' limit '.url_param('count') ;\r
+       }\r
+\r
+\r
+\r
+       # If the user has already chosen a tag, get the intersection list\r
+       if((url_param('tag')) && (logged_in() eq 1)) {\r
+               $sql = "select $tbl_tags.name,count(*) from $tbl_bookmarks\r
+                       inner join $tbl_bookmark_tags as bt1 on\r
+                               ($tbl_bookmarks.id = bt1.bookmark_id)\r
+                       inner join $tbl_tags on\r
+                               ($tbl_tags.id = bt1.tag_id)\r
+                       inner join $tbl_bookmark_tags as bt2 on\r
+                               ($tbl_bookmarks.id = bt2.bookmark_id)\r
+                       inner join $tbl_tags as t2 on\r
+                               (t2.id = bt2.tag_id and t2.name = ?)\r
+                       where ($tbl_tags.name != ?)\r
+                       group by $tbl_tags.name $limit";\r
+               $sth = $dbh->prepare($sql);\r
+               $sth->execute(url_param('tag'), url_param('tag'));\r
+       print $json_prefix ;\r
+       if($sth->rows ne 0) {\r
+               my $icount = 1 ;\r
+               while(my @r = $sth->fetchrow_array()) {\r
+                       json_show_tag($icount, $sth->rows, $r[0], $r[1]);\r
+                       $icount++ ;\r
+               }\r
+       }\r
+       print $json_suffix ;    \r
+       return ;\r
+       } else {\r
+\r
+       # Access_spec contains a where clause to count only public bookmarks\r
+       # if the user is not logged in\r
+       my $access_where = "";\r
+       if(logged_in() eq 0) {\r
+               $access_where = " where ($tbl_bookmarks.access_level = 1) ";\r
+       }\r
+\r
+       my $order_clause;\r
+       if($dbtype eq "Pg") {\r
+               $order_clause = "order by upper($tbl_tags.name)";\r
+       } else {\r
+               $order_clause = "order by $tbl_tags.name";\r
+       }\r
+\r
+       $sql = "select $tbl_tags.name, count(*)\r
+                  from $tbl_bookmarks\r
+                  inner join $tbl_bookmark_tags on\r
+                       ($tbl_bookmarks.id = $tbl_bookmark_tags.bookmark_id)\r
+                  inner join $tbl_tags on\r
+                       ($tbl_tags.id = $tbl_bookmark_tags.tag_id)\r
+                  $access_where\r
+                  group by $tbl_tags.name\r
+                  $order_clause \r
+                  $limit";\r
+\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute;\r
+       print $json_prefix;\r
+       if($sth->rows ne 0) {\r
+               my $icount = 1;\r
+               while(my @r = $sth->fetchrow_array()) {\r
+                       json_show_tag($icount, $sth->rows, $r[0], $r[1]);       \r
+                       $icount++;\r
+               }\r
+       }\r
+       print $json_suffix;\r
+       return;\r
+       }\r
+}\r
+\r
+sub json_show_tag {\r
+       my($icount, $rowscount, $tag, $tagcount) = (@_);\r
+       $tag =~ s/\"/\\"/g ;\r
+       my $json_txt = '';\r
+       $json_txt = $json_txt.'"'.$tag.'":'.$tagcount;\r
+       if ($icount ne $rowscount){\r
+               $json_txt = $json_txt.',';\r
+       }\r
+       print $json_txt ;\r
+}\r
+\r
+sub send_json_posts {\r
+       \r
+       # Building up JSON structure before parsing the data:\r
+       my ($json_prefix,$json_suffix);\r
+       # limiting url_param('callback')  to a reasonable length: \r
+       if((defined(url_param('callback'))) && (length(url_param('callback')) < 100)){\r
+               $json_prefix = ''.url_param('callback').'([';\r
+               $json_suffix = '])';\r
+               }  elsif(url_param('raw') eq 1) {\r
+                               $json_prefix = '[';\r
+                               $json_suffix = ']';\r
+                       } else {\r
+                                       $json_prefix = 'if(typeof(Insipid) == \'undefined\') Insipid = {}; Insipid.posts = [';\r
+                                       $json_suffix = ']';\r
+                               }\r
+       #limiting posts count to  url_param('count') , setting hard limit to 100 and  default limit to 50 :\r
+       my $limit ;\r
+       if ((url_param('count') =~ /^[+-]?\d+$/) && (url_param('count') < 101)) {\r
+               $limit = url_param('count') ;\r
+       } else {\r
+               $limit = 50 ;\r
+       }\r
+       \r
+       my ($subquery, $sql, $sth, @parms, @wheres, @hr);\r
+\r
+       # this first query will be used to select from a set, like when a user\r
+       # drills in on a specific tag or to get a smaller view of the entire\r
+       # dataset (for paging purposes).\r
+\r
+       # MySQL and postgres have slightly different syntax here...\r
+       if ($dbtype eq 'mysql') {\r
+           $sql = "select $tbl_bookmarks.id from $tbl_bookmarks";\r
+       } elsif ($dbtype eq 'Pg') {\r
+           $sql = "select $tbl_bookmarks.id, $tbl_bookmarks.date\r
+               from $tbl_bookmarks";\r
+       }\r
+\r
+       # Limit to tags\r
+       if(defined(url_param('tag'))) {\r
+       # Join the tag tables only when necessary\r
+\r
+               if(url_param('tag') =~ / /) {\r
+                       my @tags = split(/ /, url_param('tag'));\r
+                       my $icount = 1;\r
+\r
+                       foreach(@tags) {\r
+                               push(@parms, $_);\r
+                               $sql = "$sql inner join $tbl_bookmark_tags\r
+                                               as bt$icount on\r
+                                         ($tbl_bookmarks.id =\r
+                                               bt$icount.bookmark_id)\r
+                                       inner join $tbl_tags as t$icount on\r
+                                          (t$icount.id = bt$icount.tag_id\r
+                                               and t$icount.name = ?) ";\r
+                               $icount++;\r
+                       }\r
+               } else {\r
+                       $sql = "$sql\r
+                               left join $tbl_bookmark_tags on\r
+                                 ($tbl_bookmarks.id =\r
+                                       $tbl_bookmark_tags.bookmark_id)\r
+                               inner join $tbl_tags on\r
+                                 ($tbl_tags.id = $tbl_bookmark_tags.tag_id)\r
+                                 where ($tbl_tags.name = ?)";\r
+                       push(@parms, url_param('tag'));\r
+               }\r
+\r
+       }\r
+\r
+       # Search \r
+       # ?q=\r
+       $query = url_param('q');\r
+       if($query ne "") {\r
+               if((get_option("public_searches") eq "yes") || (logged_in() eq 1)) {\r
+                       my $sparm = $query;\r
+                       if(length($sparm) > 2) {\r
+                               $sql = "$sql where ($tbl_bookmarks.title like ?)";\r
+                               $sparm =~ s/\%//;\r
+                               $sparm = "\%$sparm\%";\r
+                               push(@parms, $sparm);\r
+                       }\r
+               }\r
+       }\r
+\r
+       # order\r
+       $sql = "$sql order by $tbl_bookmarks.date desc";\r
+\r
+       # paging functionality\r
+       $sql = "$sql limit $limit";\r
+       \r
+       \r
+       if(url_param('page')) {\r
+           my $offset = ((url_param('page') - 1) * $limit);\r
+           $sql = "$sql offset $offset";\r
+       }\r
+\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute(@parms);\r
+\r
+       $subquery = "";\r
+       if($sth->rows > 0) {\r
+               if($sth->rows ne $limit) { $last_page = 1; }\r
+\r
+               $subquery = " $tbl_bookmarks.id in (";\r
+\r
+               while(@hr = $sth->fetchrow_array) {\r
+                       $subquery = $subquery . "$hr[0],";\r
+               }\r
+               chop($subquery); # Strip off the last delimiter\r
+\r
+               $subquery = $subquery . ")";\r
+       } else {\r
+               # no bookmarks found:\r
+               ###################\r
+               print $json_prefix ;\r
+               print $json_suffix ;\r
+               ###################\r
+               return;\r
+       }\r
+\r
+       @parms = ();\r
+       @wheres = ();\r
+\r
+        $sql = "select\r
+                 $tbl_bookmarks.id,\r
+                 $tbl_bookmarks.title,\r
+                 $tbl_bookmarks.description,\r
+                 $tbl_bookmarks.access_level,\r
+                 $tbl_bookmarks.url,\r
+                 $tbl_tags.name,\r
+                 $tbl_bookmarks.date,\r
+                 $tbl_pagecache.date as cache_date,\r
+                 $tbl_bookmarks.md5\r
+               from $tbl_bookmarks\r
+               left join $tbl_bookmark_tags on\r
+                 ($tbl_bookmarks.id = $tbl_bookmark_tags.bookmark_id)\r
+               left join $tbl_tags on\r
+                 ($tbl_tags.id = $tbl_bookmark_tags.tag_id)\r
+               left join $tbl_pagecache on\r
+                 ($tbl_bookmarks.md5 = $tbl_pagecache.md5)";\r
+\r
+       # Don't show private marks for non-logged in users\r
+       if(logged_in() eq 0) {\r
+         push(@wheres, "$tbl_bookmarks.access_level");\r
+         push(@parms, "1");\r
+       }\r
+\r
+       my $max = @wheres;\r
+       if($max ne 0) {\r
+         $sql = "$sql where (";\r
+         my $count = 1;\r
+\r
+         foreach (@wheres) {\r
+           $sql = "$sql $_ = ?";\r
+           if($count < $max) {\r
+             $sql = "$sql and ";\r
+           }\r
+           $count++;\r
+         }\r
+\r
+         $sql = "$sql )";\r
+         if($subquery ne "") { $sql = "$sql and $subquery"; }\r
+       } else {\r
+         if($subquery ne "") { $sql = "$sql where $subquery "; }\r
+       }\r
+\r
+    # append sort order.\r
+       $sql = "$sql order by $tbl_bookmarks.date desc";\r
+\r
+       $sth = $dbh->prepare($sql);\r
+        $sth->execute(@parms);\r
+\r
+       my %last;\r
+       $last{id} = -1;\r
+       \r
+       \r
+       ###################\r
+       print $json_prefix ;\r
+       ###################\r
+       \r
+       while(@hr = $sth->fetchrow_array) {\r
+               if($last{id} eq -1) {\r
+                 $last{id} = $hr[0];\r
+                 $last{title} = $hr[1];\r
+                 $last{description} = $hr[2];\r
+                 $last{access_level} = $hr[3];\r
+                 $last{url} = $hr[4];\r
+                 $last{tags} = "";\r
+                 $last{timestamp} = $hr[6];\r
+               }\r
+\r
+               if($hr[0] ne $last{id}) {\r
+                 # the id changed, so show the last mark.\r
+                 json_show_post(0,$last{id}, $last{title}, $last{description}, $last{access_level}, $last{url}, $last{tags}, $last{timestamp});\r
+\r
+                 # Swap the new one in.\r
+                 $last{id} = $hr[0];\r
+                 $last{title} = $hr[1];\r
+                 $last{description} = $hr[2];\r
+                 $last{access_level} = $hr[3];\r
+                 $last{url} = $hr[4];\r
+                 $last{tags} = $hr[5];\r
+                 $last{timestamp} = $hr[6];\r
+               } else {\r
+                 # Add tag to the current bookmark\r
+                 if(defined($hr[5])) {\r
+                   $last{tags} = "$last{tags} $hr[5]";\r
+                 }\r
+               }\r
+       }\r
+\r
+       if($last{id} ne -1) {\r
+               json_show_post(1,$last{id}, $last{title}, $last{description}, $last{access_level}, $last{url}, $last{tags}, $last{timestamp});\r
+       }\r
+\r
+       ###################\r
+       print $json_suffix ;\r
+       ###################\r
+}\r
+\r
+\r
+sub json_show_post {\r
+       my($last_mark, $id, $title, $description, $access_level, $url,\r
+               $tags, $timestamp) = (@_);\r
+       $title =~ s/\"/\\"/g ;\r
+       $description =~ s/\"/\\"/g ;\r
+       $tags =~ s/\"/\\"/g ;\r
+       my $json_txt = '{';\r
+       \r
+       if($access_level eq 0) {\r
+               $json_txt = $json_txt.'"u":"'.$site_url.'/insipid.cgi?go='.$id.'",';\r
+               $json_txt = $json_txt.'"d":"'.$title.'",';\r
+       } else {\r
+               $json_txt = $json_txt.'"u":"'.$url.'",';\r
+               $json_txt = $json_txt.'"d":"'.$title.'",';\r
+       }\r
+\r
+       if($description){\r
+               $json_txt = $json_txt.'"n":"'.$description.'",';\r
+       }\r
+       \r
+       my $timestr = '';\r
+       if(logged_in() eq 1) {\r
+               $timestr = time2str('%Y-%m-%d %T EST', $timestamp, 'EST');\r
+       } else {\r
+               $timestr = time2str('%Y-%m-%d', $timestamp, 'EST');\r
+       }\r
+\r
+       $json_txt = $json_txt.'"dt":"'.$timestr.'"';\r
+\r
+       if($tags) {\r
+               $json_txt = $json_txt.',"t":[';\r
+               my $cur;\r
+               my @tags = split(/\ /, $tags);\r
+               my $icount = 1 ;\r
+               foreach my $tag (@tags) {\r
+                       if($tag){\r
+                               $json_txt = $json_txt.'"'.$tag.'"';\r
+                                       if ($icount ne @tags){\r
+                                               $json_txt = $json_txt.',';\r
+                                       }\r
+                       }\r
+               $icount++ ;\r
+               }\r
+               $json_txt = $json_txt.']';  \r
+       }\r
+       \r
+       $json_txt = $json_txt.'}';\r
+       \r
+       if($last_mark ne 1){\r
+               $json_txt = $json_txt.','; \r
+       }\r
+       \r
+       print $json_txt ;\r
+}\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/LinkExtractor.pm b/lib/Insipid/LinkExtractor.pm
new file mode 100755 (executable)
index 0000000..08cb43a
--- /dev/null
@@ -0,0 +1,128 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::LinkExtractor;\r
+\r
+use strict;\r
+use warnings;\r
+use HTML::Parser;\r
+use HTML::Entities ();\r
+use URI::URL;\r
+use URI::WithBase;\r
+use Digest::MD5 qw(md5 md5_hex);\r
+use Insipid::Config;\r
+use Insipid::Database;\r
+use Insipid::Snapshots;\r
+\r
+use vars qw(@ISA);\r
+@ISA = qw(HTML::Parser);\r
+\r
+my $url = '';\r
+\r
+sub new {\r
+       my $pack = shift;\r
+       $url = shift;\r
+       my $self = $pack->SUPER::new;\r
+       $self;\r
+}\r
+\r
+sub declaration {\r
+       my $self = shift;\r
+       my ($decl) = @_;\r
+\r
+       1;\r
+}\r
+\r
+sub start {\r
+       my $self = shift;\r
+       my ($tag, $attr, $attrseq, $origtext) = @_;\r
+\r
+       #print "Found tag $tag<br />";\r
+       if($tag eq 'a') {\r
+               my $href = $attr->{'href'};\r
+               if(defined($href)) {\r
+                       if($href =~ /(\.gif|\.jpg|\.png)/i) {\r
+                               grab_image($url, $href);\r
+                       }\r
+               }\r
+       }\r
+\r
+       1;\r
+}\r
+\r
+sub grab_image {\r
+       my ($base_url, $t) = (@_);\r
+       my ($sql, $sth);\r
+       \r
+       my $u1 = URI::WithBase->new($t, $base_url);\r
+       my $target_url = $u1->abs;\r
+       \r
+       print "Fetching $target_url... ";\r
+\r
+       my $target_md5 = md5_hex($target_url);\r
+\r
+       # Check if this already exists.\r
+       $sql = "select count(*) from $tbl_pagecache where (md5 = ?)";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute($target_md5);\r
+       my @r = $sth->fetchrow_array();\r
+\r
+       if($r[0] ne 0) {\r
+               print " Already exists. <br />";\r
+               return;\r
+       }\r
+\r
+       my $rv = Insipid::Snapshots::fetch_url($target_url, $base_url);\r
+\r
+       if($rv eq 0) {\r
+               $sql = "insert into $tbl_pagecache_references(\r
+                       md5_parent, md5_child) values(?, ?)";\r
+               $sth = $dbh->prepare($sql);\r
+               $sth->execute(md5_hex($base_url), $target_md5);\r
+       \r
+               print "OK.<br />";\r
+       }\r
+       \r
+       1;\r
+}\r
+\r
+sub end {\r
+       my $self = shift;\r
+       my ($tag) = @_;\r
+\r
+       1;\r
+}\r
+\r
+sub text {\r
+       my $self = shift;\r
+       my ($text) = @_;\r
+\r
+       1;\r
+}\r
+\r
+sub comment {\r
+       my $self = shift;\r
+       my ($comment) = @_;\r
+\r
+       1;\r
+}\r
+       \r
+1;\r
+\r
diff --git a/lib/Insipid/Main.pm b/lib/Insipid/Main.pm
new file mode 100755 (executable)
index 0000000..e062ad3
--- /dev/null
@@ -0,0 +1,1160 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Main;\r
+\r
+use warnings;\r
+use strict;\r
+\r
+use vars qw(@ISA @EXPORT @EXPORT_OK);\r
+require Exporter;\r
+\r
+@ISA = qw(Exporter);\r
+\r
+@EXPORT = qw(main);\r
+\r
+use Insipid::Config;\r
+use Insipid::Database;\r
+use Insipid::Bookmarks;\r
+use Insipid::RSS;\r
+use Insipid::JSON;\r
+use Insipid::Sessions;\r
+use Insipid::Snapshots;\r
+use Insipid::Tags;\r
+use Insipid::Util;\r
+\r
+use CGI qw/:standard/;\r
+use CGI::Carp qw(fatalsToBrowser);\r
+use URI::Escape;\r
+use IO::File;\r
+use XML::Parser;\r
+use XML::Writer;\r
+use Date::Format;\r
+use Date::Parse;\r
+use DBI qw/:sql_types/;;\r
+use Digest::MD5 qw(md5 md5_hex);\r
+use MIME::Base64;\r
+use LWP::UserAgent;\r
+use HTTP::Request;\r
+use HTTP::Response;\r
+\r
+my $NL = "<br />\n";\r
+my @valid;\r
+my $icount = 0;\r
+my %options;\r
+my $tspec = "";\r
+my $query = "";\r
+my $last_page = 0;\r
+my $site_title;\r
+\r
+if(!defined($ENV{SERVER_NAME})) {\r
+       $NL = "\n";\r
+}\r
+\r
+sub main {\r
+\r
+       my $username = getconfig('username');\r
+       my $userpass = getconfig('userpass');\r
+\r
+       my $redirect = '';\r
+       my $et = '';\r
+\r
+       # Valid options:\r
+       @valid = ('feed_name', 'site_name', 'public_searches',\r
+                       'use_rewrite', 'proxy_host', 'proxy_port');\r
+\r
+       # Get the basic options\r
+       $site_title = get_option('site_name');\r
+       if($site_title eq '') {\r
+               $site_title = 'Insipid Bookmarks';\r
+       }\r
+\r
+       # Initialize variables that can be posted and in the URL.\r
+       if(defined(url_param('q'))) {\r
+               $query = url_param('q');\r
+       }\r
+\r
+       if(defined(param('q'))) {\r
+               $query = param('q');\r
+       }\r
+\r
+       # Check to see if a username and password have been posted\r
+       if(defined(param('password')) && defined(param('username'))) {\r
+         if( (param('password') eq $userpass) && (param('username') eq $username) ) {\r
+           my $rv = login();\r
+           print $rv;\r
+         } else {\r
+           push(@errors, "Invalid username or password.");\r
+         }\r
+       }\r
+\r
+       # Operations for non-HTML content\r
+\r
+       if(defined(url_param('op'))) {\r
+               if(url_param('op') eq 'export') {\r
+                       my $sn = 'n';\r
+                       if(defined(param('snapshots'))) {\r
+                               $sn = 'y';\r
+                       }\r
+\r
+                       do_export($sn);\r
+               }\r
+       \r
+               if(defined(param('op'))) {\r
+                       if(logged_in() eq 1) {\r
+                               if(param('op') eq 'logout') {\r
+                                       my $rv = logout();\r
+                                       print $rv;\r
+                               }\r
+                       }\r
+               }\r
+\r
+         # RSS\r
+         if(url_param('op') eq 'rss') {\r
+           print "Content-Type: text/xml\r\n\r\n";\r
+           send_rss();\r
+           exit;\r
+         }\r
+         \r
+         # JSON\r
+         # JSON Show tags:\r
+         if(url_param('op') eq 'json_tags') {\r
+           print "Content-Type: application/x-javascript;charset=UTF-8\r\n\r\n";\r
+               send_json_tags();\r
+           exit;\r
+         }\r
+         # JSON Show bookmarks:\r
+         if(url_param('op') eq 'json_posts') {\r
+           print "Content-Type: application/x-javascript;charset=UTF-8\r\n\r\n";\r
+           send_json_posts();\r
+           exit;\r
+         }\r
+         \r
+         # Cache\r
+         if(url_param('op') eq 'viewsnapshot') {\r
+           check_access();\r
+           if(param('md5')) {\r
+               show_snapshot(param('md5'));\r
+           }\r
+         }\r
+       }\r
+\r
+       # Allow redirections to a bookmark if the user's logged in.\r
+       # This allows private bookmarks to not send a referer.\r
+       if(logged_in() eq 1) {\r
+         if(param('go')) {\r
+           my $bid = param('go');\r
+           my $sql = "select url from $tbl_bookmarks\r
+                        where ($tbl_bookmarks.id = ?)";\r
+           my $sth = $dbh->prepare($sql);\r
+           $sth->execute($bid);\r
+           my $hr = $sth->fetchrow_hashref;\r
+           if(defined($hr->{'url'})) {\r
+             print "Cache-Control: private, must-revalidate\n";\r
+             print "Content-Type: text/html; charset=UTF-8\n\n";\r
+             print "<META HTTP-EQUIV=Refresh CONTENT=\"0; URL=$hr->{'url'}\">\n";\r
+             exit;\r
+           } else {\r
+             push(@errors, "Bookmark does not exist.");\r
+           }\r
+         }\r
+       }\r
+\r
+       # Add description to the HTML title tag.\r
+       if(url_param('tag')) {\r
+               $tspec = "/" . url_param('tag');\r
+               $tspec =~ s/ /\+/g;\r
+               my $tt = url_param('tag');\r
+               $tt =~ s/ / \+ /g;\r
+               $et = sprintf(" - %s", $tt);\r
+       }\r
+       if($query ne "") {\r
+               $et = sprintf(" - search results for \"%s\"", $query);\r
+       }\r
+\r
+       if(logged_in() eq 1) {\r
+         print "Cache-Control: private, must-revalidate\n";\r
+       }\r
+\r
+       print "Content-Type: text/html; charset=UTF-8\n\n";\r
+\r
+\r
+       print <<DOC;\r
+       <html>\r
+         <head>\r
+           <title>$site_title$et</title>\r
+           <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />\r
+           <link rel="alternate" type="application/rss+xml" title="RSS" href="$feed_url$tspec" />\r
+           <link rel="stylesheet" href="$site_url/insipid.css" type="text/css" title="Standard" />\r
+         </head>\r
+         <body marginheight="0" marginwidth="0">\r
+DOC\r
+\r
+\r
+       ###### Operations that don't touch the screen\r
+       if(defined(param('op')) && defined(param('id'))) {\r
+         if(param('op') eq 'delete_bookmark') {\r
+           my $id = param('id');\r
+           delete_bookmark($id);\r
+         } \r
+       }\r
+\r
+\r
+       # If the user just saved a bookmark, redirect them now.\r
+       if($redirect ne "") {\r
+         print "<script language=\"JavaScript\">document.location = \"$redirect\";</script>";\r
+         print "</body></html>";\r
+         exit;\r
+       }\r
+\r
+       show_toolbar();\r
+       show_tags();\r
+\r
+       print '<table class="bodyContent" border="0"><tr><td>';\r
+\r
+       if(defined(url_param('op'))) {\r
+         if(url_param('op') eq 'export') {\r
+               if(!defined(param('target'))) {\r
+                       print "<br /><br /><form method=\"post\" class=\"formText\">";\r
+                       print "<input type=\"checkbox\" name=\"snapshots\" />Include Snapshots<br />";\r
+                       print "<input type=\"submit\" value=\"Export\" /></form>";\r
+               }\r
+         }\r
+         \r
+         if(url_param('op') eq 'import') {\r
+           check_access();\r
+           if(param('fileupload')) {\r
+             do_import();\r
+           } else {\r
+             print <<IFORM;\r
+       <p>This allows you to import either \r
+       <a href="http://www.neuro-tech.net/insipid/">Insipid</a> or \r
+       <a href="http://del.icio.us/">del.icio.us</a> backups.  For del.icio.us, you\r
+       must first use their API to export your bookmarks to an XML file.  To do this,\r
+       access the URL "http://username:password\@del.icio.us/api/posts/all?" \r
+       (using your username and password).  You can then upload that XML file here.\r
+       </p>\r
+       <br />\r
+       <form class="formtext" enctype="multipart/form-data" action="$site_url/insipid.cgi?op=import" method="post">\r
+       Import from:<br />\r
+       <input type="file" name="fileupload" size="30"><br />\r
+       <input type="hidden" name="op" value="import" />\r
+       <input type="submit" value="Import" />\r
+       </form>\r
+IFORM\r
+           }\r
+         }\r
+       }\r
+\r
+       if(defined(param('op'))) {\r
+         if(param('op') eq 'login') {\r
+             login_form();\r
+         } \r
+\r
+         if((param('op') eq 'add_bookmark') || (param('op') eq 'edit_bookmark') ) {\r
+           #check to see if the url is bookmarked, then indicate that this is an edit.\r
+           my ($id, $url, $title, $description, $button, \r
+               $tags, $extra_params, $access_level, $access_box) =\r
+              (-1, "", "", "", "",  "", "", 0, "");\r
+\r
+           $access_level = 0;\r
+         \r
+           if(defined(param('save'))) {\r
+             ($url, $title, $description, $tags) = \r
+               (param('url'), param('title'), param('description'), param('tags'));\r
+\r
+             if(defined(param('access_level'))) {\r
+               if(param('access_level') eq 'on') {\r
+                 $access_level = 1;\r
+               } else {\r
+                 $access_level = 0;\r
+               }\r
+             }\r
+               \r
+             if(param('id')) {\r
+               update_bookmark(param('id'), $url, $title, $description, $access_level, $tags);\r
+             } else {\r
+               add_bookmark($url, $title, $description, $access_level, 0, $tags);\r
+               if(param('snapshot')) {\r
+                 if(param('snapshot') eq 'on') {\r
+                   $id = get_bookmark_id(param('url'));\r
+                   do_snapshot($id);\r
+                 }\r
+               }\r
+             }\r
+\r
+             if(param('redirect')) {\r
+               if(param('redirect') eq 'on') {\r
+                 if(@errors eq 0) {\r
+                   $redirect = $url;\r
+                 }\r
+               }\r
+             }\r
+           } else {\r
+             # Show the form, populating from the database if it's an existing entry.\r
+             my $utext = "URL:";\r
+             my $snapshot_params = "";\r
+             $id = "-1";\r
+\r
+             if(defined(param('id'))) { $id = param('id'); }\r
+             if(defined(url_param('id'))) { $id = url_param('id'); }\r
+\r
+             if($id eq "-1") { \r
+               if(defined(param('url'))) {\r
+                 $id = get_bookmark_id(param('url')); \r
+               }\r
+             }\r
+           \r
+             if($id ne -1)  {\r
+               ($url, $title, $description, $access_level) = get_bookmark($id);\r
+               $tags = get_tags($url);\r
+               $button = "Save";\r
+               $utext = "<span style=\"color:red\">URL (already bookmarked):</span>";\r
+               $extra_params = "<input type=\"hidden\" name=\"id\" value=\"$id\" />";\r
+             } else {\r
+               # There has to be a nicer way to do this.\r
+               if(param('url'))         { $url = param('url'); }\r
+               if(param('title'))       { $title = param('title'); }\r
+               if(param('description')) { $description = param('description'); }\r
+               $access_level = 1;\r
+               $button = "Add";\r
+               $snapshot_params = "<span class=\"formtext\">Snapshot:</span><input type=\"checkbox\" name=\"snapshot\" />\n";\r
+             }\r
+         \r
+             my $style = "style=\"width:500px\"";\r
+             my $redir = "off";\r
+             my $redir_box = "";\r
+             \r
+             if(param('redirect')) {\r
+                     if(param('redirect') eq 'on') { $redir = 'on'; }\r
+                     if(param('redirect') eq 'true') { $redir = 'on'; }\r
+             }\r
+\r
+             if($access_level eq 0) { $access_box = ""; } \r
+             else { $access_box = "checked=\"true\" "; }\r
+             \r
+             if($redir eq 'on') { $redir_box = "checked=\"true\""; } \r
+\r
+             print <<FORM;\r
+             <br />\r
+             <form method="post">\r
+             <span class="formtext">$utext</span><br />\r
+             <input name="url" $style value="$url" /><br />\r
+             <span class="formtext">Title:</span><br />\r
+             <input name="title" $style value="$title" /><br />\r
+             <span class="formtext">Description:</span><br />\r
+             <input name="description" $style value="$description" /><br />\r
+             <span class="formtext">Tags:</span><br />\r
+             <input name="tags" $style value="$tags" /><br />\r
+             $snapshot_params\r
+             <span class="formtext">Public:</span>\r
+             <input type="checkbox" name="access_level" $access_box />\r
+             <span class="formtext">Return:</span>\r
+             <input type="checkbox" name="redirect" $redir_box />\r
+             <input type="hidden" name="save" value="true" />\r
+             <input type="hidden" name="op" value="add_bookmark" />\r
+             $extra_params\r
+             <input type="submit" value="$button" />\r
+             </form>\r
+FORM\r
+           }\r
+         }\r
+       }\r
+\r
+       # Late redirects.  TODO: Get rid of this.\r
+       if($redirect ne "") {\r
+         print "<script language=\"JavaScript\">document.location = \"$redirect\";</script>";\r
+         print "</body></html>";\r
+         exit;\r
+       }\r
+\r
+\r
+       if(defined(param('op'))) {\r
+         if(logged_in() eq 1) {\r
+           if(param('op') eq 'fetchrelated') {\r
+               if(defined(param('id'))) {\r
+                       fetch_related(param('id'));\r
+               }\r
+           }\r
+\r
+           if(param('op') eq 'snapshots') {\r
+             show_snapshots();\r
+             print "</body></html>";\r
+             exit;\r
+           }\r
+\r
+           if(param('op') eq 'snapshot') {\r
+             if(defined(param('id'))) {\r
+               do_snapshot(param('id'));\r
+               print "</body></html>";\r
+               exit;\r
+             }\r
+           }\r
+\r
+           if(param('op') eq 'bookmarklets') {\r
+             print <<DESC;\r
+<p>This bookmarklet provides a fast way to add your browser's \r
+current page to this Insipid installation.  Either drag the \r
+following link to your bookmarks toolbar or right-click on it \r
+and choose "Bookmark This Link..." to create a bookmarklet.  \r
+Then when you're on a page you'd like to save, click on your \r
+new "Add to Insipid" button and you'll be brought to a page \r
+that allows you to fill out the tags for the bookmark and save \r
+it.  Once you've clicked Save you'll be brought back to the \r
+page.</p>\r
+DESC\r
+             my $ad = <<BLET;\r
+       javascript:location.href='$site_url/insipid.cgi?op=add_bookmark&url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title)+'&redirect=true'\r
+BLET\r
+             print "<ul><li><a href=\"$ad\">Add to Insipid</a></li></ul>";\r
+             print "</body></html>";\r
+             exit;\r
+           }\r
+\r
+           # Configuration and management pages\r
+           if(param('op') eq 'tags') {\r
+             tag_operations();\r
+             print '</body></html>';\r
+             exit;\r
+           }\r
+\r
+           if(param('op') eq 'options') {\r
+             show_options();\r
+             print '</body></html>';\r
+             exit;\r
+           }\r
+         }\r
+       }\r
+\r
+       foreach (@errors) {\r
+         print "<div class=\"error\">$_</div>";\r
+       }\r
+\r
+       show_bookmarks();\r
+\r
+       print "</td></tr></table><br /></body></html>";\r
+} # main\r
+\r
+\r
+\r
+################################################################\r
+\r
+sub show_options {\r
+       # Save options if they were posted.\r
+       print "<br /><br />";\r
+       if(param('save')) {\r
+               print "<div class=\"error\">Your options have been saved.</div>";\r
+\r
+               my %save;\r
+               foreach my $p (@valid) {\r
+                       if(param($p)) {\r
+                               $save{$p} = param($p);\r
+                       }\r
+               }\r
+\r
+               foreach my $k (keys %save) {\r
+                       my $sql = "update $tbl_options set value=? \r
+                               where (name = ?)";\r
+                       my $sth = $dbh->prepare($sql);\r
+                       $sth->execute($save{$k}, $k);\r
+               }\r
+       }\r
+\r
+       # Now show em\r
+       my $sql = "select name, description, value from $tbl_options";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+\r
+       print "<form method=\"post\">";\r
+       print "<table id=\"options\" cellpadding=5 cellspacing=5>";\r
+       while(my $hr = $sth->fetchrow_hashref) {\r
+               print "<td>$hr->{'description'}</td>";\r
+               if($hr->{'name'} eq 'version') {\r
+                       print "<td>$hr->{'value'}</td>";\r
+               } else {\r
+                       print "<td><input name=\"$hr->{'name'}\" value=\"$hr->{'value'}\" /></td>";\r
+               }\r
+               print "</tr>";\r
+       }\r
+\r
+       print "<input type=hidden name=op value=options>";\r
+       print "<input type=hidden name=save value=yes>";\r
+       print "<tr><td></td><td><input type='submit' value='Save'></td></tr>";\r
+       print "</table></form>";\r
+}\r
+\r
+sub show_footer {\r
+       my $older = 2;\r
+       if(defined(url_param('page'))) {\r
+               $older = url_param('page') + 1;\r
+       }\r
+       \r
+       if($last_page eq 0) { \r
+         if($query ne "") {\r
+           print " | <a href=\"?page=$older&q=";\r
+           print $query;\r
+           print "\">More Results</a>";\r
+         } else {\r
+           print " | <a href=\"?page=$older\">older</a>"; \r
+         }\r
+       }\r
+}\r
+\r
+sub do_import {\r
+       my $old_fh = select(OUTPUT_HANDLE);\r
+       my $cbuffer = ""; my $pcount = 0;\r
+       $| = 1;\r
+       select($old_fh);\r
+\r
+       my ($omd5, $ourl, $otype, $olength, $odate, $sql, \r
+                       $oadd, $omod, $otags);\r
+       my $ispec = '';\r
+\r
+       if($dbtype eq 'mysql') { $ispec = " ignore "; }\r
+       \r
+       $sql = "insert $ispec into pagecache_references\r
+                       (md5_parent, md5_child) values(?,?)";\r
+       my $insert_reference = $dbh->prepare($sql);\r
+\r
+       $sql = "insert $ispec into pagecache\r
+                       (md5,url,content_type,\r
+                       content_length,content,date)\r
+                       values(?,?,?,?,?,?)";\r
+       my $insert_snapshot = $dbh->prepare($sql);\r
+\r
+       $sql = 'update options set value = ? where (name = ?)';\r
+       my $update_option = $dbh->prepare($sql);\r
+\r
+       my $parser = new XML::Parser();\r
+       $parser->setHandlers(Start => sub {\r
+               my %attr;\r
+               my $expat = shift;\r
+               my $element = shift;\r
+\r
+               while(@_) {\r
+                       my $att = shift;\r
+                       my $val = shift;\r
+                       $attr{$att} = $val;\r
+               }\r
+\r
+               # netscape stuff\r
+               if ( $element eq "A" ) {\r
+                   $ourl = $attr{'HREF'};\r
+                   $oadd = $attr{'ADD_DATE'};\r
+                   $omod = $attr{'LAST_MODIFIED'};\r
+                   $otags = $attr{'TAGS'};\r
+                   $cbuffer = '';\r
+               }\r
+       \r
+               # A pagecache object\r
+               if($element eq "object") {\r
+                       $omd5 = $attr{'md5'};\r
+                       $ourl = $attr{'url'};\r
+                       $otype = $attr{'type'};\r
+                       $olength = $attr{'length'};\r
+                       $odate = $attr{'date'};\r
+               }\r
+\r
+               # A pagecache relationship\r
+               if($element eq "relationship") {\r
+                       my $parent = $attr{parent};\r
+                       my $child = $attr{child};\r
+                       $insert_reference->execute($parent, $child);\r
+               }\r
+               \r
+               # A bookmark\r
+               if($element eq "post") {\r
+                       my $url = $attr{href};\r
+                       my $title = $attr{description};\r
+                       my $tagvalue = $attr{tag};\r
+                       my $datevalue = $attr{time};\r
+                       my $access_level;\r
+                       if(defined($attr{access_level})) {\r
+                               $access_level = $attr{access_level};\r
+                       } else {\r
+                               $access_level = 1;\r
+                       }\r
+\r
+                       my $epoch = str2time($datevalue);\r
+\r
+                       if($tagvalue eq "system:unfiled") {\r
+                               $tagvalue = "";\r
+                       }\r
+\r
+                       add_bookmark($url, $title, "", $access_level,\r
+                               $epoch, $tagvalue, 1);\r
+               }\r
+\r
+               # Option\r
+               if($element eq 'option') {\r
+                       $update_option->execute(\r
+                               $attr{value}, $attr{name});\r
+               }\r
+       }, Char => sub {\r
+               my $expat = shift;\r
+               my $chars = shift;\r
+               $cbuffer = $cbuffer . $chars;\r
+               1;\r
+       }, End => sub {\r
+               my $expat = shift;\r
+               my $element = shift;\r
+    \r
+        #\r
+        # netscape stuff\r
+        #\r
+        if($element eq 'A') {\r
+            add_bookmark(\r
+                $ourl,                # $url, \r
+                $cbuffer,             # $title, \r
+                "",\r
+                '1',                  # $access_level,\r
+                $omod,                # $epoch, \r
+                $otags,               # $tagvalue\r
+            );\r
+            $cbuffer = '';\r
+        }\r
+        if ( $element eq 'TITLE' || $element eq 'H1' || $element eq 'DD') {\r
+            $cbuffer = '';\r
+        }\r
+        \r
+               if($element eq 'object') {\r
+\r
+                       $insert_snapshot->bind_param(1, $omd5);\r
+                       $insert_snapshot->bind_param(2, $ourl);\r
+                       $insert_snapshot->bind_param(3, $otype);\r
+                       $insert_snapshot->bind_param(4, $olength);\r
+                       \r
+                       if($dbtype eq "Pg") {\r
+                               $insert_snapshot->bind_param(5, \r
+                                       decode_base64($cbuffer), SQL_VARBINARY);\r
+                       } else {\r
+                               $insert_snapshot->bind_param(5, \r
+                                       decode_base64($cbuffer));\r
+                       }\r
+\r
+                       $insert_snapshot->bind_param(6, $odate);\r
+                       $insert_snapshot->execute;\r
+                       \r
+                       if(!defined($DBI::errstr)) { $pcount++; }\r
+\r
+                       $cbuffer = "";\r
+               }\r
+       });\r
+\r
+       my $xml = "";\r
+\r
+#      BEGIN {\r
+#        *CORE::GLOBAL::die = sub {\r
+#          print "Some errors were detected. ";\r
+#        };\r
+#      }\r
+\r
+       if(defined($ENV{SERVER_NAME})) {\r
+               my $fh = upload('fileupload');\r
+               while(<$fh>) {\r
+                       $xml .= $_;\r
+               }\r
+       \r
+               $parser->parse($xml);\r
+       } else {\r
+               if(!defined($ARGV[0])) {\r
+                       print "Please specify the filename to import.\n\n";\r
+                       exit;\r
+               }\r
+       \r
+               my $fn = $ARGV[0];\r
+               $parser->parsefile($fn);\r
+       }\r
+       \r
+       print "Import finished - $icount bookmarks and $pcount " .\r
+                       "snapshot objects imported.$NL";\r
+}\r
+\r
+sub do_export {\r
+       my ($snapshots, $islocal) = (@_);\r
+       my $writer;\r
+       \r
+       if(!defined($islocal)) {\r
+               print "Content-Type: text/xml;charset=UTF-8\r\n";\r
+               print "Content-Disposition: attachment; filename=bookmarks.xml\r\n\r\n";\r
+       }\r
+\r
+       $writer = new XML::Writer();\r
+\r
+       $writer->xmlDecl('UTF-8');\r
+       $writer->startTag('insipid');\r
+\r
+       export_bookmarks($writer);\r
+       export_snapshots($writer);\r
+       export_options($writer);\r
+       \r
+       $writer->endTag('insipid');\r
+       $writer->end();\r
+       exit;\r
+}\r
+\r
+sub login_form {\r
+      print <<FORM;\r
+<br />\r
+<form method="post" action="$site_url/insipid.cgi">\r
+<span class="formtext">Username:</span><br />\r
+<input style="width:250px" name="username" /><br />\r
+<span class="formtext">Password:</span><br />\r
+<input style="width:250px" name="password" type="password" /><br />\r
+<input type="submit" value="Login" />\r
+</form>\r
+<br />\r
+FORM\r
+}\r
+\r
+\r
+sub show_toolbar {\r
+       my $rdata = "";\r
+       if(defined(url_param('tag'))) {\r
+               $rdata = url_param('tag');\r
+               $rdata =~ s/ /\+/g;\r
+       }\r
+\r
+       # Toolbar\r
+       print '<center>';\r
+       print '<table border="0" width="100%" cellspacing="0" cellpadding="3"><tr>';\r
+       print '<td valign="top" bgcolor="#CCCCCC">';\r
+\r
+       # Title\r
+       print '<div class="title"><a href="';\r
+       if(get_option('use_rewrite') eq 'yes') {\r
+               print $site_url . '/bookmarks';\r
+       } else {\r
+               print 'insipid.cgi';\r
+       }\r
+       print '">' . $site_title . '</a></div>';\r
+  \r
+       if((get_option("public_searches") eq "yes") || (logged_in() eq 1)) {\r
+               print "<div class=\"search\">";\r
+               print "<form action=\"$site_url/bookmarks\" method=\"post\">";\r
+               print "<input type=\"text\" name=\"q\"> <input type=\"submit\" value=\"search\">";\r
+               print "</form>";\r
+               print "</div>";\r
+       } else {\r
+               print "&nbsp;";\r
+       }\r
+    \r
+       print "</td><td valign=\"top\" bgcolor=\"#CCCCCC\" align=\"right\">";\r
+       print "<div class=\"toolbar\">";\r
+  \r
+       if(logged_in() eq 1) {\r
+               print "<a class=\"tools\" href=\"$site_url/insipid.cgi?op=options\">options</a> | "; \r
+               print "<a class=\"tools\" href=\"$site_url/insipid.cgi?op=tags\">tags</a> | ";\r
+               print "<a class=\"tools\" href=\"$site_url/insipid.cgi?op=import\">import</a> | ";\r
+               print "<a class=\"tools\" href=\"$site_url/insipid.cgi?op=export\">export</a> | ";\r
+               print "<a class=\"tools\" href=\"$site_url/insipid.cgi?op=snapshots\">snapshots</a> | ";\r
+               print "<a class=\"tools\" href=\"$site_url/insipid.cgi?op=logout\">logout</a><br />";\r
+               print "<a class=\"tools\" href=\"$site_url/insipid.cgi?op=add_bookmark\">add</a> | ";\r
+               print "<a class=\"tools\" href=\"$site_url/insipid.cgi?op=bookmarklets\">bookmarklets</a> | ";\r
+       }\r
+\r
+       my $rf;\r
+       if(get_option('use_rewrite') eq 'yes') {\r
+               $rf = $feed_url . '/' . $rdata;\r
+       } else {\r
+               $rf = $feed_url . $rdata;\r
+       }\r
+       \r
+       print "<a class=\"tools\" href=\"$rf\">RSS feed</a>";\r
+\r
+       if(logged_in() ne 1) {\r
+               print " | <a class=\"tools\" href=\"$site_url/insipid.cgi?op=login\">login</a>";\r
+       }\r
+\r
+       print " | <a class=\"tools\" href=\"javascript:void window.open('$site_url/help.html','width=300,height=500');\">help</a> ";\r
+       print " | <a class=\"tools\" href=\"http://www.neuro-tech.net/insipid/\">source</a>";\r
+\r
+       print "</div></tr></table></center>";\r
+}\r
+\r
+sub delete_bookmark {\r
+       my($id) = (@_);\r
+       my($sql, $sth, $md5) = ("", "", "");\r
+\r
+       if(logged_in() ne 1) {\r
+         push(@errors, "You have to be logged in to perform that operation.");\r
+         return;\r
+       }\r
+\r
+       # Check for cached version to delete.\r
+       $sql = "select $tbl_pagecache.md5 from $tbl_pagecache \r
+               inner join $tbl_bookmarks on \r
+               ($tbl_pagecache.md5 = $tbl_bookmarks.md5) \r
+               where ($tbl_bookmarks.id = ?)";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute($id);\r
+       while(my @r = $sth->fetchrow_array) {\r
+               $md5 = $r[0];\r
+       }\r
+\r
+       # Drop the tags for the bookmark\r
+       $sql = "delete from $tbl_bookmark_tags where (bookmark_id = ?)";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute($id);\r
+       \r
+       # Drop the bookmark.\r
+       $sql = "delete from $tbl_bookmarks where (id = ?)";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute($id);\r
+\r
+       # Delete the cached page.\r
+       if($md5 ne "") { delete_snapshot($md5); }\r
+}\r
+\r
+sub show_bookmarks {\r
+       my ($subquery, $sql, $sth, @parms, @wheres, @hr);\r
+       \r
+       # this first query will be used to select from a set, like when a user \r
+       # drills in on a specific tag or to get a smaller view of the entire\r
+       # dataset (for paging purposes).\r
+\r
+       # MySQL and postgres have slightly different syntax here...\r
+       if ($dbtype eq 'mysql') {\r
+           $sql = "select $tbl_bookmarks.id from $tbl_bookmarks";\r
+       } elsif ($dbtype eq 'Pg') {\r
+           $sql = "select $tbl_bookmarks.id, $tbl_bookmarks.date \r
+               from $tbl_bookmarks";\r
+       }\r
+\r
+       # Limit to tags\r
+       if(defined(url_param('tag'))) {\r
+               # Join the tag tables only when necessary\r
+       \r
+               if(url_param('tag') =~ / /) {\r
+                       my @tags = split(/ /, url_param('tag'));\r
+                       my $icount = 1;\r
+                       \r
+                       foreach(@tags) {                \r
+                               push(@parms, $_);\r
+                               $sql = "$sql inner join $tbl_bookmark_tags \r
+                                               as bt$icount on \r
+                                         ($tbl_bookmarks.id = \r
+                                               bt$icount.bookmark_id)\r
+                                       inner join $tbl_tags as t$icount on \r
+                                          (t$icount.id = bt$icount.tag_id \r
+                                               and t$icount.name = ?) ";\r
+                               $icount++;\r
+                       }\r
+               } else {\r
+                       $sql = "$sql \r
+                               left join $tbl_bookmark_tags on \r
+                                 ($tbl_bookmarks.id = \r
+                                       $tbl_bookmark_tags.bookmark_id) \r
+                               inner join $tbl_tags on \r
+                                 ($tbl_tags.id = $tbl_bookmark_tags.tag_id) \r
+                                 where ($tbl_tags.name = ?)";\r
+                       push(@parms, url_param('tag'));\r
+               }\r
+\r
+       }\r
+\r
+       # Search\r
+       if($query ne "") {\r
+               if((get_option("public_searches") eq "yes") || (logged_in() eq 1)) {\r
+                       my $sparm = $query;\r
+                       if(length($sparm) > 2) {\r
+                               $sql = "$sql where ($tbl_bookmarks.title like ?)";\r
+                               $sparm =~ s/\%//;\r
+                               $sparm = "\%$sparm\%";\r
+                               push(@parms, $sparm);\r
+                       }\r
+               }\r
+       }\r
+\r
+       # order\r
+       $sql = "$sql order by $tbl_bookmarks.date desc";\r
+\r
+       # paging functionality\r
+       $sql = "$sql limit 50";\r
+       \r
+       if(defined(url_param('page'))) {\r
+           my $offset = ((url_param('page') - 1) * 50);\r
+           $sql = "$sql offset $offset";\r
+       }\r
+\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute(@parms);\r
+\r
+       $subquery = "";\r
+       if($sth->rows > 0) {\r
+               if($sth->rows ne 50) { $last_page = 1; }\r
+       \r
+               $subquery = " $tbl_bookmarks.id in (";\r
+               \r
+               while(@hr = $sth->fetchrow_array) {\r
+                       $subquery = $subquery . "$hr[0],";\r
+               }\r
+               chop($subquery); # Strip off the last delimiter\r
+               \r
+               $subquery = $subquery . ")";\r
+       } else {\r
+               print "<p>No bookmarks found.</p>";\r
+               return;\r
+       }\r
+\r
+       @parms = ();\r
+       @wheres = ();\r
+       \r
+        $sql = "select \r
+                 $tbl_bookmarks.id, \r
+                 $tbl_bookmarks.title, \r
+                 $tbl_bookmarks.description, \r
+                 $tbl_bookmarks.access_level,\r
+                 $tbl_bookmarks.url,\r
+                 $tbl_tags.name,\r
+                 $tbl_bookmarks.date,\r
+                 $tbl_pagecache.date as cache_date,\r
+                 $tbl_bookmarks.md5\r
+               from $tbl_bookmarks \r
+               left join $tbl_bookmark_tags on \r
+                 ($tbl_bookmarks.id = $tbl_bookmark_tags.bookmark_id)\r
+               left join $tbl_tags on \r
+                 ($tbl_tags.id = $tbl_bookmark_tags.tag_id)\r
+               left join $tbl_pagecache on\r
+                 ($tbl_bookmarks.md5 = $tbl_pagecache.md5)";\r
+       \r
+       # Don't show private marks for non-logged in users\r
+       if(logged_in() eq 0) {\r
+         push(@wheres, "$tbl_bookmarks.access_level");\r
+         push(@parms, "1");\r
+       }\r
+\r
+       my $max = @wheres;\r
+       if($max ne 0) {\r
+         $sql = "$sql where (";\r
+         my $count = 1;\r
+         \r
+         foreach (@wheres) {\r
+           $sql = "$sql $_ = ?";\r
+           if($count < $max) {\r
+             $sql = "$sql and ";\r
+           }\r
+           $count++;\r
+         }\r
+\r
+         $sql = "$sql )";\r
+         if($subquery ne "") { $sql = "$sql and $subquery"; }\r
+       } else {\r
+         if($subquery ne "") { $sql = "$sql where $subquery "; }\r
+       }\r
+\r
+        # append sort order.\r
+       $sql = "$sql order by $tbl_bookmarks.date desc";\r
+\r
+       $sth = $dbh->prepare($sql);\r
+        $sth->execute(@parms);\r
+\r
+       my %last;\r
+       $last{id} = -1;\r
+\r
+       print '<ul><br />';\r
+\r
+       my $title = '';\r
+       if(defined(url_param('tag'))) {\r
+               my $temp = url_param('tag');\r
+               if($temp =~ / /) {\r
+                       my $count = 0;\r
+                       foreach(split(/ /, $temp)) {\r
+                               if($count++ ne 0) { $title = "$title +"; }\r
+                               $title = "$title <a class=\"bodyTitle\" href=\"$tag_url$_\">$_</a>";\r
+                       }\r
+               } else {\r
+                       $title = "<a class=\"bodyTitle\" href=\"$tag_url$temp\">$temp</a>";\r
+               }\r
+       } else {\r
+               $title = 'Most Recent Bookmarks';\r
+       }\r
+\r
+       if($query ne '') { \r
+               $title = sprintf("Search results for \"%s\"", $query); \r
+       }\r
+         \r
+       print "<span class=\"bodyTitle\">$title</span>";\r
+       show_footer();\r
+       print '<br /><br />';\r
+\r
+       print "<table class=\"bookmarklist\">";\r
+       print '<tr><td>';\r
+       print "<ul type=\"circle\">\n";\r
+       while(@hr = $sth->fetchrow_array) {\r
+               if($last{id} eq -1) {\r
+                 $last{id} = $hr[0];\r
+                 $last{title} = $hr[1];\r
+                 $last{description} = $hr[2];\r
+                 $last{access_level} = $hr[3];\r
+                 $last{url} = $hr[4];\r
+                 $last{tags} = "";\r
+                 $last{timestamp} = $hr[6];\r
+                 $last{cachetime} = $hr[7];\r
+                 $last{md5} = $hr[8];\r
+               } \r
+               \r
+               if($hr[0] ne $last{id}) {\r
+                 # the id changed, so show the last mark.\r
+                 show_bookmark($last{id}, $last{title}, $last{description}, $last{access_level}, $last{url}, $last{tags}, $last{timestamp}, $last{cachetime}, $last{md5});     \r
+\r
+                 # Swap the new one in.\r
+                 $last{id} = $hr[0];\r
+                 $last{title} = $hr[1];\r
+                 $last{description} = $hr[2];\r
+                 $last{access_level} = $hr[3];\r
+                 $last{url} = $hr[4];\r
+                 $last{tags} = $hr[5];\r
+                 $last{timestamp} = $hr[6];\r
+                 $last{cachetime} = $hr[7];\r
+                 $last{md5} = $hr[8];\r
+               } else {\r
+                 # Add tag to the current bookmark\r
+                 if(defined($hr[5])) {\r
+                   $last{tags} = "$last{tags} $hr[5]";\r
+                 }\r
+               }\r
+       }\r
+\r
+       if($last{id} ne -1) {\r
+               show_bookmark($last{id}, $last{title}, $last{description}, $last{access_level}, $last{url}, $last{tags}, $last{timestamp}, $last{cachetime}, $last{md5});\r
+       }\r
+\r
+       print "</ul></td></tr></table>";\r
+}\r
+\r
+sub show_bookmark {\r
+       my($id, $title, $description, $access_level, $url, \r
+               $tags, $timestamp, $cachetime, $md5) = (@_);\r
+\r
+       print "<div class=\"bookmarklistitem\">";\r
+       print "<li>";\r
+       if($access_level eq 0) { \r
+               print "<a href=\"$site_url/insipid.cgi?go=$id\">";\r
+               print "<i>";\r
+               print "$title";\r
+               print "</i>";\r
+       } else {\r
+               print "<a href=\"$url\">";\r
+               print $title;\r
+       }\r
+\r
+       if(logged_in() eq 1) {\r
+               if(defined($cachetime)) {\r
+                       print "</a> - <a href=\"$snapshot_url$md5\">view snapshot";\r
+               }\r
+       }\r
+       \r
+       print "</a><br /><div class=\"bookmarkOperations\">";\r
+\r
+       \r
+       my $timestr = "";\r
+       if(logged_in() eq 1) {\r
+               $timestr = time2str("%Y-%m-%d %T EST", $timestamp, "EST");\r
+       } else {\r
+               $timestr = time2str("%Y-%m-%d", $timestamp, "EST");\r
+       }\r
+\r
+       print "posted on $timestr ";\r
+           \r
+       if(defined($tags)) {\r
+         print "to ";\r
+         my $cur;\r
+           \r
+         foreach $cur (split(/\ /, $tags)) {\r
+           print '<a class="bookmarkTag" href="';\r
+           print $tag_url . $cur . '">' . $cur . '</a> ';\r
+         }\r
+       }\r
+\r
+       if(logged_in() eq 1) {\r
+               my $ex = "";\r
+\r
+               if(url_param('tag')) { $ex = "$ex&tag=" . url_param('tag'); }\r
+               if(url_param('page')) { $ex = "$ex&page=" . url_param('page'); }\r
+               if($query ne "") { $ex = "$ex&q=" . $query; }\r
+         \r
+               print "<span class=\"bodytext\">&nbsp;&mdash;&nbsp;";\r
+               print "(<a class=\"bookmarkOp\" href=\"$site_url/insipid.cgi?op=delete_bookmark&id=$id$ex\">delete</a>,&nbsp;";\r
+               print "<a class=\"bookmarkOp\" href=\"$site_url/insipid.cgi?op=edit_bookmark&id=$id$ex\">edit</a>";\r
+               if(!defined($cachetime)) {\r
+                       print ",&nbsp;<a class=\"bookmarkOp\" href=\"$site_url/insipid.cgi?op=snapshot&id=$id$ex\">snapshot</a>";\r
+               }\r
+               print ")<div class=\"bookmarkDescription\">$description</div></span></div></li>\n";\r
+       }\r
+       \r
+       print "</div>\n";\r
+}\r
+\r
+# Gets the ID for a bookmark if it already exists in the DB. Otherwise, -1.\r
+sub get_bookmark_id {\r
+       my ($url) = (@_);\r
+       \r
+       # Lookup the URL id first.\r
+       my $sql = "select $tbl_bookmarks.id from \r
+               $tbl_bookmarks where ($tbl_bookmarks.md5 = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+\r
+       $sth->execute(md5_hex($url));\r
+\r
+       if($sth->rows ne 0) {\r
+               my @r = $sth->fetchrow_array;\r
+               return $r[0];\r
+       }\r
+       \r
+       return -1;\r
+}\r
+\r
+sub get_bookmark {\r
+       my ($id) = (@_);\r
+\r
+       my $sql = "select \r
+                       $tbl_bookmarks.title, \r
+                       $tbl_bookmarks.description, \r
+                       $tbl_bookmarks.url,\r
+                       $tbl_bookmarks.access_level \r
+                       from $tbl_bookmarks \r
+                       where ($tbl_bookmarks.id = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($id);\r
+       my @r = $sth->fetchrow_array;\r
+       return ($r[2], $r[0], $r[1], $r[3]);\r
+}\r
+\r
+sub update_bookmark {\r
+       my ($id, $url, $title, $description, $access_level, $tags) = (@_);\r
+\r
+       if(logged_in() ne 1) {\r
+         push(@errors, "You have to be logged in to perform that operation.");\r
+         return;\r
+       }\r
+       \r
+       my $sql = "update $tbl_bookmarks \r
+                       set url = ?, md5 = ?, title = ?, description = ?, \r
+                       access_level = ? where (id = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($url, md5_hex("$url"), $title, $description, \r
+                       $access_level, $id);\r
+\r
+       set_tags($id, $tags);\r
+}\r
+\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/RSS.pm b/lib/Insipid/RSS.pm
new file mode 100755 (executable)
index 0000000..84bcff7
--- /dev/null
@@ -0,0 +1,121 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::RSS;\r
+\r
+use warnings;\r
+use strict;\r
+\r
+use CGI qw/:standard/;\r
+use CGI::Carp qw(fatalsToBrowser);\r
+\r
+use Insipid::Config;\r
+use Insipid::Database;\r
+use Insipid::Sessions;\r
+use Insipid::Util;\r
+\r
+use vars qw(@ISA @EXPORT $dbh);\r
+require Exporter;\r
+\r
+@ISA = qw(Exporter);\r
+@EXPORT = qw(send_rss);\r
+\r
+sub send_rss {\r
+       my @parms;\r
+       my $ew = "";\r
+       my $joins = "";\r
+       my $title = get_option("feed_name");\r
+       \r
+       if(url_param('tag')) {\r
+                if(url_param('tag') =~ / /) {\r
+                        my @tags = split(/ /, url_param('tag'));\r
+                        my $rcount = 1;\r
+\r
+                        foreach(@tags) {\r
+                                push(@parms, $_);\r
+                                $joins = "$joins inner join $tbl_bookmark_tags \r
+                                       as bt$rcount on\r
+                                          ($tbl_bookmarks.id = bt$rcount.bookmark_id)\r
+                                        inner join $tbl_tags as t$rcount on\r
+                                           (t$rcount.id = bt$rcount.tag_id and t$rcount.name = ?) ";\r
+                                $rcount++;\r
+                        }\r
+               } else {\r
+\r
+                       push(@parms, url_param('tag'));\r
+                       $joins = "\r
+                               inner join $tbl_bookmark_tags on \r
+                                       ($tbl_bookmarks.id = \r
+                                               $tbl_bookmark_tags.bookmark_id)\r
+                               inner join $tbl_tags on\r
+                                       ($tbl_bookmark_tags.tag_id = $tbl_tags.id)";\r
+                       $ew = "and ($tbl_tags.name = ?)";\r
+               }\r
+       }\r
+\r
+       my $access_where = "where (access_level = 1)";\r
+       if(logged_in() eq 1) {\r
+               $access_where = "";\r
+       }\r
+\r
+       my $sql = "\r
+               select $tbl_bookmarks.id, $tbl_bookmarks.title, $tbl_bookmarks.url\r
+                       from $tbl_bookmarks $joins $access_where $ew\r
+               order by $tbl_bookmarks.date desc limit 30";\r
+\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute(@parms);\r
+\r
+       print <<RDFHEADER;\r
+<?xml version="1.0" encoding="UTF-8"?>\r
+<rss version="2.0"\r
+    xmlns:dc="http://purl.org/dc/elements/1.1/"\r
+    xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"\r
+    xmlns:admin="http://webns.net/mvcb/"\r
+    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\r
+    xmlns:content="http://purl.org/rss/1.0/modules/content/">\r
+    \r
+<channel>\r
+  <title>$title</title>\r
+  <link>$full_url</link>\r
+  <description>Aggregated links</description>\r
+  <dc:language>en-us</dc:language>\r
+  <dc:creator>Insipid</dc:creator>\r
+  <dc:rights>Copyright 2006</dc:rights>\r
+RDFHEADER\r
+\r
+       while(my @hr = $sth->fetchrow_array) {\r
+         my $url = sanitize_html($hr[2]);\r
+         my $title = sanitize_html($hr[1]);\r
+         print <<ITEM;\r
+<item>\r
+  <title>$title</title>\r
+  <link>$url</link>\r
+  <guid isPermaLink="false">$hr[0]_$full_url</guid>\r
+  <content:encoded><![CDATA[<a href="$url">$hr[1]</a>]]></content:encoded>\r
+</item>\r
+ITEM\r
+       }\r
+\r
+       print "</channel></rss>\n\n";\r
+}\r
+\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/Schemas.pm b/lib/Insipid/Schemas.pm
new file mode 100755 (executable)
index 0000000..fcbab36
--- /dev/null
@@ -0,0 +1,252 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Schemas;\r
+\r
+use strict;\r
+use warnings;\r
+use Insipid::Config;\r
+\r
+use vars qw(\r
+@ISA\r
+@EXPORT\r
+$createMySQL\r
+$createPostgres\r
+);\r
+\r
+use Exporter;\r
+\r
+@ISA = qw(Exporter);\r
+\r
+@EXPORT = qw(\r
+$version\r
+$createMySQL\r
+$createPostgres\r
+);\r
+\r
+# Insipid will check the database version number on each initialization of\r
+# the options table (every hit essentially) and upgrade the tables if there's\r
+# any mismatch.\r
+our $version = "0.9.19";\r
+\r
+our $createPostgres = <<CPOSTGRES;\r
+CREATE TABLE $tbl_authentication (\r
+       session_id CHAR(32) NOT NULL UNIQUE,\r
+       create_time INT,\r
+       PRIMARY KEY(session_id)\r
+);\r
+\r
+CREATE TABLE $tbl_bookmarks (\r
+       id SERIAL,\r
+       url TEXT NOT NULL,\r
+       md5 CHAR(32) NOT NULL UNIQUE,\r
+       date INT NOT NULL DEFAULT 0,\r
+       title VARCHAR(255) NOT NULL,\r
+       description TEXT NOT NULL,\r
+       access_level INT NOT NULL DEFAULT 0,\r
+       PRIMARY KEY(id)\r
+);\r
+\r
+\r
+CREATE TABLE $tbl_tags (\r
+       id SERIAL,\r
+       name VARCHAR(255) NOT NULL UNIQUE,\r
+       PRIMARY KEY(id),\r
+       UNIQUE(name)\r
+);\r
+\r
+CREATE TABLE $tbl_bookmark_tags (\r
+       bookmark_id SERIAL,\r
+       tag_id INT NOT NULL,\r
+       PRIMARY KEY(bookmark_id, tag_id)\r
+);\r
+\r
+CREATE TABLE $tbl_options (\r
+        name VARCHAR(255) NOT NULL UNIQUE,\r
+        description TEXT NOT NULL,\r
+        value TEXT NOT NULL,\r
+        PRIMARY KEY(name)\r
+);\r
+\r
+CREATE TABLE $tbl_pagecache (\r
+        md5 CHAR(32) NOT NULL DEFAULT '',\r
+        url TEXT NOT NULL DEFAULT '',\r
+        content_type VARCHAR(50),\r
+        content_length INT NOT NULL DEFAULT 0,\r
+        content bytea,\r
+        date INT NOT NULL DEFAULT 0,\r
+        PRIMARY KEY(md5)\r
+);\r
+\r
+CREATE TABLE $tbl_pagecache_references (\r
+        md5_parent CHAR(32) NOT NULL DEFAULT '',\r
+        md5_child CHAR(32) NOT NULL DEFAULT '',\r
+        PRIMARY KEY(md5_parent, md5_child)\r
+);\r
+\r
+INSERT INTO $tbl_options VALUES (\r
+  'feed_name',\r
+  'The title of your feed (e.g. My Bookmarks)',\r
+  'Bookmarks'\r
+);\r
+\r
+INSERT INTO $tbl_options VALUES (\r
+  'site_name',\r
+  'The title of the main page (e.g. My Bookmarks)',\r
+  'My Bookmarks'\r
+);\r
+\r
+\r
+INSERT INTO $tbl_options VALUES (\r
+  'public_searches',\r
+  'Allow public searches - when set to yes, any visitor can search your bookmarks.',\r
+  'no'\r
+);\r
+\r
+INSERT INTO $tbl_options VALUES(\r
+  'version',\r
+  'Internal Insipid version number',\r
+  '$version'\r
+);\r
+\r
+INSERT INTO $tbl_options VALUES(\r
+  'proxy_host',\r
+  'The proxy server (if any) to use when making page snapshots.',\r
+  ''\r
+);\r
+\r
+INSERT INTO $tbl_options VALUES(\r
+  'proxy_port',\r
+  'Your proxy port number.',\r
+  '3128'\r
+);\r
+\r
+INSERT INTO $tbl_options VALUES(\r
+  'use_rewrite',\r
+  'Use mod_rewrite - disable this if you do not want .htaccess-controlled URLs, or if your Apache does not have the rewrite module installed.',\r
+  'no'\r
+);\r
+\r
+CPOSTGRES\r
+\r
+\r
+our $createMySQL = <<CMYSQL;\r
+CREATE TABLE IF NOT EXISTS $tbl_authentication (\r
+       session_id CHAR(32) NOT NULL UNIQUE,\r
+       create_time INT,\r
+       PRIMARY KEY(session_id)\r
+);\r
+\r
+CREATE TABLE IF NOT EXISTS $tbl_bookmarks (\r
+       id INT AUTO_INCREMENT NOT NULL,\r
+       url TEXT NOT NULL DEFAULT '',\r
+       md5 CHAR(32) NOT NULL DEFAULT '' UNIQUE,\r
+       date INT NOT NULL DEFAULT 0,\r
+       title VARCHAR(255) NOT NULL DEFAULT '',\r
+       description TEXT NOT NULL DEFAULT '',\r
+       access_level INT NOT NULL DEFAULT 0,\r
+       PRIMARY KEY(id)\r
+);\r
+\r
+\r
+CREATE TABLE IF NOT EXISTS $tbl_tags (\r
+       id INT AUTO_INCREMENT NOT NULL,\r
+       name VARCHAR(255) NOT NULL DEFAULT '' UNIQUE,\r
+       PRIMARY KEY(id)\r
+);\r
+\r
+CREATE TABLE IF NOT EXISTS $tbl_bookmark_tags (\r
+       bookmark_id INT NOT NULL,\r
+       tag_id INT NOT NULL,\r
+       PRIMARY KEY(bookmark_id, tag_id),\r
+       INDEX(bookmark_id),\r
+       INDEX(tag_id)\r
+);\r
+\r
+\r
+CREATE TABLE IF NOT EXISTS $tbl_options (\r
+        name VARCHAR(255) NOT NULL UNIQUE,\r
+        description TEXT NOT NULL DEFAULT '',\r
+        value TEXT NOT NULL DEFAULT '',\r
+        PRIMARY KEY(name)\r
+);\r
+\r
+CREATE TABLE IF NOT EXISTS $tbl_pagecache (\r
+        md5 CHAR(32) NOT NULL DEFAULT '',\r
+        url TEXT NOT NULL DEFAULT '',\r
+        content_type VARCHAR(50),\r
+        content_length INT NOT NULL DEFAULT 0,\r
+        content LONGBLOB,\r
+        date INT NOT NULL DEFAULT 0,\r
+        PRIMARY KEY(md5)\r
+);\r
+\r
+CREATE TABLE IF NOT EXISTS $tbl_pagecache_references (\r
+        md5_parent CHAR(32) NOT NULL DEFAULT '',\r
+        md5_child CHAR(32) NOT NULL DEFAULT '',\r
+        PRIMARY KEY(md5_parent, md5_child)\r
+);\r
+\r
+INSERT IGNORE INTO $tbl_options VALUES(\r
+  'feed_name',\r
+  'The title of your feed (e.g. My Bookmarks)',\r
+  'Bookmarks'\r
+);\r
+\r
+INSERT IGNORE INTO $tbl_options VALUES(\r
+  'site_name',\r
+  'The title of the main page (e.g. My Bookmarks)',\r
+  'My Bookmarks'\r
+);\r
+\r
+INSERT IGNORE INTO $tbl_options VALUES(\r
+  'public_searches',\r
+  'Allow public searches - when set to yes, any visitor can search your bookmarks.',\r
+  'no'\r
+);\r
+\r
+INSERT IGNORE INTO $tbl_options VALUES(\r
+  'proxy_host',\r
+  'The proxy server (if any) to use when making page snapshots.',\r
+  ''\r
+);\r
+\r
+\r
+INSERT IGNORE INTO $tbl_options VALUES(\r
+  'proxy_port',\r
+  'Your proxy port number.',\r
+  '3128'\r
+);\r
+\r
+INSERT IGNORE INTO $tbl_options VALUES(\r
+  'version',\r
+  'Internal Insipid version number',\r
+  '$version'\r
+);\r
+\r
+INSERT IGNORE INTO $tbl_options VALUES(\r
+  'use_rewrite',\r
+  'Use mod_rewrite - disable this if you do not want .htaccess-controlled URLs, or if your Apache does not have the rewrite module installed.',\r
+  'no'\r
+);\r
+CMYSQL\r
+\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/Sessions.pm b/lib/Insipid/Sessions.pm
new file mode 100755 (executable)
index 0000000..f2bce35
--- /dev/null
@@ -0,0 +1,120 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Sessions;\r
+\r
+use warnings;\r
+use strict;\r
+\r
+use CGI qw/:standard/;\r
+use CGI::Carp qw(fatalsToBrowser);\r
+use Digest::MD5;\r
+\r
+use Insipid::Config;\r
+use Insipid::Database;\r
+\r
+use vars qw(@ISA @EXPORT $dbh);\r
+require Exporter;\r
+\r
+@ISA = qw(Exporter);\r
+\r
+@EXPORT = qw(\r
+logged_in\r
+login\r
+logout\r
+);\r
+\r
+# Create/find the session\r
+my $uname = getconfig('username');\r
+my $pagepath = getconfig('pagepath');\r
+my $ctag = "INSIPID2_$uname";\r
+my $sid = cookie($ctag) || undef;\r
+my $logged_in = 0;\r
+my $options;\r
+\r
+if(defined($sid)) {\r
+       my $sql = "select create_time from $tbl_authentication \r
+                       where (session_id = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($sid);\r
+\r
+       if($sth->rows ne 0) {\r
+               $logged_in = 1;\r
+       } else {\r
+               print "Set-Cookie: $ctag=; path=$pagepath; expires=Fri, 03-Sep-2020 20:20:13 GMT\n";\r
+       }\r
+}\r
+\r
+# Depending on our context we can consider the user logged in.  If they're\r
+# running one of the programs in the "tools" and using a terminal then \r
+# we'll authorize them.\r
+if(!defined($ENV{'SERVER_NAME'})) {\r
+       $logged_in = 1;\r
+}\r
+\r
+sub logged_in {\r
+       return $logged_in;\r
+}\r
+\r
+# Creates the session and returns the cookie header for a newly-logged in user.\r
+sub login {\r
+       my $sid = generate_id();\r
+\r
+       my $sql = "insert into $tbl_authentication\r
+               (session_id, create_time) values(?, ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($sid, time());\r
+\r
+       $logged_in = 1;\r
+       \r
+       return "Set-Cookie: $ctag=$sid; path=$pagepath; expires=Fri, 03-Sep-2020 20:20:13 GMT\n";\r
+}\r
+\r
+# Logs out and returns the cookie header\r
+sub logout {\r
+       if(defined($sid)) {\r
+               my $sql = "delete from $tbl_authentication \r
+                       where (session_id = ?)";\r
+               my $sth = $dbh->prepare($sql);\r
+               $sth->execute($sid);\r
+\r
+               $logged_in = 0;\r
+               \r
+               return "Set-Cookie: $ctag=; path=$pagepath; expires=Fri, " . \r
+                       "03-Sep-2020 20:20:13 GMT\n";\r
+       }\r
+}\r
+\r
+sub generate_id {\r
+       my @valid = ('A'..'Z','a'..'z','0'..'9');\r
+       my $i;\r
+       my $rv = "";\r
+       \r
+       for($i = 0; $i < 32; $i++) {\r
+               $rv = $rv . $valid[rand @valid];\r
+       }\r
+       \r
+       return $rv;\r
+}\r
+\r
+\r
+\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/Snapshots.pm b/lib/Insipid/Snapshots.pm
new file mode 100755 (executable)
index 0000000..c95b22f
--- /dev/null
@@ -0,0 +1,605 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Snapshots;\r
+\r
+use strict;\r
+use warnings;\r
+\r
+use vars qw(@ISA @EXPORT);\r
+use Insipid::Config;\r
+use Insipid::Database;\r
+use Insipid::Util;\r
+use Insipid::LinkExtractor;\r
+use CGI qw/:standard/;\r
+use CGI::Carp qw(fatalsToBrowser);\r
+use Date::Format;\r
+use Date::Parse;\r
+use Date::Parse;\r
+use Digest::MD5 qw(md5 md5_hex);\r
+use DBI qw/:sql_types/;;\r
+use LWP::UserAgent;\r
+use HTTP::Request;\r
+use MIME::Base64;\r
+use XML::Writer;\r
+\r
+require Exporter;\r
+\r
+\r
+@ISA = qw(Exporter);\r
+\r
+@EXPORT = qw(\r
+show_snapshots\r
+do_snapshot\r
+delete_snapshot\r
+export_snapshots\r
+show_snapshot\r
+fetch_related\r
+parsepage\r
+fetch_url\r
+);\r
+\r
+use SelfLoader;\r
+__DATA__\r
+\r
+my $ua = LWP::UserAgent->new(timeout=>30);\r
+if(get_option('proxy_host') ne '') {\r
+       my $proxy_host = get_option('proxy_host');\r
+       my $proxy_port = get_option('proxy_port');\r
+       $ua->proxy(['http', 'ftp'], "http://$proxy_host:$proxy_port/");\r
+}\r
+       \r
+\r
+my $referer = "";\r
+\r
+sub export_snapshots {\r
+       my ($writer) = (@_);\r
+       my ($sql, $sth, @rs);\r
+\r
+       # Export the objects\r
+       $writer->startTag('objects');\r
+       $sql = "select md5, url, content_type, content_length, date, content\r
+               from $tbl_pagecache";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+       \r
+       while(@rs = $sth->fetchrow_array()) {\r
+               $writer->startTag('object',\r
+                               'md5' => $rs[0],\r
+                               'url' => $rs[1],\r
+                               'type' => $rs[2],\r
+                               'length' => $rs[3],\r
+                               'date' => $rs[4]\r
+                               );\r
+               $writer->characters(encode_base64($rs[5]));\r
+               $writer->endTag("object");\r
+       }\r
+       $writer->endTag('objects');     \r
+\r
+       # Export the relationships \r
+       $writer->startTag('relationships');\r
+\r
+       $sql = "select md5_parent, md5_child from $tbl_pagecache_references";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+\r
+       while(@rs = $sth->fetchrow_array()) {\r
+               $writer->startTag("relationship",\r
+                               "parent" => $rs[0],\r
+                               "child" => $rs[1]);\r
+               $writer->endTag("relationship");\r
+       }\r
+       \r
+       $writer->endTag("relationships");\r
+\r
+}\r
+\r
+# TODO: Make the insert_snapshot callable by this and the import method.\r
+sub fetch_url {\r
+       my ($url, $roverride) = (@_);\r
+\r
+       # TODO: No.\r
+       if(defined($roverride)) { $referer = $roverride; }\r
+       my $md5 = md5_hex($url);\r
+\r
+       my $req = HTTP::Request->new(GET => $url);\r
+       if($referer ne '') { $req->header( referer => $referer ); }\r
+\r
+       my $res = $ua->request($req);\r
+\r
+       if($res->is_success) {\r
+               my $content = $res->content;\r
+\r
+               # Shove the unparsed page into the cache.\r
+               my $sql = "insert into $tbl_pagecache(md5, url, content_type, \r
+                               content_length, content, date)\r
+                       values ( ? , ? , ? , ? , ? , ? )";\r
+\r
+               my $sth = $dbh->prepare($sql);\r
+               my $ct = $res->header('Content-Type');\r
+               if(length($ct) > 50) { $ct = substr($ct, 0, 50); }\r
+\r
+               $sth->bind_param(1, $md5);\r
+               $sth->bind_param(2, $url);\r
+               $sth->bind_param(3, $ct);\r
+               $sth->bind_param(4, length($content));\r
+\r
+               # Postgres needs escaping for the binary data.\r
+               if($dbtype eq 'Pg') {\r
+                       $sth->bind_param(5, $content, SQL_VARBINARY);\r
+               } else {\r
+                       $sth->bind_param(5, $content);\r
+               }\r
+               $sth->bind_param(6, time());\r
+               $sth->execute;\r
+\r
+               if($sth->err) {\r
+               #       print $sth->errstr;\r
+               #       print "<br />";\r
+                       return 1;                        \r
+               } else {\r
+                       if($ct =~ /text\/html/i) {\r
+                               print '<br />Parsing page... ';\r
+                               parsepage($url, $content, $ct);\r
+                               print 'done.';\r
+                       }\r
+\r
+                       return 0;\r
+               }\r
+       } else {\r
+               my $err = $res->status_line;\r
+               print "$err<br />";\r
+\r
+               return 1;\r
+       }\r
+}\r
+\r
+sub show_snapshot {\r
+       my ($md5) = (@_);\r
+       my ($sql, $sth, @row);\r
+       my %internalLinks = ();\r
+\r
+       $sql = "select content_type,content,url,date,content_length\r
+                       from $tbl_pagecache where (md5 = ?)";\r
+               \r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute($md5);\r
+       \r
+       @row = $sth->fetchrow_array;\r
+       \r
+       if(!@row) {\r
+               print 'Content-Type: text/plain\r\n\r\n';\r
+               print "Can't find cached item \"$md5\"";\r
+               return;\r
+       }\r
+\r
+\r
+       # Check for IMS request.\r
+       my $ims = http('If-Modified-Since');\r
+       if($ims) { \r
+\r
+               my $t = str2time($ims);\r
+\r
+               if($row[3] <= $t) {\r
+                       # Return a 304 not modified.\r
+                       print 'Status: 304 Not Modified\r\n';\r
+                       return;\r
+               }\r
+       }\r
+       \r
+       my $dt = ims_time($row[3]);\r
+       print "Last-Modified: $dt\r\n";\r
+       print "Content-Type: $row[0]\r\n";\r
+\r
+       if($row[0] =~ /text\/html/i) {\r
+               # Now we get a list of URLs that can be redirected to our \r
+               # local snapshot cache. We'll use that to build a hash of \r
+               # URL->MD5 values and match outputted links against that.\r
+               my ($resql, $resth, @rerow);\r
+               $resql = "select $tbl_pagecache_references.md5_child,\r
+                       $tbl_pagecache.url\r
+                       from $tbl_pagecache_references\r
+                       inner join $tbl_pagecache on \r
+                               ($tbl_pagecache_references.md5_child = \r
+                                       $tbl_pagecache.md5)\r
+                       where (md5_parent = ?)";\r
+               $resth = $dbh->prepare($resql);\r
+               $resth->execute($md5);\r
+\r
+               while(@rerow = $resth->fetchrow_array()) {\r
+                       $internalLinks{$rerow[1]} = $rerow[0];\r
+               }\r
+\r
+               print "\r\n";\r
+               my $p = MyParser->new($row[2], undef);\r
+               $p->setSnapshotMap(\%internalLinks);\r
+               \r
+               if($row[0] =~ /utf/i) {\r
+                       $p->utf8_mode(1);\r
+               }\r
+               $p->parse($row[1]);\r
+       } else {\r
+               print "Content-Length: $row[4]\r\n";\r
+               print "\r\n";\r
+               print $row[1];\r
+       }\r
+\r
+       exit;\r
+}\r
+\r
+sub show_details {\r
+       my ($md5) = @_;\r
+       my ($sth, $sql);\r
+       \r
+       $sql = "select $tbl_bookmarks.title from $tbl_bookmarks\r
+               where ($tbl_bookmarks.md5 = ?)";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute($md5);\r
+       my @row = $sth->fetchrow_array();\r
+\r
+       print '<h3>Cache Details for "';\r
+       print escapeHTML($row[0]);\r
+       print '"</h3>';\r
+       print '<br /><center><table cellpadding="5"><tr><th>View</th><th>URL</th><th>';\r
+       print 'Type</th><th>Size</th><th>Ref Count</th></tr>';\r
+\r
+       $sql = "select $tbl_pagecache.md5, $tbl_pagecache.url, \r
+                       $tbl_pagecache.content_type,\r
+                       $tbl_pagecache.content_length, \r
+                       pg2.md5_parent, count(*)\r
+                       from $tbl_pagecache_references\r
+                       inner join $tbl_pagecache on \r
+                               ($tbl_pagecache_references.md5_child = \r
+                                       $tbl_pagecache.md5)\r
+                       left join $tbl_pagecache_references as pg2 on \r
+                               (pg2.md5_child = $tbl_pagecache.md5)\r
+                       where ($tbl_pagecache_references.md5_parent = ?)\r
+                       group by $tbl_pagecache.md5, $tbl_pagecache.url, \r
+                               $tbl_pagecache.content_type, pg2.md5_parent, \r
+                               $tbl_pagecache.content_length, pg2.md5_child\r
+                       order by $tbl_pagecache.url";\r
+                               \r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute($md5);\r
+\r
+       while(my @rs = $sth->fetchrow_array()) {\r
+               print '<tr><td>';\r
+               my $ss = "$snapshot_url$rs[0]";\r
+               \r
+               print "<a href=\"$rs[1]\">live</a>/<a href=\"$ss\">snapshot</a>";\r
+               print '</td><td>';\r
+               print $rs[1];\r
+               print '</td><td>';\r
+               print $rs[2];\r
+               print '</td><td>';\r
+               print $rs[3];\r
+               print '</td><td>';\r
+               print $rs[5];\r
+               print '</td></tr>';\r
+       }\r
+\r
+       print '</table></center>';\r
+}\r
+\r
+#\r
+# Show a nice menu of the users snapshots.\r
+#\r
+sub show_snapshots {\r
+\r
+       # If a snapshot was asked to be deleted\r
+       if(defined(param('delete'))) {\r
+               delete_snapshot(param('delete'));\r
+       }\r
+\r
+       if(defined(param('md5'))) {\r
+               show_details(param('md5'));\r
+               return;\r
+       }\r
+\r
+       my $tcount = 0;\r
+       my $tsize = 0;\r
+       \r
+       my $sql = "select $tbl_pagecache.md5, $tbl_bookmarks.title, \r
+                               $tbl_pagecache.date,\r
+                               $tbl_pagecache.content_length + \r
+                               coalesce(sum(p2.content_length), 0), \r
+                               count(*) - 1, $tbl_bookmarks.access_level\r
+                       from $tbl_pagecache \r
+                       inner join $tbl_bookmarks on \r
+                               ($tbl_bookmarks.md5 = $tbl_pagecache.md5)\r
+                       left join $tbl_pagecache_references on \r
+                          ($tbl_pagecache.md5 = $tbl_pagecache_references.md5_parent) \r
+                       left join $tbl_pagecache as p2 on\r
+                          (p2.md5 = $tbl_pagecache_references.md5_child)\r
+                       group by \r
+                               $tbl_bookmarks.access_level,\r
+                               $tbl_pagecache.md5, $tbl_bookmarks.title,\r
+                               $tbl_pagecache.date, $tbl_pagecache.content_length\r
+                       order by $tbl_pagecache.date desc";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute;\r
+       \r
+       print '<br /><center><table cellpadding=\"5\"><tr><th>Page</th><th>';\r
+       print 'Date</th><th>Size</th><th>Objects</th><th>Functions</th></tr>';\r
+\r
+       my $count = 0;\r
+\r
+       while(my @r = $sth->fetchrow_array) {\r
+\r
+               $count++;\r
+               \r
+               my $color;\r
+               if(($count % 2) eq 1) {\r
+                       $color = ' bgcolor="#EEEEEE" ';\r
+               } else {\r
+                       $color = '';\r
+               }\r
+               \r
+               print "<tr $color>";\r
+               print '<td>';\r
+               \r
+               print "<a href=\"$snapshot_url$r[0]\">";\r
+               if($r[5] eq 0) { print '<i>'; }\r
+               print $r[1];\r
+               if($r[5] eq 0) { print '</i>'; }\r
+               print '</a></td>';\r
+               my $timestr = time2str('%Y-%m-%d', $r[2], 'EST');\r
+               my $count = $r[4] + 1; $tcount += $count;\r
+               $tsize += $r[3];\r
+               print "<td align=\"center\">$timestr</td>";\r
+               print "<td align=\"center\">$r[3]</td>";\r
+\r
+               my $link = "$site_url/insipid.cgi?op=snapshots&md5=$r[0]";\r
+\r
+               if($count ne 1) {\r
+                       print "<td align=\"center\"><a href=\"$link\">$count</a></td>";\r
+               } else {\r
+                       print "<td align=\"center\">$count</td>";\r
+               }\r
+               \r
+               print '<td>';\r
+               print "<a href=\"insipid.cgi?op=snapshots&delete=$r[0]\">delete</a>,";\r
+               print " <a href=\"insipid.cgi?op=fetchrelated&id=$r[0]\">";\r
+               print "fetch linked objects</a></td>";\r
+               print '</tr>';\r
+       }\r
+\r
+       print '<tr><td><b>Total</b></td><td>&nbsp;</td>';\r
+       print "<td align=\"center\"><b>$tsize</b></td>";\r
+       print "<td align=\"center\"><b>$tcount</b></td><td>&nbsp;</td></tr>";\r
+       \r
+       print "</table></center>";\r
+}\r
+\r
+# This fetches all linked-to objects (specifically images for now)\r
+# for a cached page.\r
+sub fetch_related {\r
+       my ($md5) = (@_);\r
+\r
+       my $sql = "select content, url\r
+               from $tbl_pagecache where (md5 = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($md5);\r
+\r
+       my @r = $sth->fetchrow_array();\r
+\r
+       my $p = Insipid::LinkExtractor->new($r[1]);\r
+       $p->parse($r[0]);\r
+\r
+}\r
+\r
+# Deletes a snapshot and all orphan cache children, taking into\r
+# account the fact that items can be shared across cached pages.\r
+#\r
+# This is horribly expensive, and someday I'll replace it with\r
+# a much nicer function.\r
+sub delete_snapshot {\r
+       my ($md5) = (@_);\r
+       \r
+       # The snapshot\r
+       my $sql = "delete from $tbl_pagecache where (md5 = ?)";\r
+       my $delstatement = $dbh->prepare($sql);\r
+       $delstatement->execute($md5);\r
+\r
+       # References\r
+       $sql = "delete from $tbl_pagecache_references where (md5_parent = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($md5);\r
+       \r
+       # Orpans - blow away any md5s in the pagecache table that aren't \r
+       # referenced as a child in the references table. First, get a list\r
+       # of valid MD5s.\r
+       $sql = "select distinct md5_child from $tbl_pagecache_references";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+\r
+       my $subquery = '';\r
+       while(my @r = $sth->fetchrow_array) {\r
+               if($subquery ne '') { $subquery = $subquery . ','; }\r
+               $subquery = "$subquery '$r[0]'";\r
+       }\r
+       \r
+       $sql = "select distinct md5_parent from $tbl_pagecache_references";\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+       while(my @r = $sth->fetchrow_array) {\r
+               if($subquery ne '') { $subquery = $subquery . ','; }\r
+               $subquery = "$subquery '$r[0]'";\r
+       }\r
+\r
+       if($subquery eq '') {\r
+               $sql = "delete from $tbl_pagecache;";\r
+       } else {\r
+               $sql = "delete from $tbl_pagecache where md5 not in ($subquery)";\r
+       }\r
+       \r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute();\r
+}\r
+\r
+sub do_snapshot {\r
+       # Save the page.\r
+       print '<br /><br />';\r
+       \r
+       my ($bookmark_id) = (@_);\r
+       my $sql = "select url,md5,title from $tbl_bookmarks where (id = ?)";\r
+        my $sth = $dbh->prepare($sql);\r
+       $sth->execute($bookmark_id);\r
+       my @row = $sth->fetchrow_array;\r
+\r
+       if(@row) {\r
+               print "<p>Fetching \"<b>$row[2]</b>\"...</p>\n";\r
+               $referer = $row[0];\r
+               fetch_url(@row);        \r
+       } else {\r
+               die "Couldn't find the row for id $bookmark_id!";\r
+       }\r
+}\r
+\r
+sub parsepage {\r
+       my ($url, $content, $content_type) = (@_);\r
+\r
+       my $p = MyParser->new($url, \&fetch_url);\r
+       if($content_type =~ /utf/i) { \r
+               $p->utf8_mode(1);\r
+       }\r
+\r
+       $p->parse($content);\r
+}\r
+\r
+## "use MyParser;" ## TODO: Make this a separate file.\r
+BEGIN {\r
+       package MyParser;\r
+       use HTML::Parser;\r
+       use HTML::Entities ();\r
+       use URI::URL;\r
+       use Digest::MD5 qw(md5 md5_hex);\r
+       use Insipid::Config;\r
+       use Insipid::Database;\r
+\r
+       use vars qw(@ISA);\r
+       @ISA = qw(HTML::Parser);\r
+\r
+       sub setSnapshotMap {\r
+               my($self, $ssMap) = (@_);\r
+               $self->{SSMAP} = $ssMap;\r
+       }\r
+\r
+       sub new {\r
+               my $pack = shift;\r
+               my $self = $pack->SUPER::new;\r
+               @{$self}{qw(__base __grabit)} = @_;\r
+               $self;\r
+       }\r
+\r
+       sub declaration {\r
+               my $self = shift;\r
+               my ($decl) = @_;\r
+       }\r
+\r
+       sub start {\r
+               my $self = shift;\r
+               my ($tag, $attr, $attrseq, $origtext) = @_;\r
+\r
+               if(!defined($self->{__grabit})) {\r
+                       print("<$tag");\r
+               }\r
+\r
+               for (keys %$attr) {\r
+                       my $val = $attr->{$_};\r
+                       if(($_ eq "/") && ($val = "/")) { next; }\r
+\r
+                       if(!defined($self->{__grabit})) { \r
+                               print(" $_=\""); \r
+                       }\r
+\r
+                       if( "$tag $_" =~ /^(link href|img src)$/i) {\r
+                               $val = url($val)->abs($self->{__base},1);\r
+       \r
+                               if(!defined($self->{__grabit})) {\r
+                                       if($val =~ /(\.gif|\.jpg|\.png|\.css)$/i) {\r
+                                               my $md5 = md5_hex("$val");\r
+                                               $val = $snapshot_url . $md5;\r
+                                       }\r
+                               } else {\r
+                                       # JPG, GIF, PNG and CSS\r
+                                       if($val =~ /(\.gif|\.jpg|\.png|\.css)$/i) {\r
+                                               join_urls($self->{__base}, $val);\r
+                                               $val = $self->{__grabit}($val, $1);\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       if(!defined($self->{__grabit})) {\r
+                               # Check against our snapshot map\r
+                               if(($tag =~ /^a/i) && ($_ =~ /^href/i)) {\r
+                                       my $sst = $self->{SSMAP};\r
+\r
+                                       if(defined($sst->{$val})) {\r
+                                               print $snapshot_url . $sst->{$val};\r
+                                               print('"');\r
+                                       } else {\r
+                                               print("$val\"");\r
+                                       }\r
+                               } else {\r
+                                       print("$val\"");\r
+                               }\r
+                       }\r
+               }\r
+\r
+               if(!defined($self->{__grabit})) { print(">"); }\r
+       }\r
+\r
+       sub end {\r
+               my $self = shift;\r
+               my ($tag) = @_;\r
+\r
+               if(!defined($self->{__grabit})) { print("</$tag>"); }\r
+       }\r
+\r
+       sub text {\r
+               my $self = shift;\r
+               my ($text) = @_;\r
+\r
+               if(!defined($self->{__grabit})) { print("$text"); }\r
+       }\r
+\r
+       sub comment {\r
+               my $self = shift;\r
+               my ($comment) = @_;\r
+\r
+               if(!defined($self->{__grabit})) { print("<!-- $comment -->"); }\r
+       }\r
+       \r
+       sub join_urls {\r
+               my($parent, $child) = (@_);\r
+               my $sql = "insert into $tbl_pagecache_references(md5_parent, md5_child) values(?, ?)";\r
+               my $sth = $dbh->prepare($sql);\r
+               $sth->execute(md5_hex($parent), md5_hex($child));\r
+               if($sth->err) {\r
+                       # ignore errors for now\r
+               }\r
+       }\r
+\r
+}\r
+## end "use MyParser;" ##\r
+\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/Tags.pm b/lib/Insipid/Tags.pm
new file mode 100755 (executable)
index 0000000..17a7bef
--- /dev/null
@@ -0,0 +1,238 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Tags;\r
+\r
+use strict;\r
+use warnings;\r
+\r
+use vars qw(@ISA @EXPORT @EXPORT_OK);\r
+use CGI qw/:standard/;\r
+use CGI::Carp qw(fatalsToBrowser);\r
+use Insipid::Config;\r
+use Insipid::Database;\r
+use Insipid::Sessions;\r
+require Exporter;\r
+\r
+@ISA = qw(Exporter);\r
+\r
+@EXPORT = qw(\r
+show_tags\r
+get_tags\r
+get_tags_list\r
+set_tags\r
+tag_operations\r
+);\r
+\r
+sub tag_operations {\r
+       print '<h2>Rename Tag</h2>';\r
+       print '<select name="rename">';\r
+       show_tags(1);\r
+       print '</select>';\r
+       print '<h2>Delete Tag</h2>';\r
+       print '<select name="delete">';\r
+       show_tags(1);\r
+       print '</select>';\r
+}\r
+\r
+# Display the tag list.  Takes one parameter for the mode - 0 is for the\r
+# sidebar, 1 is for a SELECT box. TODO: Cache the actual result set so\r
+# that when there's more than one tag list on a page we only hit the database\r
+# once.\r
+sub show_tags {\r
+       my ($mode) = shift;\r
+       if(!defined($mode)) { $mode = 0; }\r
+       \r
+       my ($sql, $sth);\r
+       if($mode eq 0) { print "<div id=\"leftside\">"; }\r
+\r
+       # If the user has already chosen a tag, get the intersection list\r
+       if(defined(url_param('tag')) && (logged_in() eq 1)) {\r
+               $sql = "select $tbl_tags.name,count(*) from $tbl_bookmarks \r
+                       inner join $tbl_bookmark_tags as bt1 on \r
+                               ($tbl_bookmarks.id = bt1.bookmark_id) \r
+                       inner join $tbl_tags on \r
+                               ($tbl_tags.id = bt1.tag_id)\r
+                       inner join $tbl_bookmark_tags as bt2 on \r
+                               ($tbl_bookmarks.id = bt2.bookmark_id) \r
+                       inner join $tbl_tags as t2 on \r
+                               (t2.id = bt2.tag_id and t2.name = ?)\r
+                       where ($tbl_tags.name != ?)\r
+                       group by $tbl_tags.name";\r
+               $sth = $dbh->prepare($sql);\r
+               $sth->execute(url_param('tag'), url_param('tag'));\r
+\r
+               if($sth->rows ne 0) {\r
+                       print "<div id=\"taglist\" style=\"\">";\r
+                       print "<table cellpadding =\"0\" cellspacing=\"0\" ";\r
+                       print 'class="tagsummarytable"><tbody>';\r
+                       print "<tr><td align=\"center\">";\r
+                       print "<div class=\"inline_title\">Add Tag</div></td>";\r
+               \r
+                       while(my @rs = $sth->fetchrow_array()) {\r
+                               my $tt = url_param('tag');\r
+                               my $link = "$tag_url$tt+$rs[0]";\r
+                               $tt =~ s/ /\+/g;\r
+\r
+                               print "<tr><td>&nbsp;<span class=\"tagtabletext\">($rs[1])&nbsp;</span><a href=\"$link\">$rs[0]</a>&nbsp;";\r
+                               print "</tr></td>\n";\r
+                       }\r
+\r
+                       print "</tbody></table></div></div>";\r
+\r
+                       return;\r
+               }\r
+       }\r
+\r
+       # Access_spec contains a where clause to count only public bookmarks \r
+       # if the user is not logged in\r
+       my $access_where = "";\r
+       if(logged_in() eq 0) {\r
+               $access_where = " where ($tbl_bookmarks.access_level = 1) ";\r
+       }\r
+\r
+       my $order_clause;\r
+       if($dbtype eq "Pg") {\r
+               $order_clause = "order by upper($tbl_tags.name)";\r
+       } else {\r
+               $order_clause = "order by $tbl_tags.name";\r
+       }\r
+\r
+       $sql = "select $tbl_tags.name, count(*) \r
+                  from $tbl_bookmarks  \r
+                  inner join $tbl_bookmark_tags on\r
+                       ($tbl_bookmarks.id = $tbl_bookmark_tags.bookmark_id)\r
+                  inner join $tbl_tags on\r
+                       ($tbl_tags.id = $tbl_bookmark_tags.tag_id)\r
+                  $access_where\r
+                  group by $tbl_tags.name\r
+                  $order_clause"; \r
+\r
+       $sth = $dbh->prepare($sql);\r
+       $sth->execute;\r
+\r
+       if($mode eq 0) {\r
+               print '<div id="taglist" style="">';\r
+               print '<table cellpadding="0" cellspacing="0" ';\r
+               print 'class="tagsummarytable"><tbody>';\r
+               print '<tr><td align="center"><div class="inline_title">';\r
+               print 'Tag List</div></td>';\r
+       }\r
+       \r
+       while(my @r = $sth->fetchrow_array) {\r
+               if($mode eq 0) {\r
+                       print "<tr><td>&nbsp;<span class=\"tagtabletext\">($r[1])";\r
+                       print "&nbsp;</span><a href=\"$tag_url$r[0]\">$r[0]</a>&nbsp;";\r
+                       print "</td></tr>\n";\r
+               } else {\r
+                       print "<option name=\"$r[0]\">$r[0]</option>";\r
+               }\r
+       }\r
+\r
+       if($mode eq 0) {\r
+               print "</tbody></table></div>";\r
+               print "</div>";\r
+       }\r
+}\r
+\r
+# Get a string representing a URLs tags\r
+sub get_tags {\r
+       my ($url) = (@_);\r
+       my @tags = get_tags_list($url);\r
+\r
+       my $rv = "";\r
+       foreach (@tags) {\r
+               $rv = "$rv $_";\r
+       }\r
+\r
+       # Trim leading whitespace\r
+       $rv =~ s/^\s+//;\r
+       return $rv;\r
+}\r
+\r
+# Get a list of the tags for a given URL id\r
+sub get_tags_list {\r
+       my ($url) = (@_);\r
+       my $sql = "select $tbl_tags.name from $tbl_tags \r
+                       inner join $tbl_bookmark_tags on \r
+                               ($tbl_tags.id = $tbl_bookmark_tags.tag_id) \r
+                       inner join $tbl_bookmarks on\r
+                               ($tbl_bookmark_tags.bookmark_id = $tbl_bookmarks.id)\r
+                       where ($tbl_bookmarks.url = ?)";\r
+  \r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($url);\r
+\r
+       my @tags;\r
+       while(my @r = $sth->fetchrow_array) {\r
+               push(@tags, $r[0]);\r
+       }\r
+\r
+       return @tags;\r
+}\r
+\r
+# Sets tags for a bookmark.  Takes a bookmark ID and a string\r
+# representing the tags as parameters.\r
+sub set_tags {\r
+       my ($bookmark_id, $tag_string) = (@_);\r
+\r
+       if(logged_in() ne 1) {\r
+               push(@errors, "You have to be logged in to perform that operation.");\r
+               return;\r
+       }\r
+       \r
+       my @tags = split(" ", $tag_string);\r
+       \r
+       # Clear old tags first.\r
+       my $sql = "delete from $tbl_bookmark_tags where (bookmark_id = ?)";\r
+       my $sth = $dbh->prepare($sql);\r
+       $sth->execute($bookmark_id);\r
+       \r
+       foreach my $cur (@tags) {\r
+           # check if this tag exists in tags table\r
+           my $sql = "select count(id) from $tbl_tags where (name = ?)";\r
+           my $sth = $dbh->prepare($sql);\r
+           $sth->execute($cur);\r
+           my @rv = $sth->fetchrow_array;\r
+           my $tagcount = $rv[0];\r
+\r
+           # or create a new tag\r
+           if ($tagcount < 1) {\r
+               my $sql = "insert into $tbl_tags (name) values(?)";\r
+               my $sth = $dbh->prepare($sql);\r
+               $sth->execute($cur);\r
+           }\r
+\r
+           # and fetch the tag ID\r
+           $sql = "select id from $tbl_tags where (name = ?)";\r
+           $sth = $dbh->prepare($sql);\r
+           $sth->execute($cur);\r
+           my $tid = $sth->fetchrow_array;\r
+\r
+           $sql = "insert into $tbl_bookmark_tags(bookmark_id, tag_id) \r
+                 values( ? , ? )";\r
+           $sth = $dbh->prepare($sql);\r
+           $sth->execute($bookmark_id, $tid);\r
+       }\r
+}\r
+\r
+\r
+1;\r
+__END__\r
diff --git a/lib/Insipid/Util.pm b/lib/Insipid/Util.pm
new file mode 100755 (executable)
index 0000000..6a9d870
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+package Insipid::Util;\r
+\r
+use strict;\r
+use warnings;\r
+\r
+use vars qw(@ISA @EXPORT @EXPORT_OK);\r
+\r
+use Insipid::Sessions;\r
+use Insipid::Config;\r
+\r
+require Exporter;\r
+\r
+@ISA = qw(Exporter);\r
+\r
+@EXPORT = qw(\r
+ims_time\r
+sanitize_html\r
+check_access\r
+);\r
+\r
+@EXPORT_OK = qw();\r
+\r
+my @DoW = qw(Sun Mon Tue Wed Thu Fri Sat);\r
+my @MoY = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);\r
+\r
+# TODO: If content is already sent, add the error to @errors instead of dying\r
+sub check_access {\r
+       if(logged_in() ne 1) {\r
+               print "Content-Type: text/plain\r\n\r\n";\r
+               print "You have to be logged in to perform that operation.";\r
+               exit;\r
+       }\r
+}\r
+\r
+sub sanitize_html {\r
+       my ($orig) = (@_);\r
+\r
+       $orig =~ s/</&lt;/gi;\r
+       $orig =~ s/>/&gt;/gi;\r
+\r
+       $orig =~ s/&amp;/&/gi;\r
+       $orig =~ s/&/&amp;/gi;\r
+\r
+       return $orig;\r
+}\r
+\r
+\r
+# From http::date\r
+sub ims_time {\r
+    my ($time) = (@_);\r
+    my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($time);\r
+    return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT",\r
+            $DoW[$wday],\r
+            $mday, $MoY[$mon], $year+1900,\r
+            $hour, $min, $sec);\r
+}\r
+\r
+1;\r
+__END__\r
diff --git a/tools/export.pl b/tools/export.pl
new file mode 100755 (executable)
index 0000000..c0c2212
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+use strict;\r
+\r
+if(defined($ENV{SERVER_NAME})) {\r
+       print "Content-Type: text/plain\r\n\r\nThis is a command-line tool.\n";\r
+       exit;\r
+}\r
+\r
+push(@INC, "../lib");\r
+\r
+eval {\r
+       require Insipid::Config;\r
+       require Insipid::Main;\r
+       \r
+       $Insipid::Config::logged_in = 1;\r
+       $Insipid::Config::logged_in = 1;\r
+\r
+       Insipid::Main::do_export('y', 'y');\r
+};\r
+\r
+if($@) {\r
+       print "$@\n";\r
+}\r
+\r
diff --git a/tools/import.pl b/tools/import.pl
new file mode 100755 (executable)
index 0000000..a23af90
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/perl\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+use strict;\r
+\r
+if(defined($ENV{SERVER_NAME})) {\r
+       print "Content-Type: text/plain\r\n\r\nThis is a command-line tool.\n";\r
+       exit;\r
+}\r
+\r
+push(@INC, "../lib");\r
+\r
+eval {\r
+       require Insipid::Config;\r
+       require Insipid::Main;\r
+       \r
+       $Insipid::Config::logged_in = 1;\r
+       $Insipid::Config::logged_in = 1;\r
+       \r
+       Insipid::Main::do_import();\r
+};\r
+\r
+if($@) {\r
+       print "$@\n";\r
+}\r
+\r
diff --git a/tools/mozilla.pl b/tools/mozilla.pl
new file mode 100755 (executable)
index 0000000..a49f50d
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/perl -T\r
+#\r
+# Copyright (C) 2006 Luke Reeves\r
+#\r
+# Modified by: Manuel de la Torre <mdltorre(a)gmail(dot)com>\r
+#\r
+# This program is free software; you can redistribute it and/or modify\r
+# it under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation; either version 2 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307\r
+# USA\r
+#\r
+\r
+use warnings;\r
+use strict;\r
+\r
+use XML::Writer;\r
+use Date::Format;\r
+use IO::File;\r
+\r
+if(defined($ENV{SERVER_NAME})) {\r
+       print "Content-Type: text/plain\r\n\r\nThis is a command-line tool.\n";\r
+       exit;\r
+}\r
+\r
+if($#ARGV eq -1) {\r
+       print "Usage: ./mozilla.pl <bookmarks filename>\n\n";\r
+       exit;\r
+}\r
+\r
+my $output = new IO::File(">insipid.xml");\r
+my $writer = new XML::Writer(OUTPUT => $output, DATA_MODE => 1, DATA_INDENT => 4);\r
+my $count = 0;\r
+my $currentLine;\r
+my $currentTag;\r
+\r
+$writer->xmlDecl('UTF-8');\r
+$writer->startTag('insipid');\r
+$writer->startTag('posts');\r
+\r
+open(FH, $ARGV[0]);\r
+\r
+while( defined($currentLine = <FH>) ) {\r
+\r
+       \r
+       if ($currentLine =~ /<H3.*\">(.*?)</) {\r
+               if(defined($1)) {\r
+                       $currentTag = $1;\r
+               }\r
+       }\r
+       \r
+       if ($currentLine =~ /\/DL/) {\r
+                       $currentTag = '';\r
+               }\r
+                       \r
+       if ($currentLine =~ /<A\sHREF=\"(.*?)\".*?\".*?>(.*?)<\/A>/) {\r
+       \r
+               if(defined($1) && defined($2)) {\r
+                       $writer->emptyTag('post',\r
+                       'access_level' => '0',\r
+                       'href' => $1,\r
+                       'description' => $2,\r
+                       'tag' => $currentTag,\r
+                       'time' => time2str("%Y-%m-%dT%TZ", time(), 'GMT')\r
+                       );\r
+               }\r
+       \r
+    }\r
+}\r
+\r
+$writer->endTag('posts');\r
+$writer->endTag('insipid');\r
+$writer->end();\r
+\r
+\r