Passed
Pull Request — 2.2 (#20357)
by Wilmer
08:37
created

FileTarget::rotateFiles()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 11
nc 9
nop 0
dl 0
loc 16
ccs 12
cts 12
cp 1
crap 6
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\log;
10
11
use Yii;
12
use yii\base\InvalidConfigException;
13
use yii\helpers\FileHelper;
14
15
/**
16
 * FileTarget records log messages in a file.
17
 *
18
 * The log file is specified via [[logFile]]. If the size of the log file exceeds
19
 * [[maxFileSize]] (in kilo-bytes), a rotation will be performed, which renames
20
 * the current log file by suffixing the file name with '.1'. All existing log
21
 * files are moved backwards by one place, i.e., '.2' to '.3', '.1' to '.2', and so on.
22
 * The property [[maxLogFiles]] specifies how many history files to keep.
23
 *
24
 * @author Qiang Xue <[email protected]>
25
 * @since 2.0
26
 */
27
class FileTarget extends Target
28
{
29
    /**
30
     * @var string|null log file path or [path alias](guide:concept-aliases). If not set, it will use the "@runtime/logs/app.log" file.
31
     * The directory containing the log files will be automatically created if not existing.
32
     */
33
    public $logFile;
34
    /**
35
     * @var bool whether log files should be rotated when they reach a certain [[maxFileSize|maximum size]].
36
     * Log rotation is enabled by default. This property allows you to disable it, when you have configured
37
     * an external tools for log rotation on your server.
38
     * @since 2.0.3
39
     */
40
    public $enableRotation = true;
41
    /**
42
     * @var int maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB.
43
     */
44
    public $maxFileSize = 10240; // in KB
45
    /**
46
     * @var int number of log files used for rotation. Defaults to 5.
47
     */
48
    public $maxLogFiles = 5;
49
    /**
50
     * @var int|null the permission to be set for newly created log files.
51
     * This value will be used by PHP chmod() function. No umask will be applied.
52
     * If not set, the permission will be determined by the current environment.
53
     */
54
    public $fileMode;
55
    /**
56
     * @var int the permission to be set for newly created directories.
57
     * This value will be used by PHP chmod() function. No umask will be applied.
58
     * Defaults to 0775, meaning the directory is read-writable by owner and group,
59
     * but read-only for other users.
60
     */
61
    public $dirMode = 0775;
62
63
    /**
64
     * Initializes the route.
65
     * This method is invoked after the route is created by the route manager.
66
     */
67 3
    public function init()
68
    {
69 3
        parent::init();
70 3
        if ($this->logFile === null) {
71 1
            $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log';
72
        } else {
73 2
            $this->logFile = Yii::getAlias($this->logFile);
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::getAlias($this->logFile) can also be of type false. However, the property $logFile is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
74
        }
75 3
        if ($this->maxLogFiles < 1) {
76
            $this->maxLogFiles = 1;
77
        }
78 3
        if ($this->maxFileSize < 1) {
79
            $this->maxFileSize = 1;
80
        }
81
    }
82
83
    /**
84
     * Writes log messages to a file.
85
     * Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
86
     * @throws InvalidConfigException if unable to open the log file for writing
87
     * @throws LogRuntimeException if unable to write complete log to file
88
     */
89 2
    public function export()
90
    {
91 2
        $text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n";
92
93 2
        if (trim($text) === '') {
94 1
            return; // No messages to export, so we exit the function early
95
        }
96
97 2
        if (strpos($this->logFile, '://') === false || strncmp($this->logFile, 'file://', 7) === 0) {
0 ignored issues
show
Bug introduced by
It seems like $this->logFile can also be of type null; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
        if (strpos(/** @scrutinizer ignore-type */ $this->logFile, '://') === false || strncmp($this->logFile, 'file://', 7) === 0) {
Loading history...
Bug introduced by
It seems like $this->logFile can also be of type null; however, parameter $string1 of strncmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
        if (strpos($this->logFile, '://') === false || strncmp(/** @scrutinizer ignore-type */ $this->logFile, 'file://', 7) === 0) {
Loading history...
98 2
            $logPath = dirname($this->logFile);
0 ignored issues
show
Bug introduced by
It seems like $this->logFile can also be of type null; however, parameter $path of dirname() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

98
            $logPath = dirname(/** @scrutinizer ignore-type */ $this->logFile);
Loading history...
99 2
            FileHelper::createDirectory($logPath, $this->dirMode, true);
100
        }
101
102 2
        if (($fp = @fopen($this->logFile, 'a')) === false) {
0 ignored issues
show
Bug introduced by
It seems like $this->logFile can also be of type null; however, parameter $filename of fopen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

102
        if (($fp = @fopen(/** @scrutinizer ignore-type */ $this->logFile, 'a')) === false) {
Loading history...
103
            throw new InvalidConfigException("Unable to append to log file: {$this->logFile}");
104
        }
105 2
        @flock($fp, LOCK_EX);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for flock(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

105
        /** @scrutinizer ignore-unhandled */ @flock($fp, LOCK_EX);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
106 2
        if ($this->enableRotation) {
107
            // clear stat cache to ensure getting the real current file size and not a cached one
108
            // this may result in rotating twice when cached file size is used on subsequent calls
109 2
            clearstatcache();
110
        }
111 2
        if ($this->enableRotation && @filesize($this->logFile) > $this->maxFileSize * 1024) {
0 ignored issues
show
Bug introduced by
It seems like $this->logFile can also be of type null; however, parameter $filename of filesize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

111
        if ($this->enableRotation && @filesize(/** @scrutinizer ignore-type */ $this->logFile) > $this->maxFileSize * 1024) {
Loading history...
112 1
            $this->rotateFiles();
113
        }
114 2
        $writeResult = @fwrite($fp, $text);
115 2
        if ($writeResult === false) {
116
            $message = "Unable to export log through file ($this->logFile)!";
117
            ($error = error_get_last()) and $message .= ": {$error['message']}";
118
            throw new LogRuntimeException($message);
119
        }
120 2
        $textSize = strlen($text);
121 2
        if ($writeResult < $textSize) {
122
            throw new LogRuntimeException("Unable to export whole log through file ({$this->logFile})! Wrote $writeResult out of $textSize bytes.");
123
        }
124 2
        @fflush($fp);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fflush(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

124
        /** @scrutinizer ignore-unhandled */ @fflush($fp);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
125 2
        @flock($fp, LOCK_UN);
126 2
        @fclose($fp);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

126
        /** @scrutinizer ignore-unhandled */ @fclose($fp);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
127
128 2
        if ($this->fileMode !== null) {
129
            @chmod($this->logFile, $this->fileMode);
0 ignored issues
show
Bug introduced by
It seems like $this->logFile can also be of type null; however, parameter $filename of chmod() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

129
            @chmod(/** @scrutinizer ignore-type */ $this->logFile, $this->fileMode);
Loading history...
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

129
            /** @scrutinizer ignore-unhandled */ @chmod($this->logFile, $this->fileMode);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
130
        }
131
    }
132
133
    /**
134
     * Rotates log files.
135
     */
136 1
    protected function rotateFiles()
137
    {
138 1
        $file = $this->logFile;
139 1
        for ($i = $this->maxLogFiles; $i >= 0; --$i) {
140
            // $i == 0 is the original log file
141 1
            $rotateFile = $file . ($i === 0 ? '' : '.' . $i);
142 1
            if (is_file($rotateFile)) {
143
                // suppress errors because it's possible multiple processes enter into this section
144 1
                if ($i === $this->maxLogFiles) {
145 1
                    @unlink($rotateFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

145
                    /** @scrutinizer ignore-unhandled */ @unlink($rotateFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
146 1
                    continue;
147
                }
148 1
                $newFile = $this->logFile . '.' . ($i + 1);
149 1
                $this->rotateByCopy($rotateFile, $newFile);
150 1
                if ($i === 0) {
151 1
                    $this->clearLogFile($rotateFile);
152
                }
153
            }
154
        }
155
    }
156
157
    /***
158
     * Clear log file without closing any other process open handles
159
     * @param string $rotateFile
160
     */
161 1
    private function clearLogFile($rotateFile)
162
    {
163 1
        if ($filePointer = @fopen($rotateFile, 'a')) {
164 1
            @ftruncate($filePointer, 0);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ftruncate(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

164
            /** @scrutinizer ignore-unhandled */ @ftruncate($filePointer, 0);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
165 1
            @fclose($filePointer);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

165
            /** @scrutinizer ignore-unhandled */ @fclose($filePointer);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
166
        }
167
    }
168
169
    /***
170
     * Copy rotated file into new file
171
     * @param string $rotateFile
172
     * @param string $newFile
173
     */
174 1
    private function rotateByCopy($rotateFile, $newFile)
175
    {
176 1
        @copy($rotateFile, $newFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for copy(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

176
        /** @scrutinizer ignore-unhandled */ @copy($rotateFile, $newFile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
177 1
        if ($this->fileMode !== null) {
178
            @chmod($newFile, $this->fileMode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

178
            /** @scrutinizer ignore-unhandled */ @chmod($newFile, $this->fileMode);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
179
        }
180
    }
181
}
182