1<?php
2
3namespace SimpleSAML\Utils;
4
5use SimpleSAML\Configuration;
6use SimpleSAML\Error;
7
8/**
9 * System-related utility methods.
10 *
11 * @package SimpleSAMLphp
12 */
13
14class System
15{
16    const WINDOWS = 1;
17    const LINUX = 2;
18    const OSX = 3;
19    const HPUX = 4;
20    const UNIX = 5;
21    const BSD = 6;
22    const IRIX = 7;
23    const SUNOS = 8;
24
25
26    /**
27     * This function returns the Operating System we are running on.
28     *
29     * @return mixed A predefined constant identifying the OS we are running on. False if we are unable to determine it.
30     *
31     * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
32     */
33    public static function getOS()
34    {
35        if (stristr(PHP_OS, 'LINUX')) {
36            return self::LINUX;
37        }
38        if (stristr(PHP_OS, 'DARWIN')) {
39            return self::OSX;
40        }
41        if (stristr(PHP_OS, 'WIN')) {
42            return self::WINDOWS;
43        }
44        if (stristr(PHP_OS, 'BSD')) {
45            return self::BSD;
46        }
47        if (stristr(PHP_OS, 'UNIX')) {
48            return self::UNIX;
49        }
50        if (stristr(PHP_OS, 'HP-UX')) {
51            return self::HPUX;
52        }
53        if (stristr(PHP_OS, 'IRIX')) {
54            return self::IRIX;
55        }
56        if (stristr(PHP_OS, 'SUNOS')) {
57            return self::SUNOS;
58        }
59        return false;
60    }
61
62
63    /**
64     * This function retrieves the path to a directory where temporary files can be saved.
65     *
66     * @return string Path to a temporary directory, without a trailing directory separator.
67     * @throws Error\Exception If the temporary directory cannot be created or it exists and does not belong
68     * to the current user.
69     *
70     * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
71     * @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
72     * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
73     */
74    public static function getTempDir()
75    {
76        $globalConfig = Configuration::getInstance();
77
78        $tempDir = rtrim(
79            $globalConfig->getString(
80                'tempdir',
81                sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'simplesaml'
82            ),
83            DIRECTORY_SEPARATOR
84        );
85
86        if (!is_dir($tempDir)) {
87            if (!mkdir($tempDir, 0700, true)) {
88                $error = error_get_last();
89                throw new Error\Exception(
90                    'Error creating temporary directory "' . $tempDir . '": ' .
91                    (is_array($error) ? $error['message'] : 'no error available')
92                );
93            }
94        } elseif (function_exists('posix_getuid')) {
95            // check that the owner of the temp directory is the current user
96            $stat = lstat($tempDir);
97            if ($stat['uid'] !== posix_getuid()) {
98                throw new Error\Exception(
99                    'Temporary directory "' . $tempDir . '" does not belong to the current user.'
100                );
101            }
102        }
103
104        return $tempDir;
105    }
106
107
108    /**
109     * Resolve a (possibly) relative path from the given base path.
110     *
111     * A path which starts with a stream wrapper pattern (e.g. s3://) will not be touched
112     * and returned as is - regardles of the value given as base path.
113     * If it starts with a '/' it is assumed to be absolute, all others are assumed to be
114     * relative. The default base path is the root of the SimpleSAMLphp installation.
115     *
116     * @param string      $path The path we should resolve.
117     * @param string|null $base The base path, where we should search for $path from. Default value is the root of the
118     *     SimpleSAMLphp installation.
119     *
120     * @return string An absolute path referring to $path.
121     *
122     * @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
123     */
124    public static function resolvePath($path, $base = null)
125    {
126        if ($base === null) {
127            $config = Configuration::getInstance();
128            $base = $config->getBaseDir();
129        }
130
131        // normalise directory separator
132        $base = str_replace('\\', '/', $base);
133        $path = str_replace('\\', '/', $path);
134
135        // remove trailing slashes
136        $base = rtrim($base, '/');
137        $path = rtrim($path, '/');
138
139        // check for absolute path
140        if (substr($path, 0, 1) === '/') {
141            // absolute path. */
142            $ret = '/';
143        } elseif (static::pathContainsDriveLetter($path)) {
144            $ret = '';
145        } else {
146            // path relative to base
147            $ret = $base;
148        }
149
150        if (static::pathContainsStreamWrapper($path)) {
151            $ret = $path;
152        } else {
153            $path = explode('/', $path);
154            foreach ($path as $d) {
155                if ($d === '.') {
156                    continue;
157                } elseif ($d === '..') {
158                    $ret = dirname($ret);
159                } else {
160                    if ($ret && substr($ret, -1) !== '/') {
161                        $ret .= '/';
162                    }
163                    $ret .= $d;
164                }
165            }
166        }
167
168        return $ret;
169    }
170
171
172    /**
173     * Atomically write a file.
174     *
175     * This is a helper function for writing data atomically to a file. It does this by writing the file data to a
176     * temporary file, then renaming it to the required file name.
177     *
178     * @param string $filename The path to the file we want to write to.
179     * @param string $data The data we should write to the file.
180     * @param int    $mode The permissions to apply to the file. Defaults to 0600.
181     *
182     * @throws \InvalidArgumentException If any of the input parameters doesn't have the proper types.
183     * @throws Error\Exception If the file cannot be saved, permissions cannot be changed or it is not
184     *     possible to write to the target file.
185     *
186     * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
187     * @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
188     * @author Andjelko Horvat
189     * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
190     *
191     * @return void
192     */
193    public static function writeFile($filename, $data, $mode = 0600)
194    {
195        if (!is_string($filename) || !is_string($data) || !is_numeric($mode)) {
196            throw new \InvalidArgumentException('Invalid input parameters');
197        }
198
199        $tmpFile = self::getTempDir() . DIRECTORY_SEPARATOR . rand();
200
201        $res = @file_put_contents($tmpFile, $data);
202        if ($res === false) {
203            $error = error_get_last();
204            throw new Error\Exception(
205                'Error saving file "' . $tmpFile . '": ' .
206                (is_array($error) ? $error['message'] : 'no error available')
207            );
208        }
209
210        if (self::getOS() !== self::WINDOWS) {
211            if (!chmod($tmpFile, $mode)) {
212                unlink($tmpFile);
213                $error = error_get_last();
214                //$error = (is_array($error) ? $error['message'] : 'no error available');
215                throw new Error\Exception(
216                    'Error changing file mode of "' . $tmpFile . '": ' .
217                    (is_array($error) ? $error['message'] : 'no error available')
218                );
219            }
220        }
221
222        if (!rename($tmpFile, $filename)) {
223            unlink($tmpFile);
224            $error = error_get_last();
225            throw new Error\Exception(
226                'Error moving "' . $tmpFile . '" to "' . $filename . '": ' .
227                (is_array($error) ? $error['message'] : 'no error available')
228            );
229        }
230
231        if (function_exists('opcache_invalidate')) {
232            opcache_invalidate($filename);
233        }
234    }
235
236    /**
237     * Check if the supplied path contains a Windows-style drive letter.
238     *
239     * @param string $path
240     *
241     * @return bool
242     */
243    private static function pathContainsDriveLetter($path)
244    {
245        $letterAsciiValue = ord(strtoupper(substr($path, 0, 1)));
246        return substr($path, 1, 1) === ':'
247                && $letterAsciiValue >= 65 && $letterAsciiValue <= 90;
248    }
249
250    /**
251     * Check if the supplied path contains a stream wrapper
252     * @param string $path
253     * @return bool
254     */
255    private static function pathContainsStreamWrapper($path)
256    {
257        return preg_match('/^[\w\d]*:\/{2}/', $path) === 1;
258    }
259}
260