summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Util/File.php
blob: dad332aeae482ceda4527712d6b61a1684cb2109 (plain)
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
<?php
/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */

namespace Icinga\Util;

use SplFileObject;
use ErrorException;
use RuntimeException;
use Icinga\Exception\NotWritableError;

/**
 * File
 *
 * A class to ease opening files and reading/writing to them.
 */
class File extends SplFileObject
{
    /**
     * The mode used to open the file
     *
     * @var string
     */
    protected $openMode;

    /**
     * The access mode to use when creating directories
     *
     * @var int
     */
    public static $dirMode = 1528; // 2770

    /**
     * @see SplFileObject::__construct()
     */
    public function __construct($filename, $openMode = 'r', $useIncludePath = false, $context = null)
    {
        $this->openMode = $openMode;
        if ($context === null) {
            parent::__construct($filename, $openMode, $useIncludePath);
        } else {
            parent::__construct($filename, $openMode, $useIncludePath, $context);
        }
    }

    /**
     * Create a file using the given access mode and return a instance of File open for writing
     *
     * @param   string  $path           The path to the file
     * @param   int     $accessMode     The access mode to set
     * @param   bool    $recursive      Whether missing nested directories of the given path should be created
     *
     * @return  File
     *
     * @throws  RuntimeException        In case the file cannot be created or the access mode cannot be set
     * @throws  NotWritableError        In case the path's (existing) parent is not writable
     */
    public static function create($path, $accessMode, $recursive = true)
    {
        $dirPath = dirname($path);
        if ($recursive && !is_dir($dirPath)) {
            static::createDirectories($dirPath);
        } elseif (! is_writable($dirPath)) {
            throw new NotWritableError(sprintf('Path "%s" is not writable', $dirPath));
        }

        $file = new static($path, 'x+');

        if (! @chmod($path, $accessMode)) {
            $error = error_get_last();
            throw new RuntimeException(sprintf(
                'Cannot set access mode "%s" on file "%s" (%s)',
                decoct($accessMode),
                $path,
                $error['message']
            ));
        }

        return $file;
    }

    /**
     * Create missing directories
     *
     * @param   string  $path
     *
     * @throws  RuntimeException        In case a directory cannot be created or the access mode cannot be set
     */
    protected static function createDirectories($path)
    {
        $part = strpos($path, DIRECTORY_SEPARATOR) === 0 ? DIRECTORY_SEPARATOR : '';
        foreach (explode(DIRECTORY_SEPARATOR, ltrim($path, DIRECTORY_SEPARATOR)) as $dir) {
            $part .= $dir . DIRECTORY_SEPARATOR;

            if (! is_dir($part)) {
                if (! @mkdir($part, static::$dirMode)) {
                    $error = error_get_last();
                    throw new RuntimeException(sprintf(
                        'Failed to create missing directory "%s" (%s)',
                        $part,
                        $error['message']
                    ));
                }

                if (! @chmod($part, static::$dirMode)) {
                    $error = error_get_last();
                    throw new RuntimeException(sprintf(
                        'Failed to set access mode "%s" for directory "%s" (%s)',
                        decoct(static::$dirMode),
                        $part,
                        $error['message']
                    ));
                }
            }
        }
    }

    #[\ReturnTypeWillChange]
    public function fwrite($str, $length = null)
    {
        $this->assertOpenForWriting();
        $this->setupErrorHandler();
        $retVal = $length === null ? parent::fwrite($str) : parent::fwrite($str, $length);
        restore_error_handler();
        return $retVal;
    }

    public function ftruncate($size): bool
    {
        $this->assertOpenForWriting();
        $this->setupErrorHandler();
        $retVal = parent::ftruncate($size);
        restore_error_handler();
        return $retVal;
    }

    #[\ReturnTypeWillChange]
    public function ftell()
    {
        $this->setupErrorHandler();
        $retVal = parent::ftell();
        restore_error_handler();
        return $retVal;
    }

    public function flock($operation, &$wouldblock = null): bool
    {
        $this->setupErrorHandler();
        $retVal = parent::flock($operation, $wouldblock);
        restore_error_handler();
        return $retVal;
    }

    #[\ReturnTypeWillChange]
    public function fgetc()
    {
        $this->setupErrorHandler();
        $retVal = parent::fgetc();
        restore_error_handler();
        return $retVal;
    }

    public function fflush(): bool
    {
        $this->setupErrorHandler();
        $retVal = parent::fflush();
        restore_error_handler();
        return $retVal;
    }

    /**
     * Setup an error handler that throws a RuntimeException for every emitted E_WARNING
     */
    protected function setupErrorHandler()
    {
        set_error_handler(
            function ($errno, $errstr, $errfile, $errline) {
                restore_error_handler();
                throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
            },
            E_WARNING
        );
    }

    /**
     * Assert that the file was opened for writing and throw an exception otherwise
     *
     * @throws  NotWritableError    In case the file was not opened for writing
     */
    protected function assertOpenForWriting()
    {
        if (!preg_match('@w|a|\+@', $this->openMode)) {
            throw new NotWritableError('File not open for writing');
        }
    }
}