<?php
/**
* Input/show/edit/save annotations.
*
* Copyright (C) 2005 Wayne Davison <hide@address.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
require 'lib/main.inc';
require 'lib/wiki.inc';
switch ($_GET['do']) {
case 'input':
if ($_POST['annoID'])
save_page(''); // Handle re-anchor
else
input_page();
break;
case 'show':
show_page();
break;
case 'edit':
edit_page('');
break;
case 'editnote':
edit_page('note');
break;
case 'save':
save_page('');
break;
case 'savenote':
save_page('note');
break;
case 'delete':
delete_page();
break;
default:
redir_main();
break;
}
function input_page()
{
global $CGI;
$book_num = $_GET['b'];
$book_sect = $_GET['s'];
$sel_pos = $_POST['pos'];
$tID = $_POST['tID'];
$nest = $_POST['nest'];
if (!preg_match('/^\d+;-?\d+;\d+;\d+$/', join(';', array($book_num, $book_sect, $sel_pos, $tID)))
|| !preg_match('!^(/[A-Z]+[1-6]?(#\d+)?)+$!', $nest)) {
redir_main();
return;
}
open_db();
list($uID, $scrname) = get_auth_cookie();
if (!$uID) {
start_content('Error', 'wide');
echo <<<EOT
You must be signed in to add an Annotation.
<p>If you already have an account, please <a href="signin.php">sign in</a>.
<p>Otherwise, either <a href="signin.php?do=signup">sign up</a> or press the "Back"
button on your browser.
EOT;
close_db();
end_content();
return;
}
sidebar_signin($uID, $scrname);
start_content('Add New Annotation', 'wide');
$group_list = get_groups($book_num, $scrname, 'CanAdd', $uID, $book_num, '');
foreach ($group_list as $gID => $name) {
if ($name == '/Private')
$group_def = $gID;
}
$sel = preg_replace(array('/&/', '/"/', '/</'), array('&', '"', '<'), $_POST['sel']);
$anno_attrs = get_attr_data($tID);
#$anno_attrs[99]->PromptText = 'Assign to group(s)';
$anno_attrs[99]->GroupList = $group_list;
$anno_attrs[99]->DefaultValue = $group_def;
$annotype = $anno_attrs['#NAME'];
$action = "$CGI?do=save&b=$book_num&s=$book_sect&a=0";
if ($anno_attrs[52]->DefaultValue != '') { // FilterURL
$redir = "<input type=hidden name=redir value=\"$action\">\n";
$action = $anno_attrs[52]->DefaultValue;
$warn = '<b>Note:</b> submitted data is being filtered through a 3rd-party site.';
}
echo <<<EOT
<h2 style="margin:0">Creating a new $annotype instance</h2>
<form name=mainform method=post action="$action">
Please enter these attributes for a $annotype attached to the text <span style="border: solid black 1px">$sel</span>:
<input type=hidden name=uID value="$uID">
<input type=hidden name=tID value="$tID">
<input type=hidden name=nest value="$nest">
<input type=hidden name=pos value="$sel_pos">
<input type=hidden name=sel value="$sel">
<input type=hidden name=hasFields value=1>
<script>document.scrname = '$scrname'</script>
$redir<p><table border=1>
EOT;
foreach ($anno_attrs['#ORDER'] as $aID) {
$attrs = $anno_attrs[$aID];
if ($attrs->Need == 'req') {
$note = '<div>(<b>*</b>) These attributes are required to have a non-empty value.';
$suffix = '<b>*</b>';
} elseif ($attrs->Need == 'const')
continue;
else
$suffix = '';
echo "<tr valign=top><td>", $attrs->PromptText, $suffix, ":</td><td>";
output_input_field($attrs->Name, $attrs, $attrs->DefaultValue, $annotype,
$attrs->InputWidth, $attrs->InputHeight, $attrs->Need == 'filt',
$book_num, 0);
echo "</td></tr>\n";
}
echo <<<EOT
</table>
$warn<center><input type=submit name=sub value="Create" tabindex=1>
<input type=button value="Cancel" onclick="history.go(-1)" tabindex=1></center>
$note
</form>
EOT;
close_db();
end_content();
}
function show_page()
{
global $CGIDIR;
$book_num = $_GET['b'];
$book_sect = $_GET['s'];
$book_anno = $_GET['a'];
if (!preg_match('/^\d+;-?\d+;\d+$/', join(';', array($book_num, $book_sect, $book_anno)))) {
redir_main();
return;
}
open_db();
list($uID, $scrname) = get_auth_cookie();
sidebar_signin($uID, $scrname);
start_content('Show', 'wide');
$do = "SELECT b.*, l.Name FROM BookAnnotations AS b LEFT JOIN AnnotationTypes AS l USING(TypeID)
WHERE b.ID = $book_anno AND b.BookID = $book_num AND b.ParentID = $book_sect
LIMIT 1";
$result = mysql_query($do) or die('SELECT failed: ' . mysql_error() . " (cmd: $do)");
if (($lnk = mysql_fetch_assoc($result)) === FALSE) {
echo 'Invalid request';
end_content();
return;
}
$do = 'SELECT Name FROM Users WHERE UserID = ' . $lnk['UserID'];
$result = mysql_query($do) or die('SELECT failed: ' . mysql_error() . " (cmd: $do)");
list($lnk_author) = mysql_fetch_row($result);
$group_list = get_groups($book_num, $scrname, '', $uID, $book_num, $lnk['_99']);
echo <<<EOT
<h2 style="margin:0">Data for {$lnk['Name']}</h2>
<p><table border=1>
EOT;
$url = $CGIDIR . 'read.php';
$glist = array();
foreach (explode(',', $lnk['_99']) as $gID)
$glist[] = $group_list[$gID];
natcasesort($glist);
$lnk['_99'] = join(', ', $glist);
$anno_attrs = get_attr_data($lnk['TypeID']);
foreach ($anno_attrs['#ORDER'] as $aID) {
if ($aID == 52) // skip FilterURL
continue;
$attrs = $anno_attrs[$aID];
echo "<tr valign=top><td>", $attrs->PromptText, ':</td><td>';
$val = $lnk["_$aID"];
if (preg_match('/<' . preg_quote($val, '/') . '=([^>]+)>/', $attrs->InheritedDefault, $out))
$val = $out[1];
if ($attrs->InputHeight > 1) {
if (strlen($val) > 256)
$val = substr($val, 0, 256) . '<b>...</b>';
echo '<pre>', $val, '</pre>';
if ($aID == 51) // NoteText
echo '<a href="', "$url?b=$book_num&s=-$book_anno", '">[View Note]</a>';
} else if ($aID == 50) { // URL
$val = preg_replace(array('/^(\d+),(\d)/', '/^(\d)/'),
array("$url?b=$1&$2", "$url?b=$book_num&$1"),
$val);
echo '<a href="', $val, '">', $val, '</a>';
} else
echo $val;
echo "</td></tr>\n";
}
$url .= "?b=$book_num&s=$book_sect&t=" . $lnk['ID'] . '#pblnk';
if ($lnk['UserID'] == $uID) {
$edit = ' '
. button('btn-editnote',
"annotate.php?do=edit&b=$book_num&s=$book_sect&a=".$lnk['ID'],
'width=34 height=20');
}
echo <<<EOT
<tr valign=top><td>Author:</td><td>$lnk_author$edit</td></tr>
<tr valign=top><td>Location:</td><td><a href="$url">$url</a></td></tr>
</table>
<p><INPUT TYPE=button onClick="history.go(-1)" VALUE="Go Back" tabindex=1>
EOT;
close_db();
end_content();
}
function edit_page($act_suffix)
{
$book_num = $_GET['b'];
$book_sect = $_GET['s'];
$book_anno = $_GET['a'];
if (!preg_match('/^\d+;-?\d+;\d+$/',
join(';', array($book_num, $book_sect, $book_anno)))) {
redir_main();
return;
}
open_db();
list($uID, $scrname) = get_auth_cookie();
if (!$uID) {
redir_main();
return;
}
sidebar_signin($uID, $scrname);
start_content('Edit Annotation', 'wide');
$do = "SELECT * FROM BookAnnotations AS b LEFT JOIN AnnotationTypes AS l USING(TypeID)
WHERE b.ID = $book_anno AND b.BookID = $book_num AND b.ParentID = $book_sect AND b.UserID = $uID
LIMIT 1";
$result = mysql_query($do) or die('SELECT failed: ' . mysql_error() . " (cmd: $do)");
if (($lnk = mysql_fetch_assoc($result)) === FALSE) {
echo 'Invalid request';
end_content();
return;
}
$group_list = get_groups($book_num, $scrname, 'CanAdd', $uID, $book_num, $lnk['_99']);
echo <<<EOT
<h2 style="margin:0">Editing: {$lnk['Name']}</h2>
<form name=mainform method=post action='?do=save$act_suffix&b=$book_num&s=$book_sect&a=$book_anno'>
<input type=hidden name=uID value="$uID">
<input type=hidden name=tID value="{$lnk['TypeID']}">
<input type=hidden name=pos value="-1">
<input type=hidden name=hasFields value=1>
<script>document.scrname = '$scrname'</script>
<p>(<b>*</b>) These attributes are required to have a non-empty value.
<p><table border=1 width=640>
EOT;
$anno_attrs = get_attr_data($lnk['TypeID']);
#$anno_attrs[99]->PromptText = 'Assign to group(s)';
$anno_attrs[99]->GroupList = $group_list;
$annotype = $anno_attrs['#NAME'];
foreach ($anno_attrs['#ORDER'] as $aID) {
$attrs = $anno_attrs[$aID];
if ($attrs->Need == 'req')
$suffix = '<b>*</b>';
elseif ($attrs->Need == 'const')
continue;
else
$suffix = '';
$val = $lnk["_$aID"];
if ($val == '')
$val = $attrs->DefaultValue;
echo "<tr valign=top><td>", $attrs->PromptText, $suffix, ":</td><td>";
output_input_field($attrs->Name, $attrs, $val, $annotype,
$attrs->InputWidth, $attrs->InputHeight, 0,
$book_num, $book_anno);
echo "</td></tr>\n";
}
echo <<<EOT
</table>
<center><input type=submit name=sub value="Save" tabindex=1>
<input type=button value="Cancel" onclick="history.go(-1)" tabindex=1></center>
EOT;
foreach ($group_list as $g) {
if ($g[0] == '~') {
echo <<<EOT
<div>Groups <span style="color:red">colored red</span> will be removed if you save the annotation.</div>
EOT;
break;
}
}
echo "</form>\n";
close_db();
end_content();
}
function save_page($act_suffix)
{
global $CGI, $icon_names;
$book_num = $_GET['b'];
$book_sect = $_GET['s'];
$book_anno = $_GET['a'];
if ($book_anno == '')
$book_anno = $_POST['annoID'];
$sel_pos = $_POST['pos'];
$tID = $_POST['tID'];
if (!preg_match(':^\d+;-?\d+;\d+;-?\d+;\d+$:',
join(';', array($book_num, $book_sect, $book_anno, $sel_pos, $tID)))) {
redir_main();
return;
}
if ($sel_pos >= 0) {
$nest = $_POST['nest'];
if (!preg_match('!^(/[A-Z]+[1-6]?(#\d+)?)+$!', $nest)) {
redir_main();
return;
}
$sel = $_POST['sel'];
$testing = $tID == 99; // AnnotationType 99 is hard-wired as a testing item.
if (preg_match('/^(\s+)/', $sel, $out)) {
$sel_pos += strlen($out[1]);
$sel = preg_replace('/^\s+/', '', $sel);
}
$sel = preg_replace(array('/\r/', '/ +$/m'), '', $sel);
$sel_len = strlen($sel);
}
open_db();
list($uID, $scrname) = get_auth_cookie();
if (!$uID || $uID != $_POST['uID']) {
redir_main();
return;
}
add_sidebar_image('btn-return_to_book', "read.php?b=$book_num&s=$book_sect");
sidebar_signin($uID, $scrname);
start_content('Saving...', 'wide');
if ($book_anno) {
$do = "SELECT UserID
FROM BookAnnotations
WHERE ID=$book_anno AND BookID=$book_num AND ParentID=$book_sect AND TypeID='$tID'";
$result = mysql_query($do) or die('SELECT failed: ' . mysql_error() . " (cmd: $do)");
$row = mysql_fetch_row($result);
if ($row === FALSE || $row[0] != $uID) {
echo 'Invalid request';
end_content();
return;
}
}
// Note: the re-anchor action does not set hasFields.
$has_fields = $_POST['hasFields'];
$do_suf = '';
// Note: $sel_pos < 0 when editing an existing annotation.
if ($sel_pos >= 0) {
if ($book_sect >= 0) {
$do = "SELECT Text, Offsets
FROM BookSections
WHERE BookID = $book_num AND SectionID = $book_sect
LIMIT 1";
} else {
$note_num = -$book_sect;
$do = "SELECT _51 AS Note, NoteOffsets
FROM BookAnnotations
WHERE ID = $note_num AND BookID = $book_num
LIMIT 1";
}
$result = mysql_query($do) or die('SELECT failed: ' . mysql_error() . " (cmd: $do)");
list($text,$tag_offsets) = mysql_fetch_row($result);
if (!preg_match('!^' . $nest . ':(\d+)\r?\n(?:.*:(\d+))?!m', $tag_offsets, $out)) {
echo "Internal error! Unable to find selected text's section in the book ($nest).\n";
end_content();
return;
}
$offset = $out[1];
if ($out[2])
$snippet = substr($text, $offset, $out[2] - $offset);
else
$snippet = substr($text, $offset);
if ($testing) {
echo "Position: $offset + $sel_pos = ", $offset + $sel_pos, ", Selection len: $sel_len<br />\n";
$s1 = preg_replace(array('/&/', '/</'), array('&', '<'), $sel);
$s2 = preg_replace(array('/&/', '/</'), array('&', '<'), $snippet);
echo "<pre>###$s1###</pre><pre style='border: solid black 1px'>", $s2, "</pre>";
}
// Trim any start-of-section wiki markup (avoiding \s on purpose).
// Also: table-start & table-end are not here because tables contain other containers.
if (preg_match('/^[*#=;:]+ *|^[|!][^|\n]+\| *|^[|!] */', $snippet, $out)) {
$len = strlen($out[0]);
$snippet = substr($snippet, $len);
$offset += $len;
}
// Find any internal wiki markup so we can adjust the character offset.
// N.B. We treat all &entities; as though everything after the '&' was
// hidden since we assume the browser collapsed it into a single char.
// Also, <> is treated like a single char, 'cuz it was mapped to nbsp.
$ign = '(?<!\f)(?:\[\[Image:[^]]+(?<!\f)\]\]|\[\[[^]<> ]+ |\[\[|\]\]|'
. '<[^>]+>|\'\'\'|\'\'|(?<=<)>|(?<=&)(?:#\d+|[A-Z]?[a-z]+);| +(?= )|\f|^ +)';
preg_match_all('/'.$ign.'/m', $snippet, $ignore, PREG_OFFSET_CAPTURE);
foreach ($ignore[0] as $match) {
list($str,$pos) = $match;
if ($pos > $sel_pos)
break;
$sel_pos += strlen($str);
}
if ($testing) {
echo "Real position: $offset + $sel_pos = ", $offset + $sel_pos, "<br />";
echo '<pre style="border: solid black 1px">', substr($snippet, $sel_pos), "</pre>";
}
$offset += $sel_pos;
$do_suf .= ", AnchorStart='$offset', AnchorLen='$sel_len'";
}
if ($has_fields) {
$group_list = get_groups($book_num, $scrname, 'CanAdd', $uID, $book_num, '');
$anno_attrs = get_attr_data($tID);
$annotype = $anno_attrs['#NAME'];
$margin_map = array();
$order = $anno_attrs['#ORDER'];
foreach ($order as $aID) {
$attrs = $anno_attrs[$aID];
if ($attrs->Need == 'const')
$val = preg_replace('/^\$_AnnoType$/', $annotype, $attrs->DefaultValue);
else {
$val = preg_replace(array('/^\$URL$/', '/\r\n?/'),
array($_POST['URL'], "\n"),
$_POST[$attrs->Name]);
if ($attrs->Need == 'req' && $val == '') {
echo "You did not specify the {$attrs->PromptText}. Please go back and try again.";
close_db();
end_content();
return;
}
}
if ($attrs->Name == 'NoteText') {
// Caution: $book_anno is 0 when creating a new annotation!
list($val) = to_internal_wiki($val, $book_anno ? $book_num : 0, -$book_anno);
$do_suf .= ', NoteOffsets=NULL';
} elseif ($attrs->Name == 'Groups') {
$groups = array();
foreach (explode(',', $val) as $gID) {
if (!preg_match('/^(?:(\+)(?! )([-A-Za-z0-9_ ]+)(?<! )|-?\d+)$/', $gID, $out))
continue;
if ($out[1] == '+') {
$gname = $out[2];
$gID = add_new_group($uID, $uID, $uID, $scrname, $gname, 1);
} elseif ($group_list[$gID] == '')
continue;
$groups[] = $gID;
}
$val = join(',', $groups);
} elseif ($attrs->Name == 'FilterURL')
continue;
$do_suf .= ", _$aID=\"" . mysql_real_escape_string($val) . '"';
if ($attrs->IconType > 0)
$margin_map[$icon_names[$attrs->IconType]] = $aID;
}
foreach ($margin_map as $mm => $id)
$do_suf .= ", $mm=_$id";
}
if ($sel_pos >= 0 && $has_fields) {
$do = "INSERT INTO BookAnnotations
SET TypeID='$tID', BookID=$book_num, ParentID=$book_sect, UserID=$uID" . $do_suf;
$action = 'Added new';
} else {
$do = "UPDATE BookAnnotations SET" . preg_replace('/^,/', '', $do_suf)
. " WHERE ID=$book_anno";
$action = 'Saved';
}
if ($testing)
echo '<pre>', $do, '</pre>';
else {
mysql_query($do) or die('INSERT/UPDATE failed: ' . mysql_error() . " (cmd: $do)");
$url = $CGI . "read.php?b=$book_num&s=$book_sect&t=" . mysql_insert_id() . '#pblnk';
}
echo $action, ' ', $annotype, ".\n";
if ($annotype == 'IncomingLink' && !$book_anno) {
$linktext = preg_replace(array('/&/', '/</'), array('&', '<'), $_POST['MarginMouseoverText']);
if ($linktext == '')
$linktext = 'link text';
$js = "<a href=\"$url\">\n$linktext</a>";
$html = preg_replace('/</', '<', $js);
$js = preg_replace(array('/"/', '/\n/'), array('\\\\"', ''), $js);
echo <<<EOT
<p>The incoming link generated was:
<pre>$url</pre>
<p>Example HTML would be:
<pre>$html</pre>
<script>
if (window.clipboardData && clipboardData.setData) {
document.write('Copying this HTML to your clipboard...');
clipboardData.setData('Text', "$js");
document.write('done');
}
</script>
<p>You may now <a href="read.php?b=$book_num&s=$book_sect">return to the book</a>.
EOT;
} elseif (!$testing) {
if ($act_suffix == '')
$to_sect = $book_sect;
else
$to_sect = -$book_anno;
echo <<<EOT
The page will automatically refresh in a moment...
<META HTTP-EQUIV=Refresh CONTENT="0; URL=read.php?b=$book_num&s=$to_sect">
EOT;
}
close_db();
end_content();
}
function delete_page()
{
$book_num = $_GET['b'];
$book_sect = $_GET['s'];
$book_anno = $_GET['a'];
if (!preg_match('/^\d+;-?\d+;\d+$/', join(';', array($book_num, $book_sect, $book_anno)))) {
redir_main();
return;
}
open_db();
list($uID, $scrname) = get_auth_cookie();
if (!$uID) {
redir_main();
return;
}
start_content('Delete', 'wide');
$do = "DELETE FROM BookAnnotations
WHERE ID=$book_anno AND BookID=$book_num AND ParentID=$book_sect AND UserID=$uID
LIMIT 1";
mysql_query($do) or die('DELETE failed: ' . mysql_error() . " (cmd: $do)");
echo <<<EOT
Annotation deleted! The page will automatically refresh in a moment...
<META HTTP-EQUIV=Refresh CONTENT="0; URL=read.php?b=$book_num&s=$book_sect">
EOT;
close_db();
end_content();
}
function get_groups($book_num, $scrname, $need, $uID, $book_num, $groups)
{
$has_perms = "REGEXP '(^|,)(0|$uID)(,|$)'";
$need = $need != '' ? "$need $has_perms" : 1;
$group_list = array();
$do = "SELECT GroupID, GroupName, $need AS IsOK,
(CanView $has_perms OR CanAdd $has_perms OR CanCull $has_perms OR CanAdmin $has_perms) AS IsVisible
FROM Groups";
$result = mysql_query($do) or die('SELECT failed: ' . mysql_error() . " (cmd: $do)");
while (($obj = mysql_fetch_object($result)) !== FALSE) {
$name = preg_replace(":^$scrname/:", '/', $obj->GroupName);
if (preg_match('/^(Book-Default)#(\d+)$/', $name, $out)) {
if ($out[2] != $book_num)
continue;
$name = '<i>' . $out[1] . '</i>';
}
if (!$obj->IsVisible)
$name = preg_replace(':/.*:', '/...', $name);
if (!$obj->IsOK) {
if (!preg_match('/(^|,)' . $obj->GroupID . '(,|$)/', $groups))
continue;
$name = "~$name";
}
$group_list[$obj->GroupID] = $name;
}
natcasesort($group_list);
reset($group_list);
return $group_list;
}
function output_input_field($var, $attrs, $default, $annotype, $width, $height,
$filt_hidden, $book_num, $book_anno)
{
if ($attrs->Name == 'Groups') {
// Note: $default is known to contain only digits and commas in this case.
echo <<<EOT
<span id=lstgroup></span>
(Or: <input type=button value="Add a new group" onclick="new_group_prompt()" tabindex=1>)
<input type=hidden name=$var value="$default">
<script>
EOT;
foreach ($attrs->GroupList as $gID => $name) {
if ($name[0] == '~') {
$disabled = 1;
$name = substr($name, 1);
} else
$disabled = 0;
echo "add_group_checkbox($gID, '$name', $disabled);\n";
}
echo <<<EOT
set_group_checkboxes();
</script>
EOT;
return;
}
$default = preg_replace('/\$_AnnoType/', $annotype, $default);
if (preg_match('/^<(.*)>$/', $attrs->InheritedDefault, $out)) {
$list = preg_replace('/(^|<)<([^>]*)>(>|$)/', '$1$2$3', $out[1]);
echo "<select name=\"$var\" tabindex=1>\n";
foreach (explode('><', $list) as $opt) {
$opt = preg_replace(array('/&/', '/"/', '/</', '/\\\\=/'), array('&', '"', '<', '='), $opt);
if (preg_match('/^(.*?)=(.*)$/', $opt, $out)) {
$opt = $out[1];
$prompt = $out[2];
} else
$prompt = $opt;
$selected = $opt == $default ? ' selected' : '';
echo "<option value=\"$opt\"$selected>$prompt\n";
}
echo "</select>\n";
} elseif ($height > 1) {
if ($attrs->Name == 'NoteText')
echo "<script>output_toolbar(\"$var\",'')</script><br>";
echo "<textarea name=\"$var\" rows=$height cols=$width tabindex=1>";
if ($attrs->Name == 'NoteText')
output_external_wiki($default, $book_num, -$book_anno);
else
echo external_wiki_string($default, 0);
echo '</textarea>';
} elseif ($filt_hidden) {
$val = external_wiki_string($default, 1);
echo "<input type=hidden name=\"$var\" value=\"$default\"><i>(Value provided by filter.)</i>";
} else {
$val = external_wiki_string($default, 1);
echo "<input type=text name=\"$var\" size=$width value=\"$default\" tabindex=1>";
}
}
?>