1<?php declare(strict_types=1); 2 3/* 4 * This file is part of the Monolog package. 5 * 6 * (c) Jordi Boggiano <j.boggiano@seld.be> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Monolog\Handler; 13 14use Monolog\Formatter\WildfireFormatter; 15use Monolog\Formatter\FormatterInterface; 16 17/** 18 * Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol. 19 * 20 * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com> 21 * 22 * @phpstan-import-type FormattedRecord from AbstractProcessingHandler 23 */ 24class FirePHPHandler extends AbstractProcessingHandler 25{ 26 use WebRequestRecognizerTrait; 27 28 /** 29 * WildFire JSON header message format 30 */ 31 protected const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2'; 32 33 /** 34 * FirePHP structure for parsing messages & their presentation 35 */ 36 protected const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'; 37 38 /** 39 * Must reference a "known" plugin, otherwise headers won't display in FirePHP 40 */ 41 protected const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3'; 42 43 /** 44 * Header prefix for Wildfire to recognize & parse headers 45 */ 46 protected const HEADER_PREFIX = 'X-Wf'; 47 48 /** 49 * Whether or not Wildfire vendor-specific headers have been generated & sent yet 50 * @var bool 51 */ 52 protected static $initialized = false; 53 54 /** 55 * Shared static message index between potentially multiple handlers 56 * @var int 57 */ 58 protected static $messageIndex = 1; 59 60 /** @var bool */ 61 protected static $sendHeaders = true; 62 63 /** 64 * Base header creation function used by init headers & record headers 65 * 66 * @param array<int|string> $meta Wildfire Plugin, Protocol & Structure Indexes 67 * @param string $message Log message 68 * 69 * @return array<string, string> Complete header string ready for the client as key and message as value 70 * 71 * @phpstan-return non-empty-array<string, string> 72 */ 73 protected function createHeader(array $meta, string $message): array 74 { 75 $header = sprintf('%s-%s', static::HEADER_PREFIX, join('-', $meta)); 76 77 return [$header => $message]; 78 } 79 80 /** 81 * Creates message header from record 82 * 83 * @return array<string, string> 84 * 85 * @phpstan-return non-empty-array<string, string> 86 * 87 * @see createHeader() 88 * 89 * @phpstan-param FormattedRecord $record 90 */ 91 protected function createRecordHeader(array $record): array 92 { 93 // Wildfire is extensible to support multiple protocols & plugins in a single request, 94 // but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake. 95 return $this->createHeader( 96 [1, 1, 1, self::$messageIndex++], 97 $record['formatted'] 98 ); 99 } 100 101 /** 102 * {@inheritDoc} 103 */ 104 protected function getDefaultFormatter(): FormatterInterface 105 { 106 return new WildfireFormatter(); 107 } 108 109 /** 110 * Wildfire initialization headers to enable message parsing 111 * 112 * @see createHeader() 113 * @see sendHeader() 114 * 115 * @return array<string, string> 116 */ 117 protected function getInitHeaders(): array 118 { 119 // Initial payload consists of required headers for Wildfire 120 return array_merge( 121 $this->createHeader(['Protocol', 1], static::PROTOCOL_URI), 122 $this->createHeader([1, 'Structure', 1], static::STRUCTURE_URI), 123 $this->createHeader([1, 'Plugin', 1], static::PLUGIN_URI) 124 ); 125 } 126 127 /** 128 * Send header string to the client 129 */ 130 protected function sendHeader(string $header, string $content): void 131 { 132 if (!headers_sent() && self::$sendHeaders) { 133 header(sprintf('%s: %s', $header, $content)); 134 } 135 } 136 137 /** 138 * Creates & sends header for a record, ensuring init headers have been sent prior 139 * 140 * @see sendHeader() 141 * @see sendInitHeaders() 142 */ 143 protected function write(array $record): void 144 { 145 if (!self::$sendHeaders || !$this->isWebRequest()) { 146 return; 147 } 148 149 // WildFire-specific headers must be sent prior to any messages 150 if (!self::$initialized) { 151 self::$initialized = true; 152 153 self::$sendHeaders = $this->headersAccepted(); 154 if (!self::$sendHeaders) { 155 return; 156 } 157 158 foreach ($this->getInitHeaders() as $header => $content) { 159 $this->sendHeader($header, $content); 160 } 161 } 162 163 $header = $this->createRecordHeader($record); 164 if (trim(current($header)) !== '') { 165 $this->sendHeader(key($header), current($header)); 166 } 167 } 168 169 /** 170 * Verifies if the headers are accepted by the current user agent 171 */ 172 protected function headersAccepted(): bool 173 { 174 if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) { 175 return true; 176 } 177 178 return isset($_SERVER['HTTP_X_FIREPHP_VERSION']); 179 } 180} 181