<?php /** * CVS Loginfo class - Parses the output when executed by loginfo. * * @author Magnus Määttä <magnus@php.net> * @copyright (C) Copyright 2004 Magnus Määttä. * @package cvs * @version $Id: class_cvs_loginfo.php,v 1.43 2005/02/01 20:32:30 magnus Exp $ * @license PHP Version 3 */ /** * CVS_Loginfo class */ class CVS_Loginfo { /**#@+ * @access protected */ /** * List of applications checked for. * * @var array */ protected $apps = array(); /** * List of applications available. * * @var array */ protected $have = array(); /** * Commit data * * @var array */ protected $data = array(); /** * Configuration * * @var array */ protected $config = array(); /**#@-*/ /**#@+ * @access public */ /** * Class constructor. * * @param string $argv argv passed to the main script. * @param string $stdin Data from php://stdin * @param int $ppid Parent process ID. * @param bool $new_format If you want to use new format (1.12.x) * @return void */ public function __construct($argv, $stdin, $ppid, $new_format = false) { /* This early check is needed for newer versions. */ if ($new_format) { if (strtolower(trim($argv[3])) == '- new directory') $this->bail("Adding a dir."); if (strtolower(trim($argv[3])) == '- imported') $this->bail("Importing."); } /* Debug ? */ $this->config['debug'] = false; /* New format ? */ $this->config['new_format'] = $new_format; /* Who to mail. */ $this->config['mailto'] = $argv[1]; /* Largest allowed diff, in bytes (150KB). */ $this->config['diffsize'] = 153600; /* Size this large won't get sent at all. */ $this->config['maxsize'] = 512000; /* Files to skip diff for. */ $this->config['skipdiff'] = 'efs|bz2|tar|gz|tgz|gif|jpe|jpg|jpeg|pdf|png|exe|zip|class|jar|rar|dll|so'; /* Ignore commit messages from users. */ $this->config['ignored_users'] = array(); /* Use sendmail to send the commit mail? */ $this->config['use_sendmail'] = true; /* SMTP server */ $this->config['smtp_server'] = "10.0.1.2"; /* Mail domain */ $this->config['domain'] = "novell.stoldgods.nu"; /* Diff include type. 0 = plain, 1 = inline, 2 = attachment. */ $this->config['diff_include'] = 1; /* Commitinfo log file. */ $this->config['loginfo_lastdir'] = "/tmp/loginfo.lastdir.$ppid"; /* cvsusers file. */ $this->config['cvsroot'] = getenv('CVSROOT'); if (is_file("{$this->config['cvsroot']}/CVSROOT/cvsusers")) { $this->config['cvsusers'] = $this->config['cvsroot'] .'/CVSROOT/cvsusers'; } else { $this->config['cvsusers'] = false; } /* Initialize data['files']. */ $this->data['files'] = array(); /* All modules/dirs where files have been modified. */ $this->data['modules'] = array(); /* File status */ $this->data['file_status'] = array('added' => '', 'modified' => '', 'removed' => 'log_message'); /* Load saved data. */ if (is_file($this->config['loginfo_lastdir'] .'.data')) { $data = unserialize(file_get_contents($this->config['loginfo_lastdir'] .'.data')); $this->data = $data; unlink($this->config['loginfo_lastdir'] .'.data'); } /* The user who made the commit. */ $this->data['user'] = $argv[2]; /* Data from stdin. */ $this->data['stdin'] = $stdin; /* Find module. */ if ($new_format) { $tmp = explode("\n", $stdin); $tmp2 = trim(str_replace($this->config['cvsroot'], '', substr($tmp[0], strpos($tmp[0], '/'))), "\n/ "); $this->data['module'] = $tmp2; $this->data['modules'][] = $tmp2; } else { $tmp = explode(" ", $argv[3], 2); $this->data['module'] = $tmp[0]; $this->data['modules'][] = $tmp[0]; } /* Don't report new directories and imports. */ if (!$new_format) { $tmp2 = $tmp[1]; if (strtolower(trim($tmp2)) == '- new directory') $this->bail("Adding a dir."); if (strtolower(trim($tmp2)) == '- imported') $this->bail("Importing."); } /* Find files. */ if ($new_format) { array_shift($argv); array_shift($argv); array_shift($argv); $this->findFiles($argv, true); } else { $this->findFiles($tmp[1]); } /* Get added, removed, modified files and tag and log message. */ $this->findFileStatus(); /* Get lastdir */ $lastdir = file_get_contents($this->config['loginfo_lastdir']); /* Check if this is the last dir. */ if (trim($this->data['module'], '/') != trim($lastdir, '/')) { $data = serialize($this->data); $fp = fopen($this->config['loginfo_lastdir'] .'.data', 'w'); fwrite($fp, $data, strlen($data)); fclose($fp); $this->bail("Not last dir."); } /* Host that made the commit. */ $this->data['remote_host'] = getenv('REMOTE_HOST'); /* Check for applications. */ $this->haveApp('diffstat'); $this->haveApp('cvs'); } /** * Kill script with a message. * * @param string $msg * @return void */ public function bail($msg) { if ($this->config['debug']) { echo "\n=== Bail: $msg\n\n"; } exit(0); } /** * Change a configuration option * * @param string $config Configuration name * @param mixed $value Configuration value * @return void */ public function changeConfig($config, $value) { $this->config[$config] = $value; } /** * Get a configuration value. * * @param string $config Configuration name * @return mixed false is not found. */ public function getConfig($config) { if (isset($this->config[$config])) { return $this->config[$config]; } else { return false; } } /** * Get the info collected by this script. * * @return array */ public function getCommitInfo() { /* We're at the last dir - Remove temp file. */ unlink($this->config['loginfo_lastdir']); /* Look up user information. */ if ($this->config['cvsusers']) { sleep(1); $userinfo = new CVS_Cvsusers($this->config['cvsusers'], 0); $cvsuser = $userinfo->getInfo($this->data['user']); if (is_array($cvsuser)) { $fullname = $cvsuser['fullname']; $email = $cvsuser['email']; } else { $fullname = false; $email = false; } } else { $fullname = false; $email = false; } $data = $this->processData(); $array = array( 'tag' => $data['tag'], 'fullname' => $fullname, 'email' => $email, 'mailto' => $this->config['mailto'], 'user' => $this->data['user'], 'module' => $this->data['module'], 'diffstat' => $data['diffstat'], 'remote_host' => $this->data['remote_host'], 'log_message' => $data['log_message'], 'added_files' => $data['added_files'], 'added_diffs' => $data['added_diffs'], 'modified_files' => $data['modified_files'], 'modified_diffs' => $data['modified_diffs'], 'removed_files' => $data['removed_files'], ); return $array; } /** * Send commit mail to defined user. * * @return bool */ public function sendCommitMail() { /* Check if the commit is from a user that should be ignored. */ if (is_array($this->config['ignored_users']) && in_array($this->data['user'], $this->config['ignored_users'])) { $this->bail("Username is in ignore list - not sending commit email."); } $info = $this->getCommitInfo(); $com_body = "{$info['user']}\t{$info['remote_host']}\t". date("r", time()) ."\n\n"; $this->addLog(&$com_body, $info['added_files'], "Added Files:", $info['tag']); $this->addLog(&$com_body, $info['modified_files'], "Modified Files:", $info['tag']); $this->addLog(&$com_body, $info['removed_files'], "Removed Files:", $info['tag']); $com_body .= " Log:\n"; foreach (explode("\n", $info['log_message']) as $line) { $com_body .= " $line\n"; } if (strlen($info['diffstat']) > 10) { /* Only include diffstat if it's more then 10 chars. */ $com_body .= " Diffstat:\n"; foreach (explode("\n", $info['diffstat']) as $line) { $com_body .= " $line\n"; } } $com_body .= "\n"; /* Mime stuff. */ $time = time(); $boundary = "{$info['user']}-". md5(time()); $mime_header = "MIME-Version: 1.0\r\n". "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n"; $mime_body = "This is a MIME encoded message\n\n". "Content-Transfer-Encoding: 7bit\n". "--$boundary\n". "Content-Type: text/plain; charset=\"iso-8859-1\"\n\n". $com_body; $diffs_added = implode("\n\n", $info['added_diffs']); $diffs_modif = implode("\n\n", $info['modified_diffs']); if ((strlen($diffs_modif) + strlen($diffs_added)) > $this->config['maxsize']) { $body = $mime_body ."--$boundary\n"; /* Only set this to mode two if we're using mode two. */ if (!$this->config['diff_include'] || $this->config['diff_include'] == 1) { $diff_include = 1; } else { $diff_include = 2; } if (strlen($diffs_modif) < $this->config['maxsize']) { echo "Only sending diff for modified files.\n"; $body .= $this->attachment($info['modified_diffs'], 'modified', $boundary, $info['user'], $diff_include, true); } elseif (strlen($diffs_added) < $this->config['maxsize']) { echo "Only sending diff for modified files.\n"; $body .= $this->attachment($info['added_diffs'], 'added', $boundary, $info['user'], $diff_include, true); } else { echo "Diffs too large. Won't include diffs in mail.\n"; $body .= $mime_body . "\n\nDiffs are too large. None included.\n--$boundary--\n"; } } else { $body = $mime_body; if ($this->config['diff_include'] != 0) $body .= "--$boundary\n"; $body .= $this->attachment($info['modified_diffs'], 'modified', $boundary, $info['user'], $this->config['diff_include']); $body .= $this->attachment($info['added_diffs'], 'added', $boundary, $info['user'], $this->config['diff_include'], true); } /* Some headers. */ $subject = "cvs: {$this->data['module']} /". $this->makeSubject($info['added_files']) . $this->makeSubject($info['modified_files']) . $this->makeSubject($info['removed_files']); $msgid = "Message-ID: <cvs.{$info['user']}.". time() ."@{$this->config['domain']}>\r\n"; if (isset($info['fullname']) && isset($info['email'])) { $strings = preg_quote("äöåüûÿúùçøßÄÖÅÛÜÚÙÇØÆ"); $string2 = "äöåüûÿúùçøßÄÖÅÛÜÚÙÇØÆ"; $tmp2 = ''; $tmp = explode(' ', $info['fullname']); for ($x = 0; $x < count($tmp); $x++) { if (preg_match("/[$strings]/", $tmp[$x])) { $replace = array(); $with = array(); for ($i = 0; $i < strlen($string2); $i++) { $replace[] = $string2{$i}; $with[] = '='. strtoupper(dechex(ord($string2{$i}))); } $tmp2 .= ' =?iso-8859-15?q?' . str_replace($replace, $with, $tmp[$x]) . '?='; } else { /* No special chars. */ $tmp2 .= " {$tmp[$x]}"; } } $fullname = trim($tmp2, ' '); $from = "From: $fullname <{$info['email']}>\r\n"; $mail_from = $info['email']; } else { $from = "From: cvs-commit@{$this->config['domain']}\r\n"; $mail_from = 'cvs-commit@'. $this->config['domain']; } $to = "To: {$this->config['mailto']}\r\n"; $cvs_module = "X-CVS-Module: {$this->data['module']}\r\n"; $commit_host = "X-CVS-Commit-Host: {$this->data['remote_host']}\r\n"; $header = $from . $to . $msgid . $cvs_module . $commit_host . $mime_header; /* Print a message to the user so he or she knows where the commit have been mailed. */ echo "Mailing {$this->config['mailto']}\n"; if ($this->config['use_sendmail']) { $status = mail($this->config['mailto'], $subject, $body, $header); if (!$status) { echo "Failed to send mail.\n"; $this->bail("Failed to send mail!"); } } else { $fp = fsockopen("tcp://{$this->config['smtp_server']}", 25, $errno, $errstr, 30); if (is_resource($fp)) { fwrite($fp, "HELO {$this->config['domain']}\r\n"); fwrite($fp, "MAIL FROM: <$mail_from>\r\n"); fwrite($fp, "RCPT TO: <{$this->config['mailto']}>\r\n"); fwrite($fp, "DATA\r\n"); fwrite($fp, "$header"); fwrite($fp, "Subject: $subject\r\n"); fwrite($fp, "$body\r\n.\r\nQUIT\r\n"); fclose($fp); } else { echo "Failed to send mail!\n."; $this->bail("Error [$errno]: $errstr"); } } } /**#@-*/ /**#@+ * @access protected */ /** * Make attachments of the diffs in array. * * @param array $array * @param string $diff_name The type, modified, added. * @param string $boundary The boundary string * @param string $user Username * @param int $type Type of attachment. 0 = Plain, 1 = inline, 2 = attachment * @param bool $last If this is the last attachment. * @return string */ protected function attachment($array, $diff_name, $boundary, $user, $type = 1, $last = false) { $data = ''; $i = 0; do { $key = key($array); if (!$key) break; $i++; if ($type == 1 || $type == 2) { $tmp = explode('/', $key); $tmp2 = $tmp[count($tmp) - 1]; $name = "$tmp2-$user-$diff_name-". date("Ymd-Hms", time()) .".txt"; $data .= "Content-Type: text/plain; name=\"$name\"\n"; /* inline or attachment? */ if ($type == 1) { $data .= "Content-Disposition: inline; filename=\"$name\"\n\n"; } else { $data .= "Content-Disposition: attachment; filename=\"$name\"\n\n"; } } $data .= "{$array[$key]}\n"; if ((!($i == count($array) - 1) || !$last) && $type != 0) { $data .= "--$boundary\n"; } } while (next($array)); if (($i >= count($array) - 1) && $last) { $data .= "--$boundary--\n"; } return $data; } /** * Add formated message to the body. * * @param string $body The mail body by reference. * @param array $array The array with files. * @param string $title The title. * @param string $tag The CVS tag (branch). * @return void */ protected function addLog(&$body, $array, $title, $tag = false) { sort($array); $last = ''; if (count($array) > 0) { is_string($tag) ? $body .= " $title\t\t\t$tag\n" : $body .= " $title\n"; } else { return; } foreach ($array as $file) { $tmp = explode('/', $file['file']); $tmp2 = ''; /* Get module and path from name. */ for ($i = 0; $i < count($tmp) - 1; $i++) { $tmp2 .= "/{$tmp[$i]}"; } if ($last == $tmp2) { $body .= ' '; for ($i = 0; $i < strlen($tmp2); $i++) { $body .= ' '; } $body .= "\t\t". $tmp[count($tmp) - 1] ."\n"; } else { $last = $tmp2; $body .= " $tmp2\t\t". $tmp[count($tmp) - 1] ."\n"; } } } /** * Make a subject line of modified, etc files. * * @param array $array An array with the files. * @return string */ protected function makeSubject($array) { $line = ''; sort($array); $last = ''; foreach ($array as $file) { $tmp = explode('/', $file['file']); $tmp2 = ''; for ($i = 1; $i < (count($tmp) - 1); $i++) { $tmp2 .= "{$tmp[$i]}/"; } if ($last == $tmp2 && $tmp[count($tmp) - 1] != $tmp2) { $line .= $tmp[count($tmp) - 1] .' '; } else { $line .= "$tmp2 ". $tmp[count($tmp) - 1] .' '; } } return $line; } /** * Test if an app is available and save it to class config * * @param string $app Application to search for. * @return void */ protected function haveApp($app) { $retval = 0; $retarr = NULL; exec("which $app", &$retarr, &$retval); if ($retval == 0) { $this->have[$app] = 1; $this->apps[$app] = $retarr[0]; } } /** * Check if the input string is a CVS file and revision string. * * @param string $string * @return bool */ protected function isFileString($string) { $tmp = explode(",", $string); if (count($tmp) == 3) { return true; } else { return false; } } /** * Split a string into filename, old revision and new revision. * * @param string $string * @return array */ protected function infoExtract($string) { $tmp = explode(",", $string); $array = array('file' => $tmp[0], 'old_rev' => $tmp[1], 'new_rev' => $tmp[2]); return $array; } /** * Find files and revisions in a string. * * @param string $string * @param bool $newform If we should use the new format. * @return void */ protected function findFiles($string, $newform = false) { if ($newform) { while (count($string)) { $file = array( 'file' => $this->data['module'] .'/'. array_shift($string), 'old_rev' => array_shift($string), 'new_rev' => array_shift($string), ); $tmp = $file['file']; $this->data['files'][$tmp] = $file; } } else { $array = explode(" ", $string); $count = count($array); $files = array(); /* Find files and add them to $files array. */ for ($i = 0; $i < $count; $i++) { if ($this->isFileString($array[$i])) { /* File without space in the name */ $file = $this->infoExtract("{$this->data['module']}/{$array[$i]}"); } else { /* * The file must have a space in it. * Find where it ends. */ for ($x = $i; $x < $count; $x++) { /* * Find next string that is a cvs commit string */ if ($this->isFileString($array[$x])) { /* * Current entry in the array is the one with the revisions. */ /* Reset tmp string. */ $tmp = ''; for ($y = $i; $y <= $x; $y++) { /* Add everything from where the last file ended. */ $tmp .= " {$array[$y]}"; } /* Trim added whitespaces in the beginning. */ $tmp = trim($tmp); /* Skip forward since we're done here. */ $i = $y; /* Add entry to files array. */ $file = $this->infoExtract("{$this->data['module']}/$tmp"); } } /* End for-loop */ } /* End if (is_file_string()) */ /* Add found data*/ $tmp = $file['file']; $this->data['files'][$tmp] = $file; } } /* End else($newform) */ } /** * Find status of files. * * @return void */ protected function findFileStatus() { $modified = false; $added = false; $removed = false; $log_message = false; $status = false; $tag = false; /* Walk through data from stdin. */ foreach (explode("\n", $this->data['stdin']) as $line) { $line = trim($line); if ($status == 'modified') { $modified = $tmp; } elseif ($status == 'added') { $added = $tmp; } elseif ($status == 'removed') { $removed = $tmp; } elseif ($status == 'log') { $log_message = $tmp; } if ($line == "Added Files:") { $status = 'added'; $tmp = ''; } elseif ($line == "Modified Files:") { $status = 'modified'; $tmp = ''; } elseif ($line == "Removed Files:") { $status = 'removed'; $tmp = ''; } elseif ($line == 'Log Message:') { $status = 'log'; $tmp = ''; } elseif (preg_match("/^Tag: [A-Za-z0-9_\-]+$/", $line)) { $tmp2 = explode(':', $line); $tag = trim($tmp2[1]); $tmp = ''; } elseif ($status) { $tmp .= $line ."\n"; } } $this->data['file_status']['added'] .= $added; $this->data['file_status']['modified'] .= $modified; $this->data['file_status']['removed'] .= $removed; $this->data['file_status']['log_message'] = $log_message; $this->data['file_status']['tag'] = $tag; } /** * Process the data collected. * * @return array */ protected function processData() { $modified_diff = array(); $added_diff = array(); $diffstat = false; $status = false; $tag = $this->data['file_status']['tag']; $tmp = false; $replace = array(' ', "\t", "\n"); $with = array(' ', ' ', ' '); /* Remove spaces etc and explode. */ $added_files = str_replace($replace, $with, trim($this->data['file_status']['added'])); $modified_files = str_replace($replace, $with, trim($this->data['file_status']['modified'])); $removed_files = str_replace($replace, $with, trim($this->data['file_status']['removed'])); $log_message = str_replace("\r\n", "\n", $this->data['file_status']['log_message']); $added_array = array(); $removed_array = array(); $modified_array = array(); foreach ($this->data['files'] as $file) { $filename = $file['file']; if ($file['old_rev'] == 'NULL' || $file['old_rev'] == 'NONE') { /* Added file. */ $added_array[$filename] = $file; $exec_str = "{$this->apps['cvs']} -d:local:{$this->config['cvsroot']} -Qn checkout -p -r1.1 $filename"; exec($exec_str, &$diff); $diff = implode("\n", $diff); /* Check that the diff isn't larger then allowed size. */ if (strlen($diff) < $this->config['diffsize']) { $added_diff[$filename] = "Index: {$file['file']}\n+++ {$file['file']}\n$diff"; } } elseif ($file['new_rev'] == 'NULL' || $file['new_rev'] == 'NONE') { /* Removed file. */ $removed_array[$filename] = $file; } elseif ($file['new_rev'] != 'NULL' && $file['old_rev'] != 'NULL' && $file['new_rev'] != 'NONE' && $file['old_rev'] != 'NONE') { /* Modified file. */ $modified_array[$filename] = $file; $exec_str = "{$this->apps['cvs']} -d:local:{$this->config['cvsroot']} -Qn rdiff -u -r "; $exec_str .= "{$file['old_rev']} -r {$file['new_rev']} $filename"; exec($exec_str, &$diff); $diff = implode("\n", $diff); /* Check that the diff isn't larger then allowed size. */ if (strlen($diff) < $this->config['diffsize']) { $modified_diff[$filename] = $diff; } } else { echo "=======================================\n"; echo "Unknown file!\n"; echo "{$file['file']}\n"; echo "=======================================\n"; } } if ($this->have['diffstat'] && (!isset($this->config['diffstat']) || $this->config['diffstat'] == true)) { $fp = fopen($this->config['loginfo_lastdir'] .'.diff', 'w'); if (is_resource($fp)) { foreach ($modified_diff as $diff) { fwrite($fp, $diff, strlen($diff)); } fclose($fp); /* Generate diffstat data. */ exec("{$this->apps['diffstat']} {$this->config['loginfo_lastdir']}.diff", &$diffstat); $diffstat = implode("\n", $diffstat); $diffstat .= ', '. count($added_array) ." files added\n"; } /* Remove temp file. */ if (is_file($this->config['loginfo_lastdir'] .'.diff')) { unlink($this->config['loginfo_lastdir'] .'.diff'); } } return array( 'added_diffs' => $added_diff, 'added_files' => $added_array, 'modified_diffs' => $modified_diff, 'modified_files' => $modified_array, 'removed_files' => $removed_array, 'diffstat' => $diffstat, 'log_message' => $log_message, 'tag' => $tag, ); } /**#@-*/ }