// http://mikespub.net/tools/cvsnotice/ // ---------------------------------------------------------------------- // LICENSE // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License (GPL) // 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. // // To read the license please visit http://www.gnu.org/copyleft/gpl.html // ---------------------------------------------------------------------- // Original Author of file: mikespub (Michel Dalle) // Purpose of file: Archive and browse CVS notices via web // ---------------------------------------------------------------------- $version = '0.1.3'; // ---------------------------------------------------------------------- // 0. Requirements // // You'll need a CVS installation that generates e-mail notices, for // starters. Please check your CVS documentation on how to set that up. // // Next you'll need a Mailbox that can receive CVS notices. Ideally, this // should be a dedicated local mailbox that PHP can access directly, so // that there's no unnecessary network traffic and mail processing when // CVSNotice checks for new CVS notices. Neither (dedicated / local) is // mandatory - but the other options have never been tested and the script // isn't fully adapted for them yet. // // The best way is probably to set up a mailing list for CVS notices first, // and then to subscribe some specific local address to that mailing list // for this script. // // Then you'll need to have IMAP support in your PHP installation. If you're // unsure whether that's the case, write a little PHP script with phpinfo(); // in it, and see for yourself. // Why do you need IMAP if you're going to work with a local mailbox anyway ? // Because the imap_* functions of PHP are pretty good for what we need to // do here. If this is really a problem, there are some alternative set-ups // you could use below. // // Note that the IMAP functions allow you to use local mbox-style mailboxes, // POP3, NNTP or a true IMAP mailbox, so in theory this script could support // all 4 types in the future... // // And finally, you'll need MySQL to archive the CVS notices, and to retrieve // them for browsing. Other databases (or even a local file or mailbox) could // be used as well, but you'll need to adapt the script accordingly. // // Other than that, a webserver, PHP support and some network connection will // help too, but then you already knew that, right ? After all, this *is* a // PHP script. If you port this to Perl, Python, Ruby, etc. just let me // know, and I'll add your script to the list here : // // // Alternative set-ups // // For those of you who don't have PHP with IMAP enabled, or don't have a // mailbox to retrieve CVS notices from, you can always inject new notices // into the MySQL database in some other way (cron job, procmail filter, ...), // and still benefit from CVSNotice to browse through your CVS notices - it // just won't handle archiving itself then. // I'll leave it to your creativity to figure out some ways to get that table // filled with up-to-date CVS notices :-) // // If you've written a tool or found a useful configuration to achieve this, // drop me a mail and I'll add it to the list here : // // // ---------------------------------------------------------------------- // 1. Installation // // Before you can start using this script, you'll need to create a MySQL // table. You can do this by copying the following table definition into // phpmyadmin, or saving it to a file and use the command-line interface of // mysql to create it : mysql -u myusername -p mydatabase < thisfile.sql // /* # # Table structure for table 'cvs_notices' # CREATE TABLE cvs_notices ( msgid int(11) NOT NULL auto_increment, msgfrom varchar(255) NOT NULL default '', subject varchar(255) NOT NULL default '', msgdate int(11) default NULL, logmsg varchar(255) NOT NULL default '', body text, dir varchar(255) NOT NULL default '', files varchar(255) NOT NULL default '', checksum varchar(33) NOT NULL default '', PRIMARY KEY (msgid), UNIQUE KEY idx_checksum (checksum), KEY idx_dir (dir), KEY idx_msgfrom (msgfrom), KEY idx_msgdate (msgdate) ) TYPE=MyISAM; */ // // Once you've done that, adapt the configuration parameters below, put // this script somewhere on your webserver, and you're about ready to // archive and browse through your CVS notices. // // But first, you'll need to adapt the permissions on your local mailbox // so that PHP can at least read it (chmod 644 mbox). This is why it's // a good idea to have a dedicated mailbox just for CVS notices :-) // // For performance reasons, you should also create a 'touch' file with // write permission by PHP (touch .mailtouch; chmod 666 .mailtouch) so // that the script can check when new messages have arrived. Otherwise, // it'll just read your mailbox *every* time, even when there's nothing new // in there. // Another option would be to check the mailbox only when you (or a cron job) // call the script with a special parameter (e.g. http://.../mycvs.php?check=1 // For this, you'll need to adapt the new_notices() function below. // // ---------------------------------------------------------------------- // 2. Configuration // // You'll need to adapt the following parameters for your project // // // 2.a. Some display options : // // The name of your project $project = 'Postnuke'; // The URL for your PHP script $url = 'http://mikespub.net/list.php'; // The URL for your development or project site (leave empty if none) $devsite = 'http://developer.hostnuke.com/'; $devtitle = 'Developer Site'; // The URL for your ViewCVS script (leave empty if none) $viewcvs = 'http://cvs.hostnuke.com/browse/viewcvs/'; $cvstitle = 'CVS Repository'; // The maximum number of notices to display on one page $maxcount = 50; // // 2.b. The configuration for your mailing list of CVS notices : // // Accept CVS notices only if they were sent *to* that mail address // (= typical configuration for mailing lists) $checkaddress = 'cvs-notices@cvs.hostnuke.com'; // Prefix to remove from all subjects $prefix = 'CVS: '; // // 2.c. The configuration for your MySQL database : // // The hostname of your MySQL server (usually 'localhost') $dbhost = 'localhost'; // The username you use to connect to your MySQL database $dbuser = 'myusername'; // The password you use to connect to your MySQL database $dbpass = 'mypassword'; // The name of the particular MySQL database you're using $dbname = 'mydatabase'; // The name of the MySQL table where the CVS notices are archived $dbtable = 'cvs_notices'; // // 2.d. The configuration to access your mailbox via the imap library : // // Local mailbox (chmod 644) $mailbox = '/home/mydir/Mailbox'; $mailuser = ''; $mailpass = ''; // IMAP mailbox (untested) //$mailbox = '{localhost:143}INBOX'; // POP3 mailbox (untested) //$mailbox = '{localhost:110/pop3}INBOX'; // NNTP newsgroup (untested) //$mailbox = '{localhost:119/nntp}comp.test'; // 'Touch' file to keep track of when we last checked the mailbox (chmod 666) // Leave empty if you don't have one (but you should) $touchfile = $mailbox . '.touch'; //$touchfile = '/home/mydir/.mailtouch'; //$touchfile = ''; // // ---------------------------------------------------------------------- // 3. The main script // // See what we have to do here $title = ''; if (!empty($PATH_INFO) && !isset($filt)) { if ($PATH_INFO == '/stats') { $stats = 1; $title = ' - Statistics'; } elseif ($PATH_INFO == '/backend.rss') { $rss = 1; } elseif (preg_match('~^/authors/(.+)$~',$PATH_INFO,$matches)) { $auth = $matches[1]; $title = ' - Author'; } elseif (preg_match('~^/(.*)/(\d+)$~',$PATH_INFO,$matches)) { $filt = $matches[1]; $msg = $matches[2]; $title = ' - Message'; } else { $filt = preg_replace('~^/~','',$PATH_INFO); $title = ' - Directory'; } } // Some simple sanity checks before we go on if (!isset($page) || !is_numeric($page) || $page < 0) { $page = 0; } // Since we're using the first part of the e-mail address as author ID here, // we restrict this to a limited expected character set - expand as needed if (isset($auth) && (!is_string($auth) || preg_match('/[^a-z0-9._-]/i',$auth))) { die('Sorry, unable to filter on author "'. htmlspecialchars($auth) .'". ' . 'Please ask the administrator of this script to revise the sanity ' . "checks for the author ID if you're not trying to hack his site..."); //unset($auth); } // Check on allowed characters in directories - expand as needed if (isset($filt) && (!is_string($filt) || preg_match('/[^a-z0-9._ \/-]/i',$filt))) { die('Sorry, unable to filter on directory "'. htmlspecialchars($filt) .'". ' . 'Please ask the administrator of this script to revise the sanity ' . "checks for the directory if you're not trying to hack his site..."); //unset($filt); } if (isset($msg) && (!is_numeric($msg) || $msg < 1)) { unset($msg); } // Connect to the database if (!mysql_connect($dbhost,$dbuser,$dbpass) || !mysql_select_db($dbname)) { die('Unable to connect to the database : ' . mysql_error()); } // See if we need to redirect to the next/previous notice if (isset($view) && isset($msg)) { if ($view == 'next') { $msg++; } else { $msg--; } // See where we need to go $query = 'SELECT msgid,dir,files FROM '. $dbtable; $query .= " WHERE msgid = '" . mysql_escape_string($msg) . "' "; list($msgid,$dir,$files) = mysql_fetch_row(mysql_query($query)); if (isset($msgid) && $msgid == $msg) { $newurl = $url . '/' . htmlspecialchars($dir) . '/' . $msgid; header("Location: $newurl"); exit; } } if (!empty($rss)) { show_rss(); exit; } // Print out the HTML header echo ' '. $project .' CVS Notices'. $title .'

'. $project .' CVS Notices'. $title .'

'; // Get some basic statistics list($total,$maxnum,$maxtime) = mysql_fetch_row(mysql_query('SELECT COUNT(*), MAX(msgid), MAX(msgdate) FROM ' . $dbtable)); if (empty($maxnum)) { $maxnum = 0; } // Check the mailbox for new notices (depends on your set-up) if (new_notices()) { echo "Updating CVS notices database...\n"; list($inserted,$skipped,$ignored) = import_notices(); echo "$inserted new notices added, $skipped notices skipped, $ignored messages ignored

\n"; } // Show statistics if (isset($stats)) { show_stats(); show_foot(); exit; } // Create navigation $navigation = 'View Statistics'; if ($msg) { if ($msg < $maxnum) { $navigation .= ' - Next Notice'; } else { $navigation .= ' - Next Notice'; } if ($msg > 0) { $navigation .= ' - Previous Notice'; } else { $navigation .= ' - Previous Notice'; } } else { if ($page > 0) { $navigation .= ' - Newer Notices'; } else { $navigation .= ' - Newer Notices'; } $query = 'SELECT COUNT(*) FROM '. $dbtable; if (isset($filt)) { if ($nosub) { $query .= " WHERE dir = '" . mysql_escape_string($filt) . "' "; } else { $query .= " WHERE dir LIKE '" . mysql_escape_string($filt) . "%' "; } } elseif (isset($auth)) { $query .= " WHERE msgfrom LIKE '%<" . mysql_escape_string($auth) . "@%' "; } list($numitems) = mysql_fetch_row(mysql_query($query)); if (isset($numitems)) { if (($page + 1) * $maxcount < $numitems) { $navigation .= ' - Older Notices'; } else { $navigation .= ' - Older Notices'; } } else { $navigation .= ' - Older Notices'; } } echo $navigation . '

'; // Create directory filter if (isset($filt)) { $dirfilt = 'Directory filter : [ all ]'; $parts = split('/',$filt); foreach ($parts as $part) { if (!isset($root)) { $root = $part; } else { $root .= '/' . $part; } if ($root == $filt && !isset($msg) && empty($nosub)) { //$dirfilt .= ' / ' . rawurlencode($part); $dirfilt .= ' / ' . htmlspecialchars($part); } else { //$dirfilt .= ' / ' // . htmlspecialchars($part) . ''; $dirfilt .= ' / ' . htmlspecialchars($part) . ''; } } $dirfilt .= ''; if (empty($nosub) && empty($msg)) { $dirfilt .= ' [ exclude subdirs ]'; } if (!empty($viewcvs)) { $dirfilt .= ' [ view in CVS ]'; } $dirfilt .= "

\n"; echo $dirfilt; } // Create author filter if (isset($auth)) { $authfilt = 'Author filter : [ all ]'; $authfilt .= ' / authors / ' . htmlspecialchars($auth) . "

\n"; echo $authfilt; } // Show (filtered) CVS notices echo ' '; if (isset($msg)) { $query = 'SELECT msgid,msgfrom,dir,files,logmsg,msgdate,body FROM ' . $dbtable; $query .= " WHERE msgid = '" . mysql_escape_string($msg) . "' "; } else { $query = 'SELECT msgid,msgfrom,dir,files,logmsg,msgdate FROM '. $dbtable; if (isset($filt)) { if ($nosub) { $query .= " WHERE dir = '" . mysql_escape_string($filt) . "' "; } else { $query .= " WHERE dir LIKE '" . mysql_escape_string($filt) . "%' "; } } elseif (isset($auth)) { $query .= " WHERE msgfrom LIKE '%<" . mysql_escape_string($auth) . "@%' "; } $query .= ' ORDER BY msgid DESC '; $start = $page * $maxcount; $maxcount++; $query .= "LIMIT $start,$maxcount "; $maxcount--; } $result = mysql_query($query) or die('Query error : ' . mysql_error()); $numrows = 0; $ismore = 0; while ($fields = mysql_fetch_assoc($result)) { $numrows++; if ($numrows > $maxcount) { $ismore = 1; break; } $from = $fields['msgfrom']; if (preg_match('/<(.*?)@/',$from,$matches)) { $fromid = $matches[1]; } $from = trim(preg_replace('/("|<.*?>)/','',$from)); $ndate = gmstrftime('%d %b %Y %H:%M:%S',$fields['msgdate']); if (!$msg) { $files = '' . htmlspecialchars($fields['files']) . ''; } else { $files = $fields['files']; } if (isset($filt) && $filt == $fields['dir'] && !$msg) { $dir = htmlspecialchars($fields['dir']); } else { $dir = dir_link($fields['dir']); } if (isset($auth) && $auth == $fromid) { $from = htmlspecialchars($from); } else { $from = '' . htmlspecialchars($from) . ''; } echo '\n"; echo '\n"; if (isset($msg)) { $body = $fields['body']; } } mysql_free_result($result); //mysql_close(); echo '
Date Directory [filter] File(s) [view] Author [filter]
'.$ndate.''.$dir.'' .$files.''.$from."
 ' . htmlspecialchars($fields['logmsg']) ."
'; // Show body if ($msg) { $body = htmlspecialchars($body); echo '

' . $body .'
'; } // Show current filters again echo '
'; if (isset($filt)) { echo $dirfilt; } if (isset($auth)) { echo $authfilt; } // Show navigation again echo $navigation; show_foot(); exit; // ---------------------------------------------------------------------- // 4. Some functions to handle the browsing and display part /* * Create a link to filter on the directory */ function dir_link ($dir,$nosub = 0) { global $url; if ($nosub) { return '' . htmlspecialchars($dir) . ''; } else { return '' . htmlspecialchars($dir) . ''; } } /* * Create a link to filter on the author */ function auth_link ($from) { global $url; if (preg_match('/<(.*?)@/',$from,$matches)) { $fromid = $matches[1]; } // we use this as a backup solution (but querying by author won't work) $fromname = trim(preg_replace('/("|<.*?>)/','',$from)); if (empty($fromid)) { $fromid = $fromname; } return '' . htmlspecialchars($fromname) . ''; } /* * Show some CVS notices statistics */ function show_stats () { global $url; $navigation = 'View CVS Notices'; echo $navigation . '

'; echo ' '; $result = mysql_query('SELECT msgfrom,COUNT(*) FROM cvs_notices GROUP BY msgfrom ORDER BY msgfrom') or die('Query error : ' . mysql_error()); echo ''; $result = mysql_query('SELECT LEFT(FROM_UNIXTIME(msgdate),7) as month,COUNT(*) FROM cvs_notices GROUP BY month ORDER BY month DESC') or die('Query error : ' . mysql_error()); echo ''; $result = mysql_query('SELECT LEFT(FROM_UNIXTIME(msgdate),10) as day,COUNT(*) FROM cvs_notices GROUP BY day ORDER BY day DESC LIMIT 20') or die('Query error : ' . mysql_error()); echo ''; $result = mysql_query('SELECT dir,COUNT(*) FROM cvs_notices GROUP BY dir ORDER BY dir') or die('Query error : ' . mysql_error()); echo ''; echo '
By Author Per Month Last 20 Days Per Directory
'; echo ''; while (list($from,$num) = mysql_fetch_row($result)) { echo '\n"; } echo '
' . $num . '' . auth_link($from) . "
'; mysql_free_result($result); echo '
'; echo ''; while (list($month,$num) = mysql_fetch_row($result)) { echo '\n"; } echo '
' . $num . '' . $month . "
'; mysql_free_result($result); echo '
'; echo ''; while (list($day,$num) = mysql_fetch_row($result)) { echo '\n"; } echo '
' . $num . '' . $day . "
'; mysql_free_result($result); echo '
'; echo ''; while (list($dir,$num) = mysql_fetch_row($result)) { echo '\n"; } echo '
' . $num . '' . dir_link($dir,1) . "
'; mysql_free_result($result); echo '
'; echo '
' . $navigation; } /* * Show the default footer */ function show_foot () { global $viewcvs, $cvstitle, $devsite, $devtitle, $version, $url; echo '


'; if (!empty($devsite)) { echo 'Visit ' . $devtitle .''; if (!empty($viewcvs)) { echo ' - '; } } if (!empty($viewcvs)) { echo 'Browse ' . $cvstitle . ''; } echo ' Syndicate via backend.rss
(max. once per hour please)
Powered by CVSNotice '.$version.'

'; } // ---------------------------------------------------------------------- // 5. Some functions to handle the archiving part /* * Check if there are new notices */ function new_notices () { global $mailbox, $touchfile; // Alternative way to decide when to check for new notices // --> call the script with ?check=1 as parameter, in your browser or // from some scheduled cron job // global $check; // if (!empty($check)) { // return true; // } // always check if there is no touch file here if (empty($touchfile)) { return true; } // TODO : some other criteria for remote mailboxes (other than 'new mail', // which may be unreliable if you access that mailbox in other ways as well) // compare the last modification date of the files to decide $mtime = filemtime($mailbox); $ftime = filemtime($touchfile); if ($mtime > $ftime) { touch($touchfile); return true; } else { return false; } } /* * Import new CVS notices */ function import_notices () { global $mailbox, $mailuser, $mailpass, $maxnum, $checkaddress, $prefix, $dbtable; $mbox = imap_open($mailbox, $mailuser, $mailpass) or die('Unable to open mailbox : ' . imap_last_error()); $check = imap_check($mbox); if (empty($check)) { echo 'Unable to check messages in mailbox : ' . imap_last_error(); imap_close($mbox); die; } $count = $check->Nmsgs; if ($count > 0) { $overview = imap_fetch_overview($mbox,"1:$count",0); if (!is_array($overview)) { echo 'Unable to get overview of messages ' . imap_last_error(); imap_close($mbox); die; } } else { $overview = array(); } reset($overview); $inserted = 0; $skipped = 0; $ignored = 0; while (list($key,$val) = each($overview)) { $msgno = $val->msgno; $subject = $val->subject; $from = $val->from; $date = $val->date; $to = $val->to; if (!preg_match("/$checkaddress/i",$to)) { $ignored++; continue; } $subject = preg_replace("/^$prefix/",'',$subject); list($dir,$files) = split(' ',$subject,2); $udate = strtotime($date); if (!$udate) { $udate = time(); } $body = imap_body($mbox,$msgno); if (preg_match('/Log Message:\s*\n(.*?)\s*\r?\n/',$body,$matches)) { $logmsg = $matches[1]; } else { $logmsg = ''; } $checksum = md5($udate.$msgfrom.$subject.$body); $query = 'SELECT COUNT(*) FROM ' . $dbtable . " WHERE checksum = '" . $checksum . "'"; list($found) = mysql_fetch_row(mysql_query($query)); if (!empty($found)) { $skipped++; @imap_delete($mbox,$msgno); continue; } $data = array( 'msgfrom' => $from, 'subject' => $subject, 'msgdate' => $udate, 'logmsg' => $logmsg, 'body' => $body, 'dir' => $dir, 'files' => $files, 'checksum' => $checksum ); $query = build_insert($data); mysql_query($query) or die ('Unable to insert message in database : ' . mysql_error()); @imap_delete($mbox,$msgno); $inserted++; } @imap_expunge($mbox); imap_close($mbox); return array($inserted,$skipped,$ignored); } /* * Create an SQL INSERT statement */ function build_insert ($fields) { $fieldlist = array(); $valuelist = array(); foreach ($fields as $field => $value) { $fieldlist[] = $field; $valuelist[] = "'" . mysql_escape_string($value) . "'"; } $query = 'INSERT INTO cvs_notices (' . join(',',$fieldlist) . ') VALUES (' . join(',',$valuelist) . ')'; return $query; } // ---------------------------------------------------------------------- // 6. Some functions to handle the RSS backend newsfeed function show_rss () { global $project, $url, $dbtable; $title = $project . ' CVS Notices'; $descr = 'The latest CVS notices for ' . $project; $lang = 'en-us'; header("Content-type: text/xml"); $out = ' ' . htmlspecialchars($title) . ' ' . htmlspecialchars($url) . ' ' . htmlspecialchars($descr) . ' ' . htmlspecialchars($lang) . ' '; // TODO: add image $query = 'SELECT msgid,msgfrom,dir,files,logmsg,msgdate FROM '. $dbtable; $query .= ' ORDER BY msgid DESC LIMIT 10'; $result = @mysql_query($query); if (empty($result)) { $out .= ' Error retrieving items from the database ' . htmlspecialchars($url) . ' ' . htmlspecialchars(mysql_error()) . ' '; } else { while ($fields = mysql_fetch_assoc($result)) { $from = $fields['msgfrom']; if (preg_match('/<(.*?)@/',$from,$matches)) { $fromid = $matches[1]; } $from = trim(preg_replace('/("|<.*?>)/','',$from)); if (empty($from)) { $from = $fromid; } $from = htmlspecialchars($from); $ndate = gmstrftime('%d %b %Y %H:%M:%S',$fields['msgdate']); $dir = htmlspecialchars($fields['dir']); $files = htmlspecialchars($fields['files']); $link = $url.'/' . $fields['dir'] . '/' . $fields['msgid']; $link = htmlspecialchars($link); $logmsg = htmlspecialchars($fields['logmsg']); $out .= ' ' . $dir . ' ' . $files . ' ' . $link . ' ' . $from . ' - ' . $ndate . ' - ' . $logmsg . ' '; } mysql_free_result($result); } $out .= ' '; // fix for *nix platforms if (!preg_match("/\r/",$out)) { $out = preg_replace("/\n/","\r\n",$out); } echo $out; } // ---------------------------------------------------------------------- // 7. The end ?>