*clean up comment
[lhc/web/wiklou.git] / includes / filerepo / FSRepo.php
1 <?php
2
3 /**
4 * A repository for files accessible via the local filesystem. Does not support
5 * database access or registration.
6 */
7
8 class FSRepo {
9 const DELETE_SOURCE = 1;
10
11 var $directory, $url, $hashLevels, $thumbScriptUrl, $transformVia404;
12 var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription;
13 var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
14
15 function __construct( $info ) {
16 // Required settings
17 $this->name = $info['name'];
18 $this->directory = $info['directory'];
19 $this->url = $info['url'];
20 $this->hashLevels = $info['hashLevels'];
21 $this->transformVia404 = !empty( $info['transformVia404'] );
22
23 // Optional settings
24 foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
25 'thumbScriptUrl' ) as $var )
26 {
27 if ( isset( $info[$var] ) ) {
28 $this->$var = $info[$var];
29 }
30 }
31 }
32
33 /**
34 * Create a new File object from the local repository
35 * @param mixed $title Title object or string
36 * @param mixed $time Time at which the image is supposed to have existed.
37 * If this is specified, the returned object will be an
38 * instance of the repository's old file class instead of
39 * a current file. Repositories not supporting version
40 * control should return false if this parameter is set.
41 */
42 function newFile( $title, $time = false ) {
43 if ( !($title instanceof Title) ) {
44 $title = Title::makeTitleSafe( NS_IMAGE, $title );
45 if ( !is_object( $title ) ) {
46 return null;
47 }
48 }
49 if ( $time ) {
50 return call_user_func( $this->oldFileFactor, $title, $this, $time );
51 } else {
52 return call_user_func( $this->fileFactory, $title, $this );
53 }
54 }
55
56 /**
57 * Find an instance of the named file that existed at the specified time
58 * Returns false if the file did not exist. Repositories not supporting
59 * version control should return false if the time is specified.
60 *
61 * @param mixed $time 14-character timestamp, or false for the current version
62 */
63 function findFile( $title, $time = false ) {
64 $img = $this->newFile( $title );
65 if ( !$img ) {
66 return false;
67 }
68 if ( $img->exists() && $img->getTimestamp() <= $time ) {
69 return $img;
70 }
71 $img = $this->newFile( $title, $time );
72 if ( $img->exists() ) {
73 return $img;
74 }
75 }
76
77 function getRootDirectory() {
78 return $this->directory;
79 }
80
81 function getRootUrl() {
82 return $this->url;
83 }
84
85 function isHashed() {
86 return (bool)$this->hashLevels;
87 }
88
89 function getThumbScriptUrl() {
90 return $this->thumbScriptUrl;
91 }
92
93 function canTransformVia404() {
94 return $this->transformVia404;
95 }
96
97 function getZonePath( $zone ) {
98 switch ( $zone ) {
99 case 'public':
100 return $this->directory;
101 case 'temp':
102 return "{$this->directory}/temp";
103 case 'deleted':
104 return $GLOBALS['wgFileStore']['deleted']['directory'];
105 default:
106 return false;
107 }
108 }
109
110 function getZoneUrl( $zone ) {
111 switch ( $zone ) {
112 case 'public':
113 return $this->url;
114 case 'temp':
115 return "{$this->url}/temp";
116 case 'deleted':
117 return $GLOBALS['wgFileStore']['deleted']['url'];
118 default:
119 return false;
120 }
121 }
122
123 /**
124 * Get a URL referring to this repository, with the private mwrepo protocol.
125 */
126 function getVirtualUrl( $suffix = false ) {
127 $path = 'mwrepo://';
128 if ( $suffix !== false ) {
129 $path .= '/' . $suffix;
130 }
131 return $path;
132 }
133
134 /**
135 * Get the local path corresponding to a virtual URL
136 */
137 function resolveVirtualUrl( $url ) {
138 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
139 throw new MWException( __METHOD__.': unknown protoocl' );
140 }
141
142 $bits = explode( '/', substr( $url, 9 ), 3 );
143 if ( count( $bits ) != 3 ) {
144 throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
145 }
146 list( $host, $zone, $rel ) = $bits;
147 if ( $host !== '' ) {
148 throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
149 }
150 $base = $this->getZonePath( $zone );
151 if ( !$base ) {
152 throw new MWException( __METHOD__.": invalid zone: $zone" );
153 }
154 return $base . '/' . urldecode( $rel );
155 }
156
157 /**
158 * Store a file to a given destination.
159 */
160 function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
161 $root = $this->getZonePath( $dstZone );
162 if ( !$root ) {
163 throw new MWException( "Invalid zone: $dstZone" );
164 }
165 $dstPath = "$root/$dstRel";
166
167 if ( !is_dir( dirname( $dstPath ) ) ) {
168 wfMkdirParents( dirname( $dstPath ) );
169 }
170
171 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
172 $srcPath = $this->resolveVirtualUrl( $srcPath );
173 }
174
175 if ( $flags & self::DELETE_SOURCE ) {
176 if ( !rename( $srcPath, $dstPath ) ) {
177 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
178 wfEscapeWikiText( $dstPath ) );
179 }
180 } else {
181 if ( !copy( $srcPath, $dstPath ) ) {
182 return new WikiErrorMsg( 'filecopyerror', wfEscapeWikiText( $srcPath ),
183 wfEscapeWikiText( $dstPath ) );
184 }
185 }
186 chmod( $dstPath, 0644 );
187 return true;
188 }
189
190 /**
191 * Pick a random name in the temp zone and store a file to it.
192 * Returns the URL, or a WikiError on failure.
193 * @param string $originalName The base name of the file as specified
194 * by the user. The file extension will be maintained.
195 * @param string $srcPath The current location of the file.
196 */
197 function storeTemp( $originalName, $srcPath ) {
198 $dstRel = $this->getHashPath( $originalName ) .
199 gmdate( "YmdHis" ) . '!' . $originalName;
200 $result = $this->store( $srcPath, 'temp', $dstRel );
201 if ( WikiError::isError( $result ) ) {
202 return $result;
203 } else {
204 return $this->getVirtualUrl( "temp/$dstRel" );
205 }
206 }
207
208 function publish( $srcPath, $dstPath, $archivePath, $flags = 0 ) {
209 if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) {
210 $srcPath = $this->resolveVirtualUrl( $srcPath );
211 }
212 $dstDir = dirname( $dstPath );
213 if ( !is_dir( $dstDir ) ) wfMkdirParents( $dstDir );
214
215 if( is_file( $dstPath ) ) {
216 $archiveDir = dirname( $archivePath );
217 if ( !is_dir( $archiveDir ) ) wfMkdirParents( $archiveDir );
218 wfSuppressWarnings();
219 $success = rename( $dstPath, $archivePath );
220 wfRestoreWarnings();
221
222 if( ! $success ) {
223 return new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $dstPath ),
224 wfEscapeWikiText( $archivePath ) );
225 }
226 else wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n");
227 $status = 'archived';
228 }
229 else {
230 $status = 'new';
231 }
232
233 $error = false;
234 wfSuppressWarnings();
235 if ( $flags & self::DELETE_SOURCE ) {
236 if ( !rename( $srcPath, $dstPath ) ) {
237 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
238 wfEscapeWikiText( $dstPath ) );
239 }
240 } else {
241 if ( !copy( $srcPath, $dstPath ) ) {
242 $error = new WikiErrorMsg( 'filerenameerror', wfEscapeWikiText( $srcPath ),
243 wfEscapeWikiText( $dstPath ) );
244 }
245 }
246 wfRestoreWarnings();
247
248 if( $error ) {
249 return $error;
250 } else {
251 wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
252 }
253
254 chmod( $dstPath, 0644 );
255 return $status;
256 }
257
258 /**
259 * Get a relative path including trailing slash, e.g. f/fa/
260 * If the repo is not hashed, returns an empty string
261 */
262 function getHashPath( $name ) {
263 if ( $this->isHashed() ) {
264 $hash = md5( $name );
265 $path = '';
266 for ( $i = 1; $i <= $this->hashLevels; $i++ ) {
267 $path .= substr( $hash, 0, $i ) . '/';
268 }
269 return $path;
270 } else {
271 return '';
272 }
273 }
274
275 function getName() {
276 return $this->name;
277 }
278
279 /**
280 * Get the file description page base URL, or false if there isn't one.
281 * @private
282 */
283 function getDescBaseUrl() {
284 if ( is_null( $this->descBaseUrl ) ) {
285 if ( !is_null( $this->articleUrl ) ) {
286 $this->descBaseUrl = str_replace( '$1',
287 urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl );
288 } elseif ( !is_null( $this->scriptDirUrl ) ) {
289 $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' .
290 urlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':';
291 } else {
292 $this->descBaseUrl = false;
293 }
294 }
295 return $this->descBaseUrl;
296 }
297
298 /**
299 * Get the URL of an image description page. May return false if it is
300 * unknown or not applicable. In general this should only be called by the
301 * File class, since it may return invalid results for certain kinds of
302 * repositories. Use File::getDescriptionUrl() in user code.
303 *
304 * In particular, it uses the article paths as specified to the repository
305 * constructor, whereas local repositories use the local Title functions.
306 */
307 function getDescriptionUrl( $name ) {
308 $base = $this->getDescBaseUrl();
309 if ( $base ) {
310 return $base . wfUrlencode( $name );
311 } else {
312 return false;
313 }
314 }
315
316 /**
317 * Get the URL of the content-only fragment of the description page. For
318 * MediaWiki this means action=render. This should only be called by the
319 * repository's file class, since it may return invalid results. User code
320 * should use File::getDescriptionText().
321 */
322 function getDescriptionRenderUrl( $name ) {
323 if ( isset( $this->scriptDirUrl ) ) {
324 return $this->scriptDirUrl . '/index.php?title=' .
325 wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) .
326 '&action=render';
327 } else {
328 $descBase = $this->getDescBaseUrl();
329 if ( $descBase ) {
330 return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' );
331 } else {
332 return false;
333 }
334 }
335 }
336
337 /**
338 * Call a callback function for every file in the repository.
339 * Uses the filesystem even in child classes.
340 */
341 function enumFilesInFS( $callback ) {
342 $numDirs = 1 << ( $this->hashLevels * 4 );
343 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
344 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
345 $path = $this->directory;
346 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
347 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
348 }
349 if ( !file_exists( $path ) || !is_dir( $path ) ) {
350 continue;
351 }
352 $dir = opendir( $path );
353 while ( false !== ( $name = readdir( $dir ) ) ) {
354 call_user_func( $callback, $path . '/' . $name );
355 }
356 }
357 }
358
359 /**
360 * Call a callaback function for every file in the repository
361 * May use either the database or the filesystem
362 */
363 function enumFiles( $callback ) {
364 $this->enumFilesInFS( $callback );
365 }
366 }
367
368 ?>