diff --git a/include/api.tickets.php b/include/api.tickets.php index d9118bc9a04042988116524ebc705a5f4d2eba7b..59f6c6f5a8b5fbb093fdd59ac97e675dde964639 100644 --- a/include/api.tickets.php +++ b/include/api.tickets.php @@ -113,6 +113,10 @@ class TicketApiController extends ApiController { return $ticket; } + if (($thread = ThreadEntry::lookupByEmailHeaders($data)) + && $thread->postEmail($data)) { + return true; + } return $this->createTicket($data); } diff --git a/include/class.api.php b/include/class.api.php index b254d6ce610d72fcce57ad26851f47be216aa881..3d61f6031d84ca49f62158bf254233ea86b078b1 100644 --- a/include/class.api.php +++ b/include/class.api.php @@ -423,11 +423,6 @@ class ApiEmailDataParser extends EmailDataParser { if(!$data['emailId']) $data['emailId'] = $cfg->getDefaultEmailId(); - if($data['email'] && preg_match ('[[#][0-9]{1,10}]', $data['subject'], $matches)) { - if(($tid=Ticket::getIdByExtId(trim(preg_replace('/[^0-9]/', '', $matches[0])), $data['email']))) - $data['ticketId'] = $tid; - } - if(!$cfg->useEmailPriority()) unset($data['priorityId']); diff --git a/include/class.mailer.php b/include/class.mailer.php index 94c92626f3efd4f5d8536f67b10c8bd181532c72..d291effb228bf81cc5b7cae62e58b03dcce37bc9 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -123,6 +123,18 @@ class Mailer { $headers+= array('Precedence' => 'auto_reply'); } + if ($options) { + if (isset($options['replyto'])) + $headers += array('In-Reply-To' => $options['replyto']); + if (isset($options['references'])) { + if (is_array($options['references'])) + $headers += array('References' => + implode(' ', $options['references'])); + else + $headers += array('References' => $options['references']); + } + } + $mime = new Mail_mime(); $mime->setTXTBody($body); //XXX: Attachments diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php index 5e58ab5a51a1d73692db077f9a6cd7d6faf2f3ca..57d828274c01594cc6c1e5487c782817add9012a 100644 --- a/include/class.mailfetch.php +++ b/include/class.mailfetch.php @@ -233,6 +233,8 @@ class MailFetcher { 'subject'=>@$headerinfo->subject, 'mid' => trim(@$headerinfo->message_id), 'header' => $this->getHeader($mid), + 'in-reply-to' => $headerinfo->in_reply_to, + 'references' => $headerinfo->references, ); if ($replyto = $headerinfo->reply_to) { @@ -400,10 +402,6 @@ class MailFetcher { return true; //Report success (moved or delete) } - //Make sure the email is NOT already fetched... (undeleted emails) - if($mailinfo['mid'] && ($id=Ticket::getIdByMessageId($mailinfo['mid'], $mailinfo['email']))) - return true; //Reporting success so the email can be moved or deleted. - $vars = $mailinfo; $vars['name']=$this->mime_decode($mailinfo['name']); $vars['subject']=$mailinfo['subject']?$this->mime_decode($mailinfo['subject']):'[No Subject]'; @@ -423,19 +421,15 @@ class MailFetcher { $ticket=null; $newticket=true; - //Check the subject line for possible ID. - if($vars['subject'] && preg_match ("[[#][0-9]{1,10}]", $vars['subject'], $regs)) { - $tid=trim(preg_replace("/[^0-9]/", "", $regs[0])); - //Allow mismatched emails?? For now NO. - if(!($ticket=Ticket::lookupByExtId($tid, $vars['email']))) - $ticket=null; - } $errors=array(); - if($ticket) { - if(!($message=$ticket->postMessage($vars, 'Email'))) - return false; - + if (($thread = ThreadEntry::lookupByEmailHeaders($vars)) + && ($message = $thread->postEmail($vars))) { + if ($message === true) + // Email has been processed previously + return true; + elseif ($message) + $ticket = $message->getTicket(); } elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) { $message = $ticket->getLastMessage(); } else { diff --git a/include/class.thread.php b/include/class.thread.php index 9b4853a420b77b67d74a51bebad8875065f349b3..d0260be905d6b1715fe10eceb5b7fcece5be7fc7 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -189,6 +189,14 @@ class Thread { function delete() { + /* XXX: Leave this out until TICKET_EMAIL_INFO_TABLE has a primary + * key + $sql = 'DELETE mid.* FROM '.TICKET_EMAIL_INFO_TABLE.' mid + INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.message_id) + WHERE thread.ticket_id = '.db_input($this->getTicketId()); + db_query($sql); + */ + $res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId())); if(!$res || !db_affected_rows()) return false; @@ -230,7 +238,7 @@ Class ThreadEntry { if(!$id && !($id=$this->getId())) return false; - $sql='SELECT thread.*, info.* ' + $sql='SELECT thread.*, info.email_mid ' .' ,count(DISTINCT attach.attach_id) as attachments ' .' FROM '.TICKET_THREAD_TABLE.' thread ' .' LEFT JOIN '.TICKET_EMAIL_INFO_TABLE.' info @@ -309,6 +317,10 @@ Class ThreadEntry { return $this->ht['ticket_id']; } + function getEmailMessageId() { + return $this->ht['email_mid']; + } + function getTicket() { if(!$this->ticket && $this->getTicketId()) @@ -471,7 +483,76 @@ Class ThreadEntry { return $str; } + /** + * postEmail + * + * After some security and sanity checks, attaches the body and subject + * of the message in reply to this thread item + * + * Parameters: + * mailinfo - (array) of information about the email, with at least the + * following keys + * - mid - (string) email message-id + * - name - (string) personal name of email originator + * - email - (string<email>) originating email address + * - subject - (string) email subject line (decoded) + * - body - (string) email message body (decoded) + */ + function postEmail($mailinfo) { + // +==================+===================+=============+ + // | Orig Thread-Type | Reply Thread-Type | Requires | + // +==================+===================+=============+ + // | * | Message (M) | From: Owner | + // | * | Note (N) | From: Staff | + // | Response (R) | Message (M) | | + // | Message (M) | Response (R) | From: Staff | + // +------------------+-------------------+-------------+ + + if (!$ticket = $this->getTicket()) + // Kind of hard to continue a discussion without a ticket ... + return false; + // Make sure the email is NOT already fetched... (undeleted emails) + elseif ($this->getEmailMessageId() == $mailinfo['mid']) + // Reporting success so the email can be moved or deleted. + return true; + + $vars = array( + 'mid' => $mailinfo['mid'], + 'ticketId' => $ticket->getId(), + 'poster' => $mailinfo['name'], + 'origin' => 'Email', + 'source' => 'Email', + 'ip' => '', + 'reply_to' => $this, + ); + + $body = $mailinfo['message']; + + // Disambiguate if the user happens also to be a staff member of the + // system. The current ticket owner should _always_ post messages + // instead of notes or responses + if ($mailinfo['email'] == $ticket->getEmail()) { + $vars['message'] = $body; + return $ticket->postMessage($vars, 'Email'); + } + elseif ($staff_id = Staff::getIdByEmail($mailinfo['email'])) { + $vars['staffId'] = $staff_id; + $poster = Staff::lookup($staff_id); + $errors = array(); + $vars['note'] = $body; + return $ticket->postNote($vars, $errors, $poster); + } + // TODO: Consider security constraints + else { + $vars['message'] = sprintf("Received From: %s\n\n%s", + $mailinfo['email'], $body); + return $ticket->postMessage($vars, 'Email'); + } + // Currently impossible, but indicate that this thread object could + // not append the incoming email. + return false; + } /* Returns file names with id as key */ function getFiles() { @@ -495,8 +576,11 @@ Class ThreadEntry { $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE .' SET message_id='.db_input($this->getId()) //TODO: change it to thread_id - .', email_mid='.db_input($vars['mid']) //TODO: change it to mid. - .', headers='.db_input($vars['header']); + .', email_mid='.db_input($vars['mid']); //TODO: change it to mid. + if (isset($vars['header'])) + $sql .= ', headers='.db_input($vars['header']); + + $this->ht['email_mid'] = $vars['mid']; return db_query($sql)?db_insert_id():0; } @@ -544,8 +628,56 @@ Class ThreadEntry { )?$e:null; } + /** + * Parameters: + * mailinfo (hash<String>) email header information. Must include keys + * - "mid" => Message-Id header of incoming mail + * - "in-reply-to" => Message-Id the email is a direct response to + * - "references" => List of Message-Id's the email is in response + * - "subject" => Find external ticket number in the subject line + */ + function lookupByEmailHeaders($mailinfo) { + // Search for messages using the References header, then the + // in-reply-to header + $search = 'SELECT message_id FROM '.TICKET_EMAIL_INFO_TABLE + . ' WHERE email_mid=%s ORDER BY message_id DESC'; + + if ($id = db_result(db_query( + sprintf($search, db_input($mailinfo['mid']))))) + return ThreadEntry::lookup($id); + + foreach (array('mid', 'in-reply-to', 'references') as $header) { + $matches = array(); + if (!isset($mailinfo[$header]) || !$mailinfo[$header]) + continue; + // Header may have multiple entries (usually separated by + // semi-colons (;)) + elseif (!preg_match_all('/<[^>@]+@[^>]+>/', $mailinfo[$header], + $matches)) + continue; + + foreach ($matches[0] as $mid) { + $res = db_query(sprintf($search, db_input($mid))); + while (list($id) = db_fetch_row($res)) { + if ($t = ThreadEntry::lookup($id)) + return $t; + } + } + } + + // Search for ticket by the [#123456] in the subject line + $subject = $mailinfo['subject']; + $match = array(); + if ($subject && preg_match("/\[#([0-9]{1,10})\]/", $subject, $match)) + // Return last message for the thread + return Message::lastByExtTicketId((int)$match[1]); + + return null; + } + //new entry ... we're trusting the caller to check validity of the data. function create($vars) { + global $cfg; //Must have... if(!$vars['ticketId'] || !$vars['type'] || !in_array($vars['type'], array('M','R','N'))) @@ -562,6 +694,12 @@ Class ThreadEntry { if(isset($vars['pid'])) $sql.=' ,pid='.db_input($vars['pid']); + // Check if 'reply_to' is in the $vars as the previous ThreadEntry + // instance. If the body of the previous message is found in the new + // body, strip it out. + elseif (isset($vars['reply_to']) + && $vars['reply_to'] instanceof ThreadEntry) + $sql.=' ,pid='.db_input($vars['reply_to']->getId()); if($vars['ip_address']) $sql.=' ,ip_address='.db_input($vars['ip_address']); @@ -584,6 +722,12 @@ Class ThreadEntry { if($vars['cannedattachments'] && is_array($vars['cannedattachments'])) $entry->saveAttachments($vars['cannedattachments']); + // Email message id (required for all thread posts) + if (!isset($vars['mid'])) + $vars['mid'] = sprintf('<%s@%s>', Misc::randCode(24), + substr(md5($cfg->getUrl()), -10)); + $entry->saveEmailInfo($vars); + return $entry; } @@ -630,6 +774,17 @@ class Message extends ThreadEntry { && $m->getId()==$id )?$m:null; } + + function lastByExtTicketId($ticketId) { + $sql = 'SELECT thread.id FROM '.TICKET_THREAD_TABLE + .' thread JOIN '.TICKET_TABLE.' ticket ON (ticket.ticket_id = thread.ticket_id) + WHERE thread_type=\'M\' AND ticket.ticketID = '.db_input($ticketId) + .' ORDER BY thread.id DESC LIMIT 1'; + if (($res = db_query($sql)) && (list($id) = db_fetch_row($res))) + return Message::lookup($id); + else + return null; + } } /* Response - Ticket thread entry of type response */ diff --git a/include/class.ticket.php b/include/class.ticket.php index f5731deddf41b267e222e33adc9ca82a0dccecf7..85b7e3b3820d447ba02c810fdbf975e899887022 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -733,6 +733,8 @@ class Ticket { if(!$dept || !($email=$dept->getAutoRespEmail())) $email =$cfg->getDefaultEmail(); + $options = array('references'=>$message->getEmailMessageId()); + //Send auto response - if enabled. if($autorespond && $email && $cfg->autoRespONNewTicket() && $dept->autoRespONNewTicket() @@ -746,7 +748,8 @@ class Ticket { if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] ="\n$tag\n\n".$msg['body']; - $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']); + $email->sendAutoReply($this->getEmail(), $msg['subj'], + $msg['body'], null, $options); } if(!($email=$cfg->getAlertEmail())) @@ -763,7 +766,8 @@ class Ticket { //Alert admin?? if($cfg->alertAdminONNewTicket()) { $alert = str_replace('%{recipient}', 'Admin', $msg['body']); - $email->sendAlert($cfg->getAdminEmail(), $msg['subj'], $alert); + $email->sendAlert($cfg->getAdminEmail(), $msg['subj'], + $alert, null, $options); $sentlist[]=$cfg->getAdminEmail(); } @@ -779,7 +783,8 @@ class Ticket { foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); + $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, + null, $options); $sentlist[] = $staff->getEmail(); } @@ -831,7 +836,7 @@ class Ticket { db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1,lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId())); } - function onMessage($autorespond=true, $alert=true) { + function onMessage($autorespond=true, $message=null) { global $cfg; db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId())); @@ -875,7 +880,11 @@ class Ticket { if($cfg->stripQuotedReply() && ($tag=$cfg->getReplySeparator())) $msg['body'] ="\n$tag\n\n".$msg['body']; - $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body']); + if (!$message) + $message = $this->getLastMessage(); + $options = array('references' => $message->getEmailMessageId()); + $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], + null, $options); } } @@ -893,7 +902,8 @@ class Ticket { $assigner = $thisstaff?$thisstaff:'SYSTEM (Auto Assignment)'; //Log an internal note - no alerts on the internal note. - $this->logNote('Ticket Assigned to '.$assignee->getName(), $comments, $assigner, false); + $note = $this->logNote('Ticket Assigned to '.$assignee->getName(), + $comments, $assigner, false); //See if we need to send alerts if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts! @@ -931,10 +941,12 @@ class Ticket { //Send the alerts. $sentlist=array(); + $options = array('references' => $note->getEmailMessageId()); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); + $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, + null, $options); $sentlist[] = $staff->getEmail(); } } @@ -1140,7 +1152,7 @@ class Ticket { /*** log the transfer comments as internal note - with alerts disabled - ***/ $title='Ticket transfered from '.$currentDept.' to '.$this->getDeptName(); $comments=$comments?$comments:$title; - $this->logNote($title, $comments, $thisstaff, false); + $note = $this->logNote($title, $comments, $thisstaff, false); $this->logEvent('transferred'); @@ -1180,10 +1192,12 @@ class Ticket { $recipients[]= $manager; $sentlist=array(); + $options = array('references' => $note->getEmailMessageId()); foreach( $recipients as $k=>$staff) { if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); + $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, + null, $options); $sentlist[] = $staff->getEmail(); } } @@ -1279,7 +1293,7 @@ class Ticket { if(list($msg) = split($tag, $vars['message'])) $vars['message'] = $msg; - if($vars['ip']) + if(isset($vars['ip'])) $vars['ip_address'] = $vars['ip']; elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR']) $vars['ip_address'] = $_SERVER['REMOTE_ADDR']; @@ -1290,16 +1304,13 @@ class Ticket { $this->setLastMsgId($message->getId()); - if (isset($vars['mid'])) - $message->saveEmailInfo($vars); - if(!$alerts) return $message; //Our work is done... $autorespond = true; if ($autorespond && $message->isAutoResponse()) $autorespond=false; - $this->onMessage($autorespond); //must be called b4 sending alerts to staff. + $this->onMessage($autorespond, $message); //must be called b4 sending alerts to staff. $dept = $this->getDept(); @@ -1330,10 +1341,12 @@ class Ticket { $recipients[]=$manager; $sentlist=array(); //I know it sucks...but..it works. + $options = array('references'=>$message->getEmailMessageId()); foreach( $recipients as $k=>$staff) { if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue; $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert); + $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, + null, $options); $sentlist[] = $staff->getEmail(); } } @@ -1386,7 +1399,9 @@ class Ticket { $msg['body'] ="\n$tag\n\n".$msg['body']; $attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array(); - $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], $attachments); + $options = array('references' => $response->getEmailMessageId()); + $email->sendAutoReply($this->getEmail(), $msg['subj'], $msg['body'], $attachments, + $options); } return $response; @@ -1441,8 +1456,10 @@ class Ticket { //Set attachments if emailing. $attachments = $cfg->emailAttachments()?$response->getAttachments():array(); + $options = array('references' => $response->getEmailMessageId()); //TODO: setup 5 param (options... e.g mid trackable on replies) - $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments); + $email->send($this->getEmail(), $msg['subj'], $msg['body'], $attachments, + $options); } return $response; @@ -1551,6 +1568,7 @@ class Ticket { $recipients[]=$dept->getManager(); $attachments = $note->getAttachments(); + $options = array('references' => $note->getEmailMessageId()); $sentlist=array(); foreach( $recipients as $k=>$staff) { if(!is_object($staff) @@ -1559,7 +1577,8 @@ class Ticket { || $note->getStaffId() == $staff->getId()) //No need to alert the poster! continue; $alert = str_replace('%{recipient}', $staff->getFirstName(), $msg['body']); - $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, $attachments); + $email->sendAlert($staff->getEmail(), $msg['subj'], $alert, $attachments, + $options); $sentlist[] = $staff->getEmail(); } } @@ -2128,7 +2147,12 @@ class Ticket { $msg['body'] ="\n$tag\n\n".$msg['body']; $attachments =($cfg->emailAttachments() && $response)?$response->getAttachments():array(); - $email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments); + $references = $ticket->getLastMessage()->getEmailMessageId(); + if (isset($response)) + $references = array($response->getEmailMessageId(), $references); + $options = array('references' => $references); + $email->send($ticket->getEmail(), $msg['subj'], $msg['body'], $attachments, + $options); } return $ticket; diff --git a/include/index.html b/include/index.html new file mode 100644 index 0000000000000000000000000000000000000000..c4c9a988e260cbb934d504315ea7bb67d5a0935b --- /dev/null +++ b/include/index.html @@ -0,0 +1,128 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" + "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> +<html version="-//W3C//DTD XHTML 1.1//EN" xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> +<head> +<link rel="stylesheet" type="text/css" href="/s/d16ebb.css" title="Default"/> +<title>xkcd: Monster</title> +<meta http-equiv="X-UA-Compatible" content="IE=edge"/> +<link rel="shortcut icon" href="/s/919f27.ico" type="image/x-icon"/> +<link rel="icon" href="/s/919f27.ico" type="image/x-icon"/> +<link rel="alternate" type="application/atom+xml" title="Atom 1.0" href="/atom.xml"/> +<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="/rss.xml"/> +<link rel="apple-touch-icon-precomposed" href="/s/d9522a.png" /> +<script type="text/javascript"> + var _gaq = _gaq || []; + _gaq.push(['_setAccount', 'UA-25700708-7']); + _gaq.push(['_setDomainName', 'xkcd.com']); + _gaq.push(['_setAllowLinker', true]); + _gaq.push(['_trackPageview']); + + (function() { + var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); + })(); +</script> + +</head> +<body> +<div id="topContainer"> +<div id="topLeft"> +<ul> +<li><a href="/archive">Archive</a></li> +<li><a href="http://what-if.xkcd.com">What If?</a></li> +<li><a href="http://blag.xkcd.com">Blag</a></li> +<li><a href="http://store.xkcd.com/">Store</a></li> +<li><a rel="author" href="/about">About</a></li> +</ul> +</div> +<div id="topRight"> +<div id="masthead"> +<span><a href="/"><img src="http://imgs.xkcd.com/static/terrible_small_logo.png" alt="xkcd.com logo" height="83" width="185"/></a></span> +<span id="slogan">A webcomic of romance,<br/> sarcasm, math, and language.</span> +</div> +<div id="news"> +You can get the Subways comic as a <a href="http://store-xkcd-com.myshopify.com/products/subways">poster</a>! +</div> +</div> +<div id="bgLeft" class="bg box"></div> +<div id="bgRight" class="bg box"></div> +</div> +<div id="middleContainer" class="box"> + +<div id="ctitle">Monster</div> +<ul class="comicNav"> +<li><a href="/1/">|<</a></li> +<li><a rel="prev" href="/1256/" accesskey="p">< Prev</a></li> +<li><a href="http://dynamic.xkcd.com/random/comic/">Random</a></li> +<li><a rel="next" href="#" accesskey="n">Next ></a></li> +<li><a href="/">>|</a></li> +</ul> +<div id="comic"> +<img src="http://imgs.xkcd.com/comics/monster.png" title="It was finally destroyed with a nuclear weapon carrying the destructive energy of the Hiroshima bomb." alt="Monster" /> +</div> +<ul class="comicNav"> +<li><a href="/1/">|<</a></li> +<li><a rel="prev" href="/1256/" accesskey="p">< Prev</a></li> +<li><a href="http://dynamic.xkcd.com/random/comic/">Random</a></li> +<li><a rel="next" href="#" accesskey="n">Next ></a></li> +<li><a href="/">>|</a></li> +</ul> +<br /> +Permanent link to this comic: http://xkcd.com/1257/<br /> +Image URL (for hotlinking/embedding): http://imgs.xkcd.com/comics/monster.png +<div id="transcript" style="display: none"></div> +</div> +<div id="bottom" class="box"> +<img src="http://imgs.xkcd.com/s/a899e84.jpg" width="520" height="100" alt="Selected Comics" usemap="#comicmap"/> +<map id="comicmap" name="comicmap"> +<!-- http://code.google.com/p/chromium/issues/detail?id=108489 Might be MIME dependent. --> +<area shape="rect" coords="0,0,100,100" href="/150/" alt="Grownups"/> +<area shape="rect" coords="104,0,204,100" href="/730/" alt="Circuit Diagram"/> +<area shape="rect" coords="208,0,308,100" href="/162/" alt="Angular Momentum"/> +<area shape="rect" coords="312,0,412,100" href="/688/" alt="Self-Description"/> +<area shape="rect" coords="416,0,520,100" href="/556/" alt="Alternative Energy Revolution"/> +</map> +<div> +Search comic titles and transcripts: +<script type="text/javascript" src="//www.google.com/jsapi"></script> +<script type="text/javascript">google.load('search', '1');google.setOnLoadCallback(function() {google.search.CustomSearchControl.attachAutoCompletion('012652707207066138651:zudjtuwe28q',document.getElementById('q'),'cse-search-box');});</script> +<form action="//www.google.com/cse" id="cse-search-box"> +<div> +<input type="hidden" name="cx" value="012652707207066138651:zudjtuwe28q"/> +<input type="hidden" name="ie" value="UTF-8"/> +<input type="text" name="q" id="q" size="31"/> +<input type="submit" name="sa" value="Search"/> +</div> +</form> +<script type="text/javascript" src="//www.google.com/cse/brand?form=cse-search-box&lang=en"></script> +<a href="/rss.xml">RSS Feed</a> - <a href="/atom.xml">Atom Feed</a> +</div> +<br /> +<div id="comicLinks"> +Comics I enjoy:<br/> + <a href="http://threewordphrase.com/">Three Word Phrase</a>, + <a href="http://oglaf.com/">Oglaf</a> (nsfw), + <a href="http://www.smbc-comics.com/">SMBC</a>, + <a href="http://www.qwantz.com">Dinosaur Comics</a>, + <a href="http://www.asofterworld.com">A Softer World</a>, + <a href="http://buttersafe.com/">Buttersafe</a>, + <a href="http://pbfcomics.com/">Perry Bible Fellowship</a>, + <a href="http://questionablecontent.net/">Questionable Content</a>, + <a href="http://www.buttercupfestival.com/">Buttercup Festival</a> +</div> +<p>Warning: this comic occasionally contains strong language (which may be unsuitable for children), unusual humor (which may be unsuitable for adults), and advanced mathematics (which may be unsuitable for liberal-arts majors).</p> +<div id="footnote">BTC 1NfBXWqseXc9rCBc3Cbbu6HjxYssFUgkH6<br />We did not invent the algorithm. The algorithm consistently finds Jesus. The algorithm killed Jeeves. <br/>The algorithm is banned in China. The algorithm is from Jersey. The algorithm constantly finds Jesus.<br/>This is not the algorithm. This is close.</div> +<div id="licenseText"> +<p> +This work is licensed under a +<a href="http://creativecommons.org/licenses/by-nc/2.5/">Creative Commons Attribution-NonCommercial 2.5 License</a>. +</p><p> +This means you're free to copy and share these comics (but not to sell them). <a rel="license" href="/license.html">More details</a>.</p> +</div> +</div> +</body> +<!-- Layout by Ian Clasbey, davean, and chromakode --> +</html> +