1<?php 2 3const DRY_RUN = false; 4 5if (!DRY_RUN) { 6 require __DIR__ . '/include/mailer.php'; 7} 8 9function verify_signature($requestBody) { 10 if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])){ 11 $sig = 'sha1=' . hash_hmac('sha1', $requestBody, getenv('GITHUB_SECRET')); 12 return $sig === $_SERVER['HTTP_X_HUB_SIGNATURE']; 13 } 14 return false; 15} 16 17function get_repo_email($repos, $repoName) { 18 // if we somehow end up receiving a PR for a repo not matching anything send it to systems so that we can fix it 19 $to = 'systems@php.net'; 20 foreach ($repos as $repoPrefix => $email) { 21 if (strpos($repoName, $repoPrefix) === 0) { 22 $to = $email; 23 } 24 } 25 26 return $to; 27} 28 29function is_pr($issue) { 30 return strpos($issue->html_url, '/pull/') !== false; 31} 32 33function prep_title($issue, $repoName) { 34 $issueNumber = $issue->number; 35 $title = $issue->title; 36 $type = is_pr($issue) ? 'PR' : 'Issue'; 37 38 $subject = sprintf('[%s][%s #%s] - %s', $repoName, $type, $issueNumber, $title); 39 40 return $subject; 41} 42 43function send_mail($to, $subject, $message, MailAddress $from, array $replyTos = []) { 44 printf("Sending mail...\nTo: %s\nFrom: %s <%s>\nSubject: %s\nMessage:\n%s", 45 $to, $from->name, $from->email, $subject, $message); 46 47 if (!DRY_RUN) { 48 mailer($to, $subject, $message, $from, $replyTos); 49 } 50} 51 52function get_first_line($message) { 53 $newLinePos = strpos($message, "\n"); 54 return $newLinePos !== false ? substr($message, 0, $newLinePos) : $message; 55} 56 57function handle_bug_close($commit) { 58 $message = $commit->message; 59 $author = $commit->author->username; 60 $committer = $commit->committer->username; 61 $url = $commit->url; 62 63 if (!preg_match_all('/^Fix(?:ed)? (?:bug )?\#([0-9]+)/mi', $message, $matches)) { 64 return; 65 } 66 $bugIds = $matches[1]; 67 68 if ($author === $committer) { 69 $blame = $author; 70 } else { 71 $blame = "$author (author) and $committer (committer)"; 72 } 73 74 $firstLine = get_first_line($message); 75 76 $comment = <<<MSG 77Automatic comment on behalf of $blame 78Revision: $url 79Log: $firstLine 80MSG; 81 82 foreach ($bugIds as $bugId) { 83 $postData = [ 84 'user' => 'git', 85 'id' => (int) $bugId, 86 'ncomment' => $comment, 87 'status' => 'Closed', 88 'MAGIC_COOKIE' => getenv('BUGS_MAGIC_COOKIE'), 89 ]; 90 $postData = http_build_query($postData, '', '&'); 91 92 echo "Closing bug #$bugId\n"; 93 if (!DRY_RUN) { 94 $context = stream_context_create(['http' => [ 95 'method' => 'POST', 96 'header' => 'Content-Type: application/x-www-form-urlencoded', 97 'content' => $postData, 98 'timeout' => 5, 99 ]]); 100 $result = file_get_contents('https://bugs.php.net/rpc.php', false, $context); 101 echo "Response: $result\n"; 102 } 103 } 104} 105 106function get_commit_mailing_list($repoName) { 107 if ($repoName === 'playground') { 108 return 'nikic@php.net'; 109 } else if ($repoName === 'php-src' || $repoName === 'karma') { 110 return 'php-cvs@lists.php.net'; 111 } else if ($repoName === 'php-langspec') { 112 return 'standards-vcs@lists.php.net'; 113 } else if ($repoName === 'phpruntests' || $repoName === 'pftt2' || $repoName === 'web-qa') { 114 return 'php-qa@lists.php.net'; 115 } else if ($repoName === 'systems') { 116 return 'systems@php.net'; 117 } else if ($repoName === 'php-gtk-src') { 118 return 'php-gtk-cvs@lists.php.net'; 119 } else if ($repoName === 'presentations' || $repoName === 'web-pres2') { 120 return 'pres@lists.php.net'; 121 } else if ($repoName === 'doc-base' || $repoName === 'doc-en' 122 || $repoName === 'phd' || $repoName === 'web-doc-editor') { 123 return 'doc-cvs@lists.php.net'; 124 } else if ($repoName === 'web-doc') { 125 return 'doc-web@lists.php.net'; 126 } else if ($repoName === 'web-pecl') { 127 return 'pecl-cvs@lists.php.net'; 128 } else if (strpos($repoName, 'web-') === 0) { 129 return 'php-webmaster@lists.php.net'; 130 } else if (strpos($repoName, 'pecl-') === 0) { 131 return 'pecl-cvs@lists.php.net'; 132 } else if (strpos($repoName, 'doc-') === 0 && $repoName !== 'doc-gtk') { 133 return str_replace('-', '_', $repoName) . '@lists.php.net'; 134 } else { 135 return null; 136 } 137} 138 139function parse_ref($ref) { 140 if (!preg_match('(^refs/([^/]+)/(.+)$)', $ref, $matches)) { 141 return null; 142 } 143 144 return [$matches[1], $matches[2]]; 145} 146 147function handle_ref_change_mail($mailingList, $payload) { 148 $repoName = $payload->repository->name; 149 $ref = $payload->ref; 150 $before = $payload->before; 151 $after = $payload->after; 152 $compare = $payload->compare; 153 $pusherName = $payload->pusher->name; 154 155 if (!$parsedRef = parse_ref($ref)) { 156 echo "Unexpected ref format: $ref"; 157 return; 158 } 159 160 list($refKind, $refName) = $parsedRef; 161 if ($refKind === 'heads') { 162 $what = "branch $refName"; 163 } else if ($refKind === 'tags') { 164 $what = "tag $refName"; 165 } else { 166 $what = "unknown ref $ref"; 167 } 168 169 if ($payload->created) { 170 $action = "created"; 171 } else if ($payload->deleted) { 172 $action = "deleted"; 173 } else if ($payload->forced) { 174 $action = "force pushed"; 175 } else { 176 $action = "performed unknown action on"; 177 } 178 179 $subject = "[$repoName] $action $what"; 180 181 $message = ucfirst($action) . " $what in repository $repoName.\n\n"; 182 $message .= "Pusher: $pusherName\n"; 183 if ($action !== 'created') { 184 $message .= "Before: https://github.com/php/$repoName/commit/$before\n"; 185 } 186 if ($action !== 'deleted') { 187 $message .= "After: https://github.com/php/$repoName/commit/$after\n"; 188 } 189 $message .= "Compare: $compare\n"; 190 $message .= "Tree: https://github.com/php/$repoName/tree/$refName\n"; 191 if ($refKind === 'tags') { 192 $message .= "Tag: https://github.com/php/$repoName/releases/tag/$refName\n"; 193 } 194 195 send_mail($mailingList, $subject, $message, MailAddress::noReply($pusherName)); 196} 197 198function handle_commit_mail($mailingList, $repoName, $ref, $pusherUser, $commit) { 199 $authorUser = isset($commit->author->username) ? $commit->author->username : null; 200 $authorName = $commit->author->name; 201 $committerUser = isset($commit->committer->username) ? $commit->committer->username : null; 202 $committerName = $commit->committer->name; 203 $message = $commit->message; 204 $timestamp = $commit->timestamp; 205 $url = $commit->url; 206 $diffUrl = $url . '.diff'; 207 $firstLine = get_first_line($message); 208 209 if (!$parsedRef = parse_ref($ref)) { 210 echo "Unexpected ref format: $ref"; 211 return; 212 } 213 214 list(, $refName) = $parsedRef; 215 216 $from = $authorName === $committerName ? $authorName : "$authorName via $committerName"; 217 $replyTos = [new MailAddress($commit->author->email, $authorName)]; 218 if ($commit->committer->email !== 'noreply@github.com') { 219 $replyTos[] = new MailAddress($commit->committer->email, $committerName); 220 } 221 222 $subject = "[$repoName] $refName: $firstLine"; 223 $body = "Author: $authorName" . ($authorUser ? " ($authorUser)" : "") . "\n"; 224 if ($authorName !== $committerName) { 225 $body .= "Committer: $committerName" . ($committerUser ? " ($committerUser)" : "") . "\n"; 226 } 227 if ($committerUser !== $pusherUser) { 228 $body .= "Pusher: $pusherUser\n"; 229 } 230 $body .= "Date: $timestamp\n\n"; 231 232 $body .= "Commit: $url\n"; 233 $body .= "Raw diff: $diffUrl\n\n"; 234 $body .= "$message\n\n"; 235 236 $body .= "Changed paths:\n"; 237 foreach ($commit->added as $file) { 238 $body .= " A $file\n"; 239 } 240 foreach ($commit->removed as $file) { 241 $body .= " D $file\n"; 242 } 243 foreach ($commit->modified as $file) { 244 $body .= " M $file\n"; 245 } 246 $body .= "\n\n"; 247 248 /*$diff = file_get_contents($diffUrl); 249 if (strlen($diff) > 128 * 1024) { 250 $body .= "Diff exceeded maximum size."; 251 } else { 252 $body .= "Diff:\n\n$diff"; 253 }*/ 254 255 send_mail($mailingList, $subject, $body, MailAddress::noReply($from), $replyTos); 256} 257 258function handle_push_mail($payload) { 259 $repoName = $payload->repository->name; 260 $ref = $payload->ref; 261 $mailingList = get_commit_mailing_list($repoName); 262 if ($mailingList === null) { 263 echo "Not sending mail for $repoName (no mailing list)"; 264 return; 265 } 266 267 if ($payload->created || $payload->deleted || $payload->forced) { 268 handle_ref_change_mail($mailingList, $payload); 269 } 270 271 // Ignore commits to PHP-x.y branches for now, to avoid duplicate commits on upwards merges. 272 // TODO: Find a better solution to this problem... 273 if ($repoName === 'php-src' && preg_match('(^refs/heads/PHP-\d\.\d$)', $ref)) { 274 echo "Skipping commit mails for push to $payload->ref of $repoName"; 275 return; 276 } 277 278 $pusherName = $payload->pusher->name; 279 foreach ($payload->commits as $commit) { 280 handle_commit_mail($mailingList, $repoName, $ref, $pusherName, $commit); 281 } 282} 283 284$CONFIG = [ 285 'repos' => [ 286 'php-langspec' => 'standards@lists.php.net', 287 'php-src' => 'git-pulls@lists.php.net', 288 'web-' => 'php-webmaster@lists.php.net', 289 'pecl-' => 'pecl-dev@lists.php.net', 290 ], 291]; 292 293if (DRY_RUN) { 294 $body = file_get_contents("php://stdin"); 295 $event = $argv[1]; 296} else { 297 $body = file_get_contents("php://input"); 298 $event = $_SERVER['HTTP_X_GITHUB_EVENT']; 299 300 if (!verify_signature($body)) { 301 header('HTTP/1.1 403 Forbidden'); 302 exit; 303 } 304} 305 306$payload = json_decode($body); 307$repoName = $payload->repository->name; 308 309switch ($event) { 310 case 'ping': 311 break; 312 case 'pull_request': 313 case 'issues': 314 $action = $payload->action; 315 $issue = $event == 'issues' ? $payload->issue : $payload->pull_request; 316 $htmlUrl = $issue->html_url; 317 318 $description = $issue->body; 319 $username = $issue->user->login; 320 321 $to = get_repo_email($CONFIG["repos"], $repoName); 322 $subject = prep_title($issue, $repoName); 323 $type = is_pr($issue) ? 'Pull Request' : 'Issue'; 324 325 $message = sprintf("You can view the %s on github:\r\n%s", $type, $htmlUrl); 326 switch ($action) { 327 case 'opened': 328 $message .= sprintf( 329 "\r\n\r\nOpened By: %s\r\n%s Description:\r\n%s", 330 $username, $type, $description); 331 break; 332 case 'closed': 333 $message .= "\r\n\r\nClosed."; 334 break; 335 case 'reopened': 336 $message .= "\r\n\r\nReopened."; 337 break; 338 case 'assigned': 339 case 'unassigned': 340 case 'labeled': 341 case 'unlabeled': 342 case 'edited': 343 case 'synchronize': 344 case 'milestoned': 345 case 'demilestoned': 346 // Ignore these actions 347 break 2; 348 } 349 350 send_mail($to, $subject, $message, MailAddress::noReply()); 351 break; 352 353 case 'pull_request_review_comment': 354 case 'issue_comment': 355 $action = $payload->action; 356 $issue = $event == 'issue_comment' ? $payload->issue : $payload->pull_request; 357 $htmlUrl = $issue->html_url; 358 359 $username = $payload->comment->user->login; 360 $comment = $payload->comment->body; 361 362 $to = get_repo_email($CONFIG["repos"], $repoName); 363 $subject = prep_title($issue, $repoName); 364 $type = is_pr($issue) ? 'Pull Request' : 'Issue'; 365 366 $message = sprintf("You can view the %s on github:\r\n%s", $type, $htmlUrl); 367 switch ($action) { 368 case 'created': 369 $message .= sprintf("\r\n\r\nComment by %s:\r\n%s", $username, $comment); 370 break; 371 case 'edited': 372 case 'deleted': 373 // Ignore these actions 374 break 2; 375 } 376 377 send_mail($to, $subject, $message, MailAddress::noReply()); 378 break; 379 380 case 'push': 381 if ($payload->ref === 'refs/heads/master') { 382 // Only close bugs for pushes to master. 383 foreach ($payload->commits as $commit) { 384 handle_bug_close($commit); 385 } 386 } 387 388 handle_push_mail($payload); 389 break; 390 391 default: 392 header('HTTP/1.1 501 Not Implemented'); 393} 394 395