r103403 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r103402‎ | r103403 | r103404 >
Date:22:55, 16 November 2011
Author:aaron
Status:ok
Tags:
Comment:
Moved File classes to filerepo/file (as well as ArchivedFile)
Modified paths:
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/filerepo/ArchivedFile.php (deleted) (history)
  • /trunk/phase3/includes/filerepo/File.php (deleted) (history)
  • /trunk/phase3/includes/filerepo/ForeignAPIFile.php (deleted) (history)
  • /trunk/phase3/includes/filerepo/ForeignDBFile.php (deleted) (history)
  • /trunk/phase3/includes/filerepo/LocalFile.php (deleted) (history)
  • /trunk/phase3/includes/filerepo/OldLocalFile.php (deleted) (history)
  • /trunk/phase3/includes/filerepo/UnregisteredLocalFile.php (deleted) (history)
  • /trunk/phase3/includes/filerepo/file (added) (history)
  • /trunk/phase3/includes/filerepo/file/ArchivedFile.php (added) (history)
  • /trunk/phase3/includes/filerepo/file/File.php (added) (history)
  • /trunk/phase3/includes/filerepo/file/ForeignAPIFile.php (added) (history)
  • /trunk/phase3/includes/filerepo/file/ForeignDBFile.php (added) (history)
  • /trunk/phase3/includes/filerepo/file/LocalFile.php (added) (history)
  • /trunk/phase3/includes/filerepo/file/OldLocalFile.php (added) (history)
  • /trunk/phase3/includes/filerepo/file/UnregisteredLocalFile.php (added) (history)

Diff [purge]

Index: trunk/phase3/includes/filerepo/OldLocalFile.php
@@ -1,295 +0,0 @@
2 -<?php
3 -/**
4 - * Old file in the oldimage table
5 - *
6 - * @file
7 - * @ingroup FileRepo
8 - */
9 -
10 -/**
11 - * Class to represent a file in the oldimage table
12 - *
13 - * @ingroup FileRepo
14 - */
15 -class OldLocalFile extends LocalFile {
16 - var $requestedTime, $archive_name;
17 -
18 - const CACHE_VERSION = 1;
19 - const MAX_CACHE_ROWS = 20;
20 -
21 - static function newFromTitle( $title, $repo, $time = null ) {
22 - # The null default value is only here to avoid an E_STRICT
23 - if ( $time === null ) {
24 - throw new MWException( __METHOD__.' got null for $time parameter' );
25 - }
26 - return new self( $title, $repo, $time, null );
27 - }
28 -
29 - static function newFromArchiveName( $title, $repo, $archiveName ) {
30 - return new self( $title, $repo, null, $archiveName );
31 - }
32 -
33 - static function newFromRow( $row, $repo ) {
34 - $title = Title::makeTitle( NS_FILE, $row->oi_name );
35 - $file = new self( $title, $repo, null, $row->oi_archive_name );
36 - $file->loadFromRow( $row, 'oi_' );
37 - return $file;
38 - }
39 -
40 - /**
41 - * Create a OldLocalFile from a SHA-1 key
42 - * Do not call this except from inside a repo class.
43 - *
44 - * @param $sha1 string base-36 SHA-1
45 - * @param $repo LocalRepo
46 - * @param string|bool $timestamp MW_timestamp (optional)
47 - *
48 - * @return bool|OldLocalFile
49 - */
50 - static function newFromKey( $sha1, $repo, $timestamp = false ) {
51 - $dbr = $repo->getSlaveDB();
52 -
53 - $conds = array( 'oi_sha1' => $sha1 );
54 - if ( $timestamp ) {
55 - $conds['oi_timestamp'] = $dbr->timestamp( $timestamp );
56 - }
57 -
58 - $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ );
59 - if ( $row ) {
60 - return self::newFromRow( $row, $repo );
61 - } else {
62 - return false;
63 - }
64 - }
65 -
66 - /**
67 - * Fields in the oldimage table
68 - */
69 - static function selectFields() {
70 - return array(
71 - 'oi_name',
72 - 'oi_archive_name',
73 - 'oi_size',
74 - 'oi_width',
75 - 'oi_height',
76 - 'oi_metadata',
77 - 'oi_bits',
78 - 'oi_media_type',
79 - 'oi_major_mime',
80 - 'oi_minor_mime',
81 - 'oi_description',
82 - 'oi_user',
83 - 'oi_user_text',
84 - 'oi_timestamp',
85 - 'oi_deleted',
86 - 'oi_sha1',
87 - );
88 - }
89 -
90 - /**
91 - * @param $title Title
92 - * @param $repo FileRepo
93 - * @param $time String: timestamp or null to load by archive name
94 - * @param $archiveName String: archive name or null to load by timestamp
95 - */
96 - function __construct( $title, $repo, $time, $archiveName ) {
97 - parent::__construct( $title, $repo );
98 - $this->requestedTime = $time;
99 - $this->archive_name = $archiveName;
100 - if ( is_null( $time ) && is_null( $archiveName ) ) {
101 - throw new MWException( __METHOD__.': must specify at least one of $time or $archiveName' );
102 - }
103 - }
104 -
105 - function getCacheKey() {
106 - return false;
107 - }
108 -
109 - function getArchiveName() {
110 - if ( !isset( $this->archive_name ) ) {
111 - $this->load();
112 - }
113 - return $this->archive_name;
114 - }
115 -
116 - function isOld() {
117 - return true;
118 - }
119 -
120 - function isVisible() {
121 - return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
122 - }
123 -
124 - function loadFromDB() {
125 - wfProfileIn( __METHOD__ );
126 - $this->dataLoaded = true;
127 - $dbr = $this->repo->getSlaveDB();
128 - $conds = array( 'oi_name' => $this->getName() );
129 - if ( is_null( $this->requestedTime ) ) {
130 - $conds['oi_archive_name'] = $this->archive_name;
131 - } else {
132 - $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) );
133 - }
134 - $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
135 - $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
136 - if ( $row ) {
137 - $this->loadFromRow( $row, 'oi_' );
138 - } else {
139 - $this->fileExists = false;
140 - }
141 - wfProfileOut( __METHOD__ );
142 - }
143 -
144 - function getCacheFields( $prefix = 'img_' ) {
145 - $fields = parent::getCacheFields( $prefix );
146 - $fields[] = $prefix . 'archive_name';
147 - $fields[] = $prefix . 'deleted';
148 - return $fields;
149 - }
150 -
151 - function getRel() {
152 - return 'archive/' . $this->getHashPath() . $this->getArchiveName();
153 - }
154 -
155 - function getUrlRel() {
156 - return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() );
157 - }
158 -
159 - function upgradeRow() {
160 - wfProfileIn( __METHOD__ );
161 - $this->loadFromFile();
162 -
163 - # Don't destroy file info of missing files
164 - if ( !$this->fileExists ) {
165 - wfDebug( __METHOD__.": file does not exist, aborting\n" );
166 - wfProfileOut( __METHOD__ );
167 - return;
168 - }
169 -
170 - $dbw = $this->repo->getMasterDB();
171 - list( $major, $minor ) = self::splitMime( $this->mime );
172 -
173 - wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n");
174 - $dbw->update( 'oldimage',
175 - array(
176 - 'oi_width' => $this->width,
177 - 'oi_height' => $this->height,
178 - 'oi_bits' => $this->bits,
179 - 'oi_media_type' => $this->media_type,
180 - 'oi_major_mime' => $major,
181 - 'oi_minor_mime' => $minor,
182 - 'oi_metadata' => $this->metadata,
183 - 'oi_sha1' => $this->sha1,
184 - ), array(
185 - 'oi_name' => $this->getName(),
186 - 'oi_archive_name' => $this->archive_name ),
187 - __METHOD__
188 - );
189 - wfProfileOut( __METHOD__ );
190 - }
191 -
192 - /**
193 - * @param $field Integer: one of DELETED_* bitfield constants
194 - * for file or revision rows
195 - * @return bool
196 - */
197 - function isDeleted( $field ) {
198 - $this->load();
199 - return ($this->deleted & $field) == $field;
200 - }
201 -
202 - /**
203 - * Returns bitfield value
204 - * @return int
205 - */
206 - function getVisibility() {
207 - $this->load();
208 - return (int)$this->deleted;
209 - }
210 -
211 - /**
212 - * Determine if the current user is allowed to view a particular
213 - * field of this image file, if it's marked as deleted.
214 - *
215 - * @param $field Integer
216 - * @param $user User object to check, or null to use $wgUser
217 - * @return bool
218 - */
219 - function userCan( $field, User $user = null ) {
220 - $this->load();
221 - return Revision::userCanBitfield( $this->deleted, $field, $user );
222 - }
223 -
224 - /**
225 - * Upload a file directly into archive. Generally for Special:Import.
226 - *
227 - * @param $srcPath string File system path of the source file
228 - * @param $archiveName string Full archive name of the file, in the form
229 - * $timestamp!$filename, where $filename must match $this->getName()
230 - *
231 - * @return FileRepoStatus
232 - */
233 - function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user, $flags = 0 ) {
234 - $this->lock();
235 -
236 - $dstRel = 'archive/' . $this->getHashPath() . $archiveName;
237 - $status = $this->publishTo( $srcPath, $dstRel,
238 - $flags & File::DELETE_SOURCE ? FileRepo::DELETE_SOURCE : 0
239 - );
240 -
241 - if ( $status->isGood() ) {
242 - if ( !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) ) {
243 - $status->fatal( 'filenotfound', $srcPath );
244 - }
245 - }
246 -
247 - $this->unlock();
248 -
249 - return $status;
250 - }
251 -
252 - /**
253 - * Record a file upload in the oldimage table, without adding log entries.
254 - *
255 - * @param $srcPath string File system path to the source file
256 - * @param $archiveName string The archive name of the file
257 - * @param $comment string Upload comment
258 - * @param $user User User who did this upload
259 - * @return bool
260 - */
261 - function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) {
262 - $dbw = $this->repo->getMasterDB();
263 - $dbw->begin();
264 -
265 - $dstPath = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
266 - $props = self::getPropsFromPath( $dstPath );
267 - if ( !$props['fileExists'] ) {
268 - return false;
269 - }
270 -
271 - $dbw->insert( 'oldimage',
272 - array(
273 - 'oi_name' => $this->getName(),
274 - 'oi_archive_name' => $archiveName,
275 - 'oi_size' => $props['size'],
276 - 'oi_width' => intval( $props['width'] ),
277 - 'oi_height' => intval( $props['height'] ),
278 - 'oi_bits' => $props['bits'],
279 - 'oi_timestamp' => $dbw->timestamp( $timestamp ),
280 - 'oi_description' => $comment,
281 - 'oi_user' => $user->getId(),
282 - 'oi_user_text' => $user->getName(),
283 - 'oi_metadata' => $props['metadata'],
284 - 'oi_media_type' => $props['media_type'],
285 - 'oi_major_mime' => $props['major_mime'],
286 - 'oi_minor_mime' => $props['minor_mime'],
287 - 'oi_sha1' => $props['sha1'],
288 - ), __METHOD__
289 - );
290 -
291 - $dbw->commit();
292 -
293 - return true;
294 - }
295 -
296 -}
Index: trunk/phase3/includes/filerepo/LocalFile.php
@@ -1,2323 +0,0 @@
2 -<?php
3 -/**
4 - * Local file in the wiki's own database
5 - *
6 - * @file
7 - * @ingroup FileRepo
8 - */
9 -
10 -/**
11 - * Bump this number when serialized cache records may be incompatible.
12 - */
13 -define( 'MW_FILE_VERSION', 8 );
14 -
15 -/**
16 - * Class to represent a local file in the wiki's own database
17 - *
18 - * Provides methods to retrieve paths (physical, logical, URL),
19 - * to generate image thumbnails or for uploading.
20 - *
21 - * Note that only the repo object knows what its file class is called. You should
22 - * never name a file class explictly outside of the repo class. Instead use the
23 - * repo's factory functions to generate file objects, for example:
24 - *
25 - * RepoGroup::singleton()->getLocalRepo()->newFile($title);
26 - *
27 - * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
28 - * in most cases.
29 - *
30 - * @ingroup FileRepo
31 - */
32 -class LocalFile extends File {
33 - /**#@+
34 - * @private
35 - */
36 - var
37 - $fileExists, # does the file exist on disk? (loadFromXxx)
38 - $historyLine, # Number of line to return by nextHistoryLine() (constructor)
39 - $historyRes, # result of the query for the file's history (nextHistoryLine)
40 - $width, # \
41 - $height, # |
42 - $bits, # --- returned by getimagesize (loadFromXxx)
43 - $attr, # /
44 - $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
45 - $mime, # MIME type, determined by MimeMagic::guessMimeType
46 - $major_mime, # Major mime type
47 - $minor_mime, # Minor mime type
48 - $size, # Size in bytes (loadFromXxx)
49 - $metadata, # Handler-specific metadata
50 - $timestamp, # Upload timestamp
51 - $sha1, # SHA-1 base 36 content hash
52 - $user, $user_text, # User, who uploaded the file
53 - $description, # Description of current revision of the file
54 - $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
55 - $upgraded, # Whether the row was upgraded on load
56 - $locked, # True if the image row is locked
57 - $missing, # True if file is not present in file system. Not to be cached in memcached
58 - $deleted; # Bitfield akin to rev_deleted
59 -
60 - /**#@-*/
61 -
62 - protected $repoClass = 'LocalRepo';
63 -
64 - /**
65 - * Create a LocalFile from a title
66 - * Do not call this except from inside a repo class.
67 - *
68 - * Note: $unused param is only here to avoid an E_STRICT
69 - *
70 - * @param $title
71 - * @param $repo
72 - * @param $unused
73 - *
74 - * @return LocalFile
75 - */
76 - static function newFromTitle( $title, $repo, $unused = null ) {
77 - return new self( $title, $repo );
78 - }
79 -
80 - /**
81 - * Create a LocalFile from a title
82 - * Do not call this except from inside a repo class.
83 - *
84 - * @param $row
85 - * @param $repo
86 - *
87 - * @return LocalFile
88 - */
89 - static function newFromRow( $row, $repo ) {
90 - $title = Title::makeTitle( NS_FILE, $row->img_name );
91 - $file = new self( $title, $repo );
92 - $file->loadFromRow( $row );
93 -
94 - return $file;
95 - }
96 -
97 - /**
98 - * Create a LocalFile from a SHA-1 key
99 - * Do not call this except from inside a repo class.
100 - *
101 - * @param $sha1 string base-36 SHA-1
102 - * @param $repo LocalRepo
103 - * @param string|bool $timestamp MW_timestamp (optional)
104 - *
105 - * @return bool|LocalFile
106 - */
107 - static function newFromKey( $sha1, $repo, $timestamp = false ) {
108 - $dbr = $repo->getSlaveDB();
109 -
110 - $conds = array( 'img_sha1' => $sha1 );
111 - if ( $timestamp ) {
112 - $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
113 - }
114 -
115 - $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
116 - if ( $row ) {
117 - return self::newFromRow( $row, $repo );
118 - } else {
119 - return false;
120 - }
121 - }
122 -
123 - /**
124 - * Fields in the image table
125 - */
126 - static function selectFields() {
127 - return array(
128 - 'img_name',
129 - 'img_size',
130 - 'img_width',
131 - 'img_height',
132 - 'img_metadata',
133 - 'img_bits',
134 - 'img_media_type',
135 - 'img_major_mime',
136 - 'img_minor_mime',
137 - 'img_description',
138 - 'img_user',
139 - 'img_user_text',
140 - 'img_timestamp',
141 - 'img_sha1',
142 - );
143 - }
144 -
145 - /**
146 - * Constructor.
147 - * Do not call this except from inside a repo class.
148 - */
149 - function __construct( $title, $repo ) {
150 - parent::__construct( $title, $repo );
151 -
152 - $this->metadata = '';
153 - $this->historyLine = 0;
154 - $this->historyRes = null;
155 - $this->dataLoaded = false;
156 -
157 - $this->assertRepoDefined();
158 - $this->assertTitleDefined();
159 - }
160 -
161 - /**
162 - * Get the memcached key for the main data for this file, or false if
163 - * there is no access to the shared cache.
164 - */
165 - function getCacheKey() {
166 - $hashedName = md5( $this->getName() );
167 -
168 - return $this->repo->getSharedCacheKey( 'file', $hashedName );
169 - }
170 -
171 - /**
172 - * Try to load file metadata from memcached. Returns true on success.
173 - */
174 - function loadFromCache() {
175 - global $wgMemc;
176 -
177 - wfProfileIn( __METHOD__ );
178 - $this->dataLoaded = false;
179 - $key = $this->getCacheKey();
180 -
181 - if ( !$key ) {
182 - wfProfileOut( __METHOD__ );
183 - return false;
184 - }
185 -
186 - $cachedValues = $wgMemc->get( $key );
187 -
188 - // Check if the key existed and belongs to this version of MediaWiki
189 - if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
190 - wfDebug( "Pulling file metadata from cache key $key\n" );
191 - $this->fileExists = $cachedValues['fileExists'];
192 - if ( $this->fileExists ) {
193 - $this->setProps( $cachedValues );
194 - }
195 - $this->dataLoaded = true;
196 - }
197 -
198 - if ( $this->dataLoaded ) {
199 - wfIncrStats( 'image_cache_hit' );
200 - } else {
201 - wfIncrStats( 'image_cache_miss' );
202 - }
203 -
204 - wfProfileOut( __METHOD__ );
205 - return $this->dataLoaded;
206 - }
207 -
208 - /**
209 - * Save the file metadata to memcached
210 - */
211 - function saveToCache() {
212 - global $wgMemc;
213 -
214 - $this->load();
215 - $key = $this->getCacheKey();
216 -
217 - if ( !$key ) {
218 - return;
219 - }
220 -
221 - $fields = $this->getCacheFields( '' );
222 - $cache = array( 'version' => MW_FILE_VERSION );
223 - $cache['fileExists'] = $this->fileExists;
224 -
225 - if ( $this->fileExists ) {
226 - foreach ( $fields as $field ) {
227 - $cache[$field] = $this->$field;
228 - }
229 - }
230 -
231 - $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
232 - }
233 -
234 - /**
235 - * Load metadata from the file itself
236 - */
237 - function loadFromFile() {
238 - $this->setProps( self::getPropsFromPath( $this->getPath() ) );
239 - }
240 -
241 - function getCacheFields( $prefix = 'img_' ) {
242 - static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
243 - 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
244 - static $results = array();
245 -
246 - if ( $prefix == '' ) {
247 - return $fields;
248 - }
249 -
250 - if ( !isset( $results[$prefix] ) ) {
251 - $prefixedFields = array();
252 - foreach ( $fields as $field ) {
253 - $prefixedFields[] = $prefix . $field;
254 - }
255 - $results[$prefix] = $prefixedFields;
256 - }
257 -
258 - return $results[$prefix];
259 - }
260 -
261 - /**
262 - * Load file metadata from the DB
263 - */
264 - function loadFromDB() {
265 - # Polymorphic function name to distinguish foreign and local fetches
266 - $fname = get_class( $this ) . '::' . __FUNCTION__;
267 - wfProfileIn( $fname );
268 -
269 - # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
270 - $this->dataLoaded = true;
271 -
272 - $dbr = $this->repo->getMasterDB();
273 -
274 - $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
275 - array( 'img_name' => $this->getName() ), $fname );
276 -
277 - if ( $row ) {
278 - $this->loadFromRow( $row );
279 - } else {
280 - $this->fileExists = false;
281 - }
282 -
283 - wfProfileOut( $fname );
284 - }
285 -
286 - /**
287 - * Decode a row from the database (either object or array) to an array
288 - * with timestamps and MIME types decoded, and the field prefix removed.
289 - */
290 - function decodeRow( $row, $prefix = 'img_' ) {
291 - $array = (array)$row;
292 - $prefixLength = strlen( $prefix );
293 -
294 - // Sanity check prefix once
295 - if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
296 - throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
297 - }
298 -
299 - $decoded = array();
300 -
301 - foreach ( $array as $name => $value ) {
302 - $decoded[substr( $name, $prefixLength )] = $value;
303 - }
304 -
305 - $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
306 -
307 - if ( empty( $decoded['major_mime'] ) ) {
308 - $decoded['mime'] = 'unknown/unknown';
309 - } else {
310 - if ( !$decoded['minor_mime'] ) {
311 - $decoded['minor_mime'] = 'unknown';
312 - }
313 - $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
314 - }
315 -
316 - # Trim zero padding from char/binary field
317 - $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
318 -
319 - return $decoded;
320 - }
321 -
322 - /**
323 - * Load file metadata from a DB result row
324 - */
325 - function loadFromRow( $row, $prefix = 'img_' ) {
326 - $this->dataLoaded = true;
327 - $array = $this->decodeRow( $row, $prefix );
328 -
329 - foreach ( $array as $name => $value ) {
330 - $this->$name = $value;
331 - }
332 -
333 - $this->fileExists = true;
334 - $this->maybeUpgradeRow();
335 - }
336 -
337 - /**
338 - * Load file metadata from cache or DB, unless already loaded
339 - */
340 - function load() {
341 - if ( !$this->dataLoaded ) {
342 - if ( !$this->loadFromCache() ) {
343 - $this->loadFromDB();
344 - $this->saveToCache();
345 - }
346 - $this->dataLoaded = true;
347 - }
348 - }
349 -
350 - /**
351 - * Upgrade a row if it needs it
352 - */
353 - function maybeUpgradeRow() {
354 - global $wgUpdateCompatibleMetadata;
355 - if ( wfReadOnly() ) {
356 - return;
357 - }
358 -
359 - if ( is_null( $this->media_type ) ||
360 - $this->mime == 'image/svg'
361 - ) {
362 - $this->upgradeRow();
363 - $this->upgraded = true;
364 - } else {
365 - $handler = $this->getHandler();
366 - if ( $handler ) {
367 - $validity = $handler->isMetadataValid( $this, $this->metadata );
368 - if ( $validity === MediaHandler::METADATA_BAD
369 - || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
370 - ) {
371 - $this->upgradeRow();
372 - $this->upgraded = true;
373 - }
374 - }
375 - }
376 - }
377 -
378 - function getUpgraded() {
379 - return $this->upgraded;
380 - }
381 -
382 - /**
383 - * Fix assorted version-related problems with the image row by reloading it from the file
384 - */
385 - function upgradeRow() {
386 - wfProfileIn( __METHOD__ );
387 -
388 - $this->loadFromFile();
389 -
390 - # Don't destroy file info of missing files
391 - if ( !$this->fileExists ) {
392 - wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
393 - wfProfileOut( __METHOD__ );
394 - return;
395 - }
396 -
397 - $dbw = $this->repo->getMasterDB();
398 - list( $major, $minor ) = self::splitMime( $this->mime );
399 -
400 - if ( wfReadOnly() ) {
401 - wfProfileOut( __METHOD__ );
402 - return;
403 - }
404 - wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
405 -
406 - $dbw->update( 'image',
407 - array(
408 - 'img_width' => $this->width,
409 - 'img_height' => $this->height,
410 - 'img_bits' => $this->bits,
411 - 'img_media_type' => $this->media_type,
412 - 'img_major_mime' => $major,
413 - 'img_minor_mime' => $minor,
414 - 'img_metadata' => $this->metadata,
415 - 'img_sha1' => $this->sha1,
416 - ), array( 'img_name' => $this->getName() ),
417 - __METHOD__
418 - );
419 -
420 - $this->saveToCache();
421 - wfProfileOut( __METHOD__ );
422 - }
423 -
424 - /**
425 - * Set properties in this object to be equal to those given in the
426 - * associative array $info. Only cacheable fields can be set.
427 - *
428 - * If 'mime' is given, it will be split into major_mime/minor_mime.
429 - * If major_mime/minor_mime are given, $this->mime will also be set.
430 - */
431 - function setProps( $info ) {
432 - $this->dataLoaded = true;
433 - $fields = $this->getCacheFields( '' );
434 - $fields[] = 'fileExists';
435 -
436 - foreach ( $fields as $field ) {
437 - if ( isset( $info[$field] ) ) {
438 - $this->$field = $info[$field];
439 - }
440 - }
441 -
442 - // Fix up mime fields
443 - if ( isset( $info['major_mime'] ) ) {
444 - $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
445 - } elseif ( isset( $info['mime'] ) ) {
446 - $this->mime = $info['mime'];
447 - list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
448 - }
449 - }
450 -
451 - /** splitMime inherited */
452 - /** getName inherited */
453 - /** getTitle inherited */
454 - /** getURL inherited */
455 - /** getViewURL inherited */
456 - /** getPath inherited */
457 - /** isVisible inhereted */
458 -
459 - function isMissing() {
460 - if ( $this->missing === null ) {
461 - list( $fileExists ) = $this->repo->fileExistsBatch( array( $this->getVirtualUrl() ), FileRepo::FILES_ONLY );
462 - $this->missing = !$fileExists;
463 - }
464 - return $this->missing;
465 - }
466 -
467 - /**
468 - * Return the width of the image
469 - *
470 - * Returns false on error
471 - */
472 - public function getWidth( $page = 1 ) {
473 - $this->load();
474 -
475 - if ( $this->isMultipage() ) {
476 - $dim = $this->getHandler()->getPageDimensions( $this, $page );
477 - if ( $dim ) {
478 - return $dim['width'];
479 - } else {
480 - return false;
481 - }
482 - } else {
483 - return $this->width;
484 - }
485 - }
486 -
487 - /**
488 - * Return the height of the image
489 - *
490 - * Returns false on error
491 - */
492 - public function getHeight( $page = 1 ) {
493 - $this->load();
494 -
495 - if ( $this->isMultipage() ) {
496 - $dim = $this->getHandler()->getPageDimensions( $this, $page );
497 - if ( $dim ) {
498 - return $dim['height'];
499 - } else {
500 - return false;
501 - }
502 - } else {
503 - return $this->height;
504 - }
505 - }
506 -
507 - /**
508 - * Returns ID or name of user who uploaded the file
509 - *
510 - * @param $type string 'text' or 'id'
511 - */
512 - function getUser( $type = 'text' ) {
513 - $this->load();
514 -
515 - if ( $type == 'text' ) {
516 - return $this->user_text;
517 - } elseif ( $type == 'id' ) {
518 - return $this->user;
519 - }
520 - }
521 -
522 - /**
523 - * Get handler-specific metadata
524 - */
525 - function getMetadata() {
526 - $this->load();
527 - return $this->metadata;
528 - }
529 -
530 - function getBitDepth() {
531 - $this->load();
532 - return $this->bits;
533 - }
534 -
535 - /**
536 - * Return the size of the image file, in bytes
537 - */
538 - public function getSize() {
539 - $this->load();
540 - return $this->size;
541 - }
542 -
543 - /**
544 - * Returns the mime type of the file.
545 - */
546 - function getMimeType() {
547 - $this->load();
548 - return $this->mime;
549 - }
550 -
551 - /**
552 - * Return the type of the media in the file.
553 - * Use the value returned by this function with the MEDIATYPE_xxx constants.
554 - */
555 - function getMediaType() {
556 - $this->load();
557 - return $this->media_type;
558 - }
559 -
560 - /** canRender inherited */
561 - /** mustRender inherited */
562 - /** allowInlineDisplay inherited */
563 - /** isSafeFile inherited */
564 - /** isTrustedFile inherited */
565 -
566 - /**
567 - * Returns true if the file exists on disk.
568 - * @return boolean Whether file exist on disk.
569 - */
570 - public function exists() {
571 - $this->load();
572 - return $this->fileExists;
573 - }
574 -
575 - /** getTransformScript inherited */
576 - /** getUnscaledThumb inherited */
577 - /** thumbName inherited */
578 - /** createThumb inherited */
579 - /** transform inherited */
580 -
581 - /**
582 - * Fix thumbnail files from 1.4 or before, with extreme prejudice
583 - */
584 - function migrateThumbFile( $thumbName ) {
585 - $thumbDir = $this->getThumbPath();
586 - $thumbPath = "$thumbDir/$thumbName";
587 -
588 - if ( is_dir( $thumbPath ) ) {
589 - // Directory where file should be
590 - // This happened occasionally due to broken migration code in 1.5
591 - // Rename to broken-*
592 - for ( $i = 0; $i < 100 ; $i++ ) {
593 - $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
594 - if ( !file_exists( $broken ) ) {
595 - rename( $thumbPath, $broken );
596 - break;
597 - }
598 - }
599 - // Doesn't exist anymore
600 - clearstatcache();
601 - }
602 -
603 - if ( is_file( $thumbDir ) ) {
604 - // File where directory should be
605 - unlink( $thumbDir );
606 - // Doesn't exist anymore
607 - clearstatcache();
608 - }
609 - }
610 -
611 - /** getHandler inherited */
612 - /** iconThumb inherited */
613 - /** getLastError inherited */
614 -
615 - /**
616 - * Get all thumbnail names previously generated for this file
617 - * @param $archiveName string|false Name of an archive file
618 - * @return array first element is the base dir, then files in that base dir.
619 - */
620 - function getThumbnails( $archiveName = false ) {
621 - $this->load();
622 -
623 - if ( $archiveName ) {
624 - $dir = $this->getArchiveThumbPath( $archiveName );
625 - } else {
626 - $dir = $this->getThumbPath();
627 - }
628 - $files = array();
629 - $files[] = $dir;
630 -
631 - if ( is_dir( $dir ) ) {
632 - $handle = opendir( $dir );
633 -
634 - if ( $handle ) {
635 - while ( false !== ( $file = readdir( $handle ) ) ) {
636 - if ( $file { 0 } != '.' ) {
637 - $files[] = $file;
638 - }
639 - }
640 -
641 - closedir( $handle );
642 - }
643 - }
644 -
645 - return $files;
646 - }
647 -
648 - /**
649 - * Refresh metadata in memcached, but don't touch thumbnails or squid
650 - */
651 - function purgeMetadataCache() {
652 - $this->loadFromDB();
653 - $this->saveToCache();
654 - $this->purgeHistory();
655 - }
656 -
657 - /**
658 - * Purge the shared history (OldLocalFile) cache
659 - */
660 - function purgeHistory() {
661 - global $wgMemc;
662 -
663 - $hashedName = md5( $this->getName() );
664 - $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
665 -
666 - // Must purge thumbnails for old versions too! bug 30192
667 - foreach( $this->getHistory() as $oldFile ) {
668 - $oldFile->purgeThumbnails();
669 - }
670 -
671 - if ( $oldKey ) {
672 - $wgMemc->delete( $oldKey );
673 - }
674 - }
675 -
676 - /**
677 - * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
678 - */
679 - function purgeCache() {
680 - // Refresh metadata cache
681 - $this->purgeMetadataCache();
682 -
683 - // Delete thumbnails
684 - $this->purgeThumbnails();
685 -
686 - // Purge squid cache for this file
687 - SquidUpdate::purge( array( $this->getURL() ) );
688 - }
689 -
690 - /**
691 - * Delete cached transformed files for an archived version only.
692 - * @param $archiveName string name of the archived file
693 - */
694 - function purgeOldThumbnails( $archiveName ) {
695 - global $wgUseSquid;
696 - // Get a list of old thumbnails and URLs
697 - $files = $this->getThumbnails( $archiveName );
698 - $dir = array_shift( $files );
699 - $this->purgeThumbList( $dir, $files );
700 -
701 - // Directory should be empty, delete it too. This will probably suck on
702 - // something like NFS or if the directory isn't actually empty, so hide
703 - // the warnings :D
704 - wfSuppressWarnings();
705 - if( !rmdir( $dir ) ) {
706 - wfDebug( __METHOD__ . ": unable to remove archive directory: $dir\n" );
707 - }
708 - wfRestoreWarnings();
709 -
710 - // Purge any custom thumbnail caches
711 - wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
712 -
713 - // Purge the squid
714 - if ( $wgUseSquid ) {
715 - $urls = array();
716 - foreach( $files as $file ) {
717 - $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
718 - }
719 - SquidUpdate::purge( $urls );
720 - }
721 - }
722 -
723 -
724 - /**
725 - * Delete cached transformed files for the current version only.
726 - */
727 - function purgeThumbnails() {
728 - global $wgUseSquid;
729 -
730 - // Delete thumbnails
731 - $files = $this->getThumbnails();
732 - $dir = array_shift( $files );
733 - $this->purgeThumbList( $dir, $files );
734 -
735 - // Purge any custom thumbnail caches
736 - wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
737 -
738 - // Purge the squid
739 - if ( $wgUseSquid ) {
740 - $urls = array();
741 - foreach( $files as $file ) {
742 - $urls[] = $this->getThumbUrl( $file );
743 - }
744 - SquidUpdate::purge( $urls );
745 - }
746 - }
747 -
748 - /**
749 - * Delete a list of thumbnails visible at urls
750 - * @param $dir string base dir of the files.
751 - * @param $files array of strings: relative filenames (to $dir)
752 - */
753 - protected function purgeThumbList($dir, $files) {
754 - wfDebug( __METHOD__ . ": " . var_export( $files, true ) . "\n" );
755 - foreach ( $files as $file ) {
756 - # Check that the base file name is part of the thumb name
757 - # This is a basic sanity check to avoid erasing unrelated directories
758 - if ( strpos( $file, $this->getName() ) !== false ) {
759 - wfSuppressWarnings();
760 - unlink( "$dir/$file" );
761 - wfRestoreWarnings();
762 - }
763 - }
764 - }
765 -
766 - /** purgeDescription inherited */
767 - /** purgeEverything inherited */
768 -
769 - function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
770 - $dbr = $this->repo->getSlaveDB();
771 - $tables = array( 'oldimage' );
772 - $fields = OldLocalFile::selectFields();
773 - $conds = $opts = $join_conds = array();
774 - $eq = $inc ? '=' : '';
775 - $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
776 -
777 - if ( $start ) {
778 - $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
779 - }
780 -
781 - if ( $end ) {
782 - $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
783 - }
784 -
785 - if ( $limit ) {
786 - $opts['LIMIT'] = $limit;
787 - }
788 -
789 - // Search backwards for time > x queries
790 - $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
791 - $opts['ORDER BY'] = "oi_timestamp $order";
792 - $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
793 -
794 - wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
795 - &$conds, &$opts, &$join_conds ) );
796 -
797 - $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
798 - $r = array();
799 -
800 - foreach ( $res as $row ) {
801 - if ( $this->repo->oldFileFromRowFactory ) {
802 - $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
803 - } else {
804 - $r[] = OldLocalFile::newFromRow( $row, $this->repo );
805 - }
806 - }
807 -
808 - if ( $order == 'ASC' ) {
809 - $r = array_reverse( $r ); // make sure it ends up descending
810 - }
811 -
812 - return $r;
813 - }
814 -
815 - /**
816 - * Return the history of this file, line by line.
817 - * starts with current version, then old versions.
818 - * uses $this->historyLine to check which line to return:
819 - * 0 return line for current version
820 - * 1 query for old versions, return first one
821 - * 2, ... return next old version from above query
822 - */
823 - public function nextHistoryLine() {
824 - # Polymorphic function name to distinguish foreign and local fetches
825 - $fname = get_class( $this ) . '::' . __FUNCTION__;
826 -
827 - $dbr = $this->repo->getSlaveDB();
828 -
829 - if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
830 - $this->historyRes = $dbr->select( 'image',
831 - array(
832 - '*',
833 - "'' AS oi_archive_name",
834 - '0 as oi_deleted',
835 - 'img_sha1'
836 - ),
837 - array( 'img_name' => $this->title->getDBkey() ),
838 - $fname
839 - );
840 -
841 - if ( 0 == $dbr->numRows( $this->historyRes ) ) {
842 - $this->historyRes = null;
843 - return false;
844 - }
845 - } elseif ( $this->historyLine == 1 ) {
846 - $this->historyRes = $dbr->select( 'oldimage', '*',
847 - array( 'oi_name' => $this->title->getDBkey() ),
848 - $fname,
849 - array( 'ORDER BY' => 'oi_timestamp DESC' )
850 - );
851 - }
852 - $this->historyLine ++;
853 -
854 - return $dbr->fetchObject( $this->historyRes );
855 - }
856 -
857 - /**
858 - * Reset the history pointer to the first element of the history
859 - */
860 - public function resetHistory() {
861 - $this->historyLine = 0;
862 -
863 - if ( !is_null( $this->historyRes ) ) {
864 - $this->historyRes = null;
865 - }
866 - }
867 -
868 - /** getHashPath inherited */
869 - /** getRel inherited */
870 - /** getUrlRel inherited */
871 - /** getArchiveRel inherited */
872 - /** getArchivePath inherited */
873 - /** getThumbPath inherited */
874 - /** getArchiveUrl inherited */
875 - /** getThumbUrl inherited */
876 - /** getArchiveVirtualUrl inherited */
877 - /** getThumbVirtualUrl inherited */
878 - /** isHashed inherited */
879 -
880 - /**
881 - * Upload a file and record it in the DB
882 - * @param $srcPath String: source path or virtual URL
883 - * @param $comment String: upload description
884 - * @param $pageText String: text to use for the new description page,
885 - * if a new description page is created
886 - * @param $flags Integer: flags for publish()
887 - * @param $props Array: File properties, if known. This can be used to reduce the
888 - * upload time when uploading virtual URLs for which the file info
889 - * is already known
890 - * @param $timestamp String: timestamp for img_timestamp, or false to use the current time
891 - * @param $user Mixed: User object or null to use $wgUser
892 - *
893 - * @return FileRepoStatus object. On success, the value member contains the
894 - * archive name, or an empty string if it was a new file.
895 - */
896 - function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
897 - global $wgContLang;
898 - // truncate nicely or the DB will do it for us
899 - // non-nicely (dangling multi-byte chars, non-truncated
900 - // version in cache).
901 - $comment = $wgContLang->truncate( $comment, 255 );
902 - $this->lock();
903 - $status = $this->publish( $srcPath, $flags );
904 -
905 - if ( $status->ok ) {
906 - if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
907 - $status->fatal( 'filenotfound', $srcPath );
908 - }
909 - }
910 -
911 - $this->unlock();
912 -
913 - return $status;
914 - }
915 -
916 - /**
917 - * Record a file upload in the upload log and the image table
918 - */
919 - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
920 - $watch = false, $timestamp = false )
921 - {
922 - $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
923 -
924 - if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
925 - return false;
926 - }
927 -
928 - if ( $watch ) {
929 - global $wgUser;
930 - $wgUser->addWatch( $this->getTitle() );
931 - }
932 - return true;
933 - }
934 -
935 - /**
936 - * Record a file upload in the upload log and the image table
937 - */
938 - function recordUpload2(
939 - $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
940 - ) {
941 - if ( is_null( $user ) ) {
942 - global $wgUser;
943 - $user = $wgUser;
944 - }
945 -
946 - $dbw = $this->repo->getMasterDB();
947 - $dbw->begin();
948 -
949 - if ( !$props ) {
950 - $props = $this->repo->getFileProps( $this->getVirtualUrl() );
951 - }
952 -
953 - if ( $timestamp === false ) {
954 - $timestamp = $dbw->timestamp();
955 - }
956 -
957 - $props['description'] = $comment;
958 - $props['user'] = $user->getId();
959 - $props['user_text'] = $user->getName();
960 - $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
961 - $this->setProps( $props );
962 -
963 - # Delete thumbnails
964 - $this->purgeThumbnails();
965 -
966 - # The file is already on its final location, remove it from the squid cache
967 - SquidUpdate::purge( array( $this->getURL() ) );
968 -
969 - # Fail now if the file isn't there
970 - if ( !$this->fileExists ) {
971 - wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
972 - return false;
973 - }
974 -
975 - $reupload = false;
976 -
977 - # Test to see if the row exists using INSERT IGNORE
978 - # This avoids race conditions by locking the row until the commit, and also
979 - # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
980 - $dbw->insert( 'image',
981 - array(
982 - 'img_name' => $this->getName(),
983 - 'img_size' => $this->size,
984 - 'img_width' => intval( $this->width ),
985 - 'img_height' => intval( $this->height ),
986 - 'img_bits' => $this->bits,
987 - 'img_media_type' => $this->media_type,
988 - 'img_major_mime' => $this->major_mime,
989 - 'img_minor_mime' => $this->minor_mime,
990 - 'img_timestamp' => $timestamp,
991 - 'img_description' => $comment,
992 - 'img_user' => $user->getId(),
993 - 'img_user_text' => $user->getName(),
994 - 'img_metadata' => $this->metadata,
995 - 'img_sha1' => $this->sha1
996 - ),
997 - __METHOD__,
998 - 'IGNORE'
999 - );
1000 -
1001 - if ( $dbw->affectedRows() == 0 ) {
1002 - $reupload = true;
1003 -
1004 - # Collision, this is an update of a file
1005 - # Insert previous contents into oldimage
1006 - $dbw->insertSelect( 'oldimage', 'image',
1007 - array(
1008 - 'oi_name' => 'img_name',
1009 - 'oi_archive_name' => $dbw->addQuotes( $oldver ),
1010 - 'oi_size' => 'img_size',
1011 - 'oi_width' => 'img_width',
1012 - 'oi_height' => 'img_height',
1013 - 'oi_bits' => 'img_bits',
1014 - 'oi_timestamp' => 'img_timestamp',
1015 - 'oi_description' => 'img_description',
1016 - 'oi_user' => 'img_user',
1017 - 'oi_user_text' => 'img_user_text',
1018 - 'oi_metadata' => 'img_metadata',
1019 - 'oi_media_type' => 'img_media_type',
1020 - 'oi_major_mime' => 'img_major_mime',
1021 - 'oi_minor_mime' => 'img_minor_mime',
1022 - 'oi_sha1' => 'img_sha1'
1023 - ), array( 'img_name' => $this->getName() ), __METHOD__
1024 - );
1025 -
1026 - # Update the current image row
1027 - $dbw->update( 'image',
1028 - array( /* SET */
1029 - 'img_size' => $this->size,
1030 - 'img_width' => intval( $this->width ),
1031 - 'img_height' => intval( $this->height ),
1032 - 'img_bits' => $this->bits,
1033 - 'img_media_type' => $this->media_type,
1034 - 'img_major_mime' => $this->major_mime,
1035 - 'img_minor_mime' => $this->minor_mime,
1036 - 'img_timestamp' => $timestamp,
1037 - 'img_description' => $comment,
1038 - 'img_user' => $user->getId(),
1039 - 'img_user_text' => $user->getName(),
1040 - 'img_metadata' => $this->metadata,
1041 - 'img_sha1' => $this->sha1
1042 - ), array( /* WHERE */
1043 - 'img_name' => $this->getName()
1044 - ), __METHOD__
1045 - );
1046 - } else {
1047 - # This is a new file
1048 - # Update the image count
1049 - $dbw->begin( __METHOD__ );
1050 - $dbw->update(
1051 - 'site_stats',
1052 - array( 'ss_images = ss_images+1' ),
1053 - '*',
1054 - __METHOD__
1055 - );
1056 - $dbw->commit( __METHOD__ );
1057 - }
1058 -
1059 - $descTitle = $this->getTitle();
1060 - $wikiPage = new WikiFilePage( $descTitle );
1061 - $wikiPage->setFile( $this );
1062 -
1063 - # Add the log entry
1064 - $log = new LogPage( 'upload' );
1065 - $action = $reupload ? 'overwrite' : 'upload';
1066 - $log->addEntry( $action, $descTitle, $comment, array(), $user );
1067 -
1068 - if ( $descTitle->exists() ) {
1069 - # Create a null revision
1070 - $latest = $descTitle->getLatestRevID();
1071 - $nullRevision = Revision::newNullRevision(
1072 - $dbw,
1073 - $descTitle->getArticleId(),
1074 - $log->getRcComment(),
1075 - false
1076 - );
1077 - if (!is_null($nullRevision)) {
1078 - $nullRevision->insertOn( $dbw );
1079 -
1080 - wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
1081 - $wikiPage->updateRevisionOn( $dbw, $nullRevision );
1082 - }
1083 - # Invalidate the cache for the description page
1084 - $descTitle->invalidateCache();
1085 - $descTitle->purgeSquid();
1086 - } else {
1087 - # New file; create the description page.
1088 - # There's already a log entry, so don't make a second RC entry
1089 - # Squid and file cache for the description page are purged by doEdit.
1090 - $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
1091 - }
1092 -
1093 - # Commit the transaction now, in case something goes wrong later
1094 - # The most important thing is that files don't get lost, especially archives
1095 - $dbw->commit();
1096 -
1097 - # Save to cache and purge the squid
1098 - # We shall not saveToCache before the commit since otherwise
1099 - # in case of a rollback there is an usable file from memcached
1100 - # which in fact doesn't really exist (bug 24978)
1101 - $this->saveToCache();
1102 -
1103 - # Hooks, hooks, the magic of hooks...
1104 - wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
1105 -
1106 - # Invalidate cache for all pages using this file
1107 - $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
1108 - $update->doUpdate();
1109 -
1110 - # Invalidate cache for all pages that redirects on this page
1111 - $redirs = $this->getTitle()->getRedirectsHere();
1112 -
1113 - foreach ( $redirs as $redir ) {
1114 - $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
1115 - $update->doUpdate();
1116 - }
1117 -
1118 - return true;
1119 - }
1120 -
1121 - /**
1122 - * Move or copy a file to its public location. If a file exists at the
1123 - * destination, move it to an archive. Returns a FileRepoStatus object with
1124 - * the archive name in the "value" member on success.
1125 - *
1126 - * The archive name should be passed through to recordUpload for database
1127 - * registration.
1128 - *
1129 - * @param $srcPath String: local filesystem path to the source image
1130 - * @param $flags Integer: a bitwise combination of:
1131 - * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
1132 - * @return FileRepoStatus object. On success, the value member contains the
1133 - * archive name, or an empty string if it was a new file.
1134 - */
1135 - function publish( $srcPath, $flags = 0 ) {
1136 - return $this->publishTo( $srcPath, $this->getRel(), $flags );
1137 - }
1138 -
1139 - /**
1140 - * Move or copy a file to a specified location. Returns a FileRepoStatus
1141 - * object with the archive name in the "value" member on success.
1142 - *
1143 - * The archive name should be passed through to recordUpload for database
1144 - * registration.
1145 - *
1146 - * @param $srcPath String: local filesystem path to the source image
1147 - * @param $dstRel String: target relative path
1148 - * @param $flags Integer: a bitwise combination of:
1149 - * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
1150 - * @return FileRepoStatus object. On success, the value member contains the
1151 - * archive name, or an empty string if it was a new file.
1152 - */
1153 - function publishTo( $srcPath, $dstRel, $flags = 0 ) {
1154 - $this->lock();
1155 -
1156 - $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
1157 - $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1158 - $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
1159 - $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
1160 -
1161 - if ( $status->value == 'new' ) {
1162 - $status->value = '';
1163 - } else {
1164 - $status->value = $archiveName;
1165 - }
1166 -
1167 - $this->unlock();
1168 -
1169 - return $status;
1170 - }
1171 -
1172 - /** getLinksTo inherited */
1173 - /** getExifData inherited */
1174 - /** isLocal inherited */
1175 - /** wasDeleted inherited */
1176 -
1177 - /**
1178 - * Move file to the new title
1179 - *
1180 - * Move current, old version and all thumbnails
1181 - * to the new filename. Old file is deleted.
1182 - *
1183 - * Cache purging is done; checks for validity
1184 - * and logging are caller's responsibility
1185 - *
1186 - * @param $target Title New file name
1187 - * @return FileRepoStatus object.
1188 - */
1189 - function move( $target ) {
1190 - wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1191 - $this->lock();
1192 -
1193 - $batch = new LocalFileMoveBatch( $this, $target );
1194 - $batch->addCurrent();
1195 - $batch->addOlds();
1196 -
1197 - $status = $batch->execute();
1198 - wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1199 -
1200 - $this->purgeEverything();
1201 - $this->unlock();
1202 -
1203 - if ( $status->isOk() ) {
1204 - // Now switch the object
1205 - $this->title = $target;
1206 - // Force regeneration of the name and hashpath
1207 - unset( $this->name );
1208 - unset( $this->hashPath );
1209 - // Purge the new image
1210 - $this->purgeEverything();
1211 - }
1212 -
1213 - return $status;
1214 - }
1215 -
1216 - /**
1217 - * Delete all versions of the file.
1218 - *
1219 - * Moves the files into an archive directory (or deletes them)
1220 - * and removes the database rows.
1221 - *
1222 - * Cache purging is done; logging is caller's responsibility.
1223 - *
1224 - * @param $reason
1225 - * @param $suppress
1226 - * @return FileRepoStatus object.
1227 - */
1228 - function delete( $reason, $suppress = false ) {
1229 - $this->lock();
1230 -
1231 - $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
1232 - $batch->addCurrent();
1233 -
1234 - # Get old version relative paths
1235 - $dbw = $this->repo->getMasterDB();
1236 - $result = $dbw->select( 'oldimage',
1237 - array( 'oi_archive_name' ),
1238 - array( 'oi_name' => $this->getName() ) );
1239 - foreach ( $result as $row ) {
1240 - $batch->addOld( $row->oi_archive_name );
1241 - $this->purgeOldThumbnails( $row->oi_archive_name );
1242 - }
1243 - $status = $batch->execute();
1244 -
1245 - if ( $status->ok ) {
1246 - // Update site_stats
1247 - $site_stats = $dbw->tableName( 'site_stats' );
1248 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
1249 - $this->purgeEverything();
1250 - }
1251 -
1252 - $this->unlock();
1253 -
1254 - return $status;
1255 - }
1256 -
1257 - /**
1258 - * Delete an old version of the file.
1259 - *
1260 - * Moves the file into an archive directory (or deletes it)
1261 - * and removes the database row.
1262 - *
1263 - * Cache purging is done; logging is caller's responsibility.
1264 - *
1265 - * @param $archiveName String
1266 - * @param $reason String
1267 - * @param $suppress Boolean
1268 - * @throws MWException or FSException on database or file store failure
1269 - * @return FileRepoStatus object.
1270 - */
1271 - function deleteOld( $archiveName, $reason, $suppress = false ) {
1272 - $this->lock();
1273 -
1274 - $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
1275 - $batch->addOld( $archiveName );
1276 - $this->purgeOldThumbnails( $archiveName );
1277 - $status = $batch->execute();
1278 -
1279 - $this->unlock();
1280 -
1281 - if ( $status->ok ) {
1282 - $this->purgeDescription();
1283 - $this->purgeHistory();
1284 - }
1285 -
1286 - return $status;
1287 - }
1288 -
1289 - /**
1290 - * Restore all or specified deleted revisions to the given file.
1291 - * Permissions and logging are left to the caller.
1292 - *
1293 - * May throw database exceptions on error.
1294 - *
1295 - * @param $versions set of record ids of deleted items to restore,
1296 - * or empty to restore all revisions.
1297 - * @param $unsuppress Boolean
1298 - * @return FileRepoStatus
1299 - */
1300 - function restore( $versions = array(), $unsuppress = false ) {
1301 - $batch = new LocalFileRestoreBatch( $this, $unsuppress );
1302 -
1303 - if ( !$versions ) {
1304 - $batch->addAll();
1305 - } else {
1306 - $batch->addIds( $versions );
1307 - }
1308 -
1309 - $status = $batch->execute();
1310 -
1311 - if ( !$status->isGood() ) {
1312 - return $status;
1313 - }
1314 -
1315 - $cleanupStatus = $batch->cleanup();
1316 - $cleanupStatus->successCount = 0;
1317 - $cleanupStatus->failCount = 0;
1318 - $status->merge( $cleanupStatus );
1319 -
1320 - return $status;
1321 - }
1322 -
1323 - /** isMultipage inherited */
1324 - /** pageCount inherited */
1325 - /** scaleHeight inherited */
1326 - /** getImageSize inherited */
1327 -
1328 - /**
1329 - * Get the URL of the file description page.
1330 - */
1331 - function getDescriptionUrl() {
1332 - return $this->title->getLocalUrl();
1333 - }
1334 -
1335 - /**
1336 - * Get the HTML text of the description page
1337 - * This is not used by ImagePage for local files, since (among other things)
1338 - * it skips the parser cache.
1339 - */
1340 - function getDescriptionText() {
1341 - global $wgParser;
1342 - $revision = Revision::newFromTitle( $this->title );
1343 - if ( !$revision ) return false;
1344 - $text = $revision->getText();
1345 - if ( !$text ) return false;
1346 - $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
1347 - return $pout->getText();
1348 - }
1349 -
1350 - function getDescription() {
1351 - $this->load();
1352 - return $this->description;
1353 - }
1354 -
1355 - function getTimestamp() {
1356 - $this->load();
1357 - return $this->timestamp;
1358 - }
1359 -
1360 - function getSha1() {
1361 - $this->load();
1362 - // Initialise now if necessary
1363 - if ( $this->sha1 == '' && $this->fileExists ) {
1364 - $this->sha1 = File::sha1Base36( $this->getPath() );
1365 - if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1366 - $dbw = $this->repo->getMasterDB();
1367 - $dbw->update( 'image',
1368 - array( 'img_sha1' => $this->sha1 ),
1369 - array( 'img_name' => $this->getName() ),
1370 - __METHOD__ );
1371 - $this->saveToCache();
1372 - }
1373 - }
1374 -
1375 - return $this->sha1;
1376 - }
1377 -
1378 - /**
1379 - * Start a transaction and lock the image for update
1380 - * Increments a reference counter if the lock is already held
1381 - * @return boolean True if the image exists, false otherwise
1382 - */
1383 - function lock() {
1384 - $dbw = $this->repo->getMasterDB();
1385 -
1386 - if ( !$this->locked ) {
1387 - $dbw->begin();
1388 - $this->locked++;
1389 - }
1390 -
1391 - return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
1392 - }
1393 -
1394 - /**
1395 - * Decrement the lock reference count. If the reference count is reduced to zero, commits
1396 - * the transaction and thereby releases the image lock.
1397 - */
1398 - function unlock() {
1399 - if ( $this->locked ) {
1400 - --$this->locked;
1401 - if ( !$this->locked ) {
1402 - $dbw = $this->repo->getMasterDB();
1403 - $dbw->commit();
1404 - }
1405 - }
1406 - }
1407 -
1408 - /**
1409 - * Roll back the DB transaction and mark the image unlocked
1410 - */
1411 - function unlockAndRollback() {
1412 - $this->locked = false;
1413 - $dbw = $this->repo->getMasterDB();
1414 - $dbw->rollback();
1415 - }
1416 -} // LocalFile class
1417 -
1418 -# ------------------------------------------------------------------------------
1419 -
1420 -/**
1421 - * Helper class for file deletion
1422 - * @ingroup FileRepo
1423 - */
1424 -class LocalFileDeleteBatch {
1425 -
1426 - /**
1427 - * @var LocalFile
1428 - */
1429 - var $file;
1430 -
1431 - var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
1432 - var $status;
1433 -
1434 - function __construct( File $file, $reason = '', $suppress = false ) {
1435 - $this->file = $file;
1436 - $this->reason = $reason;
1437 - $this->suppress = $suppress;
1438 - $this->status = $file->repo->newGood();
1439 - }
1440 -
1441 - function addCurrent() {
1442 - $this->srcRels['.'] = $this->file->getRel();
1443 - }
1444 -
1445 - function addOld( $oldName ) {
1446 - $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
1447 - $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
1448 - }
1449 -
1450 - function getOldRels() {
1451 - if ( !isset( $this->srcRels['.'] ) ) {
1452 - $oldRels =& $this->srcRels;
1453 - $deleteCurrent = false;
1454 - } else {
1455 - $oldRels = $this->srcRels;
1456 - unset( $oldRels['.'] );
1457 - $deleteCurrent = true;
1458 - }
1459 -
1460 - return array( $oldRels, $deleteCurrent );
1461 - }
1462 -
1463 - protected function getHashes() {
1464 - $hashes = array();
1465 - list( $oldRels, $deleteCurrent ) = $this->getOldRels();
1466 -
1467 - if ( $deleteCurrent ) {
1468 - $hashes['.'] = $this->file->getSha1();
1469 - }
1470 -
1471 - if ( count( $oldRels ) ) {
1472 - $dbw = $this->file->repo->getMasterDB();
1473 - $res = $dbw->select(
1474 - 'oldimage',
1475 - array( 'oi_archive_name', 'oi_sha1' ),
1476 - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
1477 - __METHOD__
1478 - );
1479 -
1480 - foreach ( $res as $row ) {
1481 - if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
1482 - // Get the hash from the file
1483 - $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
1484 - $props = $this->file->repo->getFileProps( $oldUrl );
1485 -
1486 - if ( $props['fileExists'] ) {
1487 - // Upgrade the oldimage row
1488 - $dbw->update( 'oldimage',
1489 - array( 'oi_sha1' => $props['sha1'] ),
1490 - array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
1491 - __METHOD__ );
1492 - $hashes[$row->oi_archive_name] = $props['sha1'];
1493 - } else {
1494 - $hashes[$row->oi_archive_name] = false;
1495 - }
1496 - } else {
1497 - $hashes[$row->oi_archive_name] = $row->oi_sha1;
1498 - }
1499 - }
1500 - }
1501 -
1502 - $missing = array_diff_key( $this->srcRels, $hashes );
1503 -
1504 - foreach ( $missing as $name => $rel ) {
1505 - $this->status->error( 'filedelete-old-unregistered', $name );
1506 - }
1507 -
1508 - foreach ( $hashes as $name => $hash ) {
1509 - if ( !$hash ) {
1510 - $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
1511 - unset( $hashes[$name] );
1512 - }
1513 - }
1514 -
1515 - return $hashes;
1516 - }
1517 -
1518 - function doDBInserts() {
1519 - global $wgUser;
1520 -
1521 - $dbw = $this->file->repo->getMasterDB();
1522 - $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
1523 - $encUserId = $dbw->addQuotes( $wgUser->getId() );
1524 - $encReason = $dbw->addQuotes( $this->reason );
1525 - $encGroup = $dbw->addQuotes( 'deleted' );
1526 - $ext = $this->file->getExtension();
1527 - $dotExt = $ext === '' ? '' : ".$ext";
1528 - $encExt = $dbw->addQuotes( $dotExt );
1529 - list( $oldRels, $deleteCurrent ) = $this->getOldRels();
1530 -
1531 - // Bitfields to further suppress the content
1532 - if ( $this->suppress ) {
1533 - $bitfield = 0;
1534 - // This should be 15...
1535 - $bitfield |= Revision::DELETED_TEXT;
1536 - $bitfield |= Revision::DELETED_COMMENT;
1537 - $bitfield |= Revision::DELETED_USER;
1538 - $bitfield |= Revision::DELETED_RESTRICTED;
1539 - } else {
1540 - $bitfield = 'oi_deleted';
1541 - }
1542 -
1543 - if ( $deleteCurrent ) {
1544 - $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
1545 - $where = array( 'img_name' => $this->file->getName() );
1546 - $dbw->insertSelect( 'filearchive', 'image',
1547 - array(
1548 - 'fa_storage_group' => $encGroup,
1549 - 'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
1550 - 'fa_deleted_user' => $encUserId,
1551 - 'fa_deleted_timestamp' => $encTimestamp,
1552 - 'fa_deleted_reason' => $encReason,
1553 - 'fa_deleted' => $this->suppress ? $bitfield : 0,
1554 -
1555 - 'fa_name' => 'img_name',
1556 - 'fa_archive_name' => 'NULL',
1557 - 'fa_size' => 'img_size',
1558 - 'fa_width' => 'img_width',
1559 - 'fa_height' => 'img_height',
1560 - 'fa_metadata' => 'img_metadata',
1561 - 'fa_bits' => 'img_bits',
1562 - 'fa_media_type' => 'img_media_type',
1563 - 'fa_major_mime' => 'img_major_mime',
1564 - 'fa_minor_mime' => 'img_minor_mime',
1565 - 'fa_description' => 'img_description',
1566 - 'fa_user' => 'img_user',
1567 - 'fa_user_text' => 'img_user_text',
1568 - 'fa_timestamp' => 'img_timestamp'
1569 - ), $where, __METHOD__ );
1570 - }
1571 -
1572 - if ( count( $oldRels ) ) {
1573 - $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
1574 - $where = array(
1575 - 'oi_name' => $this->file->getName(),
1576 - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
1577 - $dbw->insertSelect( 'filearchive', 'oldimage',
1578 - array(
1579 - 'fa_storage_group' => $encGroup,
1580 - 'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
1581 - 'fa_deleted_user' => $encUserId,
1582 - 'fa_deleted_timestamp' => $encTimestamp,
1583 - 'fa_deleted_reason' => $encReason,
1584 - 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
1585 -
1586 - 'fa_name' => 'oi_name',
1587 - 'fa_archive_name' => 'oi_archive_name',
1588 - 'fa_size' => 'oi_size',
1589 - 'fa_width' => 'oi_width',
1590 - 'fa_height' => 'oi_height',
1591 - 'fa_metadata' => 'oi_metadata',
1592 - 'fa_bits' => 'oi_bits',
1593 - 'fa_media_type' => 'oi_media_type',
1594 - 'fa_major_mime' => 'oi_major_mime',
1595 - 'fa_minor_mime' => 'oi_minor_mime',
1596 - 'fa_description' => 'oi_description',
1597 - 'fa_user' => 'oi_user',
1598 - 'fa_user_text' => 'oi_user_text',
1599 - 'fa_timestamp' => 'oi_timestamp',
1600 - 'fa_deleted' => $bitfield
1601 - ), $where, __METHOD__ );
1602 - }
1603 - }
1604 -
1605 - function doDBDeletes() {
1606 - $dbw = $this->file->repo->getMasterDB();
1607 - list( $oldRels, $deleteCurrent ) = $this->getOldRels();
1608 -
1609 - if ( count( $oldRels ) ) {
1610 - $dbw->delete( 'oldimage',
1611 - array(
1612 - 'oi_name' => $this->file->getName(),
1613 - 'oi_archive_name' => array_keys( $oldRels )
1614 - ), __METHOD__ );
1615 - }
1616 -
1617 - if ( $deleteCurrent ) {
1618 - $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
1619 - }
1620 - }
1621 -
1622 - /**
1623 - * Run the transaction
1624 - */
1625 - function execute() {
1626 - global $wgUseSquid;
1627 - wfProfileIn( __METHOD__ );
1628 -
1629 - $this->file->lock();
1630 - // Leave private files alone
1631 - $privateFiles = array();
1632 - list( $oldRels, $deleteCurrent ) = $this->getOldRels();
1633 - $dbw = $this->file->repo->getMasterDB();
1634 -
1635 - if ( !empty( $oldRels ) ) {
1636 - $res = $dbw->select( 'oldimage',
1637 - array( 'oi_archive_name' ),
1638 - array( 'oi_name' => $this->file->getName(),
1639 - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
1640 - $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
1641 - __METHOD__ );
1642 -
1643 - foreach ( $res as $row ) {
1644 - $privateFiles[$row->oi_archive_name] = 1;
1645 - }
1646 - }
1647 - // Prepare deletion batch
1648 - $hashes = $this->getHashes();
1649 - $this->deletionBatch = array();
1650 - $ext = $this->file->getExtension();
1651 - $dotExt = $ext === '' ? '' : ".$ext";
1652 -
1653 - foreach ( $this->srcRels as $name => $srcRel ) {
1654 - // Skip files that have no hash (missing source).
1655 - // Keep private files where they are.
1656 - if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
1657 - $hash = $hashes[$name];
1658 - $key = $hash . $dotExt;
1659 - $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
1660 - $this->deletionBatch[$name] = array( $srcRel, $dstRel );
1661 - }
1662 - }
1663 -
1664 - // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
1665 - // We acquire this lock by running the inserts now, before the file operations.
1666 - //
1667 - // This potentially has poor lock contention characteristics -- an alternative
1668 - // scheme would be to insert stub filearchive entries with no fa_name and commit
1669 - // them in a separate transaction, then run the file ops, then update the fa_name fields.
1670 - $this->doDBInserts();
1671 -
1672 - // Removes non-existent file from the batch, so we don't get errors.
1673 - $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
1674 -
1675 - // Execute the file deletion batch
1676 - $status = $this->file->repo->deleteBatch( $this->deletionBatch );
1677 -
1678 - if ( !$status->isGood() ) {
1679 - $this->status->merge( $status );
1680 - }
1681 -
1682 - if ( !$this->status->ok ) {
1683 - // Critical file deletion error
1684 - // Roll back inserts, release lock and abort
1685 - // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
1686 - $this->file->unlockAndRollback();
1687 - wfProfileOut( __METHOD__ );
1688 - return $this->status;
1689 - }
1690 -
1691 - // Purge squid
1692 - if ( $wgUseSquid ) {
1693 - $urls = array();
1694 -
1695 - foreach ( $this->srcRels as $srcRel ) {
1696 - $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
1697 - $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
1698 - }
1699 - SquidUpdate::purge( $urls );
1700 - }
1701 -
1702 - // Delete image/oldimage rows
1703 - $this->doDBDeletes();
1704 -
1705 - // Commit and return
1706 - $this->file->unlock();
1707 - wfProfileOut( __METHOD__ );
1708 -
1709 - return $this->status;
1710 - }
1711 -
1712 - /**
1713 - * Removes non-existent files from a deletion batch.
1714 - */
1715 - function removeNonexistentFiles( $batch ) {
1716 - $files = $newBatch = array();
1717 -
1718 - foreach ( $batch as $batchItem ) {
1719 - list( $src, $dest ) = $batchItem;
1720 - $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
1721 - }
1722 -
1723 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
1724 -
1725 - foreach ( $batch as $batchItem ) {
1726 - if ( $result[$batchItem[0]] ) {
1727 - $newBatch[] = $batchItem;
1728 - }
1729 - }
1730 -
1731 - return $newBatch;
1732 - }
1733 -}
1734 -
1735 -# ------------------------------------------------------------------------------
1736 -
1737 -/**
1738 - * Helper class for file undeletion
1739 - * @ingroup FileRepo
1740 - */
1741 -class LocalFileRestoreBatch {
1742 - /**
1743 - * @var LocalFile
1744 - */
1745 - var $file;
1746 -
1747 - var $cleanupBatch, $ids, $all, $unsuppress = false;
1748 -
1749 - function __construct( File $file, $unsuppress = false ) {
1750 - $this->file = $file;
1751 - $this->cleanupBatch = $this->ids = array();
1752 - $this->ids = array();
1753 - $this->unsuppress = $unsuppress;
1754 - }
1755 -
1756 - /**
1757 - * Add a file by ID
1758 - */
1759 - function addId( $fa_id ) {
1760 - $this->ids[] = $fa_id;
1761 - }
1762 -
1763 - /**
1764 - * Add a whole lot of files by ID
1765 - */
1766 - function addIds( $ids ) {
1767 - $this->ids = array_merge( $this->ids, $ids );
1768 - }
1769 -
1770 - /**
1771 - * Add all revisions of the file
1772 - */
1773 - function addAll() {
1774 - $this->all = true;
1775 - }
1776 -
1777 - /**
1778 - * Run the transaction, except the cleanup batch.
1779 - * The cleanup batch should be run in a separate transaction, because it locks different
1780 - * rows and there's no need to keep the image row locked while it's acquiring those locks
1781 - * The caller may have its own transaction open.
1782 - * So we save the batch and let the caller call cleanup()
1783 - */
1784 - function execute() {
1785 - global $wgLang;
1786 -
1787 - if ( !$this->all && !$this->ids ) {
1788 - // Do nothing
1789 - return $this->file->repo->newGood();
1790 - }
1791 -
1792 - $exists = $this->file->lock();
1793 - $dbw = $this->file->repo->getMasterDB();
1794 - $status = $this->file->repo->newGood();
1795 -
1796 - // Fetch all or selected archived revisions for the file,
1797 - // sorted from the most recent to the oldest.
1798 - $conditions = array( 'fa_name' => $this->file->getName() );
1799 -
1800 - if ( !$this->all ) {
1801 - $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
1802 - }
1803 -
1804 - $result = $dbw->select( 'filearchive', '*',
1805 - $conditions,
1806 - __METHOD__,
1807 - array( 'ORDER BY' => 'fa_timestamp DESC' )
1808 - );
1809 -
1810 - $idsPresent = array();
1811 - $storeBatch = array();
1812 - $insertBatch = array();
1813 - $insertCurrent = false;
1814 - $deleteIds = array();
1815 - $first = true;
1816 - $archiveNames = array();
1817 -
1818 - foreach ( $result as $row ) {
1819 - $idsPresent[] = $row->fa_id;
1820 -
1821 - if ( $row->fa_name != $this->file->getName() ) {
1822 - $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
1823 - $status->failCount++;
1824 - continue;
1825 - }
1826 -
1827 - if ( $row->fa_storage_key == '' ) {
1828 - // Revision was missing pre-deletion
1829 - $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
1830 - $status->failCount++;
1831 - continue;
1832 - }
1833 -
1834 - $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
1835 - $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
1836 -
1837 - $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
1838 -
1839 - # Fix leading zero
1840 - if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
1841 - $sha1 = substr( $sha1, 1 );
1842 - }
1843 -
1844 - if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
1845 - || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
1846 - || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
1847 - || is_null( $row->fa_metadata ) ) {
1848 - // Refresh our metadata
1849 - // Required for a new current revision; nice for older ones too. :)
1850 - $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
1851 - } else {
1852 - $props = array(
1853 - 'minor_mime' => $row->fa_minor_mime,
1854 - 'major_mime' => $row->fa_major_mime,
1855 - 'media_type' => $row->fa_media_type,
1856 - 'metadata' => $row->fa_metadata
1857 - );
1858 - }
1859 -
1860 - if ( $first && !$exists ) {
1861 - // This revision will be published as the new current version
1862 - $destRel = $this->file->getRel();
1863 - $insertCurrent = array(
1864 - 'img_name' => $row->fa_name,
1865 - 'img_size' => $row->fa_size,
1866 - 'img_width' => $row->fa_width,
1867 - 'img_height' => $row->fa_height,
1868 - 'img_metadata' => $props['metadata'],
1869 - 'img_bits' => $row->fa_bits,
1870 - 'img_media_type' => $props['media_type'],
1871 - 'img_major_mime' => $props['major_mime'],
1872 - 'img_minor_mime' => $props['minor_mime'],
1873 - 'img_description' => $row->fa_description,
1874 - 'img_user' => $row->fa_user,
1875 - 'img_user_text' => $row->fa_user_text,
1876 - 'img_timestamp' => $row->fa_timestamp,
1877 - 'img_sha1' => $sha1
1878 - );
1879 -
1880 - // The live (current) version cannot be hidden!
1881 - if ( !$this->unsuppress && $row->fa_deleted ) {
1882 - $storeBatch[] = array( $deletedUrl, 'public', $destRel );
1883 - $this->cleanupBatch[] = $row->fa_storage_key;
1884 - }
1885 - } else {
1886 - $archiveName = $row->fa_archive_name;
1887 -
1888 - if ( $archiveName == '' ) {
1889 - // This was originally a current version; we
1890 - // have to devise a new archive name for it.
1891 - // Format is <timestamp of archiving>!<name>
1892 - $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
1893 -
1894 - do {
1895 - $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
1896 - $timestamp++;
1897 - } while ( isset( $archiveNames[$archiveName] ) );
1898 - }
1899 -
1900 - $archiveNames[$archiveName] = true;
1901 - $destRel = $this->file->getArchiveRel( $archiveName );
1902 - $insertBatch[] = array(
1903 - 'oi_name' => $row->fa_name,
1904 - 'oi_archive_name' => $archiveName,
1905 - 'oi_size' => $row->fa_size,
1906 - 'oi_width' => $row->fa_width,
1907 - 'oi_height' => $row->fa_height,
1908 - 'oi_bits' => $row->fa_bits,
1909 - 'oi_description' => $row->fa_description,
1910 - 'oi_user' => $row->fa_user,
1911 - 'oi_user_text' => $row->fa_user_text,
1912 - 'oi_timestamp' => $row->fa_timestamp,
1913 - 'oi_metadata' => $props['metadata'],
1914 - 'oi_media_type' => $props['media_type'],
1915 - 'oi_major_mime' => $props['major_mime'],
1916 - 'oi_minor_mime' => $props['minor_mime'],
1917 - 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
1918 - 'oi_sha1' => $sha1 );
1919 - }
1920 -
1921 - $deleteIds[] = $row->fa_id;
1922 -
1923 - if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
1924 - // private files can stay where they are
1925 - $status->successCount++;
1926 - } else {
1927 - $storeBatch[] = array( $deletedUrl, 'public', $destRel );
1928 - $this->cleanupBatch[] = $row->fa_storage_key;
1929 - }
1930 -
1931 - $first = false;
1932 - }
1933 -
1934 - unset( $result );
1935 -
1936 - // Add a warning to the status object for missing IDs
1937 - $missingIds = array_diff( $this->ids, $idsPresent );
1938 -
1939 - foreach ( $missingIds as $id ) {
1940 - $status->error( 'undelete-missing-filearchive', $id );
1941 - }
1942 -
1943 - // Remove missing files from batch, so we don't get errors when undeleting them
1944 - $storeBatch = $this->removeNonexistentFiles( $storeBatch );
1945 -
1946 - // Run the store batch
1947 - // Use the OVERWRITE_SAME flag to smooth over a common error
1948 - $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
1949 - $status->merge( $storeStatus );
1950 -
1951 - if ( !$status->isGood() ) {
1952 - // Even if some files could be copied, fail entirely as that is the
1953 - // easiest thing to do without data loss
1954 - $this->cleanupFailedBatch( $storeStatus, $storeBatch );
1955 - $status->ok = false;
1956 - $this->file->unlock();
1957 -
1958 - return $status;
1959 - }
1960 -
1961 - // Run the DB updates
1962 - // Because we have locked the image row, key conflicts should be rare.
1963 - // If they do occur, we can roll back the transaction at this time with
1964 - // no data loss, but leaving unregistered files scattered throughout the
1965 - // public zone.
1966 - // This is not ideal, which is why it's important to lock the image row.
1967 - if ( $insertCurrent ) {
1968 - $dbw->insert( 'image', $insertCurrent, __METHOD__ );
1969 - }
1970 -
1971 - if ( $insertBatch ) {
1972 - $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
1973 - }
1974 -
1975 - if ( $deleteIds ) {
1976 - $dbw->delete( 'filearchive',
1977 - array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
1978 - __METHOD__ );
1979 - }
1980 -
1981 - // If store batch is empty (all files are missing), deletion is to be considered successful
1982 - if ( $status->successCount > 0 || !$storeBatch ) {
1983 - if ( !$exists ) {
1984 - wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
1985 -
1986 - // Update site_stats
1987 - $site_stats = $dbw->tableName( 'site_stats' );
1988 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
1989 -
1990 - $this->file->purgeEverything();
1991 - } else {
1992 - wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
1993 - $this->file->purgeDescription();
1994 - $this->file->purgeHistory();
1995 - }
1996 - }
1997 -
1998 - $this->file->unlock();
1999 -
2000 - return $status;
2001 - }
2002 -
2003 - /**
2004 - * Removes non-existent files from a store batch.
2005 - */
2006 - function removeNonexistentFiles( $triplets ) {
2007 - $files = $filteredTriplets = array();
2008 - foreach ( $triplets as $file )
2009 - $files[$file[0]] = $file[0];
2010 -
2011 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
2012 -
2013 - foreach ( $triplets as $file ) {
2014 - if ( $result[$file[0]] ) {
2015 - $filteredTriplets[] = $file;
2016 - }
2017 - }
2018 -
2019 - return $filteredTriplets;
2020 - }
2021 -
2022 - /**
2023 - * Removes non-existent files from a cleanup batch.
2024 - */
2025 - function removeNonexistentFromCleanup( $batch ) {
2026 - $files = $newBatch = array();
2027 - $repo = $this->file->repo;
2028 -
2029 - foreach ( $batch as $file ) {
2030 - $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2031 - rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2032 - }
2033 -
2034 - $result = $repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
2035 -
2036 - foreach ( $batch as $file ) {
2037 - if ( $result[$file] ) {
2038 - $newBatch[] = $file;
2039 - }
2040 - }
2041 -
2042 - return $newBatch;
2043 - }
2044 -
2045 - /**
2046 - * Delete unused files in the deleted zone.
2047 - * This should be called from outside the transaction in which execute() was called.
2048 - */
2049 - function cleanup() {
2050 - if ( !$this->cleanupBatch ) {
2051 - return $this->file->repo->newGood();
2052 - }
2053 -
2054 - $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2055 -
2056 - $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2057 -
2058 - return $status;
2059 - }
2060 -
2061 - /**
2062 - * Cleanup a failed batch. The batch was only partially successful, so
2063 - * rollback by removing all items that were succesfully copied.
2064 - *
2065 - * @param Status $storeStatus
2066 - * @param array $storeBatch
2067 - */
2068 - function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2069 - $cleanupBatch = array();
2070 -
2071 - foreach ( $storeStatus->success as $i => $success ) {
2072 - // Check if this item of the batch was successfully copied
2073 - if ( $success ) {
2074 - // Item was successfully copied and needs to be removed again
2075 - // Extract ($dstZone, $dstRel) from the batch
2076 - $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
2077 - }
2078 - }
2079 - $this->file->repo->cleanupBatch( $cleanupBatch );
2080 - }
2081 -}
2082 -
2083 -# ------------------------------------------------------------------------------
2084 -
2085 -/**
2086 - * Helper class for file movement
2087 - * @ingroup FileRepo
2088 - */
2089 -class LocalFileMoveBatch {
2090 -
2091 - /**
2092 - * @var File
2093 - */
2094 - var $file;
2095 -
2096 - /**
2097 - * @var Title
2098 - */
2099 - var $target;
2100 -
2101 - var $cur, $olds, $oldCount, $archive, $db;
2102 -
2103 - function __construct( File $file, Title $target ) {
2104 - $this->file = $file;
2105 - $this->target = $target;
2106 - $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
2107 - $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
2108 - $this->oldName = $this->file->getName();
2109 - $this->newName = $this->file->repo->getNameFromTitle( $this->target );
2110 - $this->oldRel = $this->oldHash . $this->oldName;
2111 - $this->newRel = $this->newHash . $this->newName;
2112 - $this->db = $file->repo->getMasterDb();
2113 - }
2114 -
2115 - /**
2116 - * Add the current image to the batch
2117 - */
2118 - function addCurrent() {
2119 - $this->cur = array( $this->oldRel, $this->newRel );
2120 - }
2121 -
2122 - /**
2123 - * Add the old versions of the image to the batch
2124 - */
2125 - function addOlds() {
2126 - $archiveBase = 'archive';
2127 - $this->olds = array();
2128 - $this->oldCount = 0;
2129 -
2130 - $result = $this->db->select( 'oldimage',
2131 - array( 'oi_archive_name', 'oi_deleted' ),
2132 - array( 'oi_name' => $this->oldName ),
2133 - __METHOD__
2134 - );
2135 -
2136 - foreach ( $result as $row ) {
2137 - $oldName = $row->oi_archive_name;
2138 - $bits = explode( '!', $oldName, 2 );
2139 -
2140 - if ( count( $bits ) != 2 ) {
2141 - wfDebug( "Old file name missing !: '$oldName' \n" );
2142 - continue;
2143 - }
2144 -
2145 - list( $timestamp, $filename ) = $bits;
2146 -
2147 - if ( $this->oldName != $filename ) {
2148 - wfDebug( "Old file name doesn't match: '$oldName' \n" );
2149 - continue;
2150 - }
2151 -
2152 - $this->oldCount++;
2153 -
2154 - // Do we want to add those to oldCount?
2155 - if ( $row->oi_deleted & File::DELETED_FILE ) {
2156 - continue;
2157 - }
2158 -
2159 - $this->olds[] = array(
2160 - "{$archiveBase}/{$this->oldHash}{$oldName}",
2161 - "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2162 - );
2163 - }
2164 - }
2165 -
2166 - /**
2167 - * Perform the move.
2168 - */
2169 - function execute() {
2170 - $repo = $this->file->repo;
2171 - $status = $repo->newGood();
2172 - $triplets = $this->getMoveTriplets();
2173 -
2174 - $triplets = $this->removeNonexistentFiles( $triplets );
2175 -
2176 - // Copy the files into their new location
2177 - $statusMove = $repo->storeBatch( $triplets );
2178 - wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2179 - if ( !$statusMove->isGood() ) {
2180 - wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
2181 - $this->cleanupTarget( $triplets );
2182 - $statusMove->ok = false;
2183 - return $statusMove;
2184 - }
2185 -
2186 - $this->db->begin();
2187 - $statusDb = $this->doDBUpdates();
2188 - wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2189 - if ( !$statusDb->isGood() ) {
2190 - $this->db->rollback();
2191 - // Something went wrong with the DB updates, so remove the target files
2192 - $this->cleanupTarget( $triplets );
2193 - $statusDb->ok = false;
2194 - return $statusDb;
2195 - }
2196 - $this->db->commit();
2197 -
2198 - // Everything went ok, remove the source files
2199 - $this->cleanupSource( $triplets );
2200 -
2201 - $status->merge( $statusDb );
2202 - $status->merge( $statusMove );
2203 -
2204 - return $status;
2205 - }
2206 -
2207 - /**
2208 - * Do the database updates and return a new FileRepoStatus indicating how
2209 - * many rows where updated.
2210 - *
2211 - * @return FileRepoStatus
2212 - */
2213 - function doDBUpdates() {
2214 - $repo = $this->file->repo;
2215 - $status = $repo->newGood();
2216 - $dbw = $this->db;
2217 -
2218 - // Update current image
2219 - $dbw->update(
2220 - 'image',
2221 - array( 'img_name' => $this->newName ),
2222 - array( 'img_name' => $this->oldName ),
2223 - __METHOD__
2224 - );
2225 -
2226 - if ( $dbw->affectedRows() ) {
2227 - $status->successCount++;
2228 - } else {
2229 - $status->failCount++;
2230 - $status->fatal( 'imageinvalidfilename' );
2231 - return $status;
2232 - }
2233 -
2234 - // Update old images
2235 - $dbw->update(
2236 - 'oldimage',
2237 - array(
2238 - 'oi_name' => $this->newName,
2239 - 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
2240 - ),
2241 - array( 'oi_name' => $this->oldName ),
2242 - __METHOD__
2243 - );
2244 -
2245 - $affected = $dbw->affectedRows();
2246 - $total = $this->oldCount;
2247 - $status->successCount += $affected;
2248 - $status->failCount += $total - $affected;
2249 - if ( $status->failCount ) {
2250 - $status->error( 'imageinvalidfilename' );
2251 - }
2252 -
2253 - return $status;
2254 - }
2255 -
2256 - /**
2257 - * Generate triplets for FSRepo::storeBatch().
2258 - */
2259 - function getMoveTriplets() {
2260 - $moves = array_merge( array( $this->cur ), $this->olds );
2261 - $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
2262 -
2263 - foreach ( $moves as $move ) {
2264 - // $move: (oldRelativePath, newRelativePath)
2265 - $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
2266 - $triplets[] = array( $srcUrl, 'public', $move[1] );
2267 - wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
2268 - }
2269 -
2270 - return $triplets;
2271 - }
2272 -
2273 - /**
2274 - * Removes non-existent files from move batch.
2275 - */
2276 - function removeNonexistentFiles( $triplets ) {
2277 - $files = array();
2278 -
2279 - foreach ( $triplets as $file ) {
2280 - $files[$file[0]] = $file[0];
2281 - }
2282 -
2283 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
2284 - $filteredTriplets = array();
2285 -
2286 - foreach ( $triplets as $file ) {
2287 - if ( $result[$file[0]] ) {
2288 - $filteredTriplets[] = $file;
2289 - } else {
2290 - wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
2291 - }
2292 - }
2293 -
2294 - return $filteredTriplets;
2295 - }
2296 -
2297 - /**
2298 - * Cleanup a partially moved array of triplets by deleting the target
2299 - * files. Called if something went wrong half way.
2300 - */
2301 - function cleanupTarget( $triplets ) {
2302 - // Create dest pairs from the triplets
2303 - $pairs = array();
2304 - foreach ( $triplets as $triplet ) {
2305 - $pairs[] = array( $triplet[1], $triplet[2] );
2306 - }
2307 -
2308 - $this->file->repo->cleanupBatch( $pairs );
2309 - }
2310 -
2311 - /**
2312 - * Cleanup a fully moved array of triplets by deleting the source files.
2313 - * Called at the end of the move process if everything else went ok.
2314 - */
2315 - function cleanupSource( $triplets ) {
2316 - // Create source file names from the triplets
2317 - $files = array();
2318 - foreach ( $triplets as $triplet ) {
2319 - $files[] = $triplet[0];
2320 - }
2321 -
2322 - $this->file->repo->cleanupBatch( $files );
2323 - }
2324 -}
Index: trunk/phase3/includes/filerepo/ForeignDBFile.php
@@ -1,78 +0,0 @@
2 -<?php
3 -/**
4 - * Foreign file with an accessible MediaWiki database
5 - *
6 - * @file
7 - * @ingroup FileRepo
8 - */
9 -
10 -/**
11 - * Foreign file with an accessible MediaWiki database
12 - *
13 - * @ingroup FileRepo
14 - */
15 -class ForeignDBFile extends LocalFile {
16 -
17 - /**
18 - * @param $title
19 - * @param $repo
20 - * @param $unused
21 - * @return ForeignDBFile
22 - */
23 - static function newFromTitle( $title, $repo, $unused = null ) {
24 - return new self( $title, $repo );
25 - }
26 -
27 - /**
28 - * Create a ForeignDBFile from a title
29 - * Do not call this except from inside a repo class.
30 - *
31 - * @param $row
32 - * @param $repo
33 - *
34 - * @return ForeignDBFile
35 - */
36 - static function newFromRow( $row, $repo ) {
37 - $title = Title::makeTitle( NS_FILE, $row->img_name );
38 - $file = new self( $title, $repo );
39 - $file->loadFromRow( $row );
40 - return $file;
41 - }
42 -
43 - function publish( $srcPath, $flags = 0 ) {
44 - $this->readOnlyError();
45 - }
46 -
47 - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
48 - $watch = false, $timestamp = false ) {
49 - $this->readOnlyError();
50 - }
51 -
52 - function restore( $versions = array(), $unsuppress = false ) {
53 - $this->readOnlyError();
54 - }
55 -
56 - function delete( $reason, $suppress = false ) {
57 - $this->readOnlyError();
58 - }
59 -
60 - function move( $target ) {
61 - $this->readOnlyError();
62 - }
63 -
64 - /**
65 - * @return string
66 - */
67 - function getDescriptionUrl() {
68 - // Restore remote behaviour
69 - return File::getDescriptionUrl();
70 - }
71 -
72 - /**
73 - * @return string
74 - */
75 - function getDescriptionText() {
76 - // Restore remote behaviour
77 - return File::getDescriptionText();
78 - }
79 -}
Index: trunk/phase3/includes/filerepo/UnregisteredLocalFile.php
@@ -1,143 +0,0 @@
2 -<?php
3 -/**
4 - * File without associated database record
5 - *
6 - * @file
7 - * @ingroup FileRepo
8 - */
9 -
10 -/**
11 - * A file object referring to either a standalone local file, or a file in a
12 - * local repository with no database, for example an FSRepo repository.
13 - *
14 - * Read-only.
15 - *
16 - * TODO: Currently it doesn't really work in the repository role, there are
17 - * lots of functions missing. It is used by the WebStore extension in the
18 - * standalone role.
19 - *
20 - * @ingroup FileRepo
21 - */
22 -class UnregisteredLocalFile extends File {
23 - var $title, $path, $mime, $dims;
24 -
25 - /**
26 - * @var MediaHandler
27 - */
28 - var $handler;
29 -
30 - /**
31 - * @param $path
32 - * @param $mime
33 - * @return UnregisteredLocalFile
34 - */
35 - static function newFromPath( $path, $mime ) {
36 - return new self( false, false, $path, $mime );
37 - }
38 -
39 - /**
40 - * @param $title
41 - * @param $repo
42 - * @return UnregisteredLocalFile
43 - */
44 - static function newFromTitle( $title, $repo ) {
45 - return new self( $title, $repo, false, false );
46 - }
47 -
48 - /**
49 - * Create an UnregisteredLocalFile based on a path or a (title,repo) pair.
50 - * A FileRepo object is not required here, unlike most other File classes.
51 - *
52 - * @throws MWException
53 - * @param $title Title|false
54 - * @param $repo FSRepo
55 - * @param $path string
56 - * @param $mime string
57 - */
58 - function __construct( $title = false, $repo = false, $path = false, $mime = false ) {
59 - if ( !( $title && $repo ) && !$path ) {
60 - throw new MWException( __METHOD__.': not enough parameters, must specify title and repo, or a full path' );
61 - }
62 - if ( $title instanceof Title ) {
63 - $this->title = File::normalizeTitle( $title, 'exception' );
64 - $this->name = $repo->getNameFromTitle( $title );
65 - } else {
66 - $this->name = basename( $path );
67 - $this->title = File::normalizeTitle( $this->name, 'exception' );
68 - }
69 - $this->repo = $repo;
70 - if ( $path ) {
71 - $this->path = $path;
72 - } else {
73 - $this->path = $repo->getRootDirectory() . '/' .
74 - $repo->getHashPath( $this->name ) . $this->name;
75 - }
76 - if ( $mime ) {
77 - $this->mime = $mime;
78 - }
79 - $this->dims = array();
80 - }
81 -
82 - private function cachePageDimensions( $page = 1 ) {
83 - if ( !isset( $this->dims[$page] ) ) {
84 - if ( !$this->getHandler() ) {
85 - return false;
86 - }
87 - $this->dims[$page] = $this->handler->getPageDimensions( $this, $page );
88 - }
89 - return $this->dims[$page];
90 - }
91 -
92 - function getWidth( $page = 1 ) {
93 - $dim = $this->cachePageDimensions( $page );
94 - return $dim['width'];
95 - }
96 -
97 - function getHeight( $page = 1 ) {
98 - $dim = $this->cachePageDimensions( $page );
99 - return $dim['height'];
100 - }
101 -
102 - function getMimeType() {
103 - if ( !isset( $this->mime ) ) {
104 - $magic = MimeMagic::singleton();
105 - $this->mime = $magic->guessMimeType( $this->getPath() );
106 - }
107 - return $this->mime;
108 - }
109 -
110 - function getImageSize( $filename ) {
111 - if ( !$this->getHandler() ) {
112 - return false;
113 - }
114 - return $this->handler->getImageSize( $this, $this->getPath() );
115 - }
116 -
117 - function getMetadata() {
118 - if ( !isset( $this->metadata ) ) {
119 - if ( !$this->getHandler() ) {
120 - $this->metadata = false;
121 - } else {
122 - $this->metadata = $this->handler->getMetadata( $this, $this->getPath() );
123 - }
124 - }
125 - return $this->metadata;
126 - }
127 -
128 - function getURL() {
129 - if ( $this->repo ) {
130 - return $this->repo->getZoneUrl( 'public' ) . '/' .
131 - $this->repo->getHashPath( $this->name ) . rawurlencode( $this->name );
132 - } else {
133 - return false;
134 - }
135 - }
136 -
137 - function getSize() {
138 - if ( file_exists( $this->path ) ) {
139 - return filesize( $this->path );
140 - } else {
141 - return false;
142 - }
143 - }
144 -}
Index: trunk/phase3/includes/filerepo/File.php
@@ -1,1683 +0,0 @@
2 -<?php
3 -/**
4 - * Base code for files.
5 - *
6 - * @file
7 - * @ingroup FileRepo
8 - */
9 -
10 -/**
11 - * Implements some public methods and some protected utility functions which
12 - * are required by multiple child classes. Contains stub functionality for
13 - * unimplemented public methods.
14 - *
15 - * Stub functions which should be overridden are marked with STUB. Some more
16 - * concrete functions are also typically overridden by child classes.
17 - *
18 - * Note that only the repo object knows what its file class is called. You should
19 - * never name a file class explictly outside of the repo class. Instead use the
20 - * repo's factory functions to generate file objects, for example:
21 - *
22 - * RepoGroup::singleton()->getLocalRepo()->newFile($title);
23 - *
24 - * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
25 - * in most cases.
26 - *
27 - * @ingroup FileRepo
28 - */
29 -abstract class File {
30 - const DELETED_FILE = 1;
31 - const DELETED_COMMENT = 2;
32 - const DELETED_USER = 4;
33 - const DELETED_RESTRICTED = 8;
34 -
35 - /** Force rendering in the current process */
36 - const RENDER_NOW = 1;
37 - /**
38 - * Force rendering even if thumbnail already exist and using RENDER_NOW
39 - * I.e. you have to pass both flags: File::RENDER_NOW | File::RENDER_FORCE
40 - */
41 - const RENDER_FORCE = 2;
42 -
43 - const DELETE_SOURCE = 1;
44 -
45 - /**
46 - * Some member variables can be lazy-initialised using __get(). The
47 - * initialisation function for these variables is always a function named
48 - * like getVar(), where Var is the variable name with upper-case first
49 - * letter.
50 - *
51 - * The following variables are initialised in this way in this base class:
52 - * name, extension, handler, path, canRender, isSafeFile,
53 - * transformScript, hashPath, pageCount, url
54 - *
55 - * Code within this class should generally use the accessor function
56 - * directly, since __get() isn't re-entrant and therefore causes bugs that
57 - * depend on initialisation order.
58 - */
59 -
60 - /**
61 - * The following member variables are not lazy-initialised
62 - */
63 -
64 - /**
65 - * @var FileRepo|false
66 - */
67 - var $repo;
68 -
69 - /**
70 - * @var Title|false
71 - */
72 - var $title;
73 -
74 - var $lastError, $redirected, $redirectedTitle;
75 -
76 - /**
77 - * @var MediaHandler
78 - */
79 - protected $handler;
80 -
81 - protected $url, $extension, $name, $path, $hashPath, $pageCount, $transformScript;
82 -
83 - /**
84 - * @var bool
85 - */
86 - protected $canRender, $isSafeFile;
87 -
88 - /**
89 - * @var string Required Repository class type
90 - */
91 - protected $repoClass = 'FileRepo';
92 -
93 - /**
94 - * Call this constructor from child classes.
95 - *
96 - * Both $title and $repo are optional, though some functions
97 - * may return false or throw exceptions if they are not set.
98 - * Most subclasses will want to call assertRepoDefined() here.
99 - *
100 - * @param $title Title|string|false
101 - * @param $repo FileRepo|false
102 - */
103 - function __construct( $title, $repo ) {
104 - if ( $title !== false ) { // subclasses may not use MW titles
105 - $title = self::normalizeTitle( $title, 'exception' );
106 - }
107 - $this->title = $title;
108 - $this->repo = $repo;
109 - }
110 -
111 - /**
112 - * Given a string or Title object return either a
113 - * valid Title object with namespace NS_FILE or null
114 - * @param $title Title|string
115 - * @param $exception string|false Use 'exception' to throw an error on bad titles
116 - * @return Title|null
117 - */
118 - static function normalizeTitle( $title, $exception = false ) {
119 - $ret = $title;
120 - if ( $ret instanceof Title ) {
121 - # Normalize NS_MEDIA -> NS_FILE
122 - if ( $ret->getNamespace() == NS_MEDIA ) {
123 - $ret = Title::makeTitleSafe( NS_FILE, $ret->getDBkey() );
124 - # Sanity check the title namespace
125 - } elseif ( $ret->getNamespace() !== NS_FILE ) {
126 - $ret = null;
127 - }
128 - } else {
129 - # Convert strings to Title objects
130 - $ret = Title::makeTitleSafe( NS_FILE, (string)$ret );
131 - }
132 - if ( !$ret && $exception !== false ) {
133 - throw new MWException( "`$title` is not a valid file title." );
134 - }
135 - return $ret;
136 - }
137 -
138 - function __get( $name ) {
139 - $function = array( $this, 'get' . ucfirst( $name ) );
140 - if ( !is_callable( $function ) ) {
141 - return null;
142 - } else {
143 - $this->$name = call_user_func( $function );
144 - return $this->$name;
145 - }
146 - }
147 -
148 - /**
149 - * Normalize a file extension to the common form, and ensure it's clean.
150 - * Extensions with non-alphanumeric characters will be discarded.
151 - *
152 - * @param $ext string (without the .)
153 - * @return string
154 - */
155 - static function normalizeExtension( $ext ) {
156 - $lower = strtolower( $ext );
157 - $squish = array(
158 - 'htm' => 'html',
159 - 'jpeg' => 'jpg',
160 - 'mpeg' => 'mpg',
161 - 'tiff' => 'tif',
162 - 'ogv' => 'ogg' );
163 - if( isset( $squish[$lower] ) ) {
164 - return $squish[$lower];
165 - } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
166 - return $lower;
167 - } else {
168 - return '';
169 - }
170 - }
171 -
172 - /**
173 - * Checks if file extensions are compatible
174 - *
175 - * @param $old File Old file
176 - * @param $new string New name
177 - *
178 - * @return bool|null
179 - */
180 - static function checkExtensionCompatibility( File $old, $new ) {
181 - $oldMime = $old->getMimeType();
182 - $n = strrpos( $new, '.' );
183 - $newExt = self::normalizeExtension(
184 - $n ? substr( $new, $n + 1 ) : '' );
185 - $mimeMagic = MimeMagic::singleton();
186 - return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
187 - }
188 -
189 - /**
190 - * Upgrade the database row if there is one
191 - * Called by ImagePage
192 - * STUB
193 - */
194 - function upgradeRow() {}
195 -
196 - /**
197 - * Split an internet media type into its two components; if not
198 - * a two-part name, set the minor type to 'unknown'.
199 - *
200 - * @param string $mime "text/html" etc
201 - * @return array ("text", "html") etc
202 - */
203 - public static function splitMime( $mime ) {
204 - if( strpos( $mime, '/' ) !== false ) {
205 - return explode( '/', $mime, 2 );
206 - } else {
207 - return array( $mime, 'unknown' );
208 - }
209 - }
210 -
211 - /**
212 - * Return the name of this file
213 - *
214 - * @return string
215 - */
216 - public function getName() {
217 - if ( !isset( $this->name ) ) {
218 - $this->assertRepoDefined();
219 - $this->name = $this->repo->getNameFromTitle( $this->title );
220 - }
221 - return $this->name;
222 - }
223 -
224 - /**
225 - * Get the file extension, e.g. "svg"
226 - *
227 - * @return string
228 - */
229 - function getExtension() {
230 - if ( !isset( $this->extension ) ) {
231 - $n = strrpos( $this->getName(), '.' );
232 - $this->extension = self::normalizeExtension(
233 - $n ? substr( $this->getName(), $n + 1 ) : '' );
234 - }
235 - return $this->extension;
236 - }
237 -
238 - /**
239 - * Return the associated title object
240 - * @return Title|false
241 - */
242 - public function getTitle() { return $this->title; }
243 -
244 - /**
245 - * Return the title used to find this file
246 - *
247 - * @return Title
248 - */
249 - public function getOriginalTitle() {
250 - if ( $this->redirected ) {
251 - return $this->getRedirectedTitle();
252 - }
253 - return $this->title;
254 - }
255 -
256 - /**
257 - * Return the URL of the file
258 - *
259 - * @return string
260 - */
261 - public function getUrl() {
262 - if ( !isset( $this->url ) ) {
263 - $this->assertRepoDefined();
264 - $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel();
265 - }
266 - return $this->url;
267 - }
268 -
269 - /**
270 - * Return a fully-qualified URL to the file.
271 - * Upload URL paths _may or may not_ be fully qualified, so
272 - * we check. Local paths are assumed to belong on $wgServer.
273 - *
274 - * @return String
275 - */
276 - public function getFullUrl() {
277 - return wfExpandUrl( $this->getUrl(), PROTO_RELATIVE );
278 - }
279 -
280 - /**
281 - * @return string
282 - */
283 - public function getCanonicalUrl() {
284 - return wfExpandUrl( $this->getUrl(), PROTO_CANONICAL );
285 - }
286 -
287 - /**
288 - * @return string
289 - */
290 - function getViewURL() {
291 - if( $this->mustRender()) {
292 - if( $this->canRender() ) {
293 - return $this->createThumb( $this->getWidth() );
294 - } else {
295 - wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n");
296 - return $this->getURL(); #hm... return NULL?
297 - }
298 - } else {
299 - return $this->getURL();
300 - }
301 - }
302 -
303 - /**
304 - * Return the full filesystem path to the file. Note that this does
305 - * not mean that a file actually exists under that location.
306 - *
307 - * This path depends on whether directory hashing is active or not,
308 - * i.e. whether the files are all found in the same directory,
309 - * or in hashed paths like /images/3/3c.
310 - *
311 - * Most callers don't check the return value, but ForeignAPIFile::getPath
312 - * returns false.
313 - *
314 - * @return string|false
315 - */
316 - public function getPath() {
317 - if ( !isset( $this->path ) ) {
318 - $this->assertRepoDefined();
319 - $this->path = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
320 - }
321 - return $this->path;
322 - }
323 -
324 - /**
325 - * Return the width of the image. Returns false if the width is unknown
326 - * or undefined.
327 - *
328 - * STUB
329 - * Overridden by LocalFile, UnregisteredLocalFile
330 - *
331 - * @param $page int
332 - *
333 - * @return number
334 - */
335 - public function getWidth( $page = 1 ) {
336 - return false;
337 - }
338 -
339 - /**
340 - * Return the height of the image. Returns false if the height is unknown
341 - * or undefined
342 - *
343 - * STUB
344 - * Overridden by LocalFile, UnregisteredLocalFile
345 - *
346 - * @param $page int
347 - *
348 - * @return false|number
349 - */
350 - public function getHeight( $page = 1 ) {
351 - return false;
352 - }
353 -
354 - /**
355 - * Returns ID or name of user who uploaded the file
356 - * STUB
357 - *
358 - * @param $type string 'text' or 'id'
359 - *
360 - * @return string|int
361 - */
362 - public function getUser( $type = 'text' ) {
363 - return null;
364 - }
365 -
366 - /**
367 - * Get the duration of a media file in seconds
368 - *
369 - * @return number
370 - */
371 - public function getLength() {
372 - $handler = $this->getHandler();
373 - if ( $handler ) {
374 - return $handler->getLength( $this );
375 - } else {
376 - return 0;
377 - }
378 - }
379 -
380 - /**
381 - * Return true if the file is vectorized
382 - *
383 - * @return bool
384 - */
385 - public function isVectorized() {
386 - $handler = $this->getHandler();
387 - if ( $handler ) {
388 - return $handler->isVectorized( $this );
389 - } else {
390 - return false;
391 - }
392 - }
393 -
394 - /**
395 - * Get handler-specific metadata
396 - * Overridden by LocalFile, UnregisteredLocalFile
397 - * STUB
398 - */
399 - public function getMetadata() {
400 - return false;
401 - }
402 -
403 - /**
404 - * get versioned metadata
405 - *
406 - * @param $metadata Mixed Array or String of (serialized) metadata
407 - * @param $version integer version number.
408 - * @return Array containing metadata, or what was passed to it on fail (unserializing if not array)
409 - */
410 - public function convertMetadataVersion($metadata, $version) {
411 - $handler = $this->getHandler();
412 - if ( !is_array( $metadata ) ) {
413 - //just to make the return type consistant
414 - $metadata = unserialize( $metadata );
415 - }
416 - if ( $handler ) {
417 - return $handler->convertMetadataVersion( $metadata, $version );
418 - } else {
419 - return $metadata;
420 - }
421 - }
422 -
423 - /**
424 - * Return the bit depth of the file
425 - * Overridden by LocalFile
426 - * STUB
427 - */
428 - public function getBitDepth() {
429 - return 0;
430 - }
431 -
432 - /**
433 - * Return the size of the image file, in bytes
434 - * Overridden by LocalFile, UnregisteredLocalFile
435 - * STUB
436 - */
437 - public function getSize() {
438 - return false;
439 - }
440 -
441 - /**
442 - * Returns the mime type of the file.
443 - * Overridden by LocalFile, UnregisteredLocalFile
444 - * STUB
445 - *
446 - * @return string
447 - */
448 - function getMimeType() {
449 - return 'unknown/unknown';
450 - }
451 -
452 - /**
453 - * Return the type of the media in the file.
454 - * Use the value returned by this function with the MEDIATYPE_xxx constants.
455 - * Overridden by LocalFile,
456 - * STUB
457 - */
458 - function getMediaType() { return MEDIATYPE_UNKNOWN; }
459 -
460 - /**
461 - * Checks if the output of transform() for this file is likely
462 - * to be valid. If this is false, various user elements will
463 - * display a placeholder instead.
464 - *
465 - * Currently, this checks if the file is an image format
466 - * that can be converted to a format
467 - * supported by all browsers (namely GIF, PNG and JPEG),
468 - * or if it is an SVG image and SVG conversion is enabled.
469 - *
470 - * @return bool
471 - */
472 - function canRender() {
473 - if ( !isset( $this->canRender ) ) {
474 - $this->canRender = $this->getHandler() && $this->handler->canRender( $this );
475 - }
476 - return $this->canRender;
477 - }
478 -
479 - /**
480 - * Accessor for __get()
481 - */
482 - protected function getCanRender() {
483 - return $this->canRender();
484 - }
485 -
486 - /**
487 - * Return true if the file is of a type that can't be directly
488 - * rendered by typical browsers and needs to be re-rasterized.
489 - *
490 - * This returns true for everything but the bitmap types
491 - * supported by all browsers, i.e. JPEG; GIF and PNG. It will
492 - * also return true for any non-image formats.
493 - *
494 - * @return bool
495 - */
496 - function mustRender() {
497 - return $this->getHandler() && $this->handler->mustRender( $this );
498 - }
499 -
500 - /**
501 - * Alias for canRender()
502 - *
503 - * @return bool
504 - */
505 - function allowInlineDisplay() {
506 - return $this->canRender();
507 - }
508 -
509 - /**
510 - * Determines if this media file is in a format that is unlikely to
511 - * contain viruses or malicious content. It uses the global
512 - * $wgTrustedMediaFormats list to determine if the file is safe.
513 - *
514 - * This is used to show a warning on the description page of non-safe files.
515 - * It may also be used to disallow direct [[media:...]] links to such files.
516 - *
517 - * Note that this function will always return true if allowInlineDisplay()
518 - * or isTrustedFile() is true for this file.
519 - *
520 - * @return bool
521 - */
522 - function isSafeFile() {
523 - if ( !isset( $this->isSafeFile ) ) {
524 - $this->isSafeFile = $this->_getIsSafeFile();
525 - }
526 - return $this->isSafeFile;
527 - }
528 -
529 - /**
530 - * Accessor for __get()
531 - *
532 - * @return bool
533 - */
534 - protected function getIsSafeFile() {
535 - return $this->isSafeFile();
536 - }
537 -
538 - /**
539 - * Uncached accessor
540 - *
541 - * @return bool
542 - */
543 - protected function _getIsSafeFile() {
544 - if ( $this->allowInlineDisplay() ) {
545 - return true;
546 - }
547 - if ($this->isTrustedFile()) {
548 - return true;
549 - }
550 -
551 - global $wgTrustedMediaFormats;
552 -
553 - $type = $this->getMediaType();
554 - $mime = $this->getMimeType();
555 - #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n");
556 -
557 - if ( !$type || $type === MEDIATYPE_UNKNOWN ) {
558 - return false; #unknown type, not trusted
559 - }
560 - if ( in_array( $type, $wgTrustedMediaFormats ) ) {
561 - return true;
562 - }
563 -
564 - if ( $mime === "unknown/unknown" ) {
565 - return false; #unknown type, not trusted
566 - }
567 - if ( in_array( $mime, $wgTrustedMediaFormats) ) {
568 - return true;
569 - }
570 -
571 - return false;
572 - }
573 -
574 - /**
575 - * Returns true if the file is flagged as trusted. Files flagged that way
576 - * can be linked to directly, even if that is not allowed for this type of
577 - * file normally.
578 - *
579 - * This is a dummy function right now and always returns false. It could be
580 - * implemented to extract a flag from the database. The trusted flag could be
581 - * set on upload, if the user has sufficient privileges, to bypass script-
582 - * and html-filters. It may even be coupled with cryptographics signatures
583 - * or such.
584 - *
585 - * @return bool
586 - */
587 - function isTrustedFile() {
588 - #this could be implemented to check a flag in the databas,
589 - #look for signatures, etc
590 - return false;
591 - }
592 -
593 - /**
594 - * Returns true if file exists in the repository.
595 - *
596 - * Overridden by LocalFile to avoid unnecessary stat calls.
597 - *
598 - * @return boolean Whether file exists in the repository.
599 - */
600 - public function exists() {
601 - return $this->getPath() && file_exists( $this->path );
602 - }
603 -
604 - /**
605 - * Returns true if file exists in the repository and can be included in a page.
606 - * It would be unsafe to include private images, making public thumbnails inadvertently
607 - *
608 - * @return boolean Whether file exists in the repository and is includable.
609 - */
610 - public function isVisible() {
611 - return $this->exists();
612 - }
613 -
614 - /**
615 - * @return string
616 - */
617 - function getTransformScript() {
618 - if ( !isset( $this->transformScript ) ) {
619 - $this->transformScript = false;
620 - if ( $this->repo ) {
621 - $script = $this->repo->getThumbScriptUrl();
622 - if ( $script ) {
623 - $this->transformScript = "$script?f=" . urlencode( $this->getName() );
624 - }
625 - }
626 - }
627 - return $this->transformScript;
628 - }
629 -
630 - /**
631 - * Get a ThumbnailImage which is the same size as the source
632 - *
633 - * @param $handlerParams array
634 - *
635 - * @return string
636 - */
637 - function getUnscaledThumb( $handlerParams = array() ) {
638 - $hp =& $handlerParams;
639 - $page = isset( $hp['page'] ) ? $hp['page'] : false;
640 - $width = $this->getWidth( $page );
641 - if ( !$width ) {
642 - return $this->iconThumb();
643 - }
644 - $hp['width'] = $width;
645 - return $this->transform( $hp );
646 - }
647 -
648 - /**
649 - * Return the file name of a thumbnail with the specified parameters
650 - *
651 - * @param $params Array: handler-specific parameters
652 - * @private -ish
653 - *
654 - * @return string
655 - */
656 - function thumbName( $params ) {
657 - return $this->generateThumbName( $this->getName(), $params );
658 - }
659 -
660 - /**
661 - * Generate a thumbnail file name from a name and specified parameters
662 - *
663 - * @param string $name
664 - * @param array $params Parameters which will be passed to MediaHandler::makeParamString
665 - *
666 - * @return string
667 - */
668 - function generateThumbName( $name, $params ) {
669 - if ( !$this->getHandler() ) {
670 - return null;
671 - }
672 - $extension = $this->getExtension();
673 - list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params );
674 - $thumbName = $this->handler->makeParamString( $params ) . '-' . $name;
675 - if ( $thumbExt != $extension ) {
676 - $thumbName .= ".$thumbExt";
677 - }
678 - return $thumbName;
679 - }
680 -
681 - /**
682 - * Create a thumbnail of the image having the specified width/height.
683 - * The thumbnail will not be created if the width is larger than the
684 - * image's width. Let the browser do the scaling in this case.
685 - * The thumbnail is stored on disk and is only computed if the thumbnail
686 - * file does not exist OR if it is older than the image.
687 - * Returns the URL.
688 - *
689 - * Keeps aspect ratio of original image. If both width and height are
690 - * specified, the generated image will be no bigger than width x height,
691 - * and will also have correct aspect ratio.
692 - *
693 - * @param $width Integer: maximum width of the generated thumbnail
694 - * @param $height Integer: maximum height of the image (optional)
695 - *
696 - * @return string
697 - */
698 - public function createThumb( $width, $height = -1 ) {
699 - $params = array( 'width' => $width );
700 - if ( $height != -1 ) {
701 - $params['height'] = $height;
702 - }
703 - $thumb = $this->transform( $params );
704 - if( is_null( $thumb ) || $thumb->isError() ) return '';
705 - return $thumb->getUrl();
706 - }
707 -
708 - /**
709 - * Do the work of a transform (from an original into a thumb).
710 - * Contains filesystem-specific functions.
711 - *
712 - * @param $thumbName string: the name of the thumbnail file.
713 - * @param $thumbUrl string: the URL of the thumbnail file.
714 - * @param $params Array: an associative array of handler-specific parameters.
715 - * Typical keys are width, height and page.
716 - * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering
717 - *
718 - * @return MediaTransformOutput | false
719 - */
720 - protected function maybeDoTransform( $thumbName, $thumbUrl, $params, $flags = 0 ) {
721 - global $wgIgnoreImageErrors, $wgThumbnailEpoch;
722 -
723 - $thumbPath = $this->getThumbPath( $thumbName );
724 - if ( $this->repo && $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) {
725 - wfDebug( __METHOD__ . " transformation deferred." );
726 - return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
727 - }
728 -
729 - wfDebug( __METHOD__.": Doing stat for $thumbPath\n" );
730 - $this->migrateThumbFile( $thumbName );
731 - if ( file_exists( $thumbPath ) && !($flags & self::RENDER_FORCE) ) {
732 - $thumbTime = filemtime( $thumbPath );
733 - if ( $thumbTime !== FALSE &&
734 - gmdate( 'YmdHis', $thumbTime ) >= $wgThumbnailEpoch ) {
735 -
736 - return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
737 - }
738 - } elseif( $flags & self::RENDER_FORCE ) {
739 - wfDebug( __METHOD__ . " forcing rendering per flag File::RENDER_FORCE\n" );
740 - }
741 - $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params );
742 -
743 - // Ignore errors if requested
744 - if ( !$thumb ) {
745 - $thumb = null;
746 - } elseif ( $thumb->isError() ) {
747 - $this->lastError = $thumb->toText();
748 - if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) {
749 - $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
750 - }
751 - }
752 -
753 - return $thumb;
754 - }
755 -
756 -
757 - /**
758 - * Transform a media file
759 - *
760 - * @param $params Array: an associative array of handler-specific parameters.
761 - * Typical keys are width, height and page.
762 - * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering
763 - * @return MediaTransformOutput | false
764 - */
765 - function transform( $params, $flags = 0 ) {
766 - global $wgUseSquid;
767 -
768 - wfProfileIn( __METHOD__ );
769 - do {
770 - if ( !$this->canRender() ) {
771 - // not a bitmap or renderable image, don't try.
772 - $thumb = $this->iconThumb();
773 - break;
774 - }
775 -
776 - // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791.
777 - $descriptionUrl = $this->getDescriptionUrl();
778 - if ( $descriptionUrl ) {
779 - $params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL );
780 - }
781 -
782 - $script = $this->getTransformScript();
783 - if ( $script && !($flags & self::RENDER_NOW) ) {
784 - // Use a script to transform on client request, if possible
785 - $thumb = $this->handler->getScriptedTransform( $this, $script, $params );
786 - if( $thumb ) {
787 - break;
788 - }
789 - }
790 -
791 - $normalisedParams = $params;
792 - $this->handler->normaliseParams( $this, $normalisedParams );
793 - $thumbName = $this->thumbName( $normalisedParams );
794 - $thumbUrl = $this->getThumbUrl( $thumbName );
795 -
796 - $thumb = $this->maybeDoTransform( $thumbName, $thumbUrl, $params, $flags );
797 -
798 - // Purge. Useful in the event of Core -> Squid connection failure or squid
799 - // purge collisions from elsewhere during failure. Don't keep triggering for
800 - // "thumbs" which have the main image URL though (bug 13776)
801 - if ( $wgUseSquid && ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL()) ) {
802 - SquidUpdate::purge( array( $thumbUrl ) );
803 - }
804 - } while (false);
805 -
806 - wfProfileOut( __METHOD__ );
807 - return is_object( $thumb ) ? $thumb : false;
808 - }
809 -
810 - /**
811 - * Hook into transform() to allow migration of thumbnail files
812 - * STUB
813 - * Overridden by LocalFile
814 - */
815 - function migrateThumbFile( $thumbName ) {}
816 -
817 - /**
818 - * Get a MediaHandler instance for this file
819 - * @return MediaHandler
820 - */
821 - function getHandler() {
822 - if ( !isset( $this->handler ) ) {
823 - $this->handler = MediaHandler::getHandler( $this->getMimeType() );
824 - }
825 - return $this->handler;
826 - }
827 -
828 - /**
829 - * Get a ThumbnailImage representing a file type icon
830 - * @return ThumbnailImage
831 - */
832 - function iconThumb() {
833 - global $wgStylePath, $wgStyleDirectory;
834 -
835 - $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' );
836 - foreach( $try as $icon ) {
837 - $path = '/common/images/icons/' . $icon;
838 - $filepath = $wgStyleDirectory . $path;
839 - if( file_exists( $filepath ) ) {
840 - return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 );
841 - }
842 - }
843 - return null;
844 - }
845 -
846 - /**
847 - * Get last thumbnailing error.
848 - * Largely obsolete.
849 - */
850 - function getLastError() {
851 - return $this->lastError;
852 - }
853 -
854 - /**
855 - * Get all thumbnail names previously generated for this file
856 - * STUB
857 - * Overridden by LocalFile
858 - */
859 - function getThumbnails() {
860 - return array();
861 - }
862 -
863 - /**
864 - * Purge shared caches such as thumbnails and DB data caching
865 - * STUB
866 - * Overridden by LocalFile
867 - */
868 - function purgeCache() {}
869 -
870 - /**
871 - * Purge the file description page, but don't go after
872 - * pages using the file. Use when modifying file history
873 - * but not the current data.
874 - */
875 - function purgeDescription() {
876 - $title = $this->getTitle();
877 - if ( $title ) {
878 - $title->invalidateCache();
879 - $title->purgeSquid();
880 - }
881 - }
882 -
883 - /**
884 - * Purge metadata and all affected pages when the file is created,
885 - * deleted, or majorly updated.
886 - */
887 - function purgeEverything() {
888 - // Delete thumbnails and refresh file metadata cache
889 - $this->purgeCache();
890 - $this->purgeDescription();
891 -
892 - // Purge cache of all pages using this file
893 - $title = $this->getTitle();
894 - if ( $title ) {
895 - $update = new HTMLCacheUpdate( $title, 'imagelinks' );
896 - $update->doUpdate();
897 - }
898 - }
899 -
900 - /**
901 - * Return a fragment of the history of file.
902 - *
903 - * STUB
904 - * @param $limit integer Limit of rows to return
905 - * @param $start timestamp Only revisions older than $start will be returned
906 - * @param $end timestamp Only revisions newer than $end will be returned
907 - * @param $inc bool Include the endpoints of the time range
908 - *
909 - * @return array
910 - */
911 - function getHistory($limit = null, $start = null, $end = null, $inc=true) {
912 - return array();
913 - }
914 -
915 - /**
916 - * Return the history of this file, line by line. Starts with current version,
917 - * then old versions. Should return an object similar to an image/oldimage
918 - * database row.
919 - *
920 - * STUB
921 - * Overridden in LocalFile
922 - */
923 - public function nextHistoryLine() {
924 - return false;
925 - }
926 -
927 - /**
928 - * Reset the history pointer to the first element of the history.
929 - * Always call this function after using nextHistoryLine() to free db resources
930 - * STUB
931 - * Overridden in LocalFile.
932 - */
933 - public function resetHistory() {}
934 -
935 - /**
936 - * Get the filename hash component of the directory including trailing slash,
937 - * e.g. f/fa/
938 - * If the repository is not hashed, returns an empty string.
939 - *
940 - * @return string
941 - */
942 - function getHashPath() {
943 - if ( !isset( $this->hashPath ) ) {
944 - $this->assertRepoDefined();
945 - $this->hashPath = $this->repo->getHashPath( $this->getName() );
946 - }
947 - return $this->hashPath;
948 - }
949 -
950 - /**
951 - * Get the path of the file relative to the public zone root
952 - *
953 - * @return string
954 - */
955 - function getRel() {
956 - return $this->getHashPath() . $this->getName();
957 - }
958 -
959 - /**
960 - * Get urlencoded relative path of the file
961 - *
962 - * @return string
963 - */
964 - function getUrlRel() {
965 - return $this->getHashPath() . rawurlencode( $this->getName() );
966 - }
967 -
968 - /**
969 - * Get the relative path for an archived file
970 - *
971 - * @param $suffix bool|string if not false, the name of an archived thumbnail file
972 - *
973 - * @return string
974 - */
975 - function getArchiveRel( $suffix = false ) {
976 - $path = 'archive/' . $this->getHashPath();
977 - if ( $suffix === false ) {
978 - $path = substr( $path, 0, -1 );
979 - } else {
980 - $path .= $suffix;
981 - }
982 - return $path;
983 - }
984 -
985 - /**
986 - * Get the relative path for an archived file's thumbs directory
987 - * or a specific thumb if the $suffix is given.
988 - *
989 - * @param $archiveName string the timestamped name of an archived image
990 - * @param $suffix bool|string if not false, the name of a thumbnail file
991 - *
992 - * @return string
993 - */
994 - function getArchiveThumbRel( $archiveName, $suffix = false ) {
995 - $path = 'archive/' . $this->getHashPath() . $archiveName . "/";
996 - if ( $suffix === false ) {
997 - $path = substr( $path, 0, -1 );
998 - } else {
999 - $path .= $suffix;
1000 - }
1001 - return $path;
1002 - }
1003 -
1004 - /**
1005 - * Get the path of the archived file.
1006 - *
1007 - * @param $suffix bool|string if not false, the name of an archived file.
1008 - *
1009 - * @return string
1010 - */
1011 - function getArchivePath( $suffix = false ) {
1012 - $this->assertRepoDefined();
1013 - return $this->repo->getZonePath( 'public' ) . '/' . $this->getArchiveRel( $suffix );
1014 - }
1015 -
1016 - /**
1017 - * Get the path of the archived file's thumbs, or a particular thumb if $suffix is specified
1018 - *
1019 - * @param $archiveName string the timestamped name of an archived image
1020 - * @param $suffix bool|string if not false, the name of a thumbnail file
1021 - *
1022 - * @return string
1023 - */
1024 - function getArchiveThumbPath( $archiveName, $suffix = false ) {
1025 - $this->assertRepoDefined();
1026 - return $this->repo->getZonePath( 'thumb' ) . '/' .
1027 - $this->getArchiveThumbRel( $archiveName, $suffix );
1028 - }
1029 -
1030 - /**
1031 - * Get the path of the thumbnail directory, or a particular file if $suffix is specified
1032 - *
1033 - * @param $suffix bool|string if not false, the name of a thumbnail file
1034 - *
1035 - * @return string
1036 - */
1037 - function getThumbPath( $suffix = false ) {
1038 - $this->assertRepoDefined();
1039 - $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getRel();
1040 - if ( $suffix !== false ) {
1041 - $path .= '/' . $suffix;
1042 - }
1043 - return $path;
1044 - }
1045 -
1046 - /**
1047 - * Get the URL of the archive directory, or a particular file if $suffix is specified
1048 - *
1049 - * @param $suffix bool|string if not false, the name of an archived file
1050 - *
1051 - * @return string
1052 - */
1053 - function getArchiveUrl( $suffix = false ) {
1054 - $this->assertRepoDefined();
1055 - $path = $this->repo->getZoneUrl( 'public' ) . '/archive/' . $this->getHashPath();
1056 - if ( $suffix === false ) {
1057 - $path = substr( $path, 0, -1 );
1058 - } else {
1059 - $path .= rawurlencode( $suffix );
1060 - }
1061 - return $path;
1062 - }
1063 -
1064 - /**
1065 - * Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified
1066 - *
1067 - * @param $archiveName string the timestamped name of an archived image
1068 - * @param $suffix bool|string if not false, the name of a thumbnail file
1069 - *
1070 - * @return string
1071 - */
1072 - function getArchiveThumbUrl( $archiveName, $suffix = false ) {
1073 - $this->assertRepoDefined();
1074 - $path = $this->repo->getZoneUrl( 'thumb' ) . '/archive/' .
1075 - $this->getHashPath() . rawurlencode( $archiveName ) . "/";
1076 - if ( $suffix === false ) {
1077 - $path = substr( $path, 0, -1 );
1078 - } else {
1079 - $path .= rawurlencode( $suffix );
1080 - }
1081 - return $path;
1082 - }
1083 -
1084 - /**
1085 - * Get the URL of the thumbnail directory, or a particular file if $suffix is specified
1086 - *
1087 - * @param $suffix bool|string if not false, the name of a thumbnail file
1088 - *
1089 - * @return path
1090 - */
1091 - function getThumbUrl( $suffix = false ) {
1092 - $this->assertRepoDefined();
1093 - $path = $this->repo->getZoneUrl('thumb') . '/' . $this->getUrlRel();
1094 - if ( $suffix !== false ) {
1095 - $path .= '/' . rawurlencode( $suffix );
1096 - }
1097 - return $path;
1098 - }
1099 -
1100 - /**
1101 - * Get the virtual URL for an archived file's thumbs, or a specific thumb.
1102 - *
1103 - * @param $suffix bool|string if not false, the name of a thumbnail file
1104 - *
1105 - * @return string
1106 - */
1107 - function getArchiveVirtualUrl( $suffix = false ) {
1108 - $this->assertRepoDefined();
1109 - $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath();
1110 - if ( $suffix === false ) {
1111 - $path = substr( $path, 0, -1 );
1112 - } else {
1113 - $path .= rawurlencode( $suffix );
1114 - }
1115 - return $path;
1116 - }
1117 -
1118 - /**
1119 - * Get the virtual URL for a thumbnail file or directory
1120 - *
1121 - * @param $suffix bool|string if not false, the name of a thumbnail file
1122 - *
1123 - * @return string
1124 - */
1125 - function getThumbVirtualUrl( $suffix = false ) {
1126 - $this->assertRepoDefined();
1127 - $path = $this->repo->getVirtualUrl() . '/thumb/' . $this->getUrlRel();
1128 - if ( $suffix !== false ) {
1129 - $path .= '/' . rawurlencode( $suffix );
1130 - }
1131 - return $path;
1132 - }
1133 -
1134 - /**
1135 - * Get the virtual URL for the file itself
1136 - *
1137 - * @param $suffix bool|string if not false, the name of a thumbnail file
1138 - *
1139 - * @return string
1140 - */
1141 - function getVirtualUrl( $suffix = false ) {
1142 - $this->assertRepoDefined();
1143 - $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel();
1144 - if ( $suffix !== false ) {
1145 - $path .= '/' . rawurlencode( $suffix );
1146 - }
1147 - return $path;
1148 - }
1149 -
1150 - /**
1151 - * @return bool
1152 - */
1153 - function isHashed() {
1154 - $this->assertRepoDefined();
1155 - return $this->repo->isHashed();
1156 - }
1157 -
1158 - /**
1159 - * @throws MWException
1160 - */
1161 - function readOnlyError() {
1162 - throw new MWException( get_class($this) . ': write operations are not supported' );
1163 - }
1164 -
1165 - /**
1166 - * Record a file upload in the upload log and the image table
1167 - * STUB
1168 - * Overridden by LocalFile
1169 - * @param $oldver
1170 - * @param $desc
1171 - * @param $license string
1172 - * @param $copyStatus string
1173 - * @param $source string
1174 - * @param $watch bool
1175 - */
1176 - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
1177 - $this->readOnlyError();
1178 - }
1179 -
1180 - /**
1181 - * Move or copy a file to its public location. If a file exists at the
1182 - * destination, move it to an archive. Returns a FileRepoStatus object with
1183 - * the archive name in the "value" member on success.
1184 - *
1185 - * The archive name should be passed through to recordUpload for database
1186 - * registration.
1187 - *
1188 - * @param $srcPath String: local filesystem path to the source image
1189 - * @param $flags Integer: a bitwise combination of:
1190 - * File::DELETE_SOURCE Delete the source file, i.e. move
1191 - * rather than copy
1192 - * @return FileRepoStatus object. On success, the value member contains the
1193 - * archive name, or an empty string if it was a new file.
1194 - *
1195 - * STUB
1196 - * Overridden by LocalFile
1197 - */
1198 - function publish( $srcPath, $flags = 0 ) {
1199 - $this->readOnlyError();
1200 - }
1201 -
1202 - /**
1203 - * @return bool
1204 - */
1205 - function formatMetadata() {
1206 - if ( !$this->getHandler() ) {
1207 - return false;
1208 - }
1209 - return $this->getHandler()->formatMetadata( $this, $this->getMetadata() );
1210 - }
1211 -
1212 - /**
1213 - * Returns true if the file comes from the local file repository.
1214 - *
1215 - * @return bool
1216 - */
1217 - function isLocal() {
1218 - $repo = $this->getRepo();
1219 - return $repo && $repo->isLocal();
1220 - }
1221 -
1222 - /**
1223 - * Returns the name of the repository.
1224 - *
1225 - * @return string
1226 - */
1227 - function getRepoName() {
1228 - return $this->repo ? $this->repo->getName() : 'unknown';
1229 - }
1230 -
1231 - /**
1232 - * Returns the repository
1233 - *
1234 - * @return FileRepo|false
1235 - */
1236 - function getRepo() {
1237 - return $this->repo;
1238 - }
1239 -
1240 - /**
1241 - * Returns true if the image is an old version
1242 - * STUB
1243 - *
1244 - * @return bool
1245 - */
1246 - function isOld() {
1247 - return false;
1248 - }
1249 -
1250 - /**
1251 - * Is this file a "deleted" file in a private archive?
1252 - * STUB
1253 - *
1254 - * @param $field
1255 - *
1256 - * @return bool
1257 - */
1258 - function isDeleted( $field ) {
1259 - return false;
1260 - }
1261 -
1262 - /**
1263 - * Return the deletion bitfield
1264 - * STUB
1265 - */
1266 - function getVisibility() {
1267 - return 0;
1268 - }
1269 -
1270 - /**
1271 - * Was this file ever deleted from the wiki?
1272 - *
1273 - * @return bool
1274 - */
1275 - function wasDeleted() {
1276 - $title = $this->getTitle();
1277 - return $title && $title->isDeletedQuick();
1278 - }
1279 -
1280 - /**
1281 - * Move file to the new title
1282 - *
1283 - * Move current, old version and all thumbnails
1284 - * to the new filename. Old file is deleted.
1285 - *
1286 - * Cache purging is done; checks for validity
1287 - * and logging are caller's responsibility
1288 - *
1289 - * @param $target Title New file name
1290 - * @return FileRepoStatus object.
1291 - */
1292 - function move( $target ) {
1293 - $this->readOnlyError();
1294 - }
1295 -
1296 - /**
1297 - * Delete all versions of the file.
1298 - *
1299 - * Moves the files into an archive directory (or deletes them)
1300 - * and removes the database rows.
1301 - *
1302 - * Cache purging is done; logging is caller's responsibility.
1303 - *
1304 - * @param $reason String
1305 - * @param $suppress Boolean: hide content from sysops?
1306 - * @return true on success, false on some kind of failure
1307 - * STUB
1308 - * Overridden by LocalFile
1309 - */
1310 - function delete( $reason, $suppress = false ) {
1311 - $this->readOnlyError();
1312 - }
1313 -
1314 - /**
1315 - * Restore all or specified deleted revisions to the given file.
1316 - * Permissions and logging are left to the caller.
1317 - *
1318 - * May throw database exceptions on error.
1319 - *
1320 - * @param $versions array set of record ids of deleted items to restore,
1321 - * or empty to restore all revisions.
1322 - * @param $unsuppress bool remove restrictions on content upon restoration?
1323 - * @return int|false the number of file revisions restored if successful,
1324 - * or false on failure
1325 - * STUB
1326 - * Overridden by LocalFile
1327 - */
1328 - function restore( $versions = array(), $unsuppress = false ) {
1329 - $this->readOnlyError();
1330 - }
1331 -
1332 - /**
1333 - * Returns 'true' if this file is a type which supports multiple pages,
1334 - * e.g. DJVU or PDF. Note that this may be true even if the file in
1335 - * question only has a single page.
1336 - *
1337 - * @return Bool
1338 - */
1339 - function isMultipage() {
1340 - return $this->getHandler() && $this->handler->isMultiPage( $this );
1341 - }
1342 -
1343 - /**
1344 - * Returns the number of pages of a multipage document, or false for
1345 - * documents which aren't multipage documents
1346 - *
1347 - * @return false|int
1348 - */
1349 - function pageCount() {
1350 - if ( !isset( $this->pageCount ) ) {
1351 - if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
1352 - $this->pageCount = $this->handler->pageCount( $this );
1353 - } else {
1354 - $this->pageCount = false;
1355 - }
1356 - }
1357 - return $this->pageCount;
1358 - }
1359 -
1360 - /**
1361 - * Calculate the height of a thumbnail using the source and destination width
1362 - *
1363 - * @param $srcWidth
1364 - * @param $srcHeight
1365 - * @param $dstWidth
1366 - *
1367 - * @return int
1368 - */
1369 - static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) {
1370 - // Exact integer multiply followed by division
1371 - if ( $srcWidth == 0 ) {
1372 - return 0;
1373 - } else {
1374 - return round( $srcHeight * $dstWidth / $srcWidth );
1375 - }
1376 - }
1377 -
1378 - /**
1379 - * Get an image size array like that returned by getImageSize(), or false if it
1380 - * can't be determined.
1381 - *
1382 - * @param $fileName String: The filename
1383 - * @return Array
1384 - */
1385 - function getImageSize( $fileName ) {
1386 - if ( !$this->getHandler() ) {
1387 - return false;
1388 - }
1389 - return $this->handler->getImageSize( $this, $fileName );
1390 - }
1391 -
1392 - /**
1393 - * Get the URL of the image description page. May return false if it is
1394 - * unknown or not applicable.
1395 - *
1396 - * @return string
1397 - */
1398 - function getDescriptionUrl() {
1399 - if ( $this->repo ) {
1400 - return $this->repo->getDescriptionUrl( $this->getName() );
1401 - } else {
1402 - return false;
1403 - }
1404 - }
1405 -
1406 - /**
1407 - * Get the HTML text of the description page, if available
1408 - *
1409 - * @return string
1410 - */
1411 - function getDescriptionText() {
1412 - global $wgMemc, $wgLang;
1413 - if ( !$this->repo || !$this->repo->fetchDescription ) {
1414 - return false;
1415 - }
1416 - $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgLang->getCode() );
1417 - if ( $renderUrl ) {
1418 - if ( $this->repo->descriptionCacheExpiry > 0 ) {
1419 - wfDebug("Attempting to get the description from cache...");
1420 - $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(),
1421 - $this->getName() );
1422 - $obj = $wgMemc->get($key);
1423 - if ($obj) {
1424 - wfDebug("success!\n");
1425 - return $obj;
1426 - }
1427 - wfDebug("miss\n");
1428 - }
1429 - wfDebug( "Fetching shared description from $renderUrl\n" );
1430 - $res = Http::get( $renderUrl );
1431 - if ( $res && $this->repo->descriptionCacheExpiry > 0 ) {
1432 - $wgMemc->set( $key, $res, $this->repo->descriptionCacheExpiry );
1433 - }
1434 - return $res;
1435 - } else {
1436 - return false;
1437 - }
1438 - }
1439 -
1440 - /**
1441 - * Get discription of file revision
1442 - * STUB
1443 - *
1444 - * @return string
1445 - */
1446 - function getDescription() {
1447 - return null;
1448 - }
1449 -
1450 - /**
1451 - * Get the 14-character timestamp of the file upload, or false if
1452 - * it doesn't exist
1453 - *
1454 - * @return string
1455 - */
1456 - function getTimestamp() {
1457 - $path = $this->getPath();
1458 - if ( !file_exists( $path ) ) {
1459 - return false;
1460 - }
1461 - return wfTimestamp( TS_MW, filemtime( $path ) );
1462 - }
1463 -
1464 - /**
1465 - * Get the SHA-1 base 36 hash of the file
1466 - *
1467 - * @return string
1468 - */
1469 - function getSha1() {
1470 - return self::sha1Base36( $this->getPath() );
1471 - }
1472 -
1473 - /**
1474 - * Get the deletion archive key, <sha1>.<ext>
1475 - *
1476 - * @return string
1477 - */
1478 - function getStorageKey() {
1479 - $hash = $this->getSha1();
1480 - if ( !$hash ) {
1481 - return false;
1482 - }
1483 - $ext = $this->getExtension();
1484 - $dotExt = $ext === '' ? '' : ".$ext";
1485 - return $hash . $dotExt;
1486 - }
1487 -
1488 - /**
1489 - * Determine if the current user is allowed to view a particular
1490 - * field of this file, if it's marked as deleted.
1491 - * STUB
1492 - * @param $field Integer
1493 - * @param $user User object to check, or null to use $wgUser
1494 - * @return Boolean
1495 - */
1496 - function userCan( $field, User $user = null ) {
1497 - return true;
1498 - }
1499 -
1500 - /**
1501 - * Get an associative array containing information about a file in the local filesystem.
1502 - *
1503 - * @param $path String: absolute local filesystem path
1504 - * @param $ext Mixed: the file extension, or true to extract it from the filename.
1505 - * Set it to false to ignore the extension.
1506 - *
1507 - * @return array
1508 - */
1509 - static function getPropsFromPath( $path, $ext = true ) {
1510 - wfProfileIn( __METHOD__ );
1511 - wfDebug( __METHOD__.": Getting file info for $path\n" );
1512 - $info = array(
1513 - 'fileExists' => file_exists( $path ) && !is_dir( $path )
1514 - );
1515 - $gis = false;
1516 - if ( $info['fileExists'] ) {
1517 - $magic = MimeMagic::singleton();
1518 -
1519 - if ( $ext === true ) {
1520 - $i = strrpos( $path, '.' );
1521 - $ext = strtolower( $i ? substr( $path, $i + 1 ) : '' );
1522 - }
1523 -
1524 - # mime type according to file contents
1525 - $info['file-mime'] = $magic->guessMimeType( $path, false );
1526 - # logical mime type
1527 - $info['mime'] = $magic->improveTypeFromExtension( $info['file-mime'], $ext );
1528 -
1529 - list( $info['major_mime'], $info['minor_mime'] ) = self::splitMime( $info['mime'] );
1530 - $info['media_type'] = $magic->getMediaType( $path, $info['mime'] );
1531 -
1532 - # Get size in bytes
1533 - $info['size'] = filesize( $path );
1534 -
1535 - # Height, width and metadata
1536 - $handler = MediaHandler::getHandler( $info['mime'] );
1537 - if ( $handler ) {
1538 - $tempImage = (object)array();
1539 - $info['metadata'] = $handler->getMetadata( $tempImage, $path );
1540 - $gis = $handler->getImageSize( $tempImage, $path, $info['metadata'] );
1541 - } else {
1542 - $gis = false;
1543 - $info['metadata'] = '';
1544 - }
1545 - $info['sha1'] = self::sha1Base36( $path );
1546 -
1547 - wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
1548 - } else {
1549 - $info['mime'] = null;
1550 - $info['media_type'] = MEDIATYPE_UNKNOWN;
1551 - $info['metadata'] = '';
1552 - $info['sha1'] = '';
1553 - wfDebug(__METHOD__.": $path NOT FOUND!\n");
1554 - }
1555 - if( $gis ) {
1556 - # NOTE: $gis[2] contains a code for the image type. This is no longer used.
1557 - $info['width'] = $gis[0];
1558 - $info['height'] = $gis[1];
1559 - if ( isset( $gis['bits'] ) ) {
1560 - $info['bits'] = $gis['bits'];
1561 - } else {
1562 - $info['bits'] = 0;
1563 - }
1564 - } else {
1565 - $info['width'] = 0;
1566 - $info['height'] = 0;
1567 - $info['bits'] = 0;
1568 - }
1569 - wfProfileOut( __METHOD__ );
1570 - return $info;
1571 - }
1572 -
1573 - /**
1574 - * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
1575 - * encoding, zero padded to 31 digits.
1576 - *
1577 - * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
1578 - * fairly neatly.
1579 - *
1580 - * @param $path string
1581 - *
1582 - * @return false|string False on failure
1583 - */
1584 - static function sha1Base36( $path ) {
1585 - wfSuppressWarnings();
1586 - $hash = sha1_file( $path );
1587 - wfRestoreWarnings();
1588 - if ( $hash === false ) {
1589 - return false;
1590 - } else {
1591 - return wfBaseConvert( $hash, 16, 36, 31 );
1592 - }
1593 - }
1594 -
1595 - /**
1596 - * @return string
1597 - */
1598 - function getLongDesc() {
1599 - $handler = $this->getHandler();
1600 - if ( $handler ) {
1601 - return $handler->getLongDesc( $this );
1602 - } else {
1603 - return MediaHandler::getGeneralLongDesc( $this );
1604 - }
1605 - }
1606 -
1607 - /**
1608 - * @return string
1609 - */
1610 - function getShortDesc() {
1611 - $handler = $this->getHandler();
1612 - if ( $handler ) {
1613 - return $handler->getShortDesc( $this );
1614 - } else {
1615 - return MediaHandler::getGeneralShortDesc( $this );
1616 - }
1617 - }
1618 -
1619 - /**
1620 - * @return string
1621 - */
1622 - function getDimensionsString() {
1623 - $handler = $this->getHandler();
1624 - if ( $handler ) {
1625 - return $handler->getDimensionsString( $this );
1626 - } else {
1627 - return '';
1628 - }
1629 - }
1630 -
1631 - /**
1632 - * @return
1633 - */
1634 - function getRedirected() {
1635 - return $this->redirected;
1636 - }
1637 -
1638 - /**
1639 - * @return Title
1640 - */
1641 - function getRedirectedTitle() {
1642 - if ( $this->redirected ) {
1643 - if ( !$this->redirectTitle ) {
1644 - $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected );
1645 - }
1646 - return $this->redirectTitle;
1647 - }
1648 - }
1649 -
1650 - /**
1651 - * @param $from
1652 - * @return void
1653 - */
1654 - function redirectedFrom( $from ) {
1655 - $this->redirected = $from;
1656 - }
1657 -
1658 - /**
1659 - * @return bool
1660 - */
1661 - function isMissing() {
1662 - return false;
1663 - }
1664 -
1665 - /**
1666 - * Assert that $this->repo is set to a valid FileRepo instance
1667 - * @throws MWException
1668 - */
1669 - protected function assertRepoDefined() {
1670 - if ( !( $this->repo instanceof $this->repoClass ) ) {
1671 - throw new MWException( "A {$this->repoClass} object is not set for this File.\n" );
1672 - }
1673 - }
1674 -
1675 - /**
1676 - * Assert that $this->title is set to a Title
1677 - * @throws MWException
1678 - */
1679 - protected function assertTitleDefined() {
1680 - if ( !( $this->title instanceof Title ) ) {
1681 - throw new MWException( "A Title object is not set for this File.\n" );
1682 - }
1683 - }
1684 -}
Index: trunk/phase3/includes/filerepo/ForeignAPIFile.php
@@ -1,242 +0,0 @@
2 -<?php
3 -/**
4 - * Foreign file accessible through api.php requests.
5 - *
6 - * @file
7 - * @ingroup FileRepo
8 - */
9 -
10 -/**
11 - * Foreign file accessible through api.php requests.
12 - * Very hacky and inefficient, do not use :D
13 - *
14 - * @ingroup FileRepo
15 - */
16 -class ForeignAPIFile extends File {
17 -
18 - private $mExists;
19 -
20 - protected $repoClass = 'ForeignApiRepo';
21 -
22 - /**
23 - * @param $title
24 - * @param $repo ForeignApiRepo
25 - * @param $info
26 - * @param bool $exists
27 - */
28 - function __construct( $title, $repo, $info, $exists = false ) {
29 - parent::__construct( $title, $repo );
30 -
31 - $this->mInfo = $info;
32 - $this->mExists = $exists;
33 -
34 - $this->assertRepoDefined();
35 - }
36 -
37 - /**
38 - * @param $title Title
39 - * @param $repo ForeignApiRepo
40 - * @return ForeignAPIFile|null
41 - */
42 - static function newFromTitle( Title $title, $repo ) {
43 - $data = $repo->fetchImageQuery( array(
44 - 'titles' => 'File:' . $title->getDBKey(),
45 - 'iiprop' => self::getProps(),
46 - 'prop' => 'imageinfo',
47 - 'iimetadataversion' => MediaHandler::getMetadataVersion()
48 - ) );
49 -
50 - $info = $repo->getImageInfo( $data );
51 -
52 - if( $info ) {
53 - $lastRedirect = isset( $data['query']['redirects'] )
54 - ? count( $data['query']['redirects'] ) - 1
55 - : -1;
56 - if( $lastRedirect >= 0 ) {
57 - $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to']);
58 - $img = new self( $newtitle, $repo, $info, true );
59 - if( $img ) {
60 - $img->redirectedFrom( $title->getDBkey() );
61 - }
62 - } else {
63 - $img = new self( $title, $repo, $info, true );
64 - }
65 - return $img;
66 - } else {
67 - return null;
68 - }
69 - }
70 -
71 - /**
72 - * Get the property string for iiprop and aiprop
73 - */
74 - static function getProps() {
75 - return 'timestamp|user|comment|url|size|sha1|metadata|mime';
76 - }
77 -
78 - // Dummy functions...
79 - public function exists() {
80 - return $this->mExists;
81 - }
82 -
83 - public function getPath() {
84 - return false;
85 - }
86 -
87 - function transform( $params, $flags = 0 ) {
88 - if( !$this->canRender() ) {
89 - // show icon
90 - return parent::transform( $params, $flags );
91 - }
92 -
93 - // Note, the this->canRender() check above implies
94 - // that we have a handler, and it can do makeParamString.
95 - $otherParams = $this->handler->makeParamString( $params );
96 -
97 - $thumbUrl = $this->repo->getThumbUrlFromCache(
98 - $this->getName(),
99 - isset( $params['width'] ) ? $params['width'] : -1,
100 - isset( $params['height'] ) ? $params['height'] : -1,
101 - $otherParams );
102 - return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );
103 - }
104 -
105 - // Info we can get from API...
106 - public function getWidth( $page = 1 ) {
107 - return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0;
108 - }
109 -
110 - /**
111 - * @param $page int
112 - * @return int
113 - */
114 - public function getHeight( $page = 1 ) {
115 - return isset( $this->mInfo['height'] ) ? intval( $this->mInfo['height'] ) : 0;
116 - }
117 -
118 - public function getMetadata() {
119 - if ( isset( $this->mInfo['metadata'] ) ) {
120 - return serialize( self::parseMetadata( $this->mInfo['metadata'] ) );
121 - }
122 - return null;
123 - }
124 -
125 - public static function parseMetadata( $metadata ) {
126 - if( !is_array( $metadata ) ) {
127 - return $metadata;
128 - }
129 - $ret = array();
130 - foreach( $metadata as $meta ) {
131 - $ret[ $meta['name'] ] = self::parseMetadata( $meta['value'] );
132 - }
133 - return $ret;
134 - }
135 -
136 - public function getSize() {
137 - return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null;
138 - }
139 -
140 - public function getUrl() {
141 - return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null;
142 - }
143 -
144 - public function getUser( $method='text' ) {
145 - return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null;
146 - }
147 -
148 - public function getDescription() {
149 - return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null;
150 - }
151 -
152 - function getSha1() {
153 - return isset( $this->mInfo['sha1'] ) ?
154 - wfBaseConvert( strval( $this->mInfo['sha1'] ), 16, 36, 31 ) :
155 - null;
156 - }
157 -
158 - function getTimestamp() {
159 - return wfTimestamp( TS_MW,
160 - isset( $this->mInfo['timestamp'] ) ?
161 - strval( $this->mInfo['timestamp'] ) :
162 - null
163 - );
164 - }
165 -
166 - function getMimeType() {
167 - if( !isset( $this->mInfo['mime'] ) ) {
168 - $magic = MimeMagic::singleton();
169 - $this->mInfo['mime'] = $magic->guessTypesForExtension( $this->getExtension() );
170 - }
171 - return $this->mInfo['mime'];
172 - }
173 -
174 - /// @todo FIXME: May guess wrong on file types that can be eg audio or video
175 - function getMediaType() {
176 - $magic = MimeMagic::singleton();
177 - return $magic->getMediaType( null, $this->getMimeType() );
178 - }
179 -
180 - function getDescriptionUrl() {
181 - return isset( $this->mInfo['descriptionurl'] )
182 - ? $this->mInfo['descriptionurl']
183 - : false;
184 - }
185 -
186 - /**
187 - * Only useful if we're locally caching thumbs anyway...
188 - */
189 - function getThumbPath( $suffix = '' ) {
190 - if ( $this->repo->canCacheThumbs() ) {
191 - $path = $this->repo->getZonePath('thumb') . '/' . $this->getHashPath( $this->getName() );
192 - if ( $suffix ) {
193 - $path = $path . $suffix . '/';
194 - }
195 - return $path;
196 - } else {
197 - return null;
198 - }
199 - }
200 -
201 - function getThumbnails() {
202 - $files = array();
203 - $dir = $this->getThumbPath( $this->getName() );
204 - if ( is_dir( $dir ) ) {
205 - $handle = opendir( $dir );
206 - if ( $handle ) {
207 - while ( false !== ( $file = readdir($handle) ) ) {
208 - if ( $file[0] != '.' ) {
209 - $files[] = $file;
210 - }
211 - }
212 - closedir( $handle );
213 - }
214 - }
215 - return $files;
216 - }
217 -
218 - function purgeCache() {
219 - $this->purgeThumbnails();
220 - $this->purgeDescriptionPage();
221 - }
222 -
223 - function purgeDescriptionPage() {
224 - global $wgMemc, $wgContLang;
225 - $url = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() );
226 - $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5($url) );
227 - $wgMemc->delete( $key );
228 - }
229 -
230 - function purgeThumbnails() {
231 - global $wgMemc;
232 - $key = $this->repo->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() );
233 - $wgMemc->delete( $key );
234 - $files = $this->getThumbnails();
235 - $dir = $this->getThumbPath( $this->getName() );
236 - foreach ( $files as $file ) {
237 - unlink( $dir . $file );
238 - }
239 - if ( is_dir( $dir ) ) {
240 - rmdir( $dir ); // Might have already gone away, spews errors if we don't.
241 - }
242 - }
243 -}
Index: trunk/phase3/includes/filerepo/ArchivedFile.php
@@ -1,471 +0,0 @@
2 -<?php
3 -/**
4 - * Deleted file in the 'filearchive' table
5 - *
6 - * @file
7 - * @ingroup FileRepo
8 - */
9 -
10 -/**
11 - * Class representing a row of the 'filearchive' table
12 - *
13 - * @ingroup FileRepo
14 - */
15 -class ArchivedFile {
16 - /**#@+
17 - * @private
18 - */
19 - var $id, # filearchive row ID
20 - $name, # image name
21 - $group, # FileStore storage group
22 - $key, # FileStore sha1 key
23 - $size, # file dimensions
24 - $bits, # size in bytes
25 - $width, # width
26 - $height, # height
27 - $metadata, # metadata string
28 - $mime, # mime type
29 - $media_type, # media type
30 - $description, # upload description
31 - $user, # user ID of uploader
32 - $user_text, # user name of uploader
33 - $timestamp, # time of upload
34 - $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
35 - $deleted, # Bitfield akin to rev_deleted
36 - $pageCount,
37 - $archive_name;
38 -
39 - /**
40 - * @var MediaHandler
41 - */
42 - var $handler;
43 - /**
44 - * @var Title
45 - */
46 - var $title; # image title
47 -
48 - /**#@-*/
49 -
50 - /**
51 - * @throws MWException
52 - * @param Title $title
53 - * @param int $id
54 - * @param string $key
55 - */
56 - function __construct( $title, $id=0, $key='' ) {
57 - $this->id = -1;
58 - $this->title = false;
59 - $this->name = false;
60 - $this->group = 'deleted'; // needed for direct use of constructor
61 - $this->key = '';
62 - $this->size = 0;
63 - $this->bits = 0;
64 - $this->width = 0;
65 - $this->height = 0;
66 - $this->metadata = '';
67 - $this->mime = "unknown/unknown";
68 - $this->media_type = '';
69 - $this->description = '';
70 - $this->user = 0;
71 - $this->user_text = '';
72 - $this->timestamp = null;
73 - $this->deleted = 0;
74 - $this->dataLoaded = false;
75 - $this->exists = false;
76 -
77 - if( $title instanceof Title ) {
78 - $this->title = File::normalizeTitle( $title, 'exception' );
79 - $this->name = $title->getDBkey();
80 - }
81 -
82 - if ($id) {
83 - $this->id = $id;
84 - }
85 -
86 - if ($key) {
87 - $this->key = $key;
88 - }
89 -
90 - if ( !$id && !$key && !( $title instanceof Title ) ) {
91 - throw new MWException( "No specifications provided to ArchivedFile constructor." );
92 - }
93 - }
94 -
95 - /**
96 - * Loads a file object from the filearchive table
97 - * @return true on success or null
98 - */
99 - public function load() {
100 - if ( $this->dataLoaded ) {
101 - return true;
102 - }
103 - $conds = array();
104 -
105 - if( $this->id > 0 ) {
106 - $conds['fa_id'] = $this->id;
107 - }
108 - if( $this->key ) {
109 - $conds['fa_storage_group'] = $this->group;
110 - $conds['fa_storage_key'] = $this->key;
111 - }
112 - if( $this->title ) {
113 - $conds['fa_name'] = $this->title->getDBkey();
114 - }
115 -
116 - if( !count($conds)) {
117 - throw new MWException( "No specific information for retrieving archived file" );
118 - }
119 -
120 - if( !$this->title || $this->title->getNamespace() == NS_FILE ) {
121 - $dbr = wfGetDB( DB_SLAVE );
122 - $res = $dbr->select( 'filearchive',
123 - array(
124 - 'fa_id',
125 - 'fa_name',
126 - 'fa_archive_name',
127 - 'fa_storage_key',
128 - 'fa_storage_group',
129 - 'fa_size',
130 - 'fa_bits',
131 - 'fa_width',
132 - 'fa_height',
133 - 'fa_metadata',
134 - 'fa_media_type',
135 - 'fa_major_mime',
136 - 'fa_minor_mime',
137 - 'fa_description',
138 - 'fa_user',
139 - 'fa_user_text',
140 - 'fa_timestamp',
141 - 'fa_deleted' ),
142 - $conds,
143 - __METHOD__,
144 - array( 'ORDER BY' => 'fa_timestamp DESC' ) );
145 - if ( $res == false || $dbr->numRows( $res ) == 0 ) {
146 - // this revision does not exist?
147 - return;
148 - }
149 - $ret = $dbr->resultObject( $res );
150 - $row = $ret->fetchObject();
151 -
152 - // initialize fields for filestore image object
153 - $this->id = intval($row->fa_id);
154 - $this->name = $row->fa_name;
155 - $this->archive_name = $row->fa_archive_name;
156 - $this->group = $row->fa_storage_group;
157 - $this->key = $row->fa_storage_key;
158 - $this->size = $row->fa_size;
159 - $this->bits = $row->fa_bits;
160 - $this->width = $row->fa_width;
161 - $this->height = $row->fa_height;
162 - $this->metadata = $row->fa_metadata;
163 - $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
164 - $this->media_type = $row->fa_media_type;
165 - $this->description = $row->fa_description;
166 - $this->user = $row->fa_user;
167 - $this->user_text = $row->fa_user_text;
168 - $this->timestamp = $row->fa_timestamp;
169 - $this->deleted = $row->fa_deleted;
170 - } else {
171 - throw new MWException( 'This title does not correspond to an image page.' );
172 - }
173 - $this->dataLoaded = true;
174 - $this->exists = true;
175 -
176 - return true;
177 - }
178 -
179 - /**
180 - * Loads a file object from the filearchive table
181 - *
182 - * @param $row
183 - *
184 - * @return ArchivedFile
185 - */
186 - public static function newFromRow( $row ) {
187 - $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
188 -
189 - $file->id = intval($row->fa_id);
190 - $file->name = $row->fa_name;
191 - $file->archive_name = $row->fa_archive_name;
192 - $file->group = $row->fa_storage_group;
193 - $file->key = $row->fa_storage_key;
194 - $file->size = $row->fa_size;
195 - $file->bits = $row->fa_bits;
196 - $file->width = $row->fa_width;
197 - $file->height = $row->fa_height;
198 - $file->metadata = $row->fa_metadata;
199 - $file->mime = "$row->fa_major_mime/$row->fa_minor_mime";
200 - $file->media_type = $row->fa_media_type;
201 - $file->description = $row->fa_description;
202 - $file->user = $row->fa_user;
203 - $file->user_text = $row->fa_user_text;
204 - $file->timestamp = $row->fa_timestamp;
205 - $file->deleted = $row->fa_deleted;
206 -
207 - return $file;
208 - }
209 -
210 - /**
211 - * Return the associated title object
212 - *
213 - * @return Title
214 - */
215 - public function getTitle() {
216 - return $this->title;
217 - }
218 -
219 - /**
220 - * Return the file name
221 - *
222 - * @return string
223 - */
224 - public function getName() {
225 - return $this->name;
226 - }
227 -
228 - /**
229 - * @return int
230 - */
231 - public function getID() {
232 - $this->load();
233 - return $this->id;
234 - }
235 -
236 - /**
237 - * @return bool
238 - */
239 - public function exists() {
240 - $this->load();
241 - return $this->exists;
242 - }
243 -
244 - /**
245 - * Return the FileStore key
246 - * @return string
247 - */
248 - public function getKey() {
249 - $this->load();
250 - return $this->key;
251 - }
252 -
253 - /**
254 - * Return the FileStore key (overriding base File class)
255 - * @return string
256 - */
257 - public function getStorageKey() {
258 - return $this->getKey();
259 - }
260 -
261 - /**
262 - * Return the FileStore storage group
263 - * @return string
264 - */
265 - public function getGroup() {
266 - return $this->group;
267 - }
268 -
269 - /**
270 - * Return the width of the image
271 - * @return int
272 - */
273 - public function getWidth() {
274 - $this->load();
275 - return $this->width;
276 - }
277 -
278 - /**
279 - * Return the height of the image
280 - * @return int
281 - */
282 - public function getHeight() {
283 - $this->load();
284 - return $this->height;
285 - }
286 -
287 - /**
288 - * Get handler-specific metadata
289 - * @return string
290 - */
291 - public function getMetadata() {
292 - $this->load();
293 - return $this->metadata;
294 - }
295 -
296 - /**
297 - * Return the size of the image file, in bytes
298 - * @return int
299 - */
300 - public function getSize() {
301 - $this->load();
302 - return $this->size;
303 - }
304 -
305 - /**
306 - * Return the bits of the image file, in bytes
307 - * @return int
308 - */
309 - public function getBits() {
310 - $this->load();
311 - return $this->bits;
312 - }
313 -
314 - /**
315 - * Returns the mime type of the file.
316 - * @return string
317 - */
318 - public function getMimeType() {
319 - $this->load();
320 - return $this->mime;
321 - }
322 -
323 - /**
324 - * Get a MediaHandler instance for this file
325 - * @return MediaHandler
326 - */
327 - function getHandler() {
328 - if ( !isset( $this->handler ) ) {
329 - $this->handler = MediaHandler::getHandler( $this->getMimeType() );
330 - }
331 - return $this->handler;
332 - }
333 -
334 - /**
335 - * Returns the number of pages of a multipage document, or false for
336 - * documents which aren't multipage documents
337 - */
338 - function pageCount() {
339 - if ( !isset( $this->pageCount ) ) {
340 - if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
341 - $this->pageCount = $this->handler->pageCount( $this );
342 - } else {
343 - $this->pageCount = false;
344 - }
345 - }
346 - return $this->pageCount;
347 - }
348 -
349 - /**
350 - * Return the type of the media in the file.
351 - * Use the value returned by this function with the MEDIATYPE_xxx constants.
352 - * @return string
353 - */
354 - public function getMediaType() {
355 - $this->load();
356 - return $this->media_type;
357 - }
358 -
359 - /**
360 - * Return upload timestamp.
361 - *
362 - * @return string
363 - */
364 - public function getTimestamp() {
365 - $this->load();
366 - return wfTimestamp( TS_MW, $this->timestamp );
367 - }
368 -
369 - /**
370 - * Return the user ID of the uploader.
371 - *
372 - * @return int
373 - */
374 - public function getUser() {
375 - $this->load();
376 - if( $this->isDeleted( File::DELETED_USER ) ) {
377 - return 0;
378 - } else {
379 - return $this->user;
380 - }
381 - }
382 -
383 - /**
384 - * Return the user name of the uploader.
385 - *
386 - * @return string
387 - */
388 - public function getUserText() {
389 - $this->load();
390 - if( $this->isDeleted( File::DELETED_USER ) ) {
391 - return 0;
392 - } else {
393 - return $this->user_text;
394 - }
395 - }
396 -
397 - /**
398 - * Return upload description.
399 - *
400 - * @return string
401 - */
402 - public function getDescription() {
403 - $this->load();
404 - if( $this->isDeleted( File::DELETED_COMMENT ) ) {
405 - return 0;
406 - } else {
407 - return $this->description;
408 - }
409 - }
410 -
411 - /**
412 - * Return the user ID of the uploader.
413 - *
414 - * @return int
415 - */
416 - public function getRawUser() {
417 - $this->load();
418 - return $this->user;
419 - }
420 -
421 - /**
422 - * Return the user name of the uploader.
423 - *
424 - * @return string
425 - */
426 - public function getRawUserText() {
427 - $this->load();
428 - return $this->user_text;
429 - }
430 -
431 - /**
432 - * Return upload description.
433 - *
434 - * @return string
435 - */
436 - public function getRawDescription() {
437 - $this->load();
438 - return $this->description;
439 - }
440 -
441 - /**
442 - * Returns the deletion bitfield
443 - * @return int
444 - */
445 - public function getVisibility() {
446 - $this->load();
447 - return $this->deleted;
448 - }
449 -
450 - /**
451 - * for file or revision rows
452 - *
453 - * @param $field Integer: one of DELETED_* bitfield constants
454 - * @return bool
455 - */
456 - public function isDeleted( $field ) {
457 - $this->load();
458 - return ($this->deleted & $field) == $field;
459 - }
460 -
461 - /**
462 - * Determine if the current user is allowed to view a particular
463 - * field of this FileStore image file, if it's marked as deleted.
464 - * @param $field Integer
465 - * @param $user User object to check, or null to use $wgUser
466 - * @return bool
467 - */
468 - public function userCan( $field, User $user = null ) {
469 - $this->load();
470 - return Revision::userCanBitfield( $this->deleted, $field, $user );
471 - }
472 -}
Index: trunk/phase3/includes/filerepo/file/OldLocalFile.php
@@ -0,0 +1,295 @@
 2+<?php
 3+/**
 4+ * Old file in the oldimage table
 5+ *
 6+ * @file
 7+ * @ingroup FileRepo
 8+ */
 9+
 10+/**
 11+ * Class to represent a file in the oldimage table
 12+ *
 13+ * @ingroup FileRepo
 14+ */
 15+class OldLocalFile extends LocalFile {
 16+ var $requestedTime, $archive_name;
 17+
 18+ const CACHE_VERSION = 1;
 19+ const MAX_CACHE_ROWS = 20;
 20+
 21+ static function newFromTitle( $title, $repo, $time = null ) {
 22+ # The null default value is only here to avoid an E_STRICT
 23+ if ( $time === null ) {
 24+ throw new MWException( __METHOD__.' got null for $time parameter' );
 25+ }
 26+ return new self( $title, $repo, $time, null );
 27+ }
 28+
 29+ static function newFromArchiveName( $title, $repo, $archiveName ) {
 30+ return new self( $title, $repo, null, $archiveName );
 31+ }
 32+
 33+ static function newFromRow( $row, $repo ) {
 34+ $title = Title::makeTitle( NS_FILE, $row->oi_name );
 35+ $file = new self( $title, $repo, null, $row->oi_archive_name );
 36+ $file->loadFromRow( $row, 'oi_' );
 37+ return $file;
 38+ }
 39+
 40+ /**
 41+ * Create a OldLocalFile from a SHA-1 key
 42+ * Do not call this except from inside a repo class.
 43+ *
 44+ * @param $sha1 string base-36 SHA-1
 45+ * @param $repo LocalRepo
 46+ * @param string|bool $timestamp MW_timestamp (optional)
 47+ *
 48+ * @return bool|OldLocalFile
 49+ */
 50+ static function newFromKey( $sha1, $repo, $timestamp = false ) {
 51+ $dbr = $repo->getSlaveDB();
 52+
 53+ $conds = array( 'oi_sha1' => $sha1 );
 54+ if ( $timestamp ) {
 55+ $conds['oi_timestamp'] = $dbr->timestamp( $timestamp );
 56+ }
 57+
 58+ $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ );
 59+ if ( $row ) {
 60+ return self::newFromRow( $row, $repo );
 61+ } else {
 62+ return false;
 63+ }
 64+ }
 65+
 66+ /**
 67+ * Fields in the oldimage table
 68+ */
 69+ static function selectFields() {
 70+ return array(
 71+ 'oi_name',
 72+ 'oi_archive_name',
 73+ 'oi_size',
 74+ 'oi_width',
 75+ 'oi_height',
 76+ 'oi_metadata',
 77+ 'oi_bits',
 78+ 'oi_media_type',
 79+ 'oi_major_mime',
 80+ 'oi_minor_mime',
 81+ 'oi_description',
 82+ 'oi_user',
 83+ 'oi_user_text',
 84+ 'oi_timestamp',
 85+ 'oi_deleted',
 86+ 'oi_sha1',
 87+ );
 88+ }
 89+
 90+ /**
 91+ * @param $title Title
 92+ * @param $repo FileRepo
 93+ * @param $time String: timestamp or null to load by archive name
 94+ * @param $archiveName String: archive name or null to load by timestamp
 95+ */
 96+ function __construct( $title, $repo, $time, $archiveName ) {
 97+ parent::__construct( $title, $repo );
 98+ $this->requestedTime = $time;
 99+ $this->archive_name = $archiveName;
 100+ if ( is_null( $time ) && is_null( $archiveName ) ) {
 101+ throw new MWException( __METHOD__.': must specify at least one of $time or $archiveName' );
 102+ }
 103+ }
 104+
 105+ function getCacheKey() {
 106+ return false;
 107+ }
 108+
 109+ function getArchiveName() {
 110+ if ( !isset( $this->archive_name ) ) {
 111+ $this->load();
 112+ }
 113+ return $this->archive_name;
 114+ }
 115+
 116+ function isOld() {
 117+ return true;
 118+ }
 119+
 120+ function isVisible() {
 121+ return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
 122+ }
 123+
 124+ function loadFromDB() {
 125+ wfProfileIn( __METHOD__ );
 126+ $this->dataLoaded = true;
 127+ $dbr = $this->repo->getSlaveDB();
 128+ $conds = array( 'oi_name' => $this->getName() );
 129+ if ( is_null( $this->requestedTime ) ) {
 130+ $conds['oi_archive_name'] = $this->archive_name;
 131+ } else {
 132+ $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) );
 133+ }
 134+ $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ),
 135+ $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) );
 136+ if ( $row ) {
 137+ $this->loadFromRow( $row, 'oi_' );
 138+ } else {
 139+ $this->fileExists = false;
 140+ }
 141+ wfProfileOut( __METHOD__ );
 142+ }
 143+
 144+ function getCacheFields( $prefix = 'img_' ) {
 145+ $fields = parent::getCacheFields( $prefix );
 146+ $fields[] = $prefix . 'archive_name';
 147+ $fields[] = $prefix . 'deleted';
 148+ return $fields;
 149+ }
 150+
 151+ function getRel() {
 152+ return 'archive/' . $this->getHashPath() . $this->getArchiveName();
 153+ }
 154+
 155+ function getUrlRel() {
 156+ return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() );
 157+ }
 158+
 159+ function upgradeRow() {
 160+ wfProfileIn( __METHOD__ );
 161+ $this->loadFromFile();
 162+
 163+ # Don't destroy file info of missing files
 164+ if ( !$this->fileExists ) {
 165+ wfDebug( __METHOD__.": file does not exist, aborting\n" );
 166+ wfProfileOut( __METHOD__ );
 167+ return;
 168+ }
 169+
 170+ $dbw = $this->repo->getMasterDB();
 171+ list( $major, $minor ) = self::splitMime( $this->mime );
 172+
 173+ wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n");
 174+ $dbw->update( 'oldimage',
 175+ array(
 176+ 'oi_width' => $this->width,
 177+ 'oi_height' => $this->height,
 178+ 'oi_bits' => $this->bits,
 179+ 'oi_media_type' => $this->media_type,
 180+ 'oi_major_mime' => $major,
 181+ 'oi_minor_mime' => $minor,
 182+ 'oi_metadata' => $this->metadata,
 183+ 'oi_sha1' => $this->sha1,
 184+ ), array(
 185+ 'oi_name' => $this->getName(),
 186+ 'oi_archive_name' => $this->archive_name ),
 187+ __METHOD__
 188+ );
 189+ wfProfileOut( __METHOD__ );
 190+ }
 191+
 192+ /**
 193+ * @param $field Integer: one of DELETED_* bitfield constants
 194+ * for file or revision rows
 195+ * @return bool
 196+ */
 197+ function isDeleted( $field ) {
 198+ $this->load();
 199+ return ($this->deleted & $field) == $field;
 200+ }
 201+
 202+ /**
 203+ * Returns bitfield value
 204+ * @return int
 205+ */
 206+ function getVisibility() {
 207+ $this->load();
 208+ return (int)$this->deleted;
 209+ }
 210+
 211+ /**
 212+ * Determine if the current user is allowed to view a particular
 213+ * field of this image file, if it's marked as deleted.
 214+ *
 215+ * @param $field Integer
 216+ * @param $user User object to check, or null to use $wgUser
 217+ * @return bool
 218+ */
 219+ function userCan( $field, User $user = null ) {
 220+ $this->load();
 221+ return Revision::userCanBitfield( $this->deleted, $field, $user );
 222+ }
 223+
 224+ /**
 225+ * Upload a file directly into archive. Generally for Special:Import.
 226+ *
 227+ * @param $srcPath string File system path of the source file
 228+ * @param $archiveName string Full archive name of the file, in the form
 229+ * $timestamp!$filename, where $filename must match $this->getName()
 230+ *
 231+ * @return FileRepoStatus
 232+ */
 233+ function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user, $flags = 0 ) {
 234+ $this->lock();
 235+
 236+ $dstRel = 'archive/' . $this->getHashPath() . $archiveName;
 237+ $status = $this->publishTo( $srcPath, $dstRel,
 238+ $flags & File::DELETE_SOURCE ? FileRepo::DELETE_SOURCE : 0
 239+ );
 240+
 241+ if ( $status->isGood() ) {
 242+ if ( !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) ) {
 243+ $status->fatal( 'filenotfound', $srcPath );
 244+ }
 245+ }
 246+
 247+ $this->unlock();
 248+
 249+ return $status;
 250+ }
 251+
 252+ /**
 253+ * Record a file upload in the oldimage table, without adding log entries.
 254+ *
 255+ * @param $srcPath string File system path to the source file
 256+ * @param $archiveName string The archive name of the file
 257+ * @param $comment string Upload comment
 258+ * @param $user User User who did this upload
 259+ * @return bool
 260+ */
 261+ function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) {
 262+ $dbw = $this->repo->getMasterDB();
 263+ $dbw->begin();
 264+
 265+ $dstPath = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
 266+ $props = self::getPropsFromPath( $dstPath );
 267+ if ( !$props['fileExists'] ) {
 268+ return false;
 269+ }
 270+
 271+ $dbw->insert( 'oldimage',
 272+ array(
 273+ 'oi_name' => $this->getName(),
 274+ 'oi_archive_name' => $archiveName,
 275+ 'oi_size' => $props['size'],
 276+ 'oi_width' => intval( $props['width'] ),
 277+ 'oi_height' => intval( $props['height'] ),
 278+ 'oi_bits' => $props['bits'],
 279+ 'oi_timestamp' => $dbw->timestamp( $timestamp ),
 280+ 'oi_description' => $comment,
 281+ 'oi_user' => $user->getId(),
 282+ 'oi_user_text' => $user->getName(),
 283+ 'oi_metadata' => $props['metadata'],
 284+ 'oi_media_type' => $props['media_type'],
 285+ 'oi_major_mime' => $props['major_mime'],
 286+ 'oi_minor_mime' => $props['minor_mime'],
 287+ 'oi_sha1' => $props['sha1'],
 288+ ), __METHOD__
 289+ );
 290+
 291+ $dbw->commit();
 292+
 293+ return true;
 294+ }
 295+
 296+}
Property changes on: trunk/phase3/includes/filerepo/file/OldLocalFile.php
___________________________________________________________________
Added: svn:eol-style
1297 + native
Index: trunk/phase3/includes/filerepo/file/LocalFile.php
@@ -0,0 +1,2323 @@
 2+<?php
 3+/**
 4+ * Local file in the wiki's own database
 5+ *
 6+ * @file
 7+ * @ingroup FileRepo
 8+ */
 9+
 10+/**
 11+ * Bump this number when serialized cache records may be incompatible.
 12+ */
 13+define( 'MW_FILE_VERSION', 8 );
 14+
 15+/**
 16+ * Class to represent a local file in the wiki's own database
 17+ *
 18+ * Provides methods to retrieve paths (physical, logical, URL),
 19+ * to generate image thumbnails or for uploading.
 20+ *
 21+ * Note that only the repo object knows what its file class is called. You should
 22+ * never name a file class explictly outside of the repo class. Instead use the
 23+ * repo's factory functions to generate file objects, for example:
 24+ *
 25+ * RepoGroup::singleton()->getLocalRepo()->newFile($title);
 26+ *
 27+ * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
 28+ * in most cases.
 29+ *
 30+ * @ingroup FileRepo
 31+ */
 32+class LocalFile extends File {
 33+ /**#@+
 34+ * @private
 35+ */
 36+ var
 37+ $fileExists, # does the file exist on disk? (loadFromXxx)
 38+ $historyLine, # Number of line to return by nextHistoryLine() (constructor)
 39+ $historyRes, # result of the query for the file's history (nextHistoryLine)
 40+ $width, # \
 41+ $height, # |
 42+ $bits, # --- returned by getimagesize (loadFromXxx)
 43+ $attr, # /
 44+ $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
 45+ $mime, # MIME type, determined by MimeMagic::guessMimeType
 46+ $major_mime, # Major mime type
 47+ $minor_mime, # Minor mime type
 48+ $size, # Size in bytes (loadFromXxx)
 49+ $metadata, # Handler-specific metadata
 50+ $timestamp, # Upload timestamp
 51+ $sha1, # SHA-1 base 36 content hash
 52+ $user, $user_text, # User, who uploaded the file
 53+ $description, # Description of current revision of the file
 54+ $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
 55+ $upgraded, # Whether the row was upgraded on load
 56+ $locked, # True if the image row is locked
 57+ $missing, # True if file is not present in file system. Not to be cached in memcached
 58+ $deleted; # Bitfield akin to rev_deleted
 59+
 60+ /**#@-*/
 61+
 62+ protected $repoClass = 'LocalRepo';
 63+
 64+ /**
 65+ * Create a LocalFile from a title
 66+ * Do not call this except from inside a repo class.
 67+ *
 68+ * Note: $unused param is only here to avoid an E_STRICT
 69+ *
 70+ * @param $title
 71+ * @param $repo
 72+ * @param $unused
 73+ *
 74+ * @return LocalFile
 75+ */
 76+ static function newFromTitle( $title, $repo, $unused = null ) {
 77+ return new self( $title, $repo );
 78+ }
 79+
 80+ /**
 81+ * Create a LocalFile from a title
 82+ * Do not call this except from inside a repo class.
 83+ *
 84+ * @param $row
 85+ * @param $repo
 86+ *
 87+ * @return LocalFile
 88+ */
 89+ static function newFromRow( $row, $repo ) {
 90+ $title = Title::makeTitle( NS_FILE, $row->img_name );
 91+ $file = new self( $title, $repo );
 92+ $file->loadFromRow( $row );
 93+
 94+ return $file;
 95+ }
 96+
 97+ /**
 98+ * Create a LocalFile from a SHA-1 key
 99+ * Do not call this except from inside a repo class.
 100+ *
 101+ * @param $sha1 string base-36 SHA-1
 102+ * @param $repo LocalRepo
 103+ * @param string|bool $timestamp MW_timestamp (optional)
 104+ *
 105+ * @return bool|LocalFile
 106+ */
 107+ static function newFromKey( $sha1, $repo, $timestamp = false ) {
 108+ $dbr = $repo->getSlaveDB();
 109+
 110+ $conds = array( 'img_sha1' => $sha1 );
 111+ if ( $timestamp ) {
 112+ $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
 113+ }
 114+
 115+ $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
 116+ if ( $row ) {
 117+ return self::newFromRow( $row, $repo );
 118+ } else {
 119+ return false;
 120+ }
 121+ }
 122+
 123+ /**
 124+ * Fields in the image table
 125+ */
 126+ static function selectFields() {
 127+ return array(
 128+ 'img_name',
 129+ 'img_size',
 130+ 'img_width',
 131+ 'img_height',
 132+ 'img_metadata',
 133+ 'img_bits',
 134+ 'img_media_type',
 135+ 'img_major_mime',
 136+ 'img_minor_mime',
 137+ 'img_description',
 138+ 'img_user',
 139+ 'img_user_text',
 140+ 'img_timestamp',
 141+ 'img_sha1',
 142+ );
 143+ }
 144+
 145+ /**
 146+ * Constructor.
 147+ * Do not call this except from inside a repo class.
 148+ */
 149+ function __construct( $title, $repo ) {
 150+ parent::__construct( $title, $repo );
 151+
 152+ $this->metadata = '';
 153+ $this->historyLine = 0;
 154+ $this->historyRes = null;
 155+ $this->dataLoaded = false;
 156+
 157+ $this->assertRepoDefined();
 158+ $this->assertTitleDefined();
 159+ }
 160+
 161+ /**
 162+ * Get the memcached key for the main data for this file, or false if
 163+ * there is no access to the shared cache.
 164+ */
 165+ function getCacheKey() {
 166+ $hashedName = md5( $this->getName() );
 167+
 168+ return $this->repo->getSharedCacheKey( 'file', $hashedName );
 169+ }
 170+
 171+ /**
 172+ * Try to load file metadata from memcached. Returns true on success.
 173+ */
 174+ function loadFromCache() {
 175+ global $wgMemc;
 176+
 177+ wfProfileIn( __METHOD__ );
 178+ $this->dataLoaded = false;
 179+ $key = $this->getCacheKey();
 180+
 181+ if ( !$key ) {
 182+ wfProfileOut( __METHOD__ );
 183+ return false;
 184+ }
 185+
 186+ $cachedValues = $wgMemc->get( $key );
 187+
 188+ // Check if the key existed and belongs to this version of MediaWiki
 189+ if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
 190+ wfDebug( "Pulling file metadata from cache key $key\n" );
 191+ $this->fileExists = $cachedValues['fileExists'];
 192+ if ( $this->fileExists ) {
 193+ $this->setProps( $cachedValues );
 194+ }
 195+ $this->dataLoaded = true;
 196+ }
 197+
 198+ if ( $this->dataLoaded ) {
 199+ wfIncrStats( 'image_cache_hit' );
 200+ } else {
 201+ wfIncrStats( 'image_cache_miss' );
 202+ }
 203+
 204+ wfProfileOut( __METHOD__ );
 205+ return $this->dataLoaded;
 206+ }
 207+
 208+ /**
 209+ * Save the file metadata to memcached
 210+ */
 211+ function saveToCache() {
 212+ global $wgMemc;
 213+
 214+ $this->load();
 215+ $key = $this->getCacheKey();
 216+
 217+ if ( !$key ) {
 218+ return;
 219+ }
 220+
 221+ $fields = $this->getCacheFields( '' );
 222+ $cache = array( 'version' => MW_FILE_VERSION );
 223+ $cache['fileExists'] = $this->fileExists;
 224+
 225+ if ( $this->fileExists ) {
 226+ foreach ( $fields as $field ) {
 227+ $cache[$field] = $this->$field;
 228+ }
 229+ }
 230+
 231+ $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
 232+ }
 233+
 234+ /**
 235+ * Load metadata from the file itself
 236+ */
 237+ function loadFromFile() {
 238+ $this->setProps( self::getPropsFromPath( $this->getPath() ) );
 239+ }
 240+
 241+ function getCacheFields( $prefix = 'img_' ) {
 242+ static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
 243+ 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
 244+ static $results = array();
 245+
 246+ if ( $prefix == '' ) {
 247+ return $fields;
 248+ }
 249+
 250+ if ( !isset( $results[$prefix] ) ) {
 251+ $prefixedFields = array();
 252+ foreach ( $fields as $field ) {
 253+ $prefixedFields[] = $prefix . $field;
 254+ }
 255+ $results[$prefix] = $prefixedFields;
 256+ }
 257+
 258+ return $results[$prefix];
 259+ }
 260+
 261+ /**
 262+ * Load file metadata from the DB
 263+ */
 264+ function loadFromDB() {
 265+ # Polymorphic function name to distinguish foreign and local fetches
 266+ $fname = get_class( $this ) . '::' . __FUNCTION__;
 267+ wfProfileIn( $fname );
 268+
 269+ # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
 270+ $this->dataLoaded = true;
 271+
 272+ $dbr = $this->repo->getMasterDB();
 273+
 274+ $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
 275+ array( 'img_name' => $this->getName() ), $fname );
 276+
 277+ if ( $row ) {
 278+ $this->loadFromRow( $row );
 279+ } else {
 280+ $this->fileExists = false;
 281+ }
 282+
 283+ wfProfileOut( $fname );
 284+ }
 285+
 286+ /**
 287+ * Decode a row from the database (either object or array) to an array
 288+ * with timestamps and MIME types decoded, and the field prefix removed.
 289+ */
 290+ function decodeRow( $row, $prefix = 'img_' ) {
 291+ $array = (array)$row;
 292+ $prefixLength = strlen( $prefix );
 293+
 294+ // Sanity check prefix once
 295+ if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
 296+ throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
 297+ }
 298+
 299+ $decoded = array();
 300+
 301+ foreach ( $array as $name => $value ) {
 302+ $decoded[substr( $name, $prefixLength )] = $value;
 303+ }
 304+
 305+ $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
 306+
 307+ if ( empty( $decoded['major_mime'] ) ) {
 308+ $decoded['mime'] = 'unknown/unknown';
 309+ } else {
 310+ if ( !$decoded['minor_mime'] ) {
 311+ $decoded['minor_mime'] = 'unknown';
 312+ }
 313+ $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
 314+ }
 315+
 316+ # Trim zero padding from char/binary field
 317+ $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
 318+
 319+ return $decoded;
 320+ }
 321+
 322+ /**
 323+ * Load file metadata from a DB result row
 324+ */
 325+ function loadFromRow( $row, $prefix = 'img_' ) {
 326+ $this->dataLoaded = true;
 327+ $array = $this->decodeRow( $row, $prefix );
 328+
 329+ foreach ( $array as $name => $value ) {
 330+ $this->$name = $value;
 331+ }
 332+
 333+ $this->fileExists = true;
 334+ $this->maybeUpgradeRow();
 335+ }
 336+
 337+ /**
 338+ * Load file metadata from cache or DB, unless already loaded
 339+ */
 340+ function load() {
 341+ if ( !$this->dataLoaded ) {
 342+ if ( !$this->loadFromCache() ) {
 343+ $this->loadFromDB();
 344+ $this->saveToCache();
 345+ }
 346+ $this->dataLoaded = true;
 347+ }
 348+ }
 349+
 350+ /**
 351+ * Upgrade a row if it needs it
 352+ */
 353+ function maybeUpgradeRow() {
 354+ global $wgUpdateCompatibleMetadata;
 355+ if ( wfReadOnly() ) {
 356+ return;
 357+ }
 358+
 359+ if ( is_null( $this->media_type ) ||
 360+ $this->mime == 'image/svg'
 361+ ) {
 362+ $this->upgradeRow();
 363+ $this->upgraded = true;
 364+ } else {
 365+ $handler = $this->getHandler();
 366+ if ( $handler ) {
 367+ $validity = $handler->isMetadataValid( $this, $this->metadata );
 368+ if ( $validity === MediaHandler::METADATA_BAD
 369+ || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
 370+ ) {
 371+ $this->upgradeRow();
 372+ $this->upgraded = true;
 373+ }
 374+ }
 375+ }
 376+ }
 377+
 378+ function getUpgraded() {
 379+ return $this->upgraded;
 380+ }
 381+
 382+ /**
 383+ * Fix assorted version-related problems with the image row by reloading it from the file
 384+ */
 385+ function upgradeRow() {
 386+ wfProfileIn( __METHOD__ );
 387+
 388+ $this->loadFromFile();
 389+
 390+ # Don't destroy file info of missing files
 391+ if ( !$this->fileExists ) {
 392+ wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
 393+ wfProfileOut( __METHOD__ );
 394+ return;
 395+ }
 396+
 397+ $dbw = $this->repo->getMasterDB();
 398+ list( $major, $minor ) = self::splitMime( $this->mime );
 399+
 400+ if ( wfReadOnly() ) {
 401+ wfProfileOut( __METHOD__ );
 402+ return;
 403+ }
 404+ wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
 405+
 406+ $dbw->update( 'image',
 407+ array(
 408+ 'img_width' => $this->width,
 409+ 'img_height' => $this->height,
 410+ 'img_bits' => $this->bits,
 411+ 'img_media_type' => $this->media_type,
 412+ 'img_major_mime' => $major,
 413+ 'img_minor_mime' => $minor,
 414+ 'img_metadata' => $this->metadata,
 415+ 'img_sha1' => $this->sha1,
 416+ ), array( 'img_name' => $this->getName() ),
 417+ __METHOD__
 418+ );
 419+
 420+ $this->saveToCache();
 421+ wfProfileOut( __METHOD__ );
 422+ }
 423+
 424+ /**
 425+ * Set properties in this object to be equal to those given in the
 426+ * associative array $info. Only cacheable fields can be set.
 427+ *
 428+ * If 'mime' is given, it will be split into major_mime/minor_mime.
 429+ * If major_mime/minor_mime are given, $this->mime will also be set.
 430+ */
 431+ function setProps( $info ) {
 432+ $this->dataLoaded = true;
 433+ $fields = $this->getCacheFields( '' );
 434+ $fields[] = 'fileExists';
 435+
 436+ foreach ( $fields as $field ) {
 437+ if ( isset( $info[$field] ) ) {
 438+ $this->$field = $info[$field];
 439+ }
 440+ }
 441+
 442+ // Fix up mime fields
 443+ if ( isset( $info['major_mime'] ) ) {
 444+ $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
 445+ } elseif ( isset( $info['mime'] ) ) {
 446+ $this->mime = $info['mime'];
 447+ list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
 448+ }
 449+ }
 450+
 451+ /** splitMime inherited */
 452+ /** getName inherited */
 453+ /** getTitle inherited */
 454+ /** getURL inherited */
 455+ /** getViewURL inherited */
 456+ /** getPath inherited */
 457+ /** isVisible inhereted */
 458+
 459+ function isMissing() {
 460+ if ( $this->missing === null ) {
 461+ list( $fileExists ) = $this->repo->fileExistsBatch( array( $this->getVirtualUrl() ), FileRepo::FILES_ONLY );
 462+ $this->missing = !$fileExists;
 463+ }
 464+ return $this->missing;
 465+ }
 466+
 467+ /**
 468+ * Return the width of the image
 469+ *
 470+ * Returns false on error
 471+ */
 472+ public function getWidth( $page = 1 ) {
 473+ $this->load();
 474+
 475+ if ( $this->isMultipage() ) {
 476+ $dim = $this->getHandler()->getPageDimensions( $this, $page );
 477+ if ( $dim ) {
 478+ return $dim['width'];
 479+ } else {
 480+ return false;
 481+ }
 482+ } else {
 483+ return $this->width;
 484+ }
 485+ }
 486+
 487+ /**
 488+ * Return the height of the image
 489+ *
 490+ * Returns false on error
 491+ */
 492+ public function getHeight( $page = 1 ) {
 493+ $this->load();
 494+
 495+ if ( $this->isMultipage() ) {
 496+ $dim = $this->getHandler()->getPageDimensions( $this, $page );
 497+ if ( $dim ) {
 498+ return $dim['height'];
 499+ } else {
 500+ return false;
 501+ }
 502+ } else {
 503+ return $this->height;
 504+ }
 505+ }
 506+
 507+ /**
 508+ * Returns ID or name of user who uploaded the file
 509+ *
 510+ * @param $type string 'text' or 'id'
 511+ */
 512+ function getUser( $type = 'text' ) {
 513+ $this->load();
 514+
 515+ if ( $type == 'text' ) {
 516+ return $this->user_text;
 517+ } elseif ( $type == 'id' ) {
 518+ return $this->user;
 519+ }
 520+ }
 521+
 522+ /**
 523+ * Get handler-specific metadata
 524+ */
 525+ function getMetadata() {
 526+ $this->load();
 527+ return $this->metadata;
 528+ }
 529+
 530+ function getBitDepth() {
 531+ $this->load();
 532+ return $this->bits;
 533+ }
 534+
 535+ /**
 536+ * Return the size of the image file, in bytes
 537+ */
 538+ public function getSize() {
 539+ $this->load();
 540+ return $this->size;
 541+ }
 542+
 543+ /**
 544+ * Returns the mime type of the file.
 545+ */
 546+ function getMimeType() {
 547+ $this->load();
 548+ return $this->mime;
 549+ }
 550+
 551+ /**
 552+ * Return the type of the media in the file.
 553+ * Use the value returned by this function with the MEDIATYPE_xxx constants.
 554+ */
 555+ function getMediaType() {
 556+ $this->load();
 557+ return $this->media_type;
 558+ }
 559+
 560+ /** canRender inherited */
 561+ /** mustRender inherited */
 562+ /** allowInlineDisplay inherited */
 563+ /** isSafeFile inherited */
 564+ /** isTrustedFile inherited */
 565+
 566+ /**
 567+ * Returns true if the file exists on disk.
 568+ * @return boolean Whether file exist on disk.
 569+ */
 570+ public function exists() {
 571+ $this->load();
 572+ return $this->fileExists;
 573+ }
 574+
 575+ /** getTransformScript inherited */
 576+ /** getUnscaledThumb inherited */
 577+ /** thumbName inherited */
 578+ /** createThumb inherited */
 579+ /** transform inherited */
 580+
 581+ /**
 582+ * Fix thumbnail files from 1.4 or before, with extreme prejudice
 583+ */
 584+ function migrateThumbFile( $thumbName ) {
 585+ $thumbDir = $this->getThumbPath();
 586+ $thumbPath = "$thumbDir/$thumbName";
 587+
 588+ if ( is_dir( $thumbPath ) ) {
 589+ // Directory where file should be
 590+ // This happened occasionally due to broken migration code in 1.5
 591+ // Rename to broken-*
 592+ for ( $i = 0; $i < 100 ; $i++ ) {
 593+ $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
 594+ if ( !file_exists( $broken ) ) {
 595+ rename( $thumbPath, $broken );
 596+ break;
 597+ }
 598+ }
 599+ // Doesn't exist anymore
 600+ clearstatcache();
 601+ }
 602+
 603+ if ( is_file( $thumbDir ) ) {
 604+ // File where directory should be
 605+ unlink( $thumbDir );
 606+ // Doesn't exist anymore
 607+ clearstatcache();
 608+ }
 609+ }
 610+
 611+ /** getHandler inherited */
 612+ /** iconThumb inherited */
 613+ /** getLastError inherited */
 614+
 615+ /**
 616+ * Get all thumbnail names previously generated for this file
 617+ * @param $archiveName string|false Name of an archive file
 618+ * @return array first element is the base dir, then files in that base dir.
 619+ */
 620+ function getThumbnails( $archiveName = false ) {
 621+ $this->load();
 622+
 623+ if ( $archiveName ) {
 624+ $dir = $this->getArchiveThumbPath( $archiveName );
 625+ } else {
 626+ $dir = $this->getThumbPath();
 627+ }
 628+ $files = array();
 629+ $files[] = $dir;
 630+
 631+ if ( is_dir( $dir ) ) {
 632+ $handle = opendir( $dir );
 633+
 634+ if ( $handle ) {
 635+ while ( false !== ( $file = readdir( $handle ) ) ) {
 636+ if ( $file { 0 } != '.' ) {
 637+ $files[] = $file;
 638+ }
 639+ }
 640+
 641+ closedir( $handle );
 642+ }
 643+ }
 644+
 645+ return $files;
 646+ }
 647+
 648+ /**
 649+ * Refresh metadata in memcached, but don't touch thumbnails or squid
 650+ */
 651+ function purgeMetadataCache() {
 652+ $this->loadFromDB();
 653+ $this->saveToCache();
 654+ $this->purgeHistory();
 655+ }
 656+
 657+ /**
 658+ * Purge the shared history (OldLocalFile) cache
 659+ */
 660+ function purgeHistory() {
 661+ global $wgMemc;
 662+
 663+ $hashedName = md5( $this->getName() );
 664+ $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
 665+
 666+ // Must purge thumbnails for old versions too! bug 30192
 667+ foreach( $this->getHistory() as $oldFile ) {
 668+ $oldFile->purgeThumbnails();
 669+ }
 670+
 671+ if ( $oldKey ) {
 672+ $wgMemc->delete( $oldKey );
 673+ }
 674+ }
 675+
 676+ /**
 677+ * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
 678+ */
 679+ function purgeCache() {
 680+ // Refresh metadata cache
 681+ $this->purgeMetadataCache();
 682+
 683+ // Delete thumbnails
 684+ $this->purgeThumbnails();
 685+
 686+ // Purge squid cache for this file
 687+ SquidUpdate::purge( array( $this->getURL() ) );
 688+ }
 689+
 690+ /**
 691+ * Delete cached transformed files for an archived version only.
 692+ * @param $archiveName string name of the archived file
 693+ */
 694+ function purgeOldThumbnails( $archiveName ) {
 695+ global $wgUseSquid;
 696+ // Get a list of old thumbnails and URLs
 697+ $files = $this->getThumbnails( $archiveName );
 698+ $dir = array_shift( $files );
 699+ $this->purgeThumbList( $dir, $files );
 700+
 701+ // Directory should be empty, delete it too. This will probably suck on
 702+ // something like NFS or if the directory isn't actually empty, so hide
 703+ // the warnings :D
 704+ wfSuppressWarnings();
 705+ if( !rmdir( $dir ) ) {
 706+ wfDebug( __METHOD__ . ": unable to remove archive directory: $dir\n" );
 707+ }
 708+ wfRestoreWarnings();
 709+
 710+ // Purge any custom thumbnail caches
 711+ wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) );
 712+
 713+ // Purge the squid
 714+ if ( $wgUseSquid ) {
 715+ $urls = array();
 716+ foreach( $files as $file ) {
 717+ $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
 718+ }
 719+ SquidUpdate::purge( $urls );
 720+ }
 721+ }
 722+
 723+
 724+ /**
 725+ * Delete cached transformed files for the current version only.
 726+ */
 727+ function purgeThumbnails() {
 728+ global $wgUseSquid;
 729+
 730+ // Delete thumbnails
 731+ $files = $this->getThumbnails();
 732+ $dir = array_shift( $files );
 733+ $this->purgeThumbList( $dir, $files );
 734+
 735+ // Purge any custom thumbnail caches
 736+ wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) );
 737+
 738+ // Purge the squid
 739+ if ( $wgUseSquid ) {
 740+ $urls = array();
 741+ foreach( $files as $file ) {
 742+ $urls[] = $this->getThumbUrl( $file );
 743+ }
 744+ SquidUpdate::purge( $urls );
 745+ }
 746+ }
 747+
 748+ /**
 749+ * Delete a list of thumbnails visible at urls
 750+ * @param $dir string base dir of the files.
 751+ * @param $files array of strings: relative filenames (to $dir)
 752+ */
 753+ protected function purgeThumbList($dir, $files) {
 754+ wfDebug( __METHOD__ . ": " . var_export( $files, true ) . "\n" );
 755+ foreach ( $files as $file ) {
 756+ # Check that the base file name is part of the thumb name
 757+ # This is a basic sanity check to avoid erasing unrelated directories
 758+ if ( strpos( $file, $this->getName() ) !== false ) {
 759+ wfSuppressWarnings();
 760+ unlink( "$dir/$file" );
 761+ wfRestoreWarnings();
 762+ }
 763+ }
 764+ }
 765+
 766+ /** purgeDescription inherited */
 767+ /** purgeEverything inherited */
 768+
 769+ function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
 770+ $dbr = $this->repo->getSlaveDB();
 771+ $tables = array( 'oldimage' );
 772+ $fields = OldLocalFile::selectFields();
 773+ $conds = $opts = $join_conds = array();
 774+ $eq = $inc ? '=' : '';
 775+ $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
 776+
 777+ if ( $start ) {
 778+ $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
 779+ }
 780+
 781+ if ( $end ) {
 782+ $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
 783+ }
 784+
 785+ if ( $limit ) {
 786+ $opts['LIMIT'] = $limit;
 787+ }
 788+
 789+ // Search backwards for time > x queries
 790+ $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
 791+ $opts['ORDER BY'] = "oi_timestamp $order";
 792+ $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
 793+
 794+ wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields,
 795+ &$conds, &$opts, &$join_conds ) );
 796+
 797+ $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
 798+ $r = array();
 799+
 800+ foreach ( $res as $row ) {
 801+ if ( $this->repo->oldFileFromRowFactory ) {
 802+ $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
 803+ } else {
 804+ $r[] = OldLocalFile::newFromRow( $row, $this->repo );
 805+ }
 806+ }
 807+
 808+ if ( $order == 'ASC' ) {
 809+ $r = array_reverse( $r ); // make sure it ends up descending
 810+ }
 811+
 812+ return $r;
 813+ }
 814+
 815+ /**
 816+ * Return the history of this file, line by line.
 817+ * starts with current version, then old versions.
 818+ * uses $this->historyLine to check which line to return:
 819+ * 0 return line for current version
 820+ * 1 query for old versions, return first one
 821+ * 2, ... return next old version from above query
 822+ */
 823+ public function nextHistoryLine() {
 824+ # Polymorphic function name to distinguish foreign and local fetches
 825+ $fname = get_class( $this ) . '::' . __FUNCTION__;
 826+
 827+ $dbr = $this->repo->getSlaveDB();
 828+
 829+ if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
 830+ $this->historyRes = $dbr->select( 'image',
 831+ array(
 832+ '*',
 833+ "'' AS oi_archive_name",
 834+ '0 as oi_deleted',
 835+ 'img_sha1'
 836+ ),
 837+ array( 'img_name' => $this->title->getDBkey() ),
 838+ $fname
 839+ );
 840+
 841+ if ( 0 == $dbr->numRows( $this->historyRes ) ) {
 842+ $this->historyRes = null;
 843+ return false;
 844+ }
 845+ } elseif ( $this->historyLine == 1 ) {
 846+ $this->historyRes = $dbr->select( 'oldimage', '*',
 847+ array( 'oi_name' => $this->title->getDBkey() ),
 848+ $fname,
 849+ array( 'ORDER BY' => 'oi_timestamp DESC' )
 850+ );
 851+ }
 852+ $this->historyLine ++;
 853+
 854+ return $dbr->fetchObject( $this->historyRes );
 855+ }
 856+
 857+ /**
 858+ * Reset the history pointer to the first element of the history
 859+ */
 860+ public function resetHistory() {
 861+ $this->historyLine = 0;
 862+
 863+ if ( !is_null( $this->historyRes ) ) {
 864+ $this->historyRes = null;
 865+ }
 866+ }
 867+
 868+ /** getHashPath inherited */
 869+ /** getRel inherited */
 870+ /** getUrlRel inherited */
 871+ /** getArchiveRel inherited */
 872+ /** getArchivePath inherited */
 873+ /** getThumbPath inherited */
 874+ /** getArchiveUrl inherited */
 875+ /** getThumbUrl inherited */
 876+ /** getArchiveVirtualUrl inherited */
 877+ /** getThumbVirtualUrl inherited */
 878+ /** isHashed inherited */
 879+
 880+ /**
 881+ * Upload a file and record it in the DB
 882+ * @param $srcPath String: source path or virtual URL
 883+ * @param $comment String: upload description
 884+ * @param $pageText String: text to use for the new description page,
 885+ * if a new description page is created
 886+ * @param $flags Integer: flags for publish()
 887+ * @param $props Array: File properties, if known. This can be used to reduce the
 888+ * upload time when uploading virtual URLs for which the file info
 889+ * is already known
 890+ * @param $timestamp String: timestamp for img_timestamp, or false to use the current time
 891+ * @param $user Mixed: User object or null to use $wgUser
 892+ *
 893+ * @return FileRepoStatus object. On success, the value member contains the
 894+ * archive name, or an empty string if it was a new file.
 895+ */
 896+ function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
 897+ global $wgContLang;
 898+ // truncate nicely or the DB will do it for us
 899+ // non-nicely (dangling multi-byte chars, non-truncated
 900+ // version in cache).
 901+ $comment = $wgContLang->truncate( $comment, 255 );
 902+ $this->lock();
 903+ $status = $this->publish( $srcPath, $flags );
 904+
 905+ if ( $status->ok ) {
 906+ if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
 907+ $status->fatal( 'filenotfound', $srcPath );
 908+ }
 909+ }
 910+
 911+ $this->unlock();
 912+
 913+ return $status;
 914+ }
 915+
 916+ /**
 917+ * Record a file upload in the upload log and the image table
 918+ */
 919+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
 920+ $watch = false, $timestamp = false )
 921+ {
 922+ $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
 923+
 924+ if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
 925+ return false;
 926+ }
 927+
 928+ if ( $watch ) {
 929+ global $wgUser;
 930+ $wgUser->addWatch( $this->getTitle() );
 931+ }
 932+ return true;
 933+ }
 934+
 935+ /**
 936+ * Record a file upload in the upload log and the image table
 937+ */
 938+ function recordUpload2(
 939+ $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null
 940+ ) {
 941+ if ( is_null( $user ) ) {
 942+ global $wgUser;
 943+ $user = $wgUser;
 944+ }
 945+
 946+ $dbw = $this->repo->getMasterDB();
 947+ $dbw->begin();
 948+
 949+ if ( !$props ) {
 950+ $props = $this->repo->getFileProps( $this->getVirtualUrl() );
 951+ }
 952+
 953+ if ( $timestamp === false ) {
 954+ $timestamp = $dbw->timestamp();
 955+ }
 956+
 957+ $props['description'] = $comment;
 958+ $props['user'] = $user->getId();
 959+ $props['user_text'] = $user->getName();
 960+ $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
 961+ $this->setProps( $props );
 962+
 963+ # Delete thumbnails
 964+ $this->purgeThumbnails();
 965+
 966+ # The file is already on its final location, remove it from the squid cache
 967+ SquidUpdate::purge( array( $this->getURL() ) );
 968+
 969+ # Fail now if the file isn't there
 970+ if ( !$this->fileExists ) {
 971+ wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
 972+ return false;
 973+ }
 974+
 975+ $reupload = false;
 976+
 977+ # Test to see if the row exists using INSERT IGNORE
 978+ # This avoids race conditions by locking the row until the commit, and also
 979+ # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
 980+ $dbw->insert( 'image',
 981+ array(
 982+ 'img_name' => $this->getName(),
 983+ 'img_size' => $this->size,
 984+ 'img_width' => intval( $this->width ),
 985+ 'img_height' => intval( $this->height ),
 986+ 'img_bits' => $this->bits,
 987+ 'img_media_type' => $this->media_type,
 988+ 'img_major_mime' => $this->major_mime,
 989+ 'img_minor_mime' => $this->minor_mime,
 990+ 'img_timestamp' => $timestamp,
 991+ 'img_description' => $comment,
 992+ 'img_user' => $user->getId(),
 993+ 'img_user_text' => $user->getName(),
 994+ 'img_metadata' => $this->metadata,
 995+ 'img_sha1' => $this->sha1
 996+ ),
 997+ __METHOD__,
 998+ 'IGNORE'
 999+ );
 1000+
 1001+ if ( $dbw->affectedRows() == 0 ) {
 1002+ $reupload = true;
 1003+
 1004+ # Collision, this is an update of a file
 1005+ # Insert previous contents into oldimage
 1006+ $dbw->insertSelect( 'oldimage', 'image',
 1007+ array(
 1008+ 'oi_name' => 'img_name',
 1009+ 'oi_archive_name' => $dbw->addQuotes( $oldver ),
 1010+ 'oi_size' => 'img_size',
 1011+ 'oi_width' => 'img_width',
 1012+ 'oi_height' => 'img_height',
 1013+ 'oi_bits' => 'img_bits',
 1014+ 'oi_timestamp' => 'img_timestamp',
 1015+ 'oi_description' => 'img_description',
 1016+ 'oi_user' => 'img_user',
 1017+ 'oi_user_text' => 'img_user_text',
 1018+ 'oi_metadata' => 'img_metadata',
 1019+ 'oi_media_type' => 'img_media_type',
 1020+ 'oi_major_mime' => 'img_major_mime',
 1021+ 'oi_minor_mime' => 'img_minor_mime',
 1022+ 'oi_sha1' => 'img_sha1'
 1023+ ), array( 'img_name' => $this->getName() ), __METHOD__
 1024+ );
 1025+
 1026+ # Update the current image row
 1027+ $dbw->update( 'image',
 1028+ array( /* SET */
 1029+ 'img_size' => $this->size,
 1030+ 'img_width' => intval( $this->width ),
 1031+ 'img_height' => intval( $this->height ),
 1032+ 'img_bits' => $this->bits,
 1033+ 'img_media_type' => $this->media_type,
 1034+ 'img_major_mime' => $this->major_mime,
 1035+ 'img_minor_mime' => $this->minor_mime,
 1036+ 'img_timestamp' => $timestamp,
 1037+ 'img_description' => $comment,
 1038+ 'img_user' => $user->getId(),
 1039+ 'img_user_text' => $user->getName(),
 1040+ 'img_metadata' => $this->metadata,
 1041+ 'img_sha1' => $this->sha1
 1042+ ), array( /* WHERE */
 1043+ 'img_name' => $this->getName()
 1044+ ), __METHOD__
 1045+ );
 1046+ } else {
 1047+ # This is a new file
 1048+ # Update the image count
 1049+ $dbw->begin( __METHOD__ );
 1050+ $dbw->update(
 1051+ 'site_stats',
 1052+ array( 'ss_images = ss_images+1' ),
 1053+ '*',
 1054+ __METHOD__
 1055+ );
 1056+ $dbw->commit( __METHOD__ );
 1057+ }
 1058+
 1059+ $descTitle = $this->getTitle();
 1060+ $wikiPage = new WikiFilePage( $descTitle );
 1061+ $wikiPage->setFile( $this );
 1062+
 1063+ # Add the log entry
 1064+ $log = new LogPage( 'upload' );
 1065+ $action = $reupload ? 'overwrite' : 'upload';
 1066+ $log->addEntry( $action, $descTitle, $comment, array(), $user );
 1067+
 1068+ if ( $descTitle->exists() ) {
 1069+ # Create a null revision
 1070+ $latest = $descTitle->getLatestRevID();
 1071+ $nullRevision = Revision::newNullRevision(
 1072+ $dbw,
 1073+ $descTitle->getArticleId(),
 1074+ $log->getRcComment(),
 1075+ false
 1076+ );
 1077+ if (!is_null($nullRevision)) {
 1078+ $nullRevision->insertOn( $dbw );
 1079+
 1080+ wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
 1081+ $wikiPage->updateRevisionOn( $dbw, $nullRevision );
 1082+ }
 1083+ # Invalidate the cache for the description page
 1084+ $descTitle->invalidateCache();
 1085+ $descTitle->purgeSquid();
 1086+ } else {
 1087+ # New file; create the description page.
 1088+ # There's already a log entry, so don't make a second RC entry
 1089+ # Squid and file cache for the description page are purged by doEdit.
 1090+ $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
 1091+ }
 1092+
 1093+ # Commit the transaction now, in case something goes wrong later
 1094+ # The most important thing is that files don't get lost, especially archives
 1095+ $dbw->commit();
 1096+
 1097+ # Save to cache and purge the squid
 1098+ # We shall not saveToCache before the commit since otherwise
 1099+ # in case of a rollback there is an usable file from memcached
 1100+ # which in fact doesn't really exist (bug 24978)
 1101+ $this->saveToCache();
 1102+
 1103+ # Hooks, hooks, the magic of hooks...
 1104+ wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
 1105+
 1106+ # Invalidate cache for all pages using this file
 1107+ $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
 1108+ $update->doUpdate();
 1109+
 1110+ # Invalidate cache for all pages that redirects on this page
 1111+ $redirs = $this->getTitle()->getRedirectsHere();
 1112+
 1113+ foreach ( $redirs as $redir ) {
 1114+ $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
 1115+ $update->doUpdate();
 1116+ }
 1117+
 1118+ return true;
 1119+ }
 1120+
 1121+ /**
 1122+ * Move or copy a file to its public location. If a file exists at the
 1123+ * destination, move it to an archive. Returns a FileRepoStatus object with
 1124+ * the archive name in the "value" member on success.
 1125+ *
 1126+ * The archive name should be passed through to recordUpload for database
 1127+ * registration.
 1128+ *
 1129+ * @param $srcPath String: local filesystem path to the source image
 1130+ * @param $flags Integer: a bitwise combination of:
 1131+ * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
 1132+ * @return FileRepoStatus object. On success, the value member contains the
 1133+ * archive name, or an empty string if it was a new file.
 1134+ */
 1135+ function publish( $srcPath, $flags = 0 ) {
 1136+ return $this->publishTo( $srcPath, $this->getRel(), $flags );
 1137+ }
 1138+
 1139+ /**
 1140+ * Move or copy a file to a specified location. Returns a FileRepoStatus
 1141+ * object with the archive name in the "value" member on success.
 1142+ *
 1143+ * The archive name should be passed through to recordUpload for database
 1144+ * registration.
 1145+ *
 1146+ * @param $srcPath String: local filesystem path to the source image
 1147+ * @param $dstRel String: target relative path
 1148+ * @param $flags Integer: a bitwise combination of:
 1149+ * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
 1150+ * @return FileRepoStatus object. On success, the value member contains the
 1151+ * archive name, or an empty string if it was a new file.
 1152+ */
 1153+ function publishTo( $srcPath, $dstRel, $flags = 0 ) {
 1154+ $this->lock();
 1155+
 1156+ $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName();
 1157+ $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
 1158+ $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
 1159+ $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
 1160+
 1161+ if ( $status->value == 'new' ) {
 1162+ $status->value = '';
 1163+ } else {
 1164+ $status->value = $archiveName;
 1165+ }
 1166+
 1167+ $this->unlock();
 1168+
 1169+ return $status;
 1170+ }
 1171+
 1172+ /** getLinksTo inherited */
 1173+ /** getExifData inherited */
 1174+ /** isLocal inherited */
 1175+ /** wasDeleted inherited */
 1176+
 1177+ /**
 1178+ * Move file to the new title
 1179+ *
 1180+ * Move current, old version and all thumbnails
 1181+ * to the new filename. Old file is deleted.
 1182+ *
 1183+ * Cache purging is done; checks for validity
 1184+ * and logging are caller's responsibility
 1185+ *
 1186+ * @param $target Title New file name
 1187+ * @return FileRepoStatus object.
 1188+ */
 1189+ function move( $target ) {
 1190+ wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
 1191+ $this->lock();
 1192+
 1193+ $batch = new LocalFileMoveBatch( $this, $target );
 1194+ $batch->addCurrent();
 1195+ $batch->addOlds();
 1196+
 1197+ $status = $batch->execute();
 1198+ wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
 1199+
 1200+ $this->purgeEverything();
 1201+ $this->unlock();
 1202+
 1203+ if ( $status->isOk() ) {
 1204+ // Now switch the object
 1205+ $this->title = $target;
 1206+ // Force regeneration of the name and hashpath
 1207+ unset( $this->name );
 1208+ unset( $this->hashPath );
 1209+ // Purge the new image
 1210+ $this->purgeEverything();
 1211+ }
 1212+
 1213+ return $status;
 1214+ }
 1215+
 1216+ /**
 1217+ * Delete all versions of the file.
 1218+ *
 1219+ * Moves the files into an archive directory (or deletes them)
 1220+ * and removes the database rows.
 1221+ *
 1222+ * Cache purging is done; logging is caller's responsibility.
 1223+ *
 1224+ * @param $reason
 1225+ * @param $suppress
 1226+ * @return FileRepoStatus object.
 1227+ */
 1228+ function delete( $reason, $suppress = false ) {
 1229+ $this->lock();
 1230+
 1231+ $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
 1232+ $batch->addCurrent();
 1233+
 1234+ # Get old version relative paths
 1235+ $dbw = $this->repo->getMasterDB();
 1236+ $result = $dbw->select( 'oldimage',
 1237+ array( 'oi_archive_name' ),
 1238+ array( 'oi_name' => $this->getName() ) );
 1239+ foreach ( $result as $row ) {
 1240+ $batch->addOld( $row->oi_archive_name );
 1241+ $this->purgeOldThumbnails( $row->oi_archive_name );
 1242+ }
 1243+ $status = $batch->execute();
 1244+
 1245+ if ( $status->ok ) {
 1246+ // Update site_stats
 1247+ $site_stats = $dbw->tableName( 'site_stats' );
 1248+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
 1249+ $this->purgeEverything();
 1250+ }
 1251+
 1252+ $this->unlock();
 1253+
 1254+ return $status;
 1255+ }
 1256+
 1257+ /**
 1258+ * Delete an old version of the file.
 1259+ *
 1260+ * Moves the file into an archive directory (or deletes it)
 1261+ * and removes the database row.
 1262+ *
 1263+ * Cache purging is done; logging is caller's responsibility.
 1264+ *
 1265+ * @param $archiveName String
 1266+ * @param $reason String
 1267+ * @param $suppress Boolean
 1268+ * @throws MWException or FSException on database or file store failure
 1269+ * @return FileRepoStatus object.
 1270+ */
 1271+ function deleteOld( $archiveName, $reason, $suppress = false ) {
 1272+ $this->lock();
 1273+
 1274+ $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
 1275+ $batch->addOld( $archiveName );
 1276+ $this->purgeOldThumbnails( $archiveName );
 1277+ $status = $batch->execute();
 1278+
 1279+ $this->unlock();
 1280+
 1281+ if ( $status->ok ) {
 1282+ $this->purgeDescription();
 1283+ $this->purgeHistory();
 1284+ }
 1285+
 1286+ return $status;
 1287+ }
 1288+
 1289+ /**
 1290+ * Restore all or specified deleted revisions to the given file.
 1291+ * Permissions and logging are left to the caller.
 1292+ *
 1293+ * May throw database exceptions on error.
 1294+ *
 1295+ * @param $versions set of record ids of deleted items to restore,
 1296+ * or empty to restore all revisions.
 1297+ * @param $unsuppress Boolean
 1298+ * @return FileRepoStatus
 1299+ */
 1300+ function restore( $versions = array(), $unsuppress = false ) {
 1301+ $batch = new LocalFileRestoreBatch( $this, $unsuppress );
 1302+
 1303+ if ( !$versions ) {
 1304+ $batch->addAll();
 1305+ } else {
 1306+ $batch->addIds( $versions );
 1307+ }
 1308+
 1309+ $status = $batch->execute();
 1310+
 1311+ if ( !$status->isGood() ) {
 1312+ return $status;
 1313+ }
 1314+
 1315+ $cleanupStatus = $batch->cleanup();
 1316+ $cleanupStatus->successCount = 0;
 1317+ $cleanupStatus->failCount = 0;
 1318+ $status->merge( $cleanupStatus );
 1319+
 1320+ return $status;
 1321+ }
 1322+
 1323+ /** isMultipage inherited */
 1324+ /** pageCount inherited */
 1325+ /** scaleHeight inherited */
 1326+ /** getImageSize inherited */
 1327+
 1328+ /**
 1329+ * Get the URL of the file description page.
 1330+ */
 1331+ function getDescriptionUrl() {
 1332+ return $this->title->getLocalUrl();
 1333+ }
 1334+
 1335+ /**
 1336+ * Get the HTML text of the description page
 1337+ * This is not used by ImagePage for local files, since (among other things)
 1338+ * it skips the parser cache.
 1339+ */
 1340+ function getDescriptionText() {
 1341+ global $wgParser;
 1342+ $revision = Revision::newFromTitle( $this->title );
 1343+ if ( !$revision ) return false;
 1344+ $text = $revision->getText();
 1345+ if ( !$text ) return false;
 1346+ $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
 1347+ return $pout->getText();
 1348+ }
 1349+
 1350+ function getDescription() {
 1351+ $this->load();
 1352+ return $this->description;
 1353+ }
 1354+
 1355+ function getTimestamp() {
 1356+ $this->load();
 1357+ return $this->timestamp;
 1358+ }
 1359+
 1360+ function getSha1() {
 1361+ $this->load();
 1362+ // Initialise now if necessary
 1363+ if ( $this->sha1 == '' && $this->fileExists ) {
 1364+ $this->sha1 = File::sha1Base36( $this->getPath() );
 1365+ if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
 1366+ $dbw = $this->repo->getMasterDB();
 1367+ $dbw->update( 'image',
 1368+ array( 'img_sha1' => $this->sha1 ),
 1369+ array( 'img_name' => $this->getName() ),
 1370+ __METHOD__ );
 1371+ $this->saveToCache();
 1372+ }
 1373+ }
 1374+
 1375+ return $this->sha1;
 1376+ }
 1377+
 1378+ /**
 1379+ * Start a transaction and lock the image for update
 1380+ * Increments a reference counter if the lock is already held
 1381+ * @return boolean True if the image exists, false otherwise
 1382+ */
 1383+ function lock() {
 1384+ $dbw = $this->repo->getMasterDB();
 1385+
 1386+ if ( !$this->locked ) {
 1387+ $dbw->begin();
 1388+ $this->locked++;
 1389+ }
 1390+
 1391+ return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
 1392+ }
 1393+
 1394+ /**
 1395+ * Decrement the lock reference count. If the reference count is reduced to zero, commits
 1396+ * the transaction and thereby releases the image lock.
 1397+ */
 1398+ function unlock() {
 1399+ if ( $this->locked ) {
 1400+ --$this->locked;
 1401+ if ( !$this->locked ) {
 1402+ $dbw = $this->repo->getMasterDB();
 1403+ $dbw->commit();
 1404+ }
 1405+ }
 1406+ }
 1407+
 1408+ /**
 1409+ * Roll back the DB transaction and mark the image unlocked
 1410+ */
 1411+ function unlockAndRollback() {
 1412+ $this->locked = false;
 1413+ $dbw = $this->repo->getMasterDB();
 1414+ $dbw->rollback();
 1415+ }
 1416+} // LocalFile class
 1417+
 1418+# ------------------------------------------------------------------------------
 1419+
 1420+/**
 1421+ * Helper class for file deletion
 1422+ * @ingroup FileRepo
 1423+ */
 1424+class LocalFileDeleteBatch {
 1425+
 1426+ /**
 1427+ * @var LocalFile
 1428+ */
 1429+ var $file;
 1430+
 1431+ var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
 1432+ var $status;
 1433+
 1434+ function __construct( File $file, $reason = '', $suppress = false ) {
 1435+ $this->file = $file;
 1436+ $this->reason = $reason;
 1437+ $this->suppress = $suppress;
 1438+ $this->status = $file->repo->newGood();
 1439+ }
 1440+
 1441+ function addCurrent() {
 1442+ $this->srcRels['.'] = $this->file->getRel();
 1443+ }
 1444+
 1445+ function addOld( $oldName ) {
 1446+ $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
 1447+ $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
 1448+ }
 1449+
 1450+ function getOldRels() {
 1451+ if ( !isset( $this->srcRels['.'] ) ) {
 1452+ $oldRels =& $this->srcRels;
 1453+ $deleteCurrent = false;
 1454+ } else {
 1455+ $oldRels = $this->srcRels;
 1456+ unset( $oldRels['.'] );
 1457+ $deleteCurrent = true;
 1458+ }
 1459+
 1460+ return array( $oldRels, $deleteCurrent );
 1461+ }
 1462+
 1463+ protected function getHashes() {
 1464+ $hashes = array();
 1465+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1466+
 1467+ if ( $deleteCurrent ) {
 1468+ $hashes['.'] = $this->file->getSha1();
 1469+ }
 1470+
 1471+ if ( count( $oldRels ) ) {
 1472+ $dbw = $this->file->repo->getMasterDB();
 1473+ $res = $dbw->select(
 1474+ 'oldimage',
 1475+ array( 'oi_archive_name', 'oi_sha1' ),
 1476+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
 1477+ __METHOD__
 1478+ );
 1479+
 1480+ foreach ( $res as $row ) {
 1481+ if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
 1482+ // Get the hash from the file
 1483+ $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
 1484+ $props = $this->file->repo->getFileProps( $oldUrl );
 1485+
 1486+ if ( $props['fileExists'] ) {
 1487+ // Upgrade the oldimage row
 1488+ $dbw->update( 'oldimage',
 1489+ array( 'oi_sha1' => $props['sha1'] ),
 1490+ array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
 1491+ __METHOD__ );
 1492+ $hashes[$row->oi_archive_name] = $props['sha1'];
 1493+ } else {
 1494+ $hashes[$row->oi_archive_name] = false;
 1495+ }
 1496+ } else {
 1497+ $hashes[$row->oi_archive_name] = $row->oi_sha1;
 1498+ }
 1499+ }
 1500+ }
 1501+
 1502+ $missing = array_diff_key( $this->srcRels, $hashes );
 1503+
 1504+ foreach ( $missing as $name => $rel ) {
 1505+ $this->status->error( 'filedelete-old-unregistered', $name );
 1506+ }
 1507+
 1508+ foreach ( $hashes as $name => $hash ) {
 1509+ if ( !$hash ) {
 1510+ $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
 1511+ unset( $hashes[$name] );
 1512+ }
 1513+ }
 1514+
 1515+ return $hashes;
 1516+ }
 1517+
 1518+ function doDBInserts() {
 1519+ global $wgUser;
 1520+
 1521+ $dbw = $this->file->repo->getMasterDB();
 1522+ $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
 1523+ $encUserId = $dbw->addQuotes( $wgUser->getId() );
 1524+ $encReason = $dbw->addQuotes( $this->reason );
 1525+ $encGroup = $dbw->addQuotes( 'deleted' );
 1526+ $ext = $this->file->getExtension();
 1527+ $dotExt = $ext === '' ? '' : ".$ext";
 1528+ $encExt = $dbw->addQuotes( $dotExt );
 1529+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1530+
 1531+ // Bitfields to further suppress the content
 1532+ if ( $this->suppress ) {
 1533+ $bitfield = 0;
 1534+ // This should be 15...
 1535+ $bitfield |= Revision::DELETED_TEXT;
 1536+ $bitfield |= Revision::DELETED_COMMENT;
 1537+ $bitfield |= Revision::DELETED_USER;
 1538+ $bitfield |= Revision::DELETED_RESTRICTED;
 1539+ } else {
 1540+ $bitfield = 'oi_deleted';
 1541+ }
 1542+
 1543+ if ( $deleteCurrent ) {
 1544+ $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
 1545+ $where = array( 'img_name' => $this->file->getName() );
 1546+ $dbw->insertSelect( 'filearchive', 'image',
 1547+ array(
 1548+ 'fa_storage_group' => $encGroup,
 1549+ 'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
 1550+ 'fa_deleted_user' => $encUserId,
 1551+ 'fa_deleted_timestamp' => $encTimestamp,
 1552+ 'fa_deleted_reason' => $encReason,
 1553+ 'fa_deleted' => $this->suppress ? $bitfield : 0,
 1554+
 1555+ 'fa_name' => 'img_name',
 1556+ 'fa_archive_name' => 'NULL',
 1557+ 'fa_size' => 'img_size',
 1558+ 'fa_width' => 'img_width',
 1559+ 'fa_height' => 'img_height',
 1560+ 'fa_metadata' => 'img_metadata',
 1561+ 'fa_bits' => 'img_bits',
 1562+ 'fa_media_type' => 'img_media_type',
 1563+ 'fa_major_mime' => 'img_major_mime',
 1564+ 'fa_minor_mime' => 'img_minor_mime',
 1565+ 'fa_description' => 'img_description',
 1566+ 'fa_user' => 'img_user',
 1567+ 'fa_user_text' => 'img_user_text',
 1568+ 'fa_timestamp' => 'img_timestamp'
 1569+ ), $where, __METHOD__ );
 1570+ }
 1571+
 1572+ if ( count( $oldRels ) ) {
 1573+ $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
 1574+ $where = array(
 1575+ 'oi_name' => $this->file->getName(),
 1576+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
 1577+ $dbw->insertSelect( 'filearchive', 'oldimage',
 1578+ array(
 1579+ 'fa_storage_group' => $encGroup,
 1580+ 'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
 1581+ 'fa_deleted_user' => $encUserId,
 1582+ 'fa_deleted_timestamp' => $encTimestamp,
 1583+ 'fa_deleted_reason' => $encReason,
 1584+ 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
 1585+
 1586+ 'fa_name' => 'oi_name',
 1587+ 'fa_archive_name' => 'oi_archive_name',
 1588+ 'fa_size' => 'oi_size',
 1589+ 'fa_width' => 'oi_width',
 1590+ 'fa_height' => 'oi_height',
 1591+ 'fa_metadata' => 'oi_metadata',
 1592+ 'fa_bits' => 'oi_bits',
 1593+ 'fa_media_type' => 'oi_media_type',
 1594+ 'fa_major_mime' => 'oi_major_mime',
 1595+ 'fa_minor_mime' => 'oi_minor_mime',
 1596+ 'fa_description' => 'oi_description',
 1597+ 'fa_user' => 'oi_user',
 1598+ 'fa_user_text' => 'oi_user_text',
 1599+ 'fa_timestamp' => 'oi_timestamp',
 1600+ 'fa_deleted' => $bitfield
 1601+ ), $where, __METHOD__ );
 1602+ }
 1603+ }
 1604+
 1605+ function doDBDeletes() {
 1606+ $dbw = $this->file->repo->getMasterDB();
 1607+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1608+
 1609+ if ( count( $oldRels ) ) {
 1610+ $dbw->delete( 'oldimage',
 1611+ array(
 1612+ 'oi_name' => $this->file->getName(),
 1613+ 'oi_archive_name' => array_keys( $oldRels )
 1614+ ), __METHOD__ );
 1615+ }
 1616+
 1617+ if ( $deleteCurrent ) {
 1618+ $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
 1619+ }
 1620+ }
 1621+
 1622+ /**
 1623+ * Run the transaction
 1624+ */
 1625+ function execute() {
 1626+ global $wgUseSquid;
 1627+ wfProfileIn( __METHOD__ );
 1628+
 1629+ $this->file->lock();
 1630+ // Leave private files alone
 1631+ $privateFiles = array();
 1632+ list( $oldRels, $deleteCurrent ) = $this->getOldRels();
 1633+ $dbw = $this->file->repo->getMasterDB();
 1634+
 1635+ if ( !empty( $oldRels ) ) {
 1636+ $res = $dbw->select( 'oldimage',
 1637+ array( 'oi_archive_name' ),
 1638+ array( 'oi_name' => $this->file->getName(),
 1639+ 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
 1640+ $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
 1641+ __METHOD__ );
 1642+
 1643+ foreach ( $res as $row ) {
 1644+ $privateFiles[$row->oi_archive_name] = 1;
 1645+ }
 1646+ }
 1647+ // Prepare deletion batch
 1648+ $hashes = $this->getHashes();
 1649+ $this->deletionBatch = array();
 1650+ $ext = $this->file->getExtension();
 1651+ $dotExt = $ext === '' ? '' : ".$ext";
 1652+
 1653+ foreach ( $this->srcRels as $name => $srcRel ) {
 1654+ // Skip files that have no hash (missing source).
 1655+ // Keep private files where they are.
 1656+ if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
 1657+ $hash = $hashes[$name];
 1658+ $key = $hash . $dotExt;
 1659+ $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
 1660+ $this->deletionBatch[$name] = array( $srcRel, $dstRel );
 1661+ }
 1662+ }
 1663+
 1664+ // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
 1665+ // We acquire this lock by running the inserts now, before the file operations.
 1666+ //
 1667+ // This potentially has poor lock contention characteristics -- an alternative
 1668+ // scheme would be to insert stub filearchive entries with no fa_name and commit
 1669+ // them in a separate transaction, then run the file ops, then update the fa_name fields.
 1670+ $this->doDBInserts();
 1671+
 1672+ // Removes non-existent file from the batch, so we don't get errors.
 1673+ $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
 1674+
 1675+ // Execute the file deletion batch
 1676+ $status = $this->file->repo->deleteBatch( $this->deletionBatch );
 1677+
 1678+ if ( !$status->isGood() ) {
 1679+ $this->status->merge( $status );
 1680+ }
 1681+
 1682+ if ( !$this->status->ok ) {
 1683+ // Critical file deletion error
 1684+ // Roll back inserts, release lock and abort
 1685+ // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
 1686+ $this->file->unlockAndRollback();
 1687+ wfProfileOut( __METHOD__ );
 1688+ return $this->status;
 1689+ }
 1690+
 1691+ // Purge squid
 1692+ if ( $wgUseSquid ) {
 1693+ $urls = array();
 1694+
 1695+ foreach ( $this->srcRels as $srcRel ) {
 1696+ $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
 1697+ $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
 1698+ }
 1699+ SquidUpdate::purge( $urls );
 1700+ }
 1701+
 1702+ // Delete image/oldimage rows
 1703+ $this->doDBDeletes();
 1704+
 1705+ // Commit and return
 1706+ $this->file->unlock();
 1707+ wfProfileOut( __METHOD__ );
 1708+
 1709+ return $this->status;
 1710+ }
 1711+
 1712+ /**
 1713+ * Removes non-existent files from a deletion batch.
 1714+ */
 1715+ function removeNonexistentFiles( $batch ) {
 1716+ $files = $newBatch = array();
 1717+
 1718+ foreach ( $batch as $batchItem ) {
 1719+ list( $src, $dest ) = $batchItem;
 1720+ $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
 1721+ }
 1722+
 1723+ $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
 1724+
 1725+ foreach ( $batch as $batchItem ) {
 1726+ if ( $result[$batchItem[0]] ) {
 1727+ $newBatch[] = $batchItem;
 1728+ }
 1729+ }
 1730+
 1731+ return $newBatch;
 1732+ }
 1733+}
 1734+
 1735+# ------------------------------------------------------------------------------
 1736+
 1737+/**
 1738+ * Helper class for file undeletion
 1739+ * @ingroup FileRepo
 1740+ */
 1741+class LocalFileRestoreBatch {
 1742+ /**
 1743+ * @var LocalFile
 1744+ */
 1745+ var $file;
 1746+
 1747+ var $cleanupBatch, $ids, $all, $unsuppress = false;
 1748+
 1749+ function __construct( File $file, $unsuppress = false ) {
 1750+ $this->file = $file;
 1751+ $this->cleanupBatch = $this->ids = array();
 1752+ $this->ids = array();
 1753+ $this->unsuppress = $unsuppress;
 1754+ }
 1755+
 1756+ /**
 1757+ * Add a file by ID
 1758+ */
 1759+ function addId( $fa_id ) {
 1760+ $this->ids[] = $fa_id;
 1761+ }
 1762+
 1763+ /**
 1764+ * Add a whole lot of files by ID
 1765+ */
 1766+ function addIds( $ids ) {
 1767+ $this->ids = array_merge( $this->ids, $ids );
 1768+ }
 1769+
 1770+ /**
 1771+ * Add all revisions of the file
 1772+ */
 1773+ function addAll() {
 1774+ $this->all = true;
 1775+ }
 1776+
 1777+ /**
 1778+ * Run the transaction, except the cleanup batch.
 1779+ * The cleanup batch should be run in a separate transaction, because it locks different
 1780+ * rows and there's no need to keep the image row locked while it's acquiring those locks
 1781+ * The caller may have its own transaction open.
 1782+ * So we save the batch and let the caller call cleanup()
 1783+ */
 1784+ function execute() {
 1785+ global $wgLang;
 1786+
 1787+ if ( !$this->all && !$this->ids ) {
 1788+ // Do nothing
 1789+ return $this->file->repo->newGood();
 1790+ }
 1791+
 1792+ $exists = $this->file->lock();
 1793+ $dbw = $this->file->repo->getMasterDB();
 1794+ $status = $this->file->repo->newGood();
 1795+
 1796+ // Fetch all or selected archived revisions for the file,
 1797+ // sorted from the most recent to the oldest.
 1798+ $conditions = array( 'fa_name' => $this->file->getName() );
 1799+
 1800+ if ( !$this->all ) {
 1801+ $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
 1802+ }
 1803+
 1804+ $result = $dbw->select( 'filearchive', '*',
 1805+ $conditions,
 1806+ __METHOD__,
 1807+ array( 'ORDER BY' => 'fa_timestamp DESC' )
 1808+ );
 1809+
 1810+ $idsPresent = array();
 1811+ $storeBatch = array();
 1812+ $insertBatch = array();
 1813+ $insertCurrent = false;
 1814+ $deleteIds = array();
 1815+ $first = true;
 1816+ $archiveNames = array();
 1817+
 1818+ foreach ( $result as $row ) {
 1819+ $idsPresent[] = $row->fa_id;
 1820+
 1821+ if ( $row->fa_name != $this->file->getName() ) {
 1822+ $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
 1823+ $status->failCount++;
 1824+ continue;
 1825+ }
 1826+
 1827+ if ( $row->fa_storage_key == '' ) {
 1828+ // Revision was missing pre-deletion
 1829+ $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
 1830+ $status->failCount++;
 1831+ continue;
 1832+ }
 1833+
 1834+ $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
 1835+ $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
 1836+
 1837+ $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
 1838+
 1839+ # Fix leading zero
 1840+ if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
 1841+ $sha1 = substr( $sha1, 1 );
 1842+ }
 1843+
 1844+ if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
 1845+ || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
 1846+ || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
 1847+ || is_null( $row->fa_metadata ) ) {
 1848+ // Refresh our metadata
 1849+ // Required for a new current revision; nice for older ones too. :)
 1850+ $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
 1851+ } else {
 1852+ $props = array(
 1853+ 'minor_mime' => $row->fa_minor_mime,
 1854+ 'major_mime' => $row->fa_major_mime,
 1855+ 'media_type' => $row->fa_media_type,
 1856+ 'metadata' => $row->fa_metadata
 1857+ );
 1858+ }
 1859+
 1860+ if ( $first && !$exists ) {
 1861+ // This revision will be published as the new current version
 1862+ $destRel = $this->file->getRel();
 1863+ $insertCurrent = array(
 1864+ 'img_name' => $row->fa_name,
 1865+ 'img_size' => $row->fa_size,
 1866+ 'img_width' => $row->fa_width,
 1867+ 'img_height' => $row->fa_height,
 1868+ 'img_metadata' => $props['metadata'],
 1869+ 'img_bits' => $row->fa_bits,
 1870+ 'img_media_type' => $props['media_type'],
 1871+ 'img_major_mime' => $props['major_mime'],
 1872+ 'img_minor_mime' => $props['minor_mime'],
 1873+ 'img_description' => $row->fa_description,
 1874+ 'img_user' => $row->fa_user,
 1875+ 'img_user_text' => $row->fa_user_text,
 1876+ 'img_timestamp' => $row->fa_timestamp,
 1877+ 'img_sha1' => $sha1
 1878+ );
 1879+
 1880+ // The live (current) version cannot be hidden!
 1881+ if ( !$this->unsuppress && $row->fa_deleted ) {
 1882+ $storeBatch[] = array( $deletedUrl, 'public', $destRel );
 1883+ $this->cleanupBatch[] = $row->fa_storage_key;
 1884+ }
 1885+ } else {
 1886+ $archiveName = $row->fa_archive_name;
 1887+
 1888+ if ( $archiveName == '' ) {
 1889+ // This was originally a current version; we
 1890+ // have to devise a new archive name for it.
 1891+ // Format is <timestamp of archiving>!<name>
 1892+ $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
 1893+
 1894+ do {
 1895+ $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
 1896+ $timestamp++;
 1897+ } while ( isset( $archiveNames[$archiveName] ) );
 1898+ }
 1899+
 1900+ $archiveNames[$archiveName] = true;
 1901+ $destRel = $this->file->getArchiveRel( $archiveName );
 1902+ $insertBatch[] = array(
 1903+ 'oi_name' => $row->fa_name,
 1904+ 'oi_archive_name' => $archiveName,
 1905+ 'oi_size' => $row->fa_size,
 1906+ 'oi_width' => $row->fa_width,
 1907+ 'oi_height' => $row->fa_height,
 1908+ 'oi_bits' => $row->fa_bits,
 1909+ 'oi_description' => $row->fa_description,
 1910+ 'oi_user' => $row->fa_user,
 1911+ 'oi_user_text' => $row->fa_user_text,
 1912+ 'oi_timestamp' => $row->fa_timestamp,
 1913+ 'oi_metadata' => $props['metadata'],
 1914+ 'oi_media_type' => $props['media_type'],
 1915+ 'oi_major_mime' => $props['major_mime'],
 1916+ 'oi_minor_mime' => $props['minor_mime'],
 1917+ 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
 1918+ 'oi_sha1' => $sha1 );
 1919+ }
 1920+
 1921+ $deleteIds[] = $row->fa_id;
 1922+
 1923+ if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
 1924+ // private files can stay where they are
 1925+ $status->successCount++;
 1926+ } else {
 1927+ $storeBatch[] = array( $deletedUrl, 'public', $destRel );
 1928+ $this->cleanupBatch[] = $row->fa_storage_key;
 1929+ }
 1930+
 1931+ $first = false;
 1932+ }
 1933+
 1934+ unset( $result );
 1935+
 1936+ // Add a warning to the status object for missing IDs
 1937+ $missingIds = array_diff( $this->ids, $idsPresent );
 1938+
 1939+ foreach ( $missingIds as $id ) {
 1940+ $status->error( 'undelete-missing-filearchive', $id );
 1941+ }
 1942+
 1943+ // Remove missing files from batch, so we don't get errors when undeleting them
 1944+ $storeBatch = $this->removeNonexistentFiles( $storeBatch );
 1945+
 1946+ // Run the store batch
 1947+ // Use the OVERWRITE_SAME flag to smooth over a common error
 1948+ $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
 1949+ $status->merge( $storeStatus );
 1950+
 1951+ if ( !$status->isGood() ) {
 1952+ // Even if some files could be copied, fail entirely as that is the
 1953+ // easiest thing to do without data loss
 1954+ $this->cleanupFailedBatch( $storeStatus, $storeBatch );
 1955+ $status->ok = false;
 1956+ $this->file->unlock();
 1957+
 1958+ return $status;
 1959+ }
 1960+
 1961+ // Run the DB updates
 1962+ // Because we have locked the image row, key conflicts should be rare.
 1963+ // If they do occur, we can roll back the transaction at this time with
 1964+ // no data loss, but leaving unregistered files scattered throughout the
 1965+ // public zone.
 1966+ // This is not ideal, which is why it's important to lock the image row.
 1967+ if ( $insertCurrent ) {
 1968+ $dbw->insert( 'image', $insertCurrent, __METHOD__ );
 1969+ }
 1970+
 1971+ if ( $insertBatch ) {
 1972+ $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
 1973+ }
 1974+
 1975+ if ( $deleteIds ) {
 1976+ $dbw->delete( 'filearchive',
 1977+ array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
 1978+ __METHOD__ );
 1979+ }
 1980+
 1981+ // If store batch is empty (all files are missing), deletion is to be considered successful
 1982+ if ( $status->successCount > 0 || !$storeBatch ) {
 1983+ if ( !$exists ) {
 1984+ wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
 1985+
 1986+ // Update site_stats
 1987+ $site_stats = $dbw->tableName( 'site_stats' );
 1988+ $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
 1989+
 1990+ $this->file->purgeEverything();
 1991+ } else {
 1992+ wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
 1993+ $this->file->purgeDescription();
 1994+ $this->file->purgeHistory();
 1995+ }
 1996+ }
 1997+
 1998+ $this->file->unlock();
 1999+
 2000+ return $status;
 2001+ }
 2002+
 2003+ /**
 2004+ * Removes non-existent files from a store batch.
 2005+ */
 2006+ function removeNonexistentFiles( $triplets ) {
 2007+ $files = $filteredTriplets = array();
 2008+ foreach ( $triplets as $file )
 2009+ $files[$file[0]] = $file[0];
 2010+
 2011+ $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
 2012+
 2013+ foreach ( $triplets as $file ) {
 2014+ if ( $result[$file[0]] ) {
 2015+ $filteredTriplets[] = $file;
 2016+ }
 2017+ }
 2018+
 2019+ return $filteredTriplets;
 2020+ }
 2021+
 2022+ /**
 2023+ * Removes non-existent files from a cleanup batch.
 2024+ */
 2025+ function removeNonexistentFromCleanup( $batch ) {
 2026+ $files = $newBatch = array();
 2027+ $repo = $this->file->repo;
 2028+
 2029+ foreach ( $batch as $file ) {
 2030+ $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
 2031+ rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
 2032+ }
 2033+
 2034+ $result = $repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
 2035+
 2036+ foreach ( $batch as $file ) {
 2037+ if ( $result[$file] ) {
 2038+ $newBatch[] = $file;
 2039+ }
 2040+ }
 2041+
 2042+ return $newBatch;
 2043+ }
 2044+
 2045+ /**
 2046+ * Delete unused files in the deleted zone.
 2047+ * This should be called from outside the transaction in which execute() was called.
 2048+ */
 2049+ function cleanup() {
 2050+ if ( !$this->cleanupBatch ) {
 2051+ return $this->file->repo->newGood();
 2052+ }
 2053+
 2054+ $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
 2055+
 2056+ $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
 2057+
 2058+ return $status;
 2059+ }
 2060+
 2061+ /**
 2062+ * Cleanup a failed batch. The batch was only partially successful, so
 2063+ * rollback by removing all items that were succesfully copied.
 2064+ *
 2065+ * @param Status $storeStatus
 2066+ * @param array $storeBatch
 2067+ */
 2068+ function cleanupFailedBatch( $storeStatus, $storeBatch ) {
 2069+ $cleanupBatch = array();
 2070+
 2071+ foreach ( $storeStatus->success as $i => $success ) {
 2072+ // Check if this item of the batch was successfully copied
 2073+ if ( $success ) {
 2074+ // Item was successfully copied and needs to be removed again
 2075+ // Extract ($dstZone, $dstRel) from the batch
 2076+ $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
 2077+ }
 2078+ }
 2079+ $this->file->repo->cleanupBatch( $cleanupBatch );
 2080+ }
 2081+}
 2082+
 2083+# ------------------------------------------------------------------------------
 2084+
 2085+/**
 2086+ * Helper class for file movement
 2087+ * @ingroup FileRepo
 2088+ */
 2089+class LocalFileMoveBatch {
 2090+
 2091+ /**
 2092+ * @var File
 2093+ */
 2094+ var $file;
 2095+
 2096+ /**
 2097+ * @var Title
 2098+ */
 2099+ var $target;
 2100+
 2101+ var $cur, $olds, $oldCount, $archive, $db;
 2102+
 2103+ function __construct( File $file, Title $target ) {
 2104+ $this->file = $file;
 2105+ $this->target = $target;
 2106+ $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
 2107+ $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
 2108+ $this->oldName = $this->file->getName();
 2109+ $this->newName = $this->file->repo->getNameFromTitle( $this->target );
 2110+ $this->oldRel = $this->oldHash . $this->oldName;
 2111+ $this->newRel = $this->newHash . $this->newName;
 2112+ $this->db = $file->repo->getMasterDb();
 2113+ }
 2114+
 2115+ /**
 2116+ * Add the current image to the batch
 2117+ */
 2118+ function addCurrent() {
 2119+ $this->cur = array( $this->oldRel, $this->newRel );
 2120+ }
 2121+
 2122+ /**
 2123+ * Add the old versions of the image to the batch
 2124+ */
 2125+ function addOlds() {
 2126+ $archiveBase = 'archive';
 2127+ $this->olds = array();
 2128+ $this->oldCount = 0;
 2129+
 2130+ $result = $this->db->select( 'oldimage',
 2131+ array( 'oi_archive_name', 'oi_deleted' ),
 2132+ array( 'oi_name' => $this->oldName ),
 2133+ __METHOD__
 2134+ );
 2135+
 2136+ foreach ( $result as $row ) {
 2137+ $oldName = $row->oi_archive_name;
 2138+ $bits = explode( '!', $oldName, 2 );
 2139+
 2140+ if ( count( $bits ) != 2 ) {
 2141+ wfDebug( "Old file name missing !: '$oldName' \n" );
 2142+ continue;
 2143+ }
 2144+
 2145+ list( $timestamp, $filename ) = $bits;
 2146+
 2147+ if ( $this->oldName != $filename ) {
 2148+ wfDebug( "Old file name doesn't match: '$oldName' \n" );
 2149+ continue;
 2150+ }
 2151+
 2152+ $this->oldCount++;
 2153+
 2154+ // Do we want to add those to oldCount?
 2155+ if ( $row->oi_deleted & File::DELETED_FILE ) {
 2156+ continue;
 2157+ }
 2158+
 2159+ $this->olds[] = array(
 2160+ "{$archiveBase}/{$this->oldHash}{$oldName}",
 2161+ "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
 2162+ );
 2163+ }
 2164+ }
 2165+
 2166+ /**
 2167+ * Perform the move.
 2168+ */
 2169+ function execute() {
 2170+ $repo = $this->file->repo;
 2171+ $status = $repo->newGood();
 2172+ $triplets = $this->getMoveTriplets();
 2173+
 2174+ $triplets = $this->removeNonexistentFiles( $triplets );
 2175+
 2176+ // Copy the files into their new location
 2177+ $statusMove = $repo->storeBatch( $triplets );
 2178+ wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
 2179+ if ( !$statusMove->isGood() ) {
 2180+ wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
 2181+ $this->cleanupTarget( $triplets );
 2182+ $statusMove->ok = false;
 2183+ return $statusMove;
 2184+ }
 2185+
 2186+ $this->db->begin();
 2187+ $statusDb = $this->doDBUpdates();
 2188+ wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
 2189+ if ( !$statusDb->isGood() ) {
 2190+ $this->db->rollback();
 2191+ // Something went wrong with the DB updates, so remove the target files
 2192+ $this->cleanupTarget( $triplets );
 2193+ $statusDb->ok = false;
 2194+ return $statusDb;
 2195+ }
 2196+ $this->db->commit();
 2197+
 2198+ // Everything went ok, remove the source files
 2199+ $this->cleanupSource( $triplets );
 2200+
 2201+ $status->merge( $statusDb );
 2202+ $status->merge( $statusMove );
 2203+
 2204+ return $status;
 2205+ }
 2206+
 2207+ /**
 2208+ * Do the database updates and return a new FileRepoStatus indicating how
 2209+ * many rows where updated.
 2210+ *
 2211+ * @return FileRepoStatus
 2212+ */
 2213+ function doDBUpdates() {
 2214+ $repo = $this->file->repo;
 2215+ $status = $repo->newGood();
 2216+ $dbw = $this->db;
 2217+
 2218+ // Update current image
 2219+ $dbw->update(
 2220+ 'image',
 2221+ array( 'img_name' => $this->newName ),
 2222+ array( 'img_name' => $this->oldName ),
 2223+ __METHOD__
 2224+ );
 2225+
 2226+ if ( $dbw->affectedRows() ) {
 2227+ $status->successCount++;
 2228+ } else {
 2229+ $status->failCount++;
 2230+ $status->fatal( 'imageinvalidfilename' );
 2231+ return $status;
 2232+ }
 2233+
 2234+ // Update old images
 2235+ $dbw->update(
 2236+ 'oldimage',
 2237+ array(
 2238+ 'oi_name' => $this->newName,
 2239+ 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
 2240+ ),
 2241+ array( 'oi_name' => $this->oldName ),
 2242+ __METHOD__
 2243+ );
 2244+
 2245+ $affected = $dbw->affectedRows();
 2246+ $total = $this->oldCount;
 2247+ $status->successCount += $affected;
 2248+ $status->failCount += $total - $affected;
 2249+ if ( $status->failCount ) {
 2250+ $status->error( 'imageinvalidfilename' );
 2251+ }
 2252+
 2253+ return $status;
 2254+ }
 2255+
 2256+ /**
 2257+ * Generate triplets for FSRepo::storeBatch().
 2258+ */
 2259+ function getMoveTriplets() {
 2260+ $moves = array_merge( array( $this->cur ), $this->olds );
 2261+ $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
 2262+
 2263+ foreach ( $moves as $move ) {
 2264+ // $move: (oldRelativePath, newRelativePath)
 2265+ $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
 2266+ $triplets[] = array( $srcUrl, 'public', $move[1] );
 2267+ wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" );
 2268+ }
 2269+
 2270+ return $triplets;
 2271+ }
 2272+
 2273+ /**
 2274+ * Removes non-existent files from move batch.
 2275+ */
 2276+ function removeNonexistentFiles( $triplets ) {
 2277+ $files = array();
 2278+
 2279+ foreach ( $triplets as $file ) {
 2280+ $files[$file[0]] = $file[0];
 2281+ }
 2282+
 2283+ $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
 2284+ $filteredTriplets = array();
 2285+
 2286+ foreach ( $triplets as $file ) {
 2287+ if ( $result[$file[0]] ) {
 2288+ $filteredTriplets[] = $file;
 2289+ } else {
 2290+ wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
 2291+ }
 2292+ }
 2293+
 2294+ return $filteredTriplets;
 2295+ }
 2296+
 2297+ /**
 2298+ * Cleanup a partially moved array of triplets by deleting the target
 2299+ * files. Called if something went wrong half way.
 2300+ */
 2301+ function cleanupTarget( $triplets ) {
 2302+ // Create dest pairs from the triplets
 2303+ $pairs = array();
 2304+ foreach ( $triplets as $triplet ) {
 2305+ $pairs[] = array( $triplet[1], $triplet[2] );
 2306+ }
 2307+
 2308+ $this->file->repo->cleanupBatch( $pairs );
 2309+ }
 2310+
 2311+ /**
 2312+ * Cleanup a fully moved array of triplets by deleting the source files.
 2313+ * Called at the end of the move process if everything else went ok.
 2314+ */
 2315+ function cleanupSource( $triplets ) {
 2316+ // Create source file names from the triplets
 2317+ $files = array();
 2318+ foreach ( $triplets as $triplet ) {
 2319+ $files[] = $triplet[0];
 2320+ }
 2321+
 2322+ $this->file->repo->cleanupBatch( $files );
 2323+ }
 2324+}
Property changes on: trunk/phase3/includes/filerepo/file/LocalFile.php
___________________________________________________________________
Added: svn:eol-style
12325 + native
Index: trunk/phase3/includes/filerepo/file/UnregisteredLocalFile.php
@@ -0,0 +1,143 @@
 2+<?php
 3+/**
 4+ * File without associated database record
 5+ *
 6+ * @file
 7+ * @ingroup FileRepo
 8+ */
 9+
 10+/**
 11+ * A file object referring to either a standalone local file, or a file in a
 12+ * local repository with no database, for example an FSRepo repository.
 13+ *
 14+ * Read-only.
 15+ *
 16+ * TODO: Currently it doesn't really work in the repository role, there are
 17+ * lots of functions missing. It is used by the WebStore extension in the
 18+ * standalone role.
 19+ *
 20+ * @ingroup FileRepo
 21+ */
 22+class UnregisteredLocalFile extends File {
 23+ var $title, $path, $mime, $dims;
 24+
 25+ /**
 26+ * @var MediaHandler
 27+ */
 28+ var $handler;
 29+
 30+ /**
 31+ * @param $path
 32+ * @param $mime
 33+ * @return UnregisteredLocalFile
 34+ */
 35+ static function newFromPath( $path, $mime ) {
 36+ return new self( false, false, $path, $mime );
 37+ }
 38+
 39+ /**
 40+ * @param $title
 41+ * @param $repo
 42+ * @return UnregisteredLocalFile
 43+ */
 44+ static function newFromTitle( $title, $repo ) {
 45+ return new self( $title, $repo, false, false );
 46+ }
 47+
 48+ /**
 49+ * Create an UnregisteredLocalFile based on a path or a (title,repo) pair.
 50+ * A FileRepo object is not required here, unlike most other File classes.
 51+ *
 52+ * @throws MWException
 53+ * @param $title Title|false
 54+ * @param $repo FSRepo
 55+ * @param $path string
 56+ * @param $mime string
 57+ */
 58+ function __construct( $title = false, $repo = false, $path = false, $mime = false ) {
 59+ if ( !( $title && $repo ) && !$path ) {
 60+ throw new MWException( __METHOD__.': not enough parameters, must specify title and repo, or a full path' );
 61+ }
 62+ if ( $title instanceof Title ) {
 63+ $this->title = File::normalizeTitle( $title, 'exception' );
 64+ $this->name = $repo->getNameFromTitle( $title );
 65+ } else {
 66+ $this->name = basename( $path );
 67+ $this->title = File::normalizeTitle( $this->name, 'exception' );
 68+ }
 69+ $this->repo = $repo;
 70+ if ( $path ) {
 71+ $this->path = $path;
 72+ } else {
 73+ $this->path = $repo->getRootDirectory() . '/' .
 74+ $repo->getHashPath( $this->name ) . $this->name;
 75+ }
 76+ if ( $mime ) {
 77+ $this->mime = $mime;
 78+ }
 79+ $this->dims = array();
 80+ }
 81+
 82+ private function cachePageDimensions( $page = 1 ) {
 83+ if ( !isset( $this->dims[$page] ) ) {
 84+ if ( !$this->getHandler() ) {
 85+ return false;
 86+ }
 87+ $this->dims[$page] = $this->handler->getPageDimensions( $this, $page );
 88+ }
 89+ return $this->dims[$page];
 90+ }
 91+
 92+ function getWidth( $page = 1 ) {
 93+ $dim = $this->cachePageDimensions( $page );
 94+ return $dim['width'];
 95+ }
 96+
 97+ function getHeight( $page = 1 ) {
 98+ $dim = $this->cachePageDimensions( $page );
 99+ return $dim['height'];
 100+ }
 101+
 102+ function getMimeType() {
 103+ if ( !isset( $this->mime ) ) {
 104+ $magic = MimeMagic::singleton();
 105+ $this->mime = $magic->guessMimeType( $this->getPath() );
 106+ }
 107+ return $this->mime;
 108+ }
 109+
 110+ function getImageSize( $filename ) {
 111+ if ( !$this->getHandler() ) {
 112+ return false;
 113+ }
 114+ return $this->handler->getImageSize( $this, $this->getPath() );
 115+ }
 116+
 117+ function getMetadata() {
 118+ if ( !isset( $this->metadata ) ) {
 119+ if ( !$this->getHandler() ) {
 120+ $this->metadata = false;
 121+ } else {
 122+ $this->metadata = $this->handler->getMetadata( $this, $this->getPath() );
 123+ }
 124+ }
 125+ return $this->metadata;
 126+ }
 127+
 128+ function getURL() {
 129+ if ( $this->repo ) {
 130+ return $this->repo->getZoneUrl( 'public' ) . '/' .
 131+ $this->repo->getHashPath( $this->name ) . rawurlencode( $this->name );
 132+ } else {
 133+ return false;
 134+ }
 135+ }
 136+
 137+ function getSize() {
 138+ if ( file_exists( $this->path ) ) {
 139+ return filesize( $this->path );
 140+ } else {
 141+ return false;
 142+ }
 143+ }
 144+}
Property changes on: trunk/phase3/includes/filerepo/file/UnregisteredLocalFile.php
___________________________________________________________________
Added: svn:eol-style
1145 + native
Index: trunk/phase3/includes/filerepo/file/File.php
@@ -0,0 +1,1683 @@
 2+<?php
 3+/**
 4+ * Base code for files.
 5+ *
 6+ * @file
 7+ * @ingroup FileRepo
 8+ */
 9+
 10+/**
 11+ * Implements some public methods and some protected utility functions which
 12+ * are required by multiple child classes. Contains stub functionality for
 13+ * unimplemented public methods.
 14+ *
 15+ * Stub functions which should be overridden are marked with STUB. Some more
 16+ * concrete functions are also typically overridden by child classes.
 17+ *
 18+ * Note that only the repo object knows what its file class is called. You should
 19+ * never name a file class explictly outside of the repo class. Instead use the
 20+ * repo's factory functions to generate file objects, for example:
 21+ *
 22+ * RepoGroup::singleton()->getLocalRepo()->newFile($title);
 23+ *
 24+ * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
 25+ * in most cases.
 26+ *
 27+ * @ingroup FileRepo
 28+ */
 29+abstract class File {
 30+ const DELETED_FILE = 1;
 31+ const DELETED_COMMENT = 2;
 32+ const DELETED_USER = 4;
 33+ const DELETED_RESTRICTED = 8;
 34+
 35+ /** Force rendering in the current process */
 36+ const RENDER_NOW = 1;
 37+ /**
 38+ * Force rendering even if thumbnail already exist and using RENDER_NOW
 39+ * I.e. you have to pass both flags: File::RENDER_NOW | File::RENDER_FORCE
 40+ */
 41+ const RENDER_FORCE = 2;
 42+
 43+ const DELETE_SOURCE = 1;
 44+
 45+ /**
 46+ * Some member variables can be lazy-initialised using __get(). The
 47+ * initialisation function for these variables is always a function named
 48+ * like getVar(), where Var is the variable name with upper-case first
 49+ * letter.
 50+ *
 51+ * The following variables are initialised in this way in this base class:
 52+ * name, extension, handler, path, canRender, isSafeFile,
 53+ * transformScript, hashPath, pageCount, url
 54+ *
 55+ * Code within this class should generally use the accessor function
 56+ * directly, since __get() isn't re-entrant and therefore causes bugs that
 57+ * depend on initialisation order.
 58+ */
 59+
 60+ /**
 61+ * The following member variables are not lazy-initialised
 62+ */
 63+
 64+ /**
 65+ * @var FileRepo|false
 66+ */
 67+ var $repo;
 68+
 69+ /**
 70+ * @var Title|false
 71+ */
 72+ var $title;
 73+
 74+ var $lastError, $redirected, $redirectedTitle;
 75+
 76+ /**
 77+ * @var MediaHandler
 78+ */
 79+ protected $handler;
 80+
 81+ protected $url, $extension, $name, $path, $hashPath, $pageCount, $transformScript;
 82+
 83+ /**
 84+ * @var bool
 85+ */
 86+ protected $canRender, $isSafeFile;
 87+
 88+ /**
 89+ * @var string Required Repository class type
 90+ */
 91+ protected $repoClass = 'FileRepo';
 92+
 93+ /**
 94+ * Call this constructor from child classes.
 95+ *
 96+ * Both $title and $repo are optional, though some functions
 97+ * may return false or throw exceptions if they are not set.
 98+ * Most subclasses will want to call assertRepoDefined() here.
 99+ *
 100+ * @param $title Title|string|false
 101+ * @param $repo FileRepo|false
 102+ */
 103+ function __construct( $title, $repo ) {
 104+ if ( $title !== false ) { // subclasses may not use MW titles
 105+ $title = self::normalizeTitle( $title, 'exception' );
 106+ }
 107+ $this->title = $title;
 108+ $this->repo = $repo;
 109+ }
 110+
 111+ /**
 112+ * Given a string or Title object return either a
 113+ * valid Title object with namespace NS_FILE or null
 114+ * @param $title Title|string
 115+ * @param $exception string|false Use 'exception' to throw an error on bad titles
 116+ * @return Title|null
 117+ */
 118+ static function normalizeTitle( $title, $exception = false ) {
 119+ $ret = $title;
 120+ if ( $ret instanceof Title ) {
 121+ # Normalize NS_MEDIA -> NS_FILE
 122+ if ( $ret->getNamespace() == NS_MEDIA ) {
 123+ $ret = Title::makeTitleSafe( NS_FILE, $ret->getDBkey() );
 124+ # Sanity check the title namespace
 125+ } elseif ( $ret->getNamespace() !== NS_FILE ) {
 126+ $ret = null;
 127+ }
 128+ } else {
 129+ # Convert strings to Title objects
 130+ $ret = Title::makeTitleSafe( NS_FILE, (string)$ret );
 131+ }
 132+ if ( !$ret && $exception !== false ) {
 133+ throw new MWException( "`$title` is not a valid file title." );
 134+ }
 135+ return $ret;
 136+ }
 137+
 138+ function __get( $name ) {
 139+ $function = array( $this, 'get' . ucfirst( $name ) );
 140+ if ( !is_callable( $function ) ) {
 141+ return null;
 142+ } else {
 143+ $this->$name = call_user_func( $function );
 144+ return $this->$name;
 145+ }
 146+ }
 147+
 148+ /**
 149+ * Normalize a file extension to the common form, and ensure it's clean.
 150+ * Extensions with non-alphanumeric characters will be discarded.
 151+ *
 152+ * @param $ext string (without the .)
 153+ * @return string
 154+ */
 155+ static function normalizeExtension( $ext ) {
 156+ $lower = strtolower( $ext );
 157+ $squish = array(
 158+ 'htm' => 'html',
 159+ 'jpeg' => 'jpg',
 160+ 'mpeg' => 'mpg',
 161+ 'tiff' => 'tif',
 162+ 'ogv' => 'ogg' );
 163+ if( isset( $squish[$lower] ) ) {
 164+ return $squish[$lower];
 165+ } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
 166+ return $lower;
 167+ } else {
 168+ return '';
 169+ }
 170+ }
 171+
 172+ /**
 173+ * Checks if file extensions are compatible
 174+ *
 175+ * @param $old File Old file
 176+ * @param $new string New name
 177+ *
 178+ * @return bool|null
 179+ */
 180+ static function checkExtensionCompatibility( File $old, $new ) {
 181+ $oldMime = $old->getMimeType();
 182+ $n = strrpos( $new, '.' );
 183+ $newExt = self::normalizeExtension(
 184+ $n ? substr( $new, $n + 1 ) : '' );
 185+ $mimeMagic = MimeMagic::singleton();
 186+ return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
 187+ }
 188+
 189+ /**
 190+ * Upgrade the database row if there is one
 191+ * Called by ImagePage
 192+ * STUB
 193+ */
 194+ function upgradeRow() {}
 195+
 196+ /**
 197+ * Split an internet media type into its two components; if not
 198+ * a two-part name, set the minor type to 'unknown'.
 199+ *
 200+ * @param string $mime "text/html" etc
 201+ * @return array ("text", "html") etc
 202+ */
 203+ public static function splitMime( $mime ) {
 204+ if( strpos( $mime, '/' ) !== false ) {
 205+ return explode( '/', $mime, 2 );
 206+ } else {
 207+ return array( $mime, 'unknown' );
 208+ }
 209+ }
 210+
 211+ /**
 212+ * Return the name of this file
 213+ *
 214+ * @return string
 215+ */
 216+ public function getName() {
 217+ if ( !isset( $this->name ) ) {
 218+ $this->assertRepoDefined();
 219+ $this->name = $this->repo->getNameFromTitle( $this->title );
 220+ }
 221+ return $this->name;
 222+ }
 223+
 224+ /**
 225+ * Get the file extension, e.g. "svg"
 226+ *
 227+ * @return string
 228+ */
 229+ function getExtension() {
 230+ if ( !isset( $this->extension ) ) {
 231+ $n = strrpos( $this->getName(), '.' );
 232+ $this->extension = self::normalizeExtension(
 233+ $n ? substr( $this->getName(), $n + 1 ) : '' );
 234+ }
 235+ return $this->extension;
 236+ }
 237+
 238+ /**
 239+ * Return the associated title object
 240+ * @return Title|false
 241+ */
 242+ public function getTitle() { return $this->title; }
 243+
 244+ /**
 245+ * Return the title used to find this file
 246+ *
 247+ * @return Title
 248+ */
 249+ public function getOriginalTitle() {
 250+ if ( $this->redirected ) {
 251+ return $this->getRedirectedTitle();
 252+ }
 253+ return $this->title;
 254+ }
 255+
 256+ /**
 257+ * Return the URL of the file
 258+ *
 259+ * @return string
 260+ */
 261+ public function getUrl() {
 262+ if ( !isset( $this->url ) ) {
 263+ $this->assertRepoDefined();
 264+ $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel();
 265+ }
 266+ return $this->url;
 267+ }
 268+
 269+ /**
 270+ * Return a fully-qualified URL to the file.
 271+ * Upload URL paths _may or may not_ be fully qualified, so
 272+ * we check. Local paths are assumed to belong on $wgServer.
 273+ *
 274+ * @return String
 275+ */
 276+ public function getFullUrl() {
 277+ return wfExpandUrl( $this->getUrl(), PROTO_RELATIVE );
 278+ }
 279+
 280+ /**
 281+ * @return string
 282+ */
 283+ public function getCanonicalUrl() {
 284+ return wfExpandUrl( $this->getUrl(), PROTO_CANONICAL );
 285+ }
 286+
 287+ /**
 288+ * @return string
 289+ */
 290+ function getViewURL() {
 291+ if( $this->mustRender()) {
 292+ if( $this->canRender() ) {
 293+ return $this->createThumb( $this->getWidth() );
 294+ } else {
 295+ wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n");
 296+ return $this->getURL(); #hm... return NULL?
 297+ }
 298+ } else {
 299+ return $this->getURL();
 300+ }
 301+ }
 302+
 303+ /**
 304+ * Return the full filesystem path to the file. Note that this does
 305+ * not mean that a file actually exists under that location.
 306+ *
 307+ * This path depends on whether directory hashing is active or not,
 308+ * i.e. whether the files are all found in the same directory,
 309+ * or in hashed paths like /images/3/3c.
 310+ *
 311+ * Most callers don't check the return value, but ForeignAPIFile::getPath
 312+ * returns false.
 313+ *
 314+ * @return string|false
 315+ */
 316+ public function getPath() {
 317+ if ( !isset( $this->path ) ) {
 318+ $this->assertRepoDefined();
 319+ $this->path = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
 320+ }
 321+ return $this->path;
 322+ }
 323+
 324+ /**
 325+ * Return the width of the image. Returns false if the width is unknown
 326+ * or undefined.
 327+ *
 328+ * STUB
 329+ * Overridden by LocalFile, UnregisteredLocalFile
 330+ *
 331+ * @param $page int
 332+ *
 333+ * @return number
 334+ */
 335+ public function getWidth( $page = 1 ) {
 336+ return false;
 337+ }
 338+
 339+ /**
 340+ * Return the height of the image. Returns false if the height is unknown
 341+ * or undefined
 342+ *
 343+ * STUB
 344+ * Overridden by LocalFile, UnregisteredLocalFile
 345+ *
 346+ * @param $page int
 347+ *
 348+ * @return false|number
 349+ */
 350+ public function getHeight( $page = 1 ) {
 351+ return false;
 352+ }
 353+
 354+ /**
 355+ * Returns ID or name of user who uploaded the file
 356+ * STUB
 357+ *
 358+ * @param $type string 'text' or 'id'
 359+ *
 360+ * @return string|int
 361+ */
 362+ public function getUser( $type = 'text' ) {
 363+ return null;
 364+ }
 365+
 366+ /**
 367+ * Get the duration of a media file in seconds
 368+ *
 369+ * @return number
 370+ */
 371+ public function getLength() {
 372+ $handler = $this->getHandler();
 373+ if ( $handler ) {
 374+ return $handler->getLength( $this );
 375+ } else {
 376+ return 0;
 377+ }
 378+ }
 379+
 380+ /**
 381+ * Return true if the file is vectorized
 382+ *
 383+ * @return bool
 384+ */
 385+ public function isVectorized() {
 386+ $handler = $this->getHandler();
 387+ if ( $handler ) {
 388+ return $handler->isVectorized( $this );
 389+ } else {
 390+ return false;
 391+ }
 392+ }
 393+
 394+ /**
 395+ * Get handler-specific metadata
 396+ * Overridden by LocalFile, UnregisteredLocalFile
 397+ * STUB
 398+ */
 399+ public function getMetadata() {
 400+ return false;
 401+ }
 402+
 403+ /**
 404+ * get versioned metadata
 405+ *
 406+ * @param $metadata Mixed Array or String of (serialized) metadata
 407+ * @param $version integer version number.
 408+ * @return Array containing metadata, or what was passed to it on fail (unserializing if not array)
 409+ */
 410+ public function convertMetadataVersion($metadata, $version) {
 411+ $handler = $this->getHandler();
 412+ if ( !is_array( $metadata ) ) {
 413+ //just to make the return type consistant
 414+ $metadata = unserialize( $metadata );
 415+ }
 416+ if ( $handler ) {
 417+ return $handler->convertMetadataVersion( $metadata, $version );
 418+ } else {
 419+ return $metadata;
 420+ }
 421+ }
 422+
 423+ /**
 424+ * Return the bit depth of the file
 425+ * Overridden by LocalFile
 426+ * STUB
 427+ */
 428+ public function getBitDepth() {
 429+ return 0;
 430+ }
 431+
 432+ /**
 433+ * Return the size of the image file, in bytes
 434+ * Overridden by LocalFile, UnregisteredLocalFile
 435+ * STUB
 436+ */
 437+ public function getSize() {
 438+ return false;
 439+ }
 440+
 441+ /**
 442+ * Returns the mime type of the file.
 443+ * Overridden by LocalFile, UnregisteredLocalFile
 444+ * STUB
 445+ *
 446+ * @return string
 447+ */
 448+ function getMimeType() {
 449+ return 'unknown/unknown';
 450+ }
 451+
 452+ /**
 453+ * Return the type of the media in the file.
 454+ * Use the value returned by this function with the MEDIATYPE_xxx constants.
 455+ * Overridden by LocalFile,
 456+ * STUB
 457+ */
 458+ function getMediaType() { return MEDIATYPE_UNKNOWN; }
 459+
 460+ /**
 461+ * Checks if the output of transform() for this file is likely
 462+ * to be valid. If this is false, various user elements will
 463+ * display a placeholder instead.
 464+ *
 465+ * Currently, this checks if the file is an image format
 466+ * that can be converted to a format
 467+ * supported by all browsers (namely GIF, PNG and JPEG),
 468+ * or if it is an SVG image and SVG conversion is enabled.
 469+ *
 470+ * @return bool
 471+ */
 472+ function canRender() {
 473+ if ( !isset( $this->canRender ) ) {
 474+ $this->canRender = $this->getHandler() && $this->handler->canRender( $this );
 475+ }
 476+ return $this->canRender;
 477+ }
 478+
 479+ /**
 480+ * Accessor for __get()
 481+ */
 482+ protected function getCanRender() {
 483+ return $this->canRender();
 484+ }
 485+
 486+ /**
 487+ * Return true if the file is of a type that can't be directly
 488+ * rendered by typical browsers and needs to be re-rasterized.
 489+ *
 490+ * This returns true for everything but the bitmap types
 491+ * supported by all browsers, i.e. JPEG; GIF and PNG. It will
 492+ * also return true for any non-image formats.
 493+ *
 494+ * @return bool
 495+ */
 496+ function mustRender() {
 497+ return $this->getHandler() && $this->handler->mustRender( $this );
 498+ }
 499+
 500+ /**
 501+ * Alias for canRender()
 502+ *
 503+ * @return bool
 504+ */
 505+ function allowInlineDisplay() {
 506+ return $this->canRender();
 507+ }
 508+
 509+ /**
 510+ * Determines if this media file is in a format that is unlikely to
 511+ * contain viruses or malicious content. It uses the global
 512+ * $wgTrustedMediaFormats list to determine if the file is safe.
 513+ *
 514+ * This is used to show a warning on the description page of non-safe files.
 515+ * It may also be used to disallow direct [[media:...]] links to such files.
 516+ *
 517+ * Note that this function will always return true if allowInlineDisplay()
 518+ * or isTrustedFile() is true for this file.
 519+ *
 520+ * @return bool
 521+ */
 522+ function isSafeFile() {
 523+ if ( !isset( $this->isSafeFile ) ) {
 524+ $this->isSafeFile = $this->_getIsSafeFile();
 525+ }
 526+ return $this->isSafeFile;
 527+ }
 528+
 529+ /**
 530+ * Accessor for __get()
 531+ *
 532+ * @return bool
 533+ */
 534+ protected function getIsSafeFile() {
 535+ return $this->isSafeFile();
 536+ }
 537+
 538+ /**
 539+ * Uncached accessor
 540+ *
 541+ * @return bool
 542+ */
 543+ protected function _getIsSafeFile() {
 544+ if ( $this->allowInlineDisplay() ) {
 545+ return true;
 546+ }
 547+ if ($this->isTrustedFile()) {
 548+ return true;
 549+ }
 550+
 551+ global $wgTrustedMediaFormats;
 552+
 553+ $type = $this->getMediaType();
 554+ $mime = $this->getMimeType();
 555+ #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n");
 556+
 557+ if ( !$type || $type === MEDIATYPE_UNKNOWN ) {
 558+ return false; #unknown type, not trusted
 559+ }
 560+ if ( in_array( $type, $wgTrustedMediaFormats ) ) {
 561+ return true;
 562+ }
 563+
 564+ if ( $mime === "unknown/unknown" ) {
 565+ return false; #unknown type, not trusted
 566+ }
 567+ if ( in_array( $mime, $wgTrustedMediaFormats) ) {
 568+ return true;
 569+ }
 570+
 571+ return false;
 572+ }
 573+
 574+ /**
 575+ * Returns true if the file is flagged as trusted. Files flagged that way
 576+ * can be linked to directly, even if that is not allowed for this type of
 577+ * file normally.
 578+ *
 579+ * This is a dummy function right now and always returns false. It could be
 580+ * implemented to extract a flag from the database. The trusted flag could be
 581+ * set on upload, if the user has sufficient privileges, to bypass script-
 582+ * and html-filters. It may even be coupled with cryptographics signatures
 583+ * or such.
 584+ *
 585+ * @return bool
 586+ */
 587+ function isTrustedFile() {
 588+ #this could be implemented to check a flag in the databas,
 589+ #look for signatures, etc
 590+ return false;
 591+ }
 592+
 593+ /**
 594+ * Returns true if file exists in the repository.
 595+ *
 596+ * Overridden by LocalFile to avoid unnecessary stat calls.
 597+ *
 598+ * @return boolean Whether file exists in the repository.
 599+ */
 600+ public function exists() {
 601+ return $this->getPath() && file_exists( $this->path );
 602+ }
 603+
 604+ /**
 605+ * Returns true if file exists in the repository and can be included in a page.
 606+ * It would be unsafe to include private images, making public thumbnails inadvertently
 607+ *
 608+ * @return boolean Whether file exists in the repository and is includable.
 609+ */
 610+ public function isVisible() {
 611+ return $this->exists();
 612+ }
 613+
 614+ /**
 615+ * @return string
 616+ */
 617+ function getTransformScript() {
 618+ if ( !isset( $this->transformScript ) ) {
 619+ $this->transformScript = false;
 620+ if ( $this->repo ) {
 621+ $script = $this->repo->getThumbScriptUrl();
 622+ if ( $script ) {
 623+ $this->transformScript = "$script?f=" . urlencode( $this->getName() );
 624+ }
 625+ }
 626+ }
 627+ return $this->transformScript;
 628+ }
 629+
 630+ /**
 631+ * Get a ThumbnailImage which is the same size as the source
 632+ *
 633+ * @param $handlerParams array
 634+ *
 635+ * @return string
 636+ */
 637+ function getUnscaledThumb( $handlerParams = array() ) {
 638+ $hp =& $handlerParams;
 639+ $page = isset( $hp['page'] ) ? $hp['page'] : false;
 640+ $width = $this->getWidth( $page );
 641+ if ( !$width ) {
 642+ return $this->iconThumb();
 643+ }
 644+ $hp['width'] = $width;
 645+ return $this->transform( $hp );
 646+ }
 647+
 648+ /**
 649+ * Return the file name of a thumbnail with the specified parameters
 650+ *
 651+ * @param $params Array: handler-specific parameters
 652+ * @private -ish
 653+ *
 654+ * @return string
 655+ */
 656+ function thumbName( $params ) {
 657+ return $this->generateThumbName( $this->getName(), $params );
 658+ }
 659+
 660+ /**
 661+ * Generate a thumbnail file name from a name and specified parameters
 662+ *
 663+ * @param string $name
 664+ * @param array $params Parameters which will be passed to MediaHandler::makeParamString
 665+ *
 666+ * @return string
 667+ */
 668+ function generateThumbName( $name, $params ) {
 669+ if ( !$this->getHandler() ) {
 670+ return null;
 671+ }
 672+ $extension = $this->getExtension();
 673+ list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params );
 674+ $thumbName = $this->handler->makeParamString( $params ) . '-' . $name;
 675+ if ( $thumbExt != $extension ) {
 676+ $thumbName .= ".$thumbExt";
 677+ }
 678+ return $thumbName;
 679+ }
 680+
 681+ /**
 682+ * Create a thumbnail of the image having the specified width/height.
 683+ * The thumbnail will not be created if the width is larger than the
 684+ * image's width. Let the browser do the scaling in this case.
 685+ * The thumbnail is stored on disk and is only computed if the thumbnail
 686+ * file does not exist OR if it is older than the image.
 687+ * Returns the URL.
 688+ *
 689+ * Keeps aspect ratio of original image. If both width and height are
 690+ * specified, the generated image will be no bigger than width x height,
 691+ * and will also have correct aspect ratio.
 692+ *
 693+ * @param $width Integer: maximum width of the generated thumbnail
 694+ * @param $height Integer: maximum height of the image (optional)
 695+ *
 696+ * @return string
 697+ */
 698+ public function createThumb( $width, $height = -1 ) {
 699+ $params = array( 'width' => $width );
 700+ if ( $height != -1 ) {
 701+ $params['height'] = $height;
 702+ }
 703+ $thumb = $this->transform( $params );
 704+ if( is_null( $thumb ) || $thumb->isError() ) return '';
 705+ return $thumb->getUrl();
 706+ }
 707+
 708+ /**
 709+ * Do the work of a transform (from an original into a thumb).
 710+ * Contains filesystem-specific functions.
 711+ *
 712+ * @param $thumbName string: the name of the thumbnail file.
 713+ * @param $thumbUrl string: the URL of the thumbnail file.
 714+ * @param $params Array: an associative array of handler-specific parameters.
 715+ * Typical keys are width, height and page.
 716+ * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering
 717+ *
 718+ * @return MediaTransformOutput | false
 719+ */
 720+ protected function maybeDoTransform( $thumbName, $thumbUrl, $params, $flags = 0 ) {
 721+ global $wgIgnoreImageErrors, $wgThumbnailEpoch;
 722+
 723+ $thumbPath = $this->getThumbPath( $thumbName );
 724+ if ( $this->repo && $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) {
 725+ wfDebug( __METHOD__ . " transformation deferred." );
 726+ return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
 727+ }
 728+
 729+ wfDebug( __METHOD__.": Doing stat for $thumbPath\n" );
 730+ $this->migrateThumbFile( $thumbName );
 731+ if ( file_exists( $thumbPath ) && !($flags & self::RENDER_FORCE) ) {
 732+ $thumbTime = filemtime( $thumbPath );
 733+ if ( $thumbTime !== FALSE &&
 734+ gmdate( 'YmdHis', $thumbTime ) >= $wgThumbnailEpoch ) {
 735+
 736+ return $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
 737+ }
 738+ } elseif( $flags & self::RENDER_FORCE ) {
 739+ wfDebug( __METHOD__ . " forcing rendering per flag File::RENDER_FORCE\n" );
 740+ }
 741+ $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params );
 742+
 743+ // Ignore errors if requested
 744+ if ( !$thumb ) {
 745+ $thumb = null;
 746+ } elseif ( $thumb->isError() ) {
 747+ $this->lastError = $thumb->toText();
 748+ if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) {
 749+ $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
 750+ }
 751+ }
 752+
 753+ return $thumb;
 754+ }
 755+
 756+
 757+ /**
 758+ * Transform a media file
 759+ *
 760+ * @param $params Array: an associative array of handler-specific parameters.
 761+ * Typical keys are width, height and page.
 762+ * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering
 763+ * @return MediaTransformOutput | false
 764+ */
 765+ function transform( $params, $flags = 0 ) {
 766+ global $wgUseSquid;
 767+
 768+ wfProfileIn( __METHOD__ );
 769+ do {
 770+ if ( !$this->canRender() ) {
 771+ // not a bitmap or renderable image, don't try.
 772+ $thumb = $this->iconThumb();
 773+ break;
 774+ }
 775+
 776+ // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791.
 777+ $descriptionUrl = $this->getDescriptionUrl();
 778+ if ( $descriptionUrl ) {
 779+ $params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL );
 780+ }
 781+
 782+ $script = $this->getTransformScript();
 783+ if ( $script && !($flags & self::RENDER_NOW) ) {
 784+ // Use a script to transform on client request, if possible
 785+ $thumb = $this->handler->getScriptedTransform( $this, $script, $params );
 786+ if( $thumb ) {
 787+ break;
 788+ }
 789+ }
 790+
 791+ $normalisedParams = $params;
 792+ $this->handler->normaliseParams( $this, $normalisedParams );
 793+ $thumbName = $this->thumbName( $normalisedParams );
 794+ $thumbUrl = $this->getThumbUrl( $thumbName );
 795+
 796+ $thumb = $this->maybeDoTransform( $thumbName, $thumbUrl, $params, $flags );
 797+
 798+ // Purge. Useful in the event of Core -> Squid connection failure or squid
 799+ // purge collisions from elsewhere during failure. Don't keep triggering for
 800+ // "thumbs" which have the main image URL though (bug 13776)
 801+ if ( $wgUseSquid && ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL()) ) {
 802+ SquidUpdate::purge( array( $thumbUrl ) );
 803+ }
 804+ } while (false);
 805+
 806+ wfProfileOut( __METHOD__ );
 807+ return is_object( $thumb ) ? $thumb : false;
 808+ }
 809+
 810+ /**
 811+ * Hook into transform() to allow migration of thumbnail files
 812+ * STUB
 813+ * Overridden by LocalFile
 814+ */
 815+ function migrateThumbFile( $thumbName ) {}
 816+
 817+ /**
 818+ * Get a MediaHandler instance for this file
 819+ * @return MediaHandler
 820+ */
 821+ function getHandler() {
 822+ if ( !isset( $this->handler ) ) {
 823+ $this->handler = MediaHandler::getHandler( $this->getMimeType() );
 824+ }
 825+ return $this->handler;
 826+ }
 827+
 828+ /**
 829+ * Get a ThumbnailImage representing a file type icon
 830+ * @return ThumbnailImage
 831+ */
 832+ function iconThumb() {
 833+ global $wgStylePath, $wgStyleDirectory;
 834+
 835+ $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' );
 836+ foreach( $try as $icon ) {
 837+ $path = '/common/images/icons/' . $icon;
 838+ $filepath = $wgStyleDirectory . $path;
 839+ if( file_exists( $filepath ) ) {
 840+ return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 );
 841+ }
 842+ }
 843+ return null;
 844+ }
 845+
 846+ /**
 847+ * Get last thumbnailing error.
 848+ * Largely obsolete.
 849+ */
 850+ function getLastError() {
 851+ return $this->lastError;
 852+ }
 853+
 854+ /**
 855+ * Get all thumbnail names previously generated for this file
 856+ * STUB
 857+ * Overridden by LocalFile
 858+ */
 859+ function getThumbnails() {
 860+ return array();
 861+ }
 862+
 863+ /**
 864+ * Purge shared caches such as thumbnails and DB data caching
 865+ * STUB
 866+ * Overridden by LocalFile
 867+ */
 868+ function purgeCache() {}
 869+
 870+ /**
 871+ * Purge the file description page, but don't go after
 872+ * pages using the file. Use when modifying file history
 873+ * but not the current data.
 874+ */
 875+ function purgeDescription() {
 876+ $title = $this->getTitle();
 877+ if ( $title ) {
 878+ $title->invalidateCache();
 879+ $title->purgeSquid();
 880+ }
 881+ }
 882+
 883+ /**
 884+ * Purge metadata and all affected pages when the file is created,
 885+ * deleted, or majorly updated.
 886+ */
 887+ function purgeEverything() {
 888+ // Delete thumbnails and refresh file metadata cache
 889+ $this->purgeCache();
 890+ $this->purgeDescription();
 891+
 892+ // Purge cache of all pages using this file
 893+ $title = $this->getTitle();
 894+ if ( $title ) {
 895+ $update = new HTMLCacheUpdate( $title, 'imagelinks' );
 896+ $update->doUpdate();
 897+ }
 898+ }
 899+
 900+ /**
 901+ * Return a fragment of the history of file.
 902+ *
 903+ * STUB
 904+ * @param $limit integer Limit of rows to return
 905+ * @param $start timestamp Only revisions older than $start will be returned
 906+ * @param $end timestamp Only revisions newer than $end will be returned
 907+ * @param $inc bool Include the endpoints of the time range
 908+ *
 909+ * @return array
 910+ */
 911+ function getHistory($limit = null, $start = null, $end = null, $inc=true) {
 912+ return array();
 913+ }
 914+
 915+ /**
 916+ * Return the history of this file, line by line. Starts with current version,
 917+ * then old versions. Should return an object similar to an image/oldimage
 918+ * database row.
 919+ *
 920+ * STUB
 921+ * Overridden in LocalFile
 922+ */
 923+ public function nextHistoryLine() {
 924+ return false;
 925+ }
 926+
 927+ /**
 928+ * Reset the history pointer to the first element of the history.
 929+ * Always call this function after using nextHistoryLine() to free db resources
 930+ * STUB
 931+ * Overridden in LocalFile.
 932+ */
 933+ public function resetHistory() {}
 934+
 935+ /**
 936+ * Get the filename hash component of the directory including trailing slash,
 937+ * e.g. f/fa/
 938+ * If the repository is not hashed, returns an empty string.
 939+ *
 940+ * @return string
 941+ */
 942+ function getHashPath() {
 943+ if ( !isset( $this->hashPath ) ) {
 944+ $this->assertRepoDefined();
 945+ $this->hashPath = $this->repo->getHashPath( $this->getName() );
 946+ }
 947+ return $this->hashPath;
 948+ }
 949+
 950+ /**
 951+ * Get the path of the file relative to the public zone root
 952+ *
 953+ * @return string
 954+ */
 955+ function getRel() {
 956+ return $this->getHashPath() . $this->getName();
 957+ }
 958+
 959+ /**
 960+ * Get urlencoded relative path of the file
 961+ *
 962+ * @return string
 963+ */
 964+ function getUrlRel() {
 965+ return $this->getHashPath() . rawurlencode( $this->getName() );
 966+ }
 967+
 968+ /**
 969+ * Get the relative path for an archived file
 970+ *
 971+ * @param $suffix bool|string if not false, the name of an archived thumbnail file
 972+ *
 973+ * @return string
 974+ */
 975+ function getArchiveRel( $suffix = false ) {
 976+ $path = 'archive/' . $this->getHashPath();
 977+ if ( $suffix === false ) {
 978+ $path = substr( $path, 0, -1 );
 979+ } else {
 980+ $path .= $suffix;
 981+ }
 982+ return $path;
 983+ }
 984+
 985+ /**
 986+ * Get the relative path for an archived file's thumbs directory
 987+ * or a specific thumb if the $suffix is given.
 988+ *
 989+ * @param $archiveName string the timestamped name of an archived image
 990+ * @param $suffix bool|string if not false, the name of a thumbnail file
 991+ *
 992+ * @return string
 993+ */
 994+ function getArchiveThumbRel( $archiveName, $suffix = false ) {
 995+ $path = 'archive/' . $this->getHashPath() . $archiveName . "/";
 996+ if ( $suffix === false ) {
 997+ $path = substr( $path, 0, -1 );
 998+ } else {
 999+ $path .= $suffix;
 1000+ }
 1001+ return $path;
 1002+ }
 1003+
 1004+ /**
 1005+ * Get the path of the archived file.
 1006+ *
 1007+ * @param $suffix bool|string if not false, the name of an archived file.
 1008+ *
 1009+ * @return string
 1010+ */
 1011+ function getArchivePath( $suffix = false ) {
 1012+ $this->assertRepoDefined();
 1013+ return $this->repo->getZonePath( 'public' ) . '/' . $this->getArchiveRel( $suffix );
 1014+ }
 1015+
 1016+ /**
 1017+ * Get the path of the archived file's thumbs, or a particular thumb if $suffix is specified
 1018+ *
 1019+ * @param $archiveName string the timestamped name of an archived image
 1020+ * @param $suffix bool|string if not false, the name of a thumbnail file
 1021+ *
 1022+ * @return string
 1023+ */
 1024+ function getArchiveThumbPath( $archiveName, $suffix = false ) {
 1025+ $this->assertRepoDefined();
 1026+ return $this->repo->getZonePath( 'thumb' ) . '/' .
 1027+ $this->getArchiveThumbRel( $archiveName, $suffix );
 1028+ }
 1029+
 1030+ /**
 1031+ * Get the path of the thumbnail directory, or a particular file if $suffix is specified
 1032+ *
 1033+ * @param $suffix bool|string if not false, the name of a thumbnail file
 1034+ *
 1035+ * @return string
 1036+ */
 1037+ function getThumbPath( $suffix = false ) {
 1038+ $this->assertRepoDefined();
 1039+ $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getRel();
 1040+ if ( $suffix !== false ) {
 1041+ $path .= '/' . $suffix;
 1042+ }
 1043+ return $path;
 1044+ }
 1045+
 1046+ /**
 1047+ * Get the URL of the archive directory, or a particular file if $suffix is specified
 1048+ *
 1049+ * @param $suffix bool|string if not false, the name of an archived file
 1050+ *
 1051+ * @return string
 1052+ */
 1053+ function getArchiveUrl( $suffix = false ) {
 1054+ $this->assertRepoDefined();
 1055+ $path = $this->repo->getZoneUrl( 'public' ) . '/archive/' . $this->getHashPath();
 1056+ if ( $suffix === false ) {
 1057+ $path = substr( $path, 0, -1 );
 1058+ } else {
 1059+ $path .= rawurlencode( $suffix );
 1060+ }
 1061+ return $path;
 1062+ }
 1063+
 1064+ /**
 1065+ * Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified
 1066+ *
 1067+ * @param $archiveName string the timestamped name of an archived image
 1068+ * @param $suffix bool|string if not false, the name of a thumbnail file
 1069+ *
 1070+ * @return string
 1071+ */
 1072+ function getArchiveThumbUrl( $archiveName, $suffix = false ) {
 1073+ $this->assertRepoDefined();
 1074+ $path = $this->repo->getZoneUrl( 'thumb' ) . '/archive/' .
 1075+ $this->getHashPath() . rawurlencode( $archiveName ) . "/";
 1076+ if ( $suffix === false ) {
 1077+ $path = substr( $path, 0, -1 );
 1078+ } else {
 1079+ $path .= rawurlencode( $suffix );
 1080+ }
 1081+ return $path;
 1082+ }
 1083+
 1084+ /**
 1085+ * Get the URL of the thumbnail directory, or a particular file if $suffix is specified
 1086+ *
 1087+ * @param $suffix bool|string if not false, the name of a thumbnail file
 1088+ *
 1089+ * @return path
 1090+ */
 1091+ function getThumbUrl( $suffix = false ) {
 1092+ $this->assertRepoDefined();
 1093+ $path = $this->repo->getZoneUrl('thumb') . '/' . $this->getUrlRel();
 1094+ if ( $suffix !== false ) {
 1095+ $path .= '/' . rawurlencode( $suffix );
 1096+ }
 1097+ return $path;
 1098+ }
 1099+
 1100+ /**
 1101+ * Get the virtual URL for an archived file's thumbs, or a specific thumb.
 1102+ *
 1103+ * @param $suffix bool|string if not false, the name of a thumbnail file
 1104+ *
 1105+ * @return string
 1106+ */
 1107+ function getArchiveVirtualUrl( $suffix = false ) {
 1108+ $this->assertRepoDefined();
 1109+ $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath();
 1110+ if ( $suffix === false ) {
 1111+ $path = substr( $path, 0, -1 );
 1112+ } else {
 1113+ $path .= rawurlencode( $suffix );
 1114+ }
 1115+ return $path;
 1116+ }
 1117+
 1118+ /**
 1119+ * Get the virtual URL for a thumbnail file or directory
 1120+ *
 1121+ * @param $suffix bool|string if not false, the name of a thumbnail file
 1122+ *
 1123+ * @return string
 1124+ */
 1125+ function getThumbVirtualUrl( $suffix = false ) {
 1126+ $this->assertRepoDefined();
 1127+ $path = $this->repo->getVirtualUrl() . '/thumb/' . $this->getUrlRel();
 1128+ if ( $suffix !== false ) {
 1129+ $path .= '/' . rawurlencode( $suffix );
 1130+ }
 1131+ return $path;
 1132+ }
 1133+
 1134+ /**
 1135+ * Get the virtual URL for the file itself
 1136+ *
 1137+ * @param $suffix bool|string if not false, the name of a thumbnail file
 1138+ *
 1139+ * @return string
 1140+ */
 1141+ function getVirtualUrl( $suffix = false ) {
 1142+ $this->assertRepoDefined();
 1143+ $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel();
 1144+ if ( $suffix !== false ) {
 1145+ $path .= '/' . rawurlencode( $suffix );
 1146+ }
 1147+ return $path;
 1148+ }
 1149+
 1150+ /**
 1151+ * @return bool
 1152+ */
 1153+ function isHashed() {
 1154+ $this->assertRepoDefined();
 1155+ return $this->repo->isHashed();
 1156+ }
 1157+
 1158+ /**
 1159+ * @throws MWException
 1160+ */
 1161+ function readOnlyError() {
 1162+ throw new MWException( get_class($this) . ': write operations are not supported' );
 1163+ }
 1164+
 1165+ /**
 1166+ * Record a file upload in the upload log and the image table
 1167+ * STUB
 1168+ * Overridden by LocalFile
 1169+ * @param $oldver
 1170+ * @param $desc
 1171+ * @param $license string
 1172+ * @param $copyStatus string
 1173+ * @param $source string
 1174+ * @param $watch bool
 1175+ */
 1176+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
 1177+ $this->readOnlyError();
 1178+ }
 1179+
 1180+ /**
 1181+ * Move or copy a file to its public location. If a file exists at the
 1182+ * destination, move it to an archive. Returns a FileRepoStatus object with
 1183+ * the archive name in the "value" member on success.
 1184+ *
 1185+ * The archive name should be passed through to recordUpload for database
 1186+ * registration.
 1187+ *
 1188+ * @param $srcPath String: local filesystem path to the source image
 1189+ * @param $flags Integer: a bitwise combination of:
 1190+ * File::DELETE_SOURCE Delete the source file, i.e. move
 1191+ * rather than copy
 1192+ * @return FileRepoStatus object. On success, the value member contains the
 1193+ * archive name, or an empty string if it was a new file.
 1194+ *
 1195+ * STUB
 1196+ * Overridden by LocalFile
 1197+ */
 1198+ function publish( $srcPath, $flags = 0 ) {
 1199+ $this->readOnlyError();
 1200+ }
 1201+
 1202+ /**
 1203+ * @return bool
 1204+ */
 1205+ function formatMetadata() {
 1206+ if ( !$this->getHandler() ) {
 1207+ return false;
 1208+ }
 1209+ return $this->getHandler()->formatMetadata( $this, $this->getMetadata() );
 1210+ }
 1211+
 1212+ /**
 1213+ * Returns true if the file comes from the local file repository.
 1214+ *
 1215+ * @return bool
 1216+ */
 1217+ function isLocal() {
 1218+ $repo = $this->getRepo();
 1219+ return $repo && $repo->isLocal();
 1220+ }
 1221+
 1222+ /**
 1223+ * Returns the name of the repository.
 1224+ *
 1225+ * @return string
 1226+ */
 1227+ function getRepoName() {
 1228+ return $this->repo ? $this->repo->getName() : 'unknown';
 1229+ }
 1230+
 1231+ /**
 1232+ * Returns the repository
 1233+ *
 1234+ * @return FileRepo|false
 1235+ */
 1236+ function getRepo() {
 1237+ return $this->repo;
 1238+ }
 1239+
 1240+ /**
 1241+ * Returns true if the image is an old version
 1242+ * STUB
 1243+ *
 1244+ * @return bool
 1245+ */
 1246+ function isOld() {
 1247+ return false;
 1248+ }
 1249+
 1250+ /**
 1251+ * Is this file a "deleted" file in a private archive?
 1252+ * STUB
 1253+ *
 1254+ * @param $field
 1255+ *
 1256+ * @return bool
 1257+ */
 1258+ function isDeleted( $field ) {
 1259+ return false;
 1260+ }
 1261+
 1262+ /**
 1263+ * Return the deletion bitfield
 1264+ * STUB
 1265+ */
 1266+ function getVisibility() {
 1267+ return 0;
 1268+ }
 1269+
 1270+ /**
 1271+ * Was this file ever deleted from the wiki?
 1272+ *
 1273+ * @return bool
 1274+ */
 1275+ function wasDeleted() {
 1276+ $title = $this->getTitle();
 1277+ return $title && $title->isDeletedQuick();
 1278+ }
 1279+
 1280+ /**
 1281+ * Move file to the new title
 1282+ *
 1283+ * Move current, old version and all thumbnails
 1284+ * to the new filename. Old file is deleted.
 1285+ *
 1286+ * Cache purging is done; checks for validity
 1287+ * and logging are caller's responsibility
 1288+ *
 1289+ * @param $target Title New file name
 1290+ * @return FileRepoStatus object.
 1291+ */
 1292+ function move( $target ) {
 1293+ $this->readOnlyError();
 1294+ }
 1295+
 1296+ /**
 1297+ * Delete all versions of the file.
 1298+ *
 1299+ * Moves the files into an archive directory (or deletes them)
 1300+ * and removes the database rows.
 1301+ *
 1302+ * Cache purging is done; logging is caller's responsibility.
 1303+ *
 1304+ * @param $reason String
 1305+ * @param $suppress Boolean: hide content from sysops?
 1306+ * @return true on success, false on some kind of failure
 1307+ * STUB
 1308+ * Overridden by LocalFile
 1309+ */
 1310+ function delete( $reason, $suppress = false ) {
 1311+ $this->readOnlyError();
 1312+ }
 1313+
 1314+ /**
 1315+ * Restore all or specified deleted revisions to the given file.
 1316+ * Permissions and logging are left to the caller.
 1317+ *
 1318+ * May throw database exceptions on error.
 1319+ *
 1320+ * @param $versions array set of record ids of deleted items to restore,
 1321+ * or empty to restore all revisions.
 1322+ * @param $unsuppress bool remove restrictions on content upon restoration?
 1323+ * @return int|false the number of file revisions restored if successful,
 1324+ * or false on failure
 1325+ * STUB
 1326+ * Overridden by LocalFile
 1327+ */
 1328+ function restore( $versions = array(), $unsuppress = false ) {
 1329+ $this->readOnlyError();
 1330+ }
 1331+
 1332+ /**
 1333+ * Returns 'true' if this file is a type which supports multiple pages,
 1334+ * e.g. DJVU or PDF. Note that this may be true even if the file in
 1335+ * question only has a single page.
 1336+ *
 1337+ * @return Bool
 1338+ */
 1339+ function isMultipage() {
 1340+ return $this->getHandler() && $this->handler->isMultiPage( $this );
 1341+ }
 1342+
 1343+ /**
 1344+ * Returns the number of pages of a multipage document, or false for
 1345+ * documents which aren't multipage documents
 1346+ *
 1347+ * @return false|int
 1348+ */
 1349+ function pageCount() {
 1350+ if ( !isset( $this->pageCount ) ) {
 1351+ if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
 1352+ $this->pageCount = $this->handler->pageCount( $this );
 1353+ } else {
 1354+ $this->pageCount = false;
 1355+ }
 1356+ }
 1357+ return $this->pageCount;
 1358+ }
 1359+
 1360+ /**
 1361+ * Calculate the height of a thumbnail using the source and destination width
 1362+ *
 1363+ * @param $srcWidth
 1364+ * @param $srcHeight
 1365+ * @param $dstWidth
 1366+ *
 1367+ * @return int
 1368+ */
 1369+ static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) {
 1370+ // Exact integer multiply followed by division
 1371+ if ( $srcWidth == 0 ) {
 1372+ return 0;
 1373+ } else {
 1374+ return round( $srcHeight * $dstWidth / $srcWidth );
 1375+ }
 1376+ }
 1377+
 1378+ /**
 1379+ * Get an image size array like that returned by getImageSize(), or false if it
 1380+ * can't be determined.
 1381+ *
 1382+ * @param $fileName String: The filename
 1383+ * @return Array
 1384+ */
 1385+ function getImageSize( $fileName ) {
 1386+ if ( !$this->getHandler() ) {
 1387+ return false;
 1388+ }
 1389+ return $this->handler->getImageSize( $this, $fileName );
 1390+ }
 1391+
 1392+ /**
 1393+ * Get the URL of the image description page. May return false if it is
 1394+ * unknown or not applicable.
 1395+ *
 1396+ * @return string
 1397+ */
 1398+ function getDescriptionUrl() {
 1399+ if ( $this->repo ) {
 1400+ return $this->repo->getDescriptionUrl( $this->getName() );
 1401+ } else {
 1402+ return false;
 1403+ }
 1404+ }
 1405+
 1406+ /**
 1407+ * Get the HTML text of the description page, if available
 1408+ *
 1409+ * @return string
 1410+ */
 1411+ function getDescriptionText() {
 1412+ global $wgMemc, $wgLang;
 1413+ if ( !$this->repo || !$this->repo->fetchDescription ) {
 1414+ return false;
 1415+ }
 1416+ $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgLang->getCode() );
 1417+ if ( $renderUrl ) {
 1418+ if ( $this->repo->descriptionCacheExpiry > 0 ) {
 1419+ wfDebug("Attempting to get the description from cache...");
 1420+ $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(),
 1421+ $this->getName() );
 1422+ $obj = $wgMemc->get($key);
 1423+ if ($obj) {
 1424+ wfDebug("success!\n");
 1425+ return $obj;
 1426+ }
 1427+ wfDebug("miss\n");
 1428+ }
 1429+ wfDebug( "Fetching shared description from $renderUrl\n" );
 1430+ $res = Http::get( $renderUrl );
 1431+ if ( $res && $this->repo->descriptionCacheExpiry > 0 ) {
 1432+ $wgMemc->set( $key, $res, $this->repo->descriptionCacheExpiry );
 1433+ }
 1434+ return $res;
 1435+ } else {
 1436+ return false;
 1437+ }
 1438+ }
 1439+
 1440+ /**
 1441+ * Get discription of file revision
 1442+ * STUB
 1443+ *
 1444+ * @return string
 1445+ */
 1446+ function getDescription() {
 1447+ return null;
 1448+ }
 1449+
 1450+ /**
 1451+ * Get the 14-character timestamp of the file upload, or false if
 1452+ * it doesn't exist
 1453+ *
 1454+ * @return string
 1455+ */
 1456+ function getTimestamp() {
 1457+ $path = $this->getPath();
 1458+ if ( !file_exists( $path ) ) {
 1459+ return false;
 1460+ }
 1461+ return wfTimestamp( TS_MW, filemtime( $path ) );
 1462+ }
 1463+
 1464+ /**
 1465+ * Get the SHA-1 base 36 hash of the file
 1466+ *
 1467+ * @return string
 1468+ */
 1469+ function getSha1() {
 1470+ return self::sha1Base36( $this->getPath() );
 1471+ }
 1472+
 1473+ /**
 1474+ * Get the deletion archive key, <sha1>.<ext>
 1475+ *
 1476+ * @return string
 1477+ */
 1478+ function getStorageKey() {
 1479+ $hash = $this->getSha1();
 1480+ if ( !$hash ) {
 1481+ return false;
 1482+ }
 1483+ $ext = $this->getExtension();
 1484+ $dotExt = $ext === '' ? '' : ".$ext";
 1485+ return $hash . $dotExt;
 1486+ }
 1487+
 1488+ /**
 1489+ * Determine if the current user is allowed to view a particular
 1490+ * field of this file, if it's marked as deleted.
 1491+ * STUB
 1492+ * @param $field Integer
 1493+ * @param $user User object to check, or null to use $wgUser
 1494+ * @return Boolean
 1495+ */
 1496+ function userCan( $field, User $user = null ) {
 1497+ return true;
 1498+ }
 1499+
 1500+ /**
 1501+ * Get an associative array containing information about a file in the local filesystem.
 1502+ *
 1503+ * @param $path String: absolute local filesystem path
 1504+ * @param $ext Mixed: the file extension, or true to extract it from the filename.
 1505+ * Set it to false to ignore the extension.
 1506+ *
 1507+ * @return array
 1508+ */
 1509+ static function getPropsFromPath( $path, $ext = true ) {
 1510+ wfProfileIn( __METHOD__ );
 1511+ wfDebug( __METHOD__.": Getting file info for $path\n" );
 1512+ $info = array(
 1513+ 'fileExists' => file_exists( $path ) && !is_dir( $path )
 1514+ );
 1515+ $gis = false;
 1516+ if ( $info['fileExists'] ) {
 1517+ $magic = MimeMagic::singleton();
 1518+
 1519+ if ( $ext === true ) {
 1520+ $i = strrpos( $path, '.' );
 1521+ $ext = strtolower( $i ? substr( $path, $i + 1 ) : '' );
 1522+ }
 1523+
 1524+ # mime type according to file contents
 1525+ $info['file-mime'] = $magic->guessMimeType( $path, false );
 1526+ # logical mime type
 1527+ $info['mime'] = $magic->improveTypeFromExtension( $info['file-mime'], $ext );
 1528+
 1529+ list( $info['major_mime'], $info['minor_mime'] ) = self::splitMime( $info['mime'] );
 1530+ $info['media_type'] = $magic->getMediaType( $path, $info['mime'] );
 1531+
 1532+ # Get size in bytes
 1533+ $info['size'] = filesize( $path );
 1534+
 1535+ # Height, width and metadata
 1536+ $handler = MediaHandler::getHandler( $info['mime'] );
 1537+ if ( $handler ) {
 1538+ $tempImage = (object)array();
 1539+ $info['metadata'] = $handler->getMetadata( $tempImage, $path );
 1540+ $gis = $handler->getImageSize( $tempImage, $path, $info['metadata'] );
 1541+ } else {
 1542+ $gis = false;
 1543+ $info['metadata'] = '';
 1544+ }
 1545+ $info['sha1'] = self::sha1Base36( $path );
 1546+
 1547+ wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
 1548+ } else {
 1549+ $info['mime'] = null;
 1550+ $info['media_type'] = MEDIATYPE_UNKNOWN;
 1551+ $info['metadata'] = '';
 1552+ $info['sha1'] = '';
 1553+ wfDebug(__METHOD__.": $path NOT FOUND!\n");
 1554+ }
 1555+ if( $gis ) {
 1556+ # NOTE: $gis[2] contains a code for the image type. This is no longer used.
 1557+ $info['width'] = $gis[0];
 1558+ $info['height'] = $gis[1];
 1559+ if ( isset( $gis['bits'] ) ) {
 1560+ $info['bits'] = $gis['bits'];
 1561+ } else {
 1562+ $info['bits'] = 0;
 1563+ }
 1564+ } else {
 1565+ $info['width'] = 0;
 1566+ $info['height'] = 0;
 1567+ $info['bits'] = 0;
 1568+ }
 1569+ wfProfileOut( __METHOD__ );
 1570+ return $info;
 1571+ }
 1572+
 1573+ /**
 1574+ * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
 1575+ * encoding, zero padded to 31 digits.
 1576+ *
 1577+ * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
 1578+ * fairly neatly.
 1579+ *
 1580+ * @param $path string
 1581+ *
 1582+ * @return false|string False on failure
 1583+ */
 1584+ static function sha1Base36( $path ) {
 1585+ wfSuppressWarnings();
 1586+ $hash = sha1_file( $path );
 1587+ wfRestoreWarnings();
 1588+ if ( $hash === false ) {
 1589+ return false;
 1590+ } else {
 1591+ return wfBaseConvert( $hash, 16, 36, 31 );
 1592+ }
 1593+ }
 1594+
 1595+ /**
 1596+ * @return string
 1597+ */
 1598+ function getLongDesc() {
 1599+ $handler = $this->getHandler();
 1600+ if ( $handler ) {
 1601+ return $handler->getLongDesc( $this );
 1602+ } else {
 1603+ return MediaHandler::getGeneralLongDesc( $this );
 1604+ }
 1605+ }
 1606+
 1607+ /**
 1608+ * @return string
 1609+ */
 1610+ function getShortDesc() {
 1611+ $handler = $this->getHandler();
 1612+ if ( $handler ) {
 1613+ return $handler->getShortDesc( $this );
 1614+ } else {
 1615+ return MediaHandler::getGeneralShortDesc( $this );
 1616+ }
 1617+ }
 1618+
 1619+ /**
 1620+ * @return string
 1621+ */
 1622+ function getDimensionsString() {
 1623+ $handler = $this->getHandler();
 1624+ if ( $handler ) {
 1625+ return $handler->getDimensionsString( $this );
 1626+ } else {
 1627+ return '';
 1628+ }
 1629+ }
 1630+
 1631+ /**
 1632+ * @return
 1633+ */
 1634+ function getRedirected() {
 1635+ return $this->redirected;
 1636+ }
 1637+
 1638+ /**
 1639+ * @return Title
 1640+ */
 1641+ function getRedirectedTitle() {
 1642+ if ( $this->redirected ) {
 1643+ if ( !$this->redirectTitle ) {
 1644+ $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected );
 1645+ }
 1646+ return $this->redirectTitle;
 1647+ }
 1648+ }
 1649+
 1650+ /**
 1651+ * @param $from
 1652+ * @return void
 1653+ */
 1654+ function redirectedFrom( $from ) {
 1655+ $this->redirected = $from;
 1656+ }
 1657+
 1658+ /**
 1659+ * @return bool
 1660+ */
 1661+ function isMissing() {
 1662+ return false;
 1663+ }
 1664+
 1665+ /**
 1666+ * Assert that $this->repo is set to a valid FileRepo instance
 1667+ * @throws MWException
 1668+ */
 1669+ protected function assertRepoDefined() {
 1670+ if ( !( $this->repo instanceof $this->repoClass ) ) {
 1671+ throw new MWException( "A {$this->repoClass} object is not set for this File.\n" );
 1672+ }
 1673+ }
 1674+
 1675+ /**
 1676+ * Assert that $this->title is set to a Title
 1677+ * @throws MWException
 1678+ */
 1679+ protected function assertTitleDefined() {
 1680+ if ( !( $this->title instanceof Title ) ) {
 1681+ throw new MWException( "A Title object is not set for this File.\n" );
 1682+ }
 1683+ }
 1684+}
Property changes on: trunk/phase3/includes/filerepo/file/File.php
___________________________________________________________________
Added: svn:keywords
11685 + Author Date Id Revision
Added: svn:mergeinfo
21686 Merged /branches/REL1_15/phase3/includes/filerepo/File.php:r51646
31687 Merged /branches/sqlite/includes/filerepo/File.php:r58211-58321
41688 Merged /branches/new-installer/phase3/includes/filerepo/File.php:r43664-66004
51689 Merged /branches/wmf-deployment/includes/filerepo/File.php:r53381
61690 Merged /branches/uploadwizard/phase3/includes/filerepo/File.php:r73550-75905
Added: svn:eol-style
71691 + native
Index: trunk/phase3/includes/filerepo/file/ForeignDBFile.php
@@ -0,0 +1,78 @@
 2+<?php
 3+/**
 4+ * Foreign file with an accessible MediaWiki database
 5+ *
 6+ * @file
 7+ * @ingroup FileRepo
 8+ */
 9+
 10+/**
 11+ * Foreign file with an accessible MediaWiki database
 12+ *
 13+ * @ingroup FileRepo
 14+ */
 15+class ForeignDBFile extends LocalFile {
 16+
 17+ /**
 18+ * @param $title
 19+ * @param $repo
 20+ * @param $unused
 21+ * @return ForeignDBFile
 22+ */
 23+ static function newFromTitle( $title, $repo, $unused = null ) {
 24+ return new self( $title, $repo );
 25+ }
 26+
 27+ /**
 28+ * Create a ForeignDBFile from a title
 29+ * Do not call this except from inside a repo class.
 30+ *
 31+ * @param $row
 32+ * @param $repo
 33+ *
 34+ * @return ForeignDBFile
 35+ */
 36+ static function newFromRow( $row, $repo ) {
 37+ $title = Title::makeTitle( NS_FILE, $row->img_name );
 38+ $file = new self( $title, $repo );
 39+ $file->loadFromRow( $row );
 40+ return $file;
 41+ }
 42+
 43+ function publish( $srcPath, $flags = 0 ) {
 44+ $this->readOnlyError();
 45+ }
 46+
 47+ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
 48+ $watch = false, $timestamp = false ) {
 49+ $this->readOnlyError();
 50+ }
 51+
 52+ function restore( $versions = array(), $unsuppress = false ) {
 53+ $this->readOnlyError();
 54+ }
 55+
 56+ function delete( $reason, $suppress = false ) {
 57+ $this->readOnlyError();
 58+ }
 59+
 60+ function move( $target ) {
 61+ $this->readOnlyError();
 62+ }
 63+
 64+ /**
 65+ * @return string
 66+ */
 67+ function getDescriptionUrl() {
 68+ // Restore remote behaviour
 69+ return File::getDescriptionUrl();
 70+ }
 71+
 72+ /**
 73+ * @return string
 74+ */
 75+ function getDescriptionText() {
 76+ // Restore remote behaviour
 77+ return File::getDescriptionText();
 78+ }
 79+}
Property changes on: trunk/phase3/includes/filerepo/file/ForeignDBFile.php
___________________________________________________________________
Added: svn:eol-style
180 + native
Index: trunk/phase3/includes/filerepo/file/ForeignAPIFile.php
@@ -0,0 +1,242 @@
 2+<?php
 3+/**
 4+ * Foreign file accessible through api.php requests.
 5+ *
 6+ * @file
 7+ * @ingroup FileRepo
 8+ */
 9+
 10+/**
 11+ * Foreign file accessible through api.php requests.
 12+ * Very hacky and inefficient, do not use :D
 13+ *
 14+ * @ingroup FileRepo
 15+ */
 16+class ForeignAPIFile extends File {
 17+
 18+ private $mExists;
 19+
 20+ protected $repoClass = 'ForeignApiRepo';
 21+
 22+ /**
 23+ * @param $title
 24+ * @param $repo ForeignApiRepo
 25+ * @param $info
 26+ * @param bool $exists
 27+ */
 28+ function __construct( $title, $repo, $info, $exists = false ) {
 29+ parent::__construct( $title, $repo );
 30+
 31+ $this->mInfo = $info;
 32+ $this->mExists = $exists;
 33+
 34+ $this->assertRepoDefined();
 35+ }
 36+
 37+ /**
 38+ * @param $title Title
 39+ * @param $repo ForeignApiRepo
 40+ * @return ForeignAPIFile|null
 41+ */
 42+ static function newFromTitle( Title $title, $repo ) {
 43+ $data = $repo->fetchImageQuery( array(
 44+ 'titles' => 'File:' . $title->getDBKey(),
 45+ 'iiprop' => self::getProps(),
 46+ 'prop' => 'imageinfo',
 47+ 'iimetadataversion' => MediaHandler::getMetadataVersion()
 48+ ) );
 49+
 50+ $info = $repo->getImageInfo( $data );
 51+
 52+ if( $info ) {
 53+ $lastRedirect = isset( $data['query']['redirects'] )
 54+ ? count( $data['query']['redirects'] ) - 1
 55+ : -1;
 56+ if( $lastRedirect >= 0 ) {
 57+ $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to']);
 58+ $img = new self( $newtitle, $repo, $info, true );
 59+ if( $img ) {
 60+ $img->redirectedFrom( $title->getDBkey() );
 61+ }
 62+ } else {
 63+ $img = new self( $title, $repo, $info, true );
 64+ }
 65+ return $img;
 66+ } else {
 67+ return null;
 68+ }
 69+ }
 70+
 71+ /**
 72+ * Get the property string for iiprop and aiprop
 73+ */
 74+ static function getProps() {
 75+ return 'timestamp|user|comment|url|size|sha1|metadata|mime';
 76+ }
 77+
 78+ // Dummy functions...
 79+ public function exists() {
 80+ return $this->mExists;
 81+ }
 82+
 83+ public function getPath() {
 84+ return false;
 85+ }
 86+
 87+ function transform( $params, $flags = 0 ) {
 88+ if( !$this->canRender() ) {
 89+ // show icon
 90+ return parent::transform( $params, $flags );
 91+ }
 92+
 93+ // Note, the this->canRender() check above implies
 94+ // that we have a handler, and it can do makeParamString.
 95+ $otherParams = $this->handler->makeParamString( $params );
 96+
 97+ $thumbUrl = $this->repo->getThumbUrlFromCache(
 98+ $this->getName(),
 99+ isset( $params['width'] ) ? $params['width'] : -1,
 100+ isset( $params['height'] ) ? $params['height'] : -1,
 101+ $otherParams );
 102+ return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );
 103+ }
 104+
 105+ // Info we can get from API...
 106+ public function getWidth( $page = 1 ) {
 107+ return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0;
 108+ }
 109+
 110+ /**
 111+ * @param $page int
 112+ * @return int
 113+ */
 114+ public function getHeight( $page = 1 ) {
 115+ return isset( $this->mInfo['height'] ) ? intval( $this->mInfo['height'] ) : 0;
 116+ }
 117+
 118+ public function getMetadata() {
 119+ if ( isset( $this->mInfo['metadata'] ) ) {
 120+ return serialize( self::parseMetadata( $this->mInfo['metadata'] ) );
 121+ }
 122+ return null;
 123+ }
 124+
 125+ public static function parseMetadata( $metadata ) {
 126+ if( !is_array( $metadata ) ) {
 127+ return $metadata;
 128+ }
 129+ $ret = array();
 130+ foreach( $metadata as $meta ) {
 131+ $ret[ $meta['name'] ] = self::parseMetadata( $meta['value'] );
 132+ }
 133+ return $ret;
 134+ }
 135+
 136+ public function getSize() {
 137+ return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null;
 138+ }
 139+
 140+ public function getUrl() {
 141+ return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null;
 142+ }
 143+
 144+ public function getUser( $method='text' ) {
 145+ return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null;
 146+ }
 147+
 148+ public function getDescription() {
 149+ return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null;
 150+ }
 151+
 152+ function getSha1() {
 153+ return isset( $this->mInfo['sha1'] ) ?
 154+ wfBaseConvert( strval( $this->mInfo['sha1'] ), 16, 36, 31 ) :
 155+ null;
 156+ }
 157+
 158+ function getTimestamp() {
 159+ return wfTimestamp( TS_MW,
 160+ isset( $this->mInfo['timestamp'] ) ?
 161+ strval( $this->mInfo['timestamp'] ) :
 162+ null
 163+ );
 164+ }
 165+
 166+ function getMimeType() {
 167+ if( !isset( $this->mInfo['mime'] ) ) {
 168+ $magic = MimeMagic::singleton();
 169+ $this->mInfo['mime'] = $magic->guessTypesForExtension( $this->getExtension() );
 170+ }
 171+ return $this->mInfo['mime'];
 172+ }
 173+
 174+ /// @todo FIXME: May guess wrong on file types that can be eg audio or video
 175+ function getMediaType() {
 176+ $magic = MimeMagic::singleton();
 177+ return $magic->getMediaType( null, $this->getMimeType() );
 178+ }
 179+
 180+ function getDescriptionUrl() {
 181+ return isset( $this->mInfo['descriptionurl'] )
 182+ ? $this->mInfo['descriptionurl']
 183+ : false;
 184+ }
 185+
 186+ /**
 187+ * Only useful if we're locally caching thumbs anyway...
 188+ */
 189+ function getThumbPath( $suffix = '' ) {
 190+ if ( $this->repo->canCacheThumbs() ) {
 191+ $path = $this->repo->getZonePath('thumb') . '/' . $this->getHashPath( $this->getName() );
 192+ if ( $suffix ) {
 193+ $path = $path . $suffix . '/';
 194+ }
 195+ return $path;
 196+ } else {
 197+ return null;
 198+ }
 199+ }
 200+
 201+ function getThumbnails() {
 202+ $files = array();
 203+ $dir = $this->getThumbPath( $this->getName() );
 204+ if ( is_dir( $dir ) ) {
 205+ $handle = opendir( $dir );
 206+ if ( $handle ) {
 207+ while ( false !== ( $file = readdir($handle) ) ) {
 208+ if ( $file[0] != '.' ) {
 209+ $files[] = $file;
 210+ }
 211+ }
 212+ closedir( $handle );
 213+ }
 214+ }
 215+ return $files;
 216+ }
 217+
 218+ function purgeCache() {
 219+ $this->purgeThumbnails();
 220+ $this->purgeDescriptionPage();
 221+ }
 222+
 223+ function purgeDescriptionPage() {
 224+ global $wgMemc, $wgContLang;
 225+ $url = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() );
 226+ $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5($url) );
 227+ $wgMemc->delete( $key );
 228+ }
 229+
 230+ function purgeThumbnails() {
 231+ global $wgMemc;
 232+ $key = $this->repo->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() );
 233+ $wgMemc->delete( $key );
 234+ $files = $this->getThumbnails();
 235+ $dir = $this->getThumbPath( $this->getName() );
 236+ foreach ( $files as $file ) {
 237+ unlink( $dir . $file );
 238+ }
 239+ if ( is_dir( $dir ) ) {
 240+ rmdir( $dir ); // Might have already gone away, spews errors if we don't.
 241+ }
 242+ }
 243+}
Property changes on: trunk/phase3/includes/filerepo/file/ForeignAPIFile.php
___________________________________________________________________
Added: svn:eol-style
1244 + native
Index: trunk/phase3/includes/filerepo/file/ArchivedFile.php
@@ -0,0 +1,471 @@
 2+<?php
 3+/**
 4+ * Deleted file in the 'filearchive' table
 5+ *
 6+ * @file
 7+ * @ingroup FileRepo
 8+ */
 9+
 10+/**
 11+ * Class representing a row of the 'filearchive' table
 12+ *
 13+ * @ingroup FileRepo
 14+ */
 15+class ArchivedFile {
 16+ /**#@+
 17+ * @private
 18+ */
 19+ var $id, # filearchive row ID
 20+ $name, # image name
 21+ $group, # FileStore storage group
 22+ $key, # FileStore sha1 key
 23+ $size, # file dimensions
 24+ $bits, # size in bytes
 25+ $width, # width
 26+ $height, # height
 27+ $metadata, # metadata string
 28+ $mime, # mime type
 29+ $media_type, # media type
 30+ $description, # upload description
 31+ $user, # user ID of uploader
 32+ $user_text, # user name of uploader
 33+ $timestamp, # time of upload
 34+ $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
 35+ $deleted, # Bitfield akin to rev_deleted
 36+ $pageCount,
 37+ $archive_name;
 38+
 39+ /**
 40+ * @var MediaHandler
 41+ */
 42+ var $handler;
 43+ /**
 44+ * @var Title
 45+ */
 46+ var $title; # image title
 47+
 48+ /**#@-*/
 49+
 50+ /**
 51+ * @throws MWException
 52+ * @param Title $title
 53+ * @param int $id
 54+ * @param string $key
 55+ */
 56+ function __construct( $title, $id=0, $key='' ) {
 57+ $this->id = -1;
 58+ $this->title = false;
 59+ $this->name = false;
 60+ $this->group = 'deleted'; // needed for direct use of constructor
 61+ $this->key = '';
 62+ $this->size = 0;
 63+ $this->bits = 0;
 64+ $this->width = 0;
 65+ $this->height = 0;
 66+ $this->metadata = '';
 67+ $this->mime = "unknown/unknown";
 68+ $this->media_type = '';
 69+ $this->description = '';
 70+ $this->user = 0;
 71+ $this->user_text = '';
 72+ $this->timestamp = null;
 73+ $this->deleted = 0;
 74+ $this->dataLoaded = false;
 75+ $this->exists = false;
 76+
 77+ if( $title instanceof Title ) {
 78+ $this->title = File::normalizeTitle( $title, 'exception' );
 79+ $this->name = $title->getDBkey();
 80+ }
 81+
 82+ if ($id) {
 83+ $this->id = $id;
 84+ }
 85+
 86+ if ($key) {
 87+ $this->key = $key;
 88+ }
 89+
 90+ if ( !$id && !$key && !( $title instanceof Title ) ) {
 91+ throw new MWException( "No specifications provided to ArchivedFile constructor." );
 92+ }
 93+ }
 94+
 95+ /**
 96+ * Loads a file object from the filearchive table
 97+ * @return true on success or null
 98+ */
 99+ public function load() {
 100+ if ( $this->dataLoaded ) {
 101+ return true;
 102+ }
 103+ $conds = array();
 104+
 105+ if( $this->id > 0 ) {
 106+ $conds['fa_id'] = $this->id;
 107+ }
 108+ if( $this->key ) {
 109+ $conds['fa_storage_group'] = $this->group;
 110+ $conds['fa_storage_key'] = $this->key;
 111+ }
 112+ if( $this->title ) {
 113+ $conds['fa_name'] = $this->title->getDBkey();
 114+ }
 115+
 116+ if( !count($conds)) {
 117+ throw new MWException( "No specific information for retrieving archived file" );
 118+ }
 119+
 120+ if( !$this->title || $this->title->getNamespace() == NS_FILE ) {
 121+ $dbr = wfGetDB( DB_SLAVE );
 122+ $res = $dbr->select( 'filearchive',
 123+ array(
 124+ 'fa_id',
 125+ 'fa_name',
 126+ 'fa_archive_name',
 127+ 'fa_storage_key',
 128+ 'fa_storage_group',
 129+ 'fa_size',
 130+ 'fa_bits',
 131+ 'fa_width',
 132+ 'fa_height',
 133+ 'fa_metadata',
 134+ 'fa_media_type',
 135+ 'fa_major_mime',
 136+ 'fa_minor_mime',
 137+ 'fa_description',
 138+ 'fa_user',
 139+ 'fa_user_text',
 140+ 'fa_timestamp',
 141+ 'fa_deleted' ),
 142+ $conds,
 143+ __METHOD__,
 144+ array( 'ORDER BY' => 'fa_timestamp DESC' ) );
 145+ if ( $res == false || $dbr->numRows( $res ) == 0 ) {
 146+ // this revision does not exist?
 147+ return;
 148+ }
 149+ $ret = $dbr->resultObject( $res );
 150+ $row = $ret->fetchObject();
 151+
 152+ // initialize fields for filestore image object
 153+ $this->id = intval($row->fa_id);
 154+ $this->name = $row->fa_name;
 155+ $this->archive_name = $row->fa_archive_name;
 156+ $this->group = $row->fa_storage_group;
 157+ $this->key = $row->fa_storage_key;
 158+ $this->size = $row->fa_size;
 159+ $this->bits = $row->fa_bits;
 160+ $this->width = $row->fa_width;
 161+ $this->height = $row->fa_height;
 162+ $this->metadata = $row->fa_metadata;
 163+ $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
 164+ $this->media_type = $row->fa_media_type;
 165+ $this->description = $row->fa_description;
 166+ $this->user = $row->fa_user;
 167+ $this->user_text = $row->fa_user_text;
 168+ $this->timestamp = $row->fa_timestamp;
 169+ $this->deleted = $row->fa_deleted;
 170+ } else {
 171+ throw new MWException( 'This title does not correspond to an image page.' );
 172+ }
 173+ $this->dataLoaded = true;
 174+ $this->exists = true;
 175+
 176+ return true;
 177+ }
 178+
 179+ /**
 180+ * Loads a file object from the filearchive table
 181+ *
 182+ * @param $row
 183+ *
 184+ * @return ArchivedFile
 185+ */
 186+ public static function newFromRow( $row ) {
 187+ $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
 188+
 189+ $file->id = intval($row->fa_id);
 190+ $file->name = $row->fa_name;
 191+ $file->archive_name = $row->fa_archive_name;
 192+ $file->group = $row->fa_storage_group;
 193+ $file->key = $row->fa_storage_key;
 194+ $file->size = $row->fa_size;
 195+ $file->bits = $row->fa_bits;
 196+ $file->width = $row->fa_width;
 197+ $file->height = $row->fa_height;
 198+ $file->metadata = $row->fa_metadata;
 199+ $file->mime = "$row->fa_major_mime/$row->fa_minor_mime";
 200+ $file->media_type = $row->fa_media_type;
 201+ $file->description = $row->fa_description;
 202+ $file->user = $row->fa_user;
 203+ $file->user_text = $row->fa_user_text;
 204+ $file->timestamp = $row->fa_timestamp;
 205+ $file->deleted = $row->fa_deleted;
 206+
 207+ return $file;
 208+ }
 209+
 210+ /**
 211+ * Return the associated title object
 212+ *
 213+ * @return Title
 214+ */
 215+ public function getTitle() {
 216+ return $this->title;
 217+ }
 218+
 219+ /**
 220+ * Return the file name
 221+ *
 222+ * @return string
 223+ */
 224+ public function getName() {
 225+ return $this->name;
 226+ }
 227+
 228+ /**
 229+ * @return int
 230+ */
 231+ public function getID() {
 232+ $this->load();
 233+ return $this->id;
 234+ }
 235+
 236+ /**
 237+ * @return bool
 238+ */
 239+ public function exists() {
 240+ $this->load();
 241+ return $this->exists;
 242+ }
 243+
 244+ /**
 245+ * Return the FileStore key
 246+ * @return string
 247+ */
 248+ public function getKey() {
 249+ $this->load();
 250+ return $this->key;
 251+ }
 252+
 253+ /**
 254+ * Return the FileStore key (overriding base File class)
 255+ * @return string
 256+ */
 257+ public function getStorageKey() {
 258+ return $this->getKey();
 259+ }
 260+
 261+ /**
 262+ * Return the FileStore storage group
 263+ * @return string
 264+ */
 265+ public function getGroup() {
 266+ return $this->group;
 267+ }
 268+
 269+ /**
 270+ * Return the width of the image
 271+ * @return int
 272+ */
 273+ public function getWidth() {
 274+ $this->load();
 275+ return $this->width;
 276+ }
 277+
 278+ /**
 279+ * Return the height of the image
 280+ * @return int
 281+ */
 282+ public function getHeight() {
 283+ $this->load();
 284+ return $this->height;
 285+ }
 286+
 287+ /**
 288+ * Get handler-specific metadata
 289+ * @return string
 290+ */
 291+ public function getMetadata() {
 292+ $this->load();
 293+ return $this->metadata;
 294+ }
 295+
 296+ /**
 297+ * Return the size of the image file, in bytes
 298+ * @return int
 299+ */
 300+ public function getSize() {
 301+ $this->load();
 302+ return $this->size;
 303+ }
 304+
 305+ /**
 306+ * Return the bits of the image file, in bytes
 307+ * @return int
 308+ */
 309+ public function getBits() {
 310+ $this->load();
 311+ return $this->bits;
 312+ }
 313+
 314+ /**
 315+ * Returns the mime type of the file.
 316+ * @return string
 317+ */
 318+ public function getMimeType() {
 319+ $this->load();
 320+ return $this->mime;
 321+ }
 322+
 323+ /**
 324+ * Get a MediaHandler instance for this file
 325+ * @return MediaHandler
 326+ */
 327+ function getHandler() {
 328+ if ( !isset( $this->handler ) ) {
 329+ $this->handler = MediaHandler::getHandler( $this->getMimeType() );
 330+ }
 331+ return $this->handler;
 332+ }
 333+
 334+ /**
 335+ * Returns the number of pages of a multipage document, or false for
 336+ * documents which aren't multipage documents
 337+ */
 338+ function pageCount() {
 339+ if ( !isset( $this->pageCount ) ) {
 340+ if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
 341+ $this->pageCount = $this->handler->pageCount( $this );
 342+ } else {
 343+ $this->pageCount = false;
 344+ }
 345+ }
 346+ return $this->pageCount;
 347+ }
 348+
 349+ /**
 350+ * Return the type of the media in the file.
 351+ * Use the value returned by this function with the MEDIATYPE_xxx constants.
 352+ * @return string
 353+ */
 354+ public function getMediaType() {
 355+ $this->load();
 356+ return $this->media_type;
 357+ }
 358+
 359+ /**
 360+ * Return upload timestamp.
 361+ *
 362+ * @return string
 363+ */
 364+ public function getTimestamp() {
 365+ $this->load();
 366+ return wfTimestamp( TS_MW, $this->timestamp );
 367+ }
 368+
 369+ /**
 370+ * Return the user ID of the uploader.
 371+ *
 372+ * @return int
 373+ */
 374+ public function getUser() {
 375+ $this->load();
 376+ if( $this->isDeleted( File::DELETED_USER ) ) {
 377+ return 0;
 378+ } else {
 379+ return $this->user;
 380+ }
 381+ }
 382+
 383+ /**
 384+ * Return the user name of the uploader.
 385+ *
 386+ * @return string
 387+ */
 388+ public function getUserText() {
 389+ $this->load();
 390+ if( $this->isDeleted( File::DELETED_USER ) ) {
 391+ return 0;
 392+ } else {
 393+ return $this->user_text;
 394+ }
 395+ }
 396+
 397+ /**
 398+ * Return upload description.
 399+ *
 400+ * @return string
 401+ */
 402+ public function getDescription() {
 403+ $this->load();
 404+ if( $this->isDeleted( File::DELETED_COMMENT ) ) {
 405+ return 0;
 406+ } else {
 407+ return $this->description;
 408+ }
 409+ }
 410+
 411+ /**
 412+ * Return the user ID of the uploader.
 413+ *
 414+ * @return int
 415+ */
 416+ public function getRawUser() {
 417+ $this->load();
 418+ return $this->user;
 419+ }
 420+
 421+ /**
 422+ * Return the user name of the uploader.
 423+ *
 424+ * @return string
 425+ */
 426+ public function getRawUserText() {
 427+ $this->load();
 428+ return $this->user_text;
 429+ }
 430+
 431+ /**
 432+ * Return upload description.
 433+ *
 434+ * @return string
 435+ */
 436+ public function getRawDescription() {
 437+ $this->load();
 438+ return $this->description;
 439+ }
 440+
 441+ /**
 442+ * Returns the deletion bitfield
 443+ * @return int
 444+ */
 445+ public function getVisibility() {
 446+ $this->load();
 447+ return $this->deleted;
 448+ }
 449+
 450+ /**
 451+ * for file or revision rows
 452+ *
 453+ * @param $field Integer: one of DELETED_* bitfield constants
 454+ * @return bool
 455+ */
 456+ public function isDeleted( $field ) {
 457+ $this->load();
 458+ return ($this->deleted & $field) == $field;
 459+ }
 460+
 461+ /**
 462+ * Determine if the current user is allowed to view a particular
 463+ * field of this FileStore image file, if it's marked as deleted.
 464+ * @param $field Integer
 465+ * @param $user User object to check, or null to use $wgUser
 466+ * @return bool
 467+ */
 468+ public function userCan( $field, User $user = null ) {
 469+ $this->load();
 470+ return Revision::userCanBitfield( $this->deleted, $field, $user );
 471+ }
 472+}
Property changes on: trunk/phase3/includes/filerepo/file/ArchivedFile.php
___________________________________________________________________
Added: svn:eol-style
1473 + native
Index: trunk/phase3/includes/AutoLoader.php
@@ -459,25 +459,27 @@
460460 'ExternalUser_vB' => 'includes/extauth/vB.php',
461461
462462 # includes/filerepo
463 - 'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
464 - 'File' => 'includes/filerepo/File.php',
465463 'FileRepo' => 'includes/filerepo/FileRepo.php',
466464 'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
467 - 'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
468465 'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php',
469 - 'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
470466 'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
471467 'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
472468 'FSRepo' => 'includes/filerepo/FSRepo.php',
473 - 'LocalFile' => 'includes/filerepo/LocalFile.php',
474 - 'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
475 - 'LocalFileMoveBatch' => 'includes/filerepo/LocalFile.php',
476 - 'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
477469 'LocalRepo' => 'includes/filerepo/LocalRepo.php',
478470 'NullRepo' => 'includes/filerepo/NullRepo.php',
479 - 'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
480471 'RepoGroup' => 'includes/filerepo/RepoGroup.php',
481 - 'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php',
 472+
 473+ # includes/filerepo/file
 474+ 'ArchivedFile' => 'includes/filerepo/file/ArchivedFile.php',
 475+ 'File' => 'includes/filerepo/file/File.php',
 476+ 'ForeignAPIFile' => 'includes/filerepo/file/ForeignAPIFile.php',
 477+ 'ForeignDBFile' => 'includes/filerepo/file/ForeignDBFile.php',
 478+ 'LocalFile' => 'includes/filerepo/file/LocalFile.php',
 479+ 'LocalFileDeleteBatch' => 'includes/filerepo/file/LocalFile.php',
 480+ 'LocalFileMoveBatch' => 'includes/filerepo/file/LocalFile.php',
 481+ 'LocalFileRestoreBatch' => 'includes/filerepo/file/LocalFile.php',
 482+ 'OldLocalFile' => 'includes/filerepo/file/OldLocalFile.php',
 483+ 'UnregisteredLocalFile' => 'includes/filerepo/file/UnregisteredLocalFile.php',
482484
483485 # includes/installer
484486 'CliInstaller' => 'includes/installer/CliInstaller.php',

Status & tagging log