├── Services
├── Hoptoad
│ └── CodeIgniter.php
└── Hoptoad.php
├── doc
└── example.php
├── README.markdown
└── test
├── hoptoad_2_0.xsd
└── test.php
/Services/Hoptoad/CodeIgniter.php:
--------------------------------------------------------------------------------
1 | ci =& get_instance();
7 | }
8 |
9 | function action() {
10 | return $this->ci->router->method;
11 | }
12 |
13 | function component() {
14 | return $this->ci->router->class;
15 | }
16 |
17 | function project_path() {
18 | return APPPATH;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/doc/example.php:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
14 |
20 |
21 |
30 |
31 |
41 |
42 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | This is a simple [Hoptoad](http://hoptoadapp.com) notifier for PHP. It's been used in a few production sites now with success. It's not quite as fully featured as the official Ruby notifier but it works well.
4 |
5 | # Thanks
6 |
7 | Big thanks to Lou Kosak, Till Klampaeckel and Scott Woods who have contributed extensively to this project.
8 |
9 | # Limitations
10 |
11 | This notifier does not contain two big features from the Ruby notifier. The two are error filtering and deploy tracking. Error filtering will be coming in a future release.
12 |
13 | For deploy tracking, since I use Capistrano to deploy my PHP apps, I simply use the Ruby notifier to perform the deploy tracking. For this reason, unless someone wants to contribute patches, I don't see deploy tracking coming to the php notifier.
14 |
15 | # Requirements
16 |
17 | To use the default _pear_ client install Pear's HTTP_Request2:
18 |
19 | pear install HTTP_Request2
20 |
21 | To use the _curl_ client, install the PHP curl extension. To install on Ubuntu if you are using PHP 5 you would run:
22 | sudo apt-get install php5-curl
23 | sudo /etc/init.d/apache2 reload
24 |
25 | # BSD License
26 |
27 | Copyright (c) 2010, Rich Cavanaugh
28 | All rights reserved.
29 |
30 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
31 |
32 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
33 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
34 | * The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
35 |
36 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 |
--------------------------------------------------------------------------------
/test/hoptoad_2_0.xsd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/test/test.php:
--------------------------------------------------------------------------------
1 |
2 | 'localhost',
8 | 'REQUEST_URI' => '/example.php',
9 | 'HTTP_REFERER' => 'http://localhost/reports/something',
10 | 'QUERY_STRING' => 'arg1=val1&arg2=val2'
11 | );
12 |
13 | $_SESSION = array(
14 | 'var1' => 'val1',
15 | 'var2' => 'val2',
16 | );
17 |
18 | $_GET = array(
19 | 'get1' => 'val1',
20 | 'get2' => 'val2',
21 | );
22 |
23 | $_POST = array(
24 | 'post1' => 'val3',
25 | 'post2' => 'val4',
26 | );
27 |
28 | $_REQUEST = array_merge($_GET, $_POST);
29 |
30 | class HoptoadTest extends PHPUnit_Framework_TestCase
31 | {
32 | protected function setUp()
33 | {
34 | $this->hoptoad = new Services_Hoptoad('myAPIKey', 'production', 'pear', false, 2);
35 |
36 | $trace = array(
37 | array(
38 | 'class' => 'Hoptoad',
39 | 'file' => 'file.php',
40 | 'line' => 23,
41 | 'function' => 'foo',
42 | ),
43 | array(
44 | 'class' => 'Foo',
45 | 'file' => 'foo.php',
46 | 'line' => 242,
47 | 'function' => 'foo',
48 | ),
49 | array(
50 | 'class' => 'Bar',
51 | 'file' => 'bar.php',
52 | 'line' => 42,
53 | 'function' => 'bar',
54 | ),
55 | );
56 | $this->hoptoad->setParamsForNotify('ERROR', 'Something went wrong', 'foo', 23, $trace);
57 | }
58 |
59 | public function testRequestURI()
60 | {
61 | // check protocol support
62 | $this->assertEquals('http://localhost/example.php?arg1=val1&arg2=val2', $this->hoptoad->request_uri());
63 | $_SERVER['SERVER_PORT'] = 443;
64 | $this->assertEquals('https://localhost/example.php?arg1=val1&arg2=val2', $this->hoptoad->request_uri());
65 | $_SERVER['SERVER_PORT'] = 80;
66 |
67 | // Check without query string.
68 | $old_query_string = $_SERVER['QUERY_STRING'];
69 | $_SERVER['QUERY_STRING'] = '';
70 | $this->assertEquals('http://localhost/example.php', $this->hoptoad->request_uri());
71 | $_SERVER['QUERY_STRING'] = $old_query_string;
72 | }
73 |
74 | public function testXMLBacktrace()
75 | {
76 | $expected_xml = <<
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | XML;
86 | $doc = new SimpleXMLElement('');
87 | $this->hoptoad->addXmlBacktrace($doc);
88 | $this->assertXmlStringEqualsXmlString($expected_xml, $doc->asXML());
89 | }
90 |
91 | public function testXMLParams()
92 | {
93 | $expected_xml = <<
95 |
96 | val1
97 | val2
98 | val3
99 | val4
100 |
101 |
102 | XML;
103 | $doc = new SimpleXMLElement('');
104 | $this->hoptoad->addXmlVars($doc, 'params', $_REQUEST);
105 | $this->assertXmlStringEqualsXmlString($expected_xml, $doc->asXML());
106 | }
107 |
108 | public function testNotificationBody()
109 | {
110 | $xmllint = popen('xmllint --noout --schema test/hoptoad_2_0.xsd - 2> /dev/null', 'w');
111 | if ($xmllint) {
112 | fwrite($xmllint, $this->hoptoad->buildXmlNotice());
113 | $status = pclose($xmllint);
114 | $this->assertEquals(0, $status, "XML output did not validate against schema.");
115 | } else {
116 | $this->fail("Couldn't run xmllint command.");
117 | }
118 | }
119 |
120 | }
121 | ?>
122 |
123 |
--------------------------------------------------------------------------------
/Services/Hoptoad.php:
--------------------------------------------------------------------------------
1 |
8 | * @author Till Klampaeckel
9 | * @license
10 | * @version GIT: $Id$
11 | * @link http://github.com/till/php-hoptoad-notifier
12 | */
13 |
14 |
15 | /**
16 | * Services_Hoptoad
17 | *
18 | * @category error
19 | * @package Services_Hoptoad
20 | * @author Rich Cavanaugh
21 | * @author Till Klampaeckel
22 | * @license
23 | * @version Release: @package_version@
24 | * @link http://github.com/rich/php-hoptoad-notifier
25 | */
26 | class Services_Hoptoad
27 | {
28 | const NOTIFIER_NAME = 'php-hoptoad-notifier';
29 | const NOTIFIER_VERSION = '0.2.0';
30 | const NOTIFIER_URL = 'http://github.com/rich/php-hoptoad-notifier';
31 | const NOTIFIER_API_VERSION = '2.0';
32 |
33 | protected $error_class;
34 | protected $message;
35 | protected $file;
36 | protected $line;
37 | protected $trace;
38 |
39 | /**
40 | * Report E_STRICT
41 | *
42 | * @var bool $reportESTRICT
43 | * @todo Implement set!
44 | */
45 | protected $reportESTRICT;
46 |
47 | /**
48 | * Timeout for cUrl.
49 | * @var int $timeout
50 | */
51 | protected $timeout;
52 |
53 | public $client; // pear, curl or zend
54 |
55 | /**
56 | * @var mixed $apiKey
57 | */
58 | public $apiKey;
59 |
60 | /**
61 | * @var string
62 | **/
63 | public $environment;
64 |
65 | /**
66 | * Initialize the chosen notifier and install the error
67 | * and exception handlers that connect to Hoptoad
68 | *
69 | * @return void
70 | * @author Rich Cavanaugh
71 | */
72 | public static function installHandlers($apiKey=NULL, $environment=NULL, $client=NULL, $class='Services_Hoptoad')
73 | {
74 | $hoptoad = new $class($apiKey, $environment, $client);
75 | $hoptoad->installNotifierHandlers();
76 | }
77 |
78 | /**
79 | * Hook's this notifier to PHP error and exception handlers
80 | * @return void
81 | * @author Rich Cavanaugh
82 | **/
83 | public function installNotifierHandlers()
84 | {
85 | set_error_handler(array($this, "errorHandler"));
86 | set_exception_handler(array($this, "exceptionHandler"));
87 | }
88 |
89 | /**
90 | * Initialize the Hoptad client
91 | *
92 | * @param string $apiKey
93 | * @param string $environment
94 | * @param string $client
95 | * @param string $reportESTRICT
96 | * @param int $timeout
97 | * @return void
98 | * @author Rich Cavanaugh
99 | */
100 | function __construct($apiKey, $environment='production', $client='pear', $reportESTRICT=false, $timeout=2)
101 | {
102 | $this->apiKey = $apiKey;
103 | $this->environment = $environment;
104 | $this->client = $client;
105 | $this->reportESTRICT = $reportESTRICT;
106 | $this->timeout = $timeout;
107 | $this->setup();
108 | }
109 |
110 | /**
111 | * A method meant specifically for subclasses to override so they don't need
112 | * to handle the constructor
113 | * @return void
114 | * @author Rich Cavanaugh
115 | **/
116 | public function setup()
117 | {
118 | // we don't do anything here in the base class
119 | }
120 |
121 | /**
122 | * Handle a php error
123 | *
124 | * @param string $code
125 | * @param string $message
126 | * @param string $file
127 | * @param string $line
128 | * @return void
129 | * @author Rich Cavanaugh
130 | */
131 | public function errorHandler($code, $message, $file, $line)
132 | {
133 | if ($code == E_STRICT && $this->reportESTRICT === false) return;
134 |
135 | $this->notify($code, $message, $file, $line, debug_backtrace());
136 | }
137 |
138 | /**
139 | * Handle a raised exception
140 | *
141 | * @param Exception $exception
142 | * @return void
143 | * @author Rich Cavanaugh
144 | */
145 | public function exceptionHandler($exception)
146 | {
147 | $this->notify(get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine(), $exception->getTrace());
148 | }
149 |
150 | /**
151 | * Set the values to be used for the next notice sent to Hoptoad
152 | * @return void
153 | * @author Rich Cavanaugh
154 | **/
155 | public function setParamsForNotify($error_class, $message, $file, $line, $trace, $component=NULL)
156 | {
157 | $this->error_class = $error_class;
158 | $this->message = $message;
159 | $this->file = $file;
160 | $this->line = $line;
161 | $this->trace = $trace;
162 | $this->component = $component;
163 | }
164 |
165 | /**
166 | * Pass the error and environment data on to Hoptoad
167 | *
168 | * @param mixed $error_class
169 | * @param string $message
170 | * @param string $file
171 | * @param string $line
172 | * @param array $trace
173 | * @param string $environment
174 | *
175 | * @author Rich Cavanaugh
176 | * @todo Handle response (e.g. errors)
177 | */
178 | function notify($error_class, $message, $file, $line, $trace, $component=NULL)
179 | {
180 | $this->setParamsForNotify($error_class, $message, $file, $line, $trace, $component);
181 |
182 | $url = "http://hoptoadapp.com/notifier_api/v2/notices";
183 | $headers = array(
184 | 'Accept' => 'text/xml, application/xml',
185 | 'Content-Type' => 'text/xml'
186 | );
187 | $body = $this->buildXmlNotice();
188 |
189 | try {
190 | $status = call_user_func_array(array($this, $this->client . 'Request'), array($url, $headers, $body));
191 | if ($status != 200) $this->handleErrorResponse($status);
192 | } catch (RuntimeException $e) {
193 | // TODO do something reasonable with the runtime exception.
194 | // we can't really throw our runtime exception since we're likely in
195 | // an exception handler. Punt on this for now and come back to it.
196 | }
197 | }
198 |
199 | /**
200 | * Build up the XML to post according to the documentation at:
201 | * http://help.hoptoadapp.com/faqs/api-2/notifier-api-v2
202 | * @return string
203 | * @author Rich Cavanaugh
204 | **/
205 | function buildXmlNotice()
206 | {
207 | $doc = new SimpleXMLElement('');
208 | $doc->addAttribute('version', self::NOTIFIER_API_VERSION);
209 | $doc->addChild('api-key', $this->apiKey);
210 |
211 | $notifier = $doc->addChild('notifier');
212 | $notifier->addChild('name', self::NOTIFIER_NAME);
213 | $notifier->addChild('version', self::NOTIFIER_VERSION);
214 | $notifier->addChild('url', self::NOTIFIER_URL);
215 |
216 | $error = $doc->addChild('error');
217 | $error->addChild('class', $this->error_class);
218 | $error->addChild('message', $this->message);
219 | $this->addXmlBacktrace($error);
220 |
221 | $request = $doc->addChild('request');
222 | $request->addChild('url', htmlspecialchars($this->request_uri()));
223 | $request->addChild('component', $this->component());
224 | $request->addChild('action', $this->action());
225 |
226 | if (isset($_REQUEST)) $this->addXmlVars($request, 'params', $this->params());
227 | if (isset($_SESSION)) $this->addXmlVars($request, 'session', $this->session());
228 | if (isset($_SERVER)) $this->addXmlVars($request, 'cgi-data', $this->cgi_data());
229 |
230 | $env = $doc->addChild('server-environment');
231 | $env->addChild('project-root', $this->project_root());
232 | $env->addChild('environment-name', $this->environment());
233 |
234 | return $doc->asXML();
235 | }
236 |
237 | /**
238 | * Add a Hoptoad var block to the XML
239 | * @return void
240 | * @author Rich Cavanaugh
241 | **/
242 | function addXmlVars($parent, $key, $source)
243 | {
244 | if (empty($source)) return;
245 |
246 | $node = $parent->addChild($key);
247 | foreach ($source as $key => $val) {
248 | $var_node = $node->addChild('var', htmlspecialchars(var_export($val, true)));
249 | $var_node->addAttribute('key', $key);
250 | }
251 | }
252 |
253 | /**
254 | * Add a Hoptoad backtrace to the XML
255 | * @return void
256 | * @author Rich Cavanaugh
257 | **/
258 | function addXmlBacktrace($parent)
259 | {
260 | $backtrace = $parent->addChild('backtrace');
261 | $line_node = $backtrace->addChild('line');
262 | $line_node->addAttribute('file', $this->file);
263 | $line_node->addAttribute('number', $this->line);
264 |
265 | foreach ($this->trace as $entry) {
266 | if (isset($entry['class']) && $entry['class'] == 'Services_Hoptoad') continue;
267 |
268 | $line_node = $backtrace->addChild('line');
269 | $line_node->addAttribute('file', $entry['file']);
270 | $line_node->addAttribute('number', $entry['line']);
271 | $line_node->addAttribute('method', $entry['function']);
272 | }
273 | }
274 |
275 | /**
276 | * params
277 | * @return array
278 | * @author Scott Woods
279 | **/
280 | function params() {
281 | return $_REQUEST;
282 | }
283 |
284 | /**
285 | * session
286 | * @return array
287 | * @author Scott Woods
288 | **/
289 | function session() {
290 | return $_SESSION;
291 | }
292 |
293 | /**
294 | * cgi_data
295 | * @return array
296 | * @author Scott Woods
297 | **/
298 | function cgi_data() {
299 | if (isset($_ENV) && !empty($_ENV)) {
300 | return array_merge($_SERVER, $_ENV);
301 | }
302 | return $_SERVER;
303 | }
304 |
305 | /**
306 | * component
307 | * @return mixed
308 | * @author Scott Woods
309 | **/
310 | function component() {
311 | return $this->component;
312 | }
313 |
314 | /**
315 | * action
316 | * @return mixed
317 | * @author Scott Woods
318 | **/
319 | function action() {
320 | return '';
321 | }
322 |
323 | /**
324 | * environment
325 | * @return string
326 | * @author Rich Cavanaugh
327 | **/
328 | function environment() {
329 | return $this->environment;
330 | }
331 |
332 | /**
333 | * project_root
334 | * @return string
335 | * @author Scott Woods
336 | **/
337 | function project_root() {
338 | if (isset($_SERVER['DOCUMENT_ROOT'])) {
339 | return $_SERVER['DOCUMENT_ROOT'];
340 | } else {
341 | return dirname(__FILE__);
342 | }
343 | }
344 |
345 |
346 | /**
347 | * get the request uri
348 | * @return string
349 | * @author Scott Woods
350 | **/
351 | function request_uri() {
352 | if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) {
353 | $protocol = 'https';
354 | } else {
355 | $protocol = 'http';
356 | }
357 | $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '';
358 | $path = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
359 | $query_string = isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING']) ? ('?' . $_SERVER['QUERY_STRING']) : '';
360 | return "{$protocol}://{$host}{$path}{$query_string}";
361 | }
362 |
363 | /**
364 | * @param mixed $code The HTTP status code from Hoptoad.
365 | *
366 | * @return void
367 | * @throws RuntimeException Error message from hoptoad, translated to a RuntimeException.
368 | */
369 | protected function handleErrorResponse($code)
370 | {
371 | switch ($code) {
372 | case '403':
373 | $msg = 'The requested project does not support SSL - resubmit in an http request.';
374 | break;
375 | case '422':
376 | $msg = 'The submitted notice was invalid - check the notice xml against the schema.';
377 | break;
378 | case '500':
379 | $msg = 'Unexpected errors - submit a bug report at http://help.hoptoadapp.com.';
380 | break;
381 | default:
382 | $msg = 'Unknown error code from Hoptoad\'s API: ' . $code;
383 | break;
384 | }
385 |
386 | throw new RuntimeException($msg, $code);
387 | }
388 |
389 | /**
390 | * Send the request to Hoptoad using PEAR
391 | * @return integer
392 | * @author Rich Cavanaugh
393 | **/
394 | public function pearRequest($url, $headers, $body)
395 | {
396 | if (!class_exists('HTTP_Request2')) require_once('HTTP/Request2.php');
397 | if (!class_exists('HTTP_Request2_Adapter_Socket')) require_once 'HTTP/Request2/Adapter/Socket.php';
398 |
399 | $adapter = new HTTP_Request2_Adapter_Socket;
400 | $req = new HTTP_Request2($url, HTTP_Request2::METHOD_POST);
401 | $req->setAdapter($adapter);
402 | $req->setHeader($headers);
403 | $req->setBody($body);
404 | return $req->send()->getStatus();
405 | }
406 |
407 | /**
408 | * Send the request to Hoptoad using Curl
409 | * @return integer
410 | * @author Rich Cavanaugh
411 | **/
412 | public function curlRequest($url, $headers, $body)
413 | {
414 | $header_strings = array();
415 | foreach ($headers as $key => $val) {
416 | $header_strings[] = "{$key}: {$val}";
417 | }
418 |
419 | $curlHandle = curl_init();
420 | curl_setopt($curlHandle, CURLOPT_URL, $url);
421 | curl_setopt($curlHandle, CURLOPT_POST, 1);
422 | curl_setopt($curlHandle, CURLOPT_HEADER, 0);
423 | curl_setopt($curlHandle, CURLOPT_TIMEOUT, $this->timeout);
424 | curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body);
425 | curl_setopt($curlHandle, CURLOPT_HTTPHEADER, $header_strings);
426 | curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, 1);
427 | curl_exec($curlHandle);
428 | $status = curl_getinfo($curlHandle, CURLINFO_HTTP_CODE);
429 | curl_close($curlHandle);
430 | return $status;
431 | }
432 |
433 | /**
434 | * Send the request to Hoptoad using Zend
435 | * @return integer
436 | * @author Rich Cavanaugh
437 | **/
438 | public function zendRequest($url, $headers, $body)
439 | {
440 | $header_strings = array();
441 | foreach ($headers as $key => $val) {
442 | $header_strings[] = "{$key}: {$val}";
443 | }
444 |
445 | $client = new Zend_Http_Client($url);
446 | $client->setHeaders($header_strings);
447 | $client->setRawData($body, 'text/xml');
448 |
449 | $response = $client->request('POST');
450 |
451 | return $response->getStatus();
452 | }
453 | }
454 |
--------------------------------------------------------------------------------