Completed
Push — master ( 6fbb52...2e9214 )
by Carlos C
06:52
created

Locator::getDownloader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
namespace XmlSchemaValidator;
3
4
use XmlSchemaValidator\Downloader\DownloaderInterface;
5
use XmlSchemaValidator\Downloader\PhpDownloader;
6
7
/**
8
 * File locator and cache.
9
 * It provides a file locator and cache of urls.
10
 * Use the Downloader interface to provide different methods to download
11
 *
12
 * @package XmlSchemaValidator
13
 */
14
class Locator
15
{
16
    /**
17
     * Location of the local repository of cached files
18
     * @var string
19
     */
20
    protected $repository;
21
22
    /**
23
     * Seconds to timeout when a file is required
24
     * @var int
25
     */
26
    protected $timeout;
27
28
    /**
29
     * Seconds to know if a cached file is expired
30
     * @var int
31
     */
32
    protected $expire;
33
34
    /**
35
     * Registered urls and file location
36
     * @var array
37
     */
38
    protected $register = [];
39
40
    /**
41
     * @var FileMimeChecker
42
     */
43
    protected $mimeChecker;
44
45
    /** @var DownloaderInterface */
46
    protected $downloader;
47
48
    /**
49
     * @param string $repository Location for place to store the cached files
50
     * @param int $timeout Seconds to timeout when get a file when download is needed
51
     * @param int $expire Seconds to wait to expire cache, a value of 0 means never expires
52
     * @param DownloaderInterface $downloader Downloader object, if null a Downloader\PhpDownloader will be used.
53
     */
54
    public function __construct($repository = '', $timeout = 20, $expire = 0, DownloaderInterface $downloader = null)
55
    {
56
        if ('' === $repository) {
57
            $repository = sys_get_temp_dir();
58
        }
59
        $this->repository = (string) $repository;
60
        $this->timeout = max(1, (integer) $timeout);
61
        $this->expire = max(0, (integer) $expire);
62
        $this->mimeChecker = new FileMimeChecker();
63
        $this->downloader = $downloader ? : new PhpDownloader();
64
    }
65
66
    /**
67
     * Location of the local repository of cached files
68
     * @return string
69
     */
70
    public function getRepository()
71
    {
72
        return $this->repository;
73
    }
74
75
    /**
76
     * Seconds to timeout when a file is required
77
     * @return int
78
     */
79
    public function getTimeout()
80
    {
81
        return $this->timeout;
82
    }
83
84
    /**
85
     * Seconds to know if a cached file is expired
86
     * @return int
87
     */
88
    public function getExpire()
89
    {
90
        return $this->expire;
91
    }
92
93
    /**
94
     * @return DownloaderInterface
95
     */
96
    public function getDownloader()
97
    {
98
        return $this->downloader;
99
    }
100
101
    /**
102
     * Return a filename for a given URL based on the registry.
103
     * If the file is not registered then it is downloaded and stored in the repository location
104
     * @param string $url
105
     * @return string
106
     */
107
    public function get($url)
108
    {
109
        $this->assertUrlIsValid($url);
110
        // get the file or refresh the cache
111
        $filename = $this->cacheFileName($url);
112
        // register if not previously registered
113
        if (! $this->registered($url) || $this->register[$url] == $filename) {
114
            $filename = $this->cache($url);
115
            $this->register($url, $filename);
116
        }
117
        return $this->register[$url];
118
    }
119
120
    /**
121
     * Build a unique name for a url including the repository
122
     * @param string $url
123
     * @return string
124
     */
125
    public function cacheFileName($url)
126
    {
127
        $this->assertUrlIsValid($url);
128
        return $this->repository . DIRECTORY_SEPARATOR . 'cache-' . md5(strtolower($url));
129
    }
130
131
    /**
132
     * Register a url with a file without download it. However the file must exists and be readable.
133
     * @param string $url
134
     * @param string $filename
135
     */
136
    public function register($url, $filename)
137
    {
138
        $this->assertUrlIsValid($url);
139
        $mimetype = $this->mimeChecker->getMimeType($filename);
140
        if ('' === $mimetype) {
141
            throw new \RuntimeException("File $filename does not exists or is not readable");
142
        }
143
        if (! $this->mimeChecker->checkMime($mimetype)) {
144
            throw new \RuntimeException("File $filename is not a valid mime type");
145
        }
146
        $this->register[$url] = $filename;
147
    }
148
149
    /**
150
     * Unregister a url from the cache
151
     * @param string $url
152
     */
153
    public function unregister($url)
154
    {
155
        unset($this->register[(string) $url]);
156
    }
157
158
    /**
159
     * Return a copy of the registry
160
     * @return array
161
     */
162
    public function registry()
163
    {
164
        return $this->register;
165
    }
166
167
    /**
168
     * Return if a given url exists in the registry
169
     * @param string $url
170
     * @return bool
171
     */
172
    public function registered($url)
173
    {
174
        return array_key_exists($url, $this->register);
175
    }
176
177
    /**
178
     * Return the filename of an url, of needs to download a new copy then
179
     * it try to download, validate the mime of the downloaded file and place the file on the repository
180
     * @param string $url
181
     * @return string
182
     */
183
    protected function cache($url)
184
    {
185
        // get the filename
186
        $filename = $this->cacheFileName($url);
187
        // if no need to download then return
188
        if (! $this->needToDownload($filename)) {
189
            return $filename;
190
        }
191
        // download the file and set into a temporary file
192
        $temporal = $this->downloader->download($url, $this->getTimeout());
193
        if (! $this->mimeIsAllowed($temporal)) {
194
            unlink($temporal);
195
            throw new \RuntimeException("Downloaded file from $url is not a valid mime");
196
        }
197
        // move temporal to final destination
198
        // if $filename exists, it will be overwritten.
199
        if (! @rename($temporal, $filename)) {
200
            unlink($temporal);
201
            throw new \RuntimeException("Cannot move the temporary file to $filename");
202
        }
203
        // return the filename
204
        return $filename;
205
    }
206
207
    /**
208
     * append a mime to the list of mimes allowed
209
     * @param string $mime
210
     */
211
    public function mimeAllow($mime)
212
    {
213
        $this->mimeChecker->add($mime);
214
    }
215
216
    /**
217
     * Remove a mime to the list of mimes allowed
218
     * NOTE: This method does not affect previously registered urls
219
     * @param string $mime
220
     */
221
    public function mimeDisallow($mime)
222
    {
223
        $this->mimeChecker->remove($mime);
224
    }
225
226
    /**
227
     * return the list of allowed mimes
228
     * @return bool
229
     */
230
    public function mimeList()
231
    {
232
        return $this->mimeChecker->all();
233
    }
234
235
    /**
236
     * check if a the mime of a file is allowed
237
     *
238
     * @param string $filename path to the file
239
     * @return bool
240
     */
241
    public function mimeIsAllowed($filename)
242
    {
243
        return $this->mimeChecker->check($filename);
244
    }
245
246
    /**
247
     * Internal function to assert if URL is valid, if not throw an exception
248
     * @param string $url
249
     * @throws \RuntimeException
250
     */
251
    private function assertUrlIsValid($url)
252
    {
253
        if (empty($url)) {
254
            throw new \RuntimeException('Url (empty) is not valid');
255
        }
256
        if (! filter_var($url, FILTER_VALIDATE_URL)) {
257
            throw new \RuntimeException("Url $url is not valid");
258
        }
259
    }
260
261
    /**
262
     * Rules to determine if the file needs to be downloaded:
263
     * 1. file does not exists
264
     * 2. files do not expire
265
     * 3. file is expired
266
     * @param string $filename
267
     * @return bool
268
     */
269
    protected function needToDownload($filename)
270
    {
271
        // the file does not exists -> yes
272
        if (! file_exists($filename)) {
273
            return true;
274
        }
275
        // the files stored never expire -> no need
276
        if ($this->expire <= 0) {
277
            return false;
278
        }
279
        // if aging of the file is more than the expiration then need to refresh
280
        clearstatcache(false, $filename);
281
        if (time() - filemtime($filename) > $this->expire) {
282
            return true;
283
        }
284
        // no need to expire
285
        return false;
286
    }
287
}
288