Passed
Push — master ( 6a76e0...a126cd )
by Sergey
06:56
created

SlackTarget.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Slack log target for Yii 2
4
 *
5
 * @see       https://github.com/sergeymakinen/yii2-slack-log
6
 * @copyright Copyright (c) 2016 Sergey Makinen (https://makinen.ru)
7
 * @license   https://github.com/sergeymakinen/yii2-slack-log/blob/master/LICENSE The MIT License
8
 */
9
10
namespace sergeymakinen\log;
11
12
use yii\helpers\Json;
13
use yii\helpers\Url;
14
use yii\helpers\VarDumper;
15
use yii\log\Logger;
16
use yii\log\Target;
17
use yii\web\Request;
18
19
class SlackTarget extends Target
20
{
21
    /**
22
     * Incoming Webhook URL.
23
     *
24
     * @var string
25
     */
26
    public $webhookUrl;
27
28
    /**
29
     * Displayed username.
30
     *
31
     * @var string
32
     */
33
    public $username;
34
35
    /**
36
     * Icon URL.
37
     *
38
     * @var string
39
     */
40
    public $iconUrl;
41
42
    /**
43
     * Icon Emoji.
44
     *
45
     * @var string
46
     */
47
    public $iconEmoji;
48
49
    /**
50
     * Channel or Direct Message.
51
     *
52
     * @var string
53
     */
54
    public $channel;
55
56
    /**
57
     * Colors per a Logger level.
58
     *
59
     * @var array
60
     */
61
    public $colors = [
62
        Logger::LEVEL_ERROR => 'danger',
63
        Logger::LEVEL_WARNING => 'warning'
64
    ];
65
66
    /**
67
     * @inheritDoc
68
     */
69
    public function export()
70
    {
71
        if (!isset($this->webhookUrl)) {
72
            return;
73
        }
74
75
        $payload = [
76
            'parse' => 'none',
77
            'attachments' => array_map([$this, 'formatMessageAttachment'], $this->messages)
78
        ];
79
        if (isset($this->username)) {
80
            $payload['username'] = $this->username;
81
        }
82
        if (isset($this->iconUrl)) {
83
            $payload['icon_url'] = $this->iconUrl;
84
        }
85
        if (isset($this->iconEmoji)) {
86
            $payload['icon_emoji'] = $this->iconEmoji;
87
        }
88
        if (isset($this->channel)) {
89
            $payload['channel'] = $this->channel;
90
        }
91
        if (extension_loaded('curl')) {
92
            $curl = curl_init();
93
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
94
            curl_setopt($curl, CURLOPT_POST, true);
95
            curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=UTF-8']);
96
            curl_setopt($curl, CURLOPT_POSTFIELDS, Json::encode($payload));
97
            curl_setopt($curl, CURLOPT_URL, $this->webhookUrl);
98
            curl_exec($curl);
99
            curl_close($curl);
100
        } else {
101
            $context = stream_context_create([
102
                'http' => [
103
                    'method' => 'POST',
104
                    'header' => "Content-Type: application/json; charset=UTF-8\r\n",
105
                    'content' => Json::encode($payload)
106
                ]
107
            ]);
108
            @file_get_contents($this->webhookUrl, false, $context);
109
        }
110
    }
111
112
    /**
113
     * Encodes special chars in a message as HTML entities.
114
     *
115
     * @param string $message
116
     *
117
     * @return string
118
     */
119
    protected static function encodeMessage($message)
120
    {
121
        return htmlspecialchars($message, ENT_NOQUOTES, 'UTF-8');
122
    }
123
124
    /**
125
     * Returns a properly formatted message attachment for Slack API.
126
     *
127
     * @param array $message
128
     *
129
     * @return array
130
     */
131
    protected function formatMessageAttachment($message)
0 ignored issues
show
formatMessageAttachment uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
132
    {
133
        list($text, $level, $category, $timestamp) = $message;
134
        $attachment = [
135
            'fallback' => static::encodeMessage($this->formatMessage($message)),
136
            'title' => ucwords(Logger::getLevelName($level)),
137
            'fields' => [
138
                [
139
                    'title' => 'Level',
140
                    'value' => Logger::getLevelName($level),
141
                    'short' => true
142
                ],
143
                [
144
                    'title' => 'Category',
145
                    'value' => '`' . static::encodeMessage($category) . '`',
146
                    'short' => true
147
                ]
148
            ],
149
            'footer' => static::class,
150
            'ts' => (integer) round($timestamp),
151
            'mrkdwn_in' => [
152
                'fields',
153
                'text'
154
            ]
155
        ];
156
        if ($this->prefix !== null) {
157
            $attachment['fields'][] = [
158
                'title' => 'Prefix',
159
                'value' => '`' . static::encodeMessage(call_user_func($this->prefix, $message)) . '`',
160
                'short' => true,
161
            ];
162
        }
163
        if (isset(\Yii::$app)) {
164
            if (isset($_SERVER['argv'])) {
165
                $attachment['author_name'] = implode(' ', $_SERVER['argv']);
166
            } elseif (\Yii::$app->getRequest() instanceof Request) {
167
                $attachment['author_name'] = Url::current([], true);
1 ignored issue
show
The method current() does not seem to exist on object<yii\helpers\Url>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
168
                $attachment['author_link'] = $attachment['author_name'];
169
                $attachment['fields'][] = [
170
                    'title' => 'User IP',
171
                    'value' => \Yii::$app->getRequest()->getUserIP(),
172
                    'short' => true,
173
                ];
174
            }
175
            if (\Yii::$app->has('user', true) && !is_null(\Yii::$app->getUser())) {
176
                $user = \Yii::$app->getUser()->getIdentity(false);
1 ignored issue
show
The method getUser does only exist in yii\web\Application, but not in yii\console\Application.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
177
                if (isset($user)) {
178
                    $attachment['fields'][] = [
179
                        'title' => 'User ID',
180
                        'value' => $user->getId(),
181
                        'short' => true,
182
                    ];
183
                }
184
            }
185
            if (\Yii::$app->has('session', true) && !is_null(\Yii::$app->getSession())) {
186
                if (\Yii::$app->getSession()->getIsActive()) {
187
                    $attachment['fields'][] = [
188
                        'title' => 'Session ID',
189
                        'value' => \Yii::$app->getSession()->getId(),
1 ignored issue
show
The method getSession does only exist in yii\web\Application, but not in yii\console\Application.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
190
                        'short' => true,
191
                    ];
192
                }
193
            }
194
        }
195
        if (isset($this->colors[$level])) {
196
            $attachment['color'] = $this->colors[$level];
197
        }
198
        if (!is_string($text)) {
199
            if ($text instanceof \Throwable || $text instanceof \Exception) {
200
                $text = (string) $text;
201
            } else {
202
                $text = VarDumper::export($text);
1 ignored issue
show
The method export() does not seem to exist on object<yii\helpers\VarDumper>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
203
            }
204
        }
205
        $attachment['text'] = "```\n" . static::encodeMessage($text) . "\n```";
206
        $traces = [];
207
        if (isset($message[4])) {
208
            foreach ($message[4] as $trace) {
209
                $traces[] = "in {$trace['file']}:{$trace['line']}";
210
            }
211
        }
212
        if (!empty($traces)) {
213
            $attachment['fields'][] = [
214
                'title' => 'Stack Trace',
215
                'value' => "```\n" . static::encodeMessage(implode("\n", $traces)) . "\n```",
216
                'short' => false,
217
            ];
218
        }
219
        return $attachment;
220
    }
221
}
222