12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515 |
- <?php
- /**
- * Local file in the wiki's own database.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup FileAbstraction
- */
- use MediaWiki\Logger\LoggerFactory;
- use Wikimedia\Rdbms\Database;
- use Wikimedia\Rdbms\IDatabase;
- use MediaWiki\MediaWikiServices;
- /**
- * Class to represent a local file in the wiki's own database
- *
- * Provides methods to retrieve paths (physical, logical, URL),
- * to generate image thumbnails or for uploading.
- *
- * Note that only the repo object knows what its file class is called. You should
- * never name a file class explictly outside of the repo class. Instead use the
- * repo's factory functions to generate file objects, for example:
- *
- * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
- *
- * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
- * in most cases.
- *
- * @ingroup FileAbstraction
- */
- class LocalFile extends File {
- const VERSION = 11; // cache version
- const CACHE_FIELD_MAX_LEN = 1000;
- /** @var bool Does the file exist on disk? (loadFromXxx) */
- protected $fileExists;
- /** @var int Image width */
- protected $width;
- /** @var int Image height */
- protected $height;
- /** @var int Returned by getimagesize (loadFromXxx) */
- protected $bits;
- /** @var string MEDIATYPE_xxx (bitmap, drawing, audio...) */
- protected $media_type;
- /** @var string MIME type, determined by MimeAnalyzer::guessMimeType */
- protected $mime;
- /** @var int Size in bytes (loadFromXxx) */
- protected $size;
- /** @var string Handler-specific metadata */
- protected $metadata;
- /** @var string SHA-1 base 36 content hash */
- protected $sha1;
- /** @var bool Whether or not core data has been loaded from the database (loadFromXxx) */
- protected $dataLoaded;
- /** @var bool Whether or not lazy-loaded data has been loaded from the database */
- protected $extraDataLoaded;
- /** @var int Bitfield akin to rev_deleted */
- protected $deleted;
- /** @var string */
- protected $repoClass = LocalRepo::class;
- /** @var int Number of line to return by nextHistoryLine() (constructor) */
- private $historyLine;
- /** @var int Result of the query for the file's history (nextHistoryLine) */
- private $historyRes;
- /** @var string Major MIME type */
- private $major_mime;
- /** @var string Minor MIME type */
- private $minor_mime;
- /** @var string Upload timestamp */
- private $timestamp;
- /** @var User Uploader */
- private $user;
- /** @var string Description of current revision of the file */
- private $description;
- /** @var string TS_MW timestamp of the last change of the file description */
- private $descriptionTouched;
- /** @var bool Whether the row was upgraded on load */
- private $upgraded;
- /** @var bool Whether the row was scheduled to upgrade on load */
- private $upgrading;
- /** @var bool True if the image row is locked */
- private $locked;
- /** @var bool True if the image row is locked with a lock initiated transaction */
- private $lockedOwnTrx;
- /** @var bool True if file is not present in file system. Not to be cached in memcached */
- private $missing;
- // @note: higher than IDBAccessObject constants
- const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
- const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
- /**
- * Create a LocalFile from a title
- * Do not call this except from inside a repo class.
- *
- * Note: $unused param is only here to avoid an E_STRICT
- *
- * @param Title $title
- * @param FileRepo $repo
- * @param null $unused
- *
- * @return self
- */
- static function newFromTitle( $title, $repo, $unused = null ) {
- return new self( $title, $repo );
- }
- /**
- * Create a LocalFile from a title
- * Do not call this except from inside a repo class.
- *
- * @param stdClass $row
- * @param FileRepo $repo
- *
- * @return self
- */
- static function newFromRow( $row, $repo ) {
- $title = Title::makeTitle( NS_FILE, $row->img_name );
- $file = new self( $title, $repo );
- $file->loadFromRow( $row );
- return $file;
- }
- /**
- * Create a LocalFile from a SHA-1 key
- * Do not call this except from inside a repo class.
- *
- * @param string $sha1 Base-36 SHA-1
- * @param LocalRepo $repo
- * @param string|bool $timestamp MW_timestamp (optional)
- * @return bool|LocalFile
- */
- static function newFromKey( $sha1, $repo, $timestamp = false ) {
- $dbr = $repo->getReplicaDB();
- $conds = [ 'img_sha1' => $sha1 ];
- if ( $timestamp ) {
- $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
- }
- $fileQuery = self::getQueryInfo();
- $row = $dbr->selectRow(
- $fileQuery['tables'], $fileQuery['fields'], $conds, __METHOD__, [], $fileQuery['joins']
- );
- if ( $row ) {
- return self::newFromRow( $row, $repo );
- } else {
- return false;
- }
- }
- /**
- * Fields in the image table
- * @deprecated since 1.31, use self::getQueryInfo() instead.
- * @return string[]
- */
- static function selectFields() {
- global $wgActorTableSchemaMigrationStage;
- wfDeprecated( __METHOD__, '1.31' );
- if ( $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH ) {
- // If code is using this instead of self::getQueryInfo(), there's a
- // decent chance it's going to try to directly access
- // $row->img_user or $row->img_user_text and we can't give it
- // useful values here once those aren't being written anymore.
- throw new BadMethodCallException(
- 'Cannot use ' . __METHOD__ . ' when $wgActorTableSchemaMigrationStage > MIGRATION_WRITE_BOTH'
- );
- }
- return [
- 'img_name',
- 'img_size',
- 'img_width',
- 'img_height',
- 'img_metadata',
- 'img_bits',
- 'img_media_type',
- 'img_major_mime',
- 'img_minor_mime',
- 'img_user',
- 'img_user_text',
- 'img_actor' => $wgActorTableSchemaMigrationStage > MIGRATION_OLD ? 'img_actor' : 'NULL',
- 'img_timestamp',
- 'img_sha1',
- ] + MediaWikiServices::getInstance()->getCommentStore()->getFields( 'img_description' );
- }
- /**
- * Return the tables, fields, and join conditions to be selected to create
- * a new localfile object.
- * @since 1.31
- * @param string[] $options
- * - omit-lazy: Omit fields that are lazily cached.
- * @return array[] With three keys:
- * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
- * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
- * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
- */
- public static function getQueryInfo( array $options = [] ) {
- $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'img_description' );
- $actorQuery = ActorMigration::newMigration()->getJoin( 'img_user' );
- $ret = [
- 'tables' => [ 'image' ] + $commentQuery['tables'] + $actorQuery['tables'],
- 'fields' => [
- 'img_name',
- 'img_size',
- 'img_width',
- 'img_height',
- 'img_metadata',
- 'img_bits',
- 'img_media_type',
- 'img_major_mime',
- 'img_minor_mime',
- 'img_timestamp',
- 'img_sha1',
- ] + $commentQuery['fields'] + $actorQuery['fields'],
- 'joins' => $commentQuery['joins'] + $actorQuery['joins'],
- ];
- if ( in_array( 'omit-nonlazy', $options, true ) ) {
- // Internal use only for getting only the lazy fields
- $ret['fields'] = [];
- }
- if ( !in_array( 'omit-lazy', $options, true ) ) {
- // Note: Keep this in sync with self::getLazyCacheFields()
- $ret['fields'][] = 'img_metadata';
- }
- return $ret;
- }
- /**
- * Do not call this except from inside a repo class.
- * @param Title $title
- * @param FileRepo $repo
- */
- function __construct( $title, $repo ) {
- parent::__construct( $title, $repo );
- $this->metadata = '';
- $this->historyLine = 0;
- $this->historyRes = null;
- $this->dataLoaded = false;
- $this->extraDataLoaded = false;
- $this->assertRepoDefined();
- $this->assertTitleDefined();
- }
- /**
- * Get the memcached key for the main data for this file, or false if
- * there is no access to the shared cache.
- * @return string|bool
- */
- function getCacheKey() {
- return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
- }
- /**
- * @param WANObjectCache $cache
- * @return string[]
- * @since 1.28
- */
- public function getMutableCacheKeys( WANObjectCache $cache ) {
- return [ $this->getCacheKey() ];
- }
- /**
- * Try to load file metadata from memcached, falling back to the database
- */
- private function loadFromCache() {
- $this->dataLoaded = false;
- $this->extraDataLoaded = false;
- $key = $this->getCacheKey();
- if ( !$key ) {
- $this->loadFromDB( self::READ_NORMAL );
- return;
- }
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- $cachedValues = $cache->getWithSetCallback(
- $key,
- $cache::TTL_WEEK,
- function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
- $setOpts += Database::getCacheSetOptions( $this->repo->getReplicaDB() );
- $this->loadFromDB( self::READ_NORMAL );
- $fields = $this->getCacheFields( '' );
- $cacheVal['fileExists'] = $this->fileExists;
- if ( $this->fileExists ) {
- foreach ( $fields as $field ) {
- $cacheVal[$field] = $this->$field;
- }
- }
- $cacheVal['user'] = $this->user ? $this->user->getId() : 0;
- $cacheVal['user_text'] = $this->user ? $this->user->getName() : '';
- $cacheVal['actor'] = $this->user ? $this->user->getActorId() : null;
- // Strip off excessive entries from the subset of fields that can become large.
- // If the cache value gets to large it will not fit in memcached and nothing will
- // get cached at all, causing master queries for any file access.
- foreach ( $this->getLazyCacheFields( '' ) as $field ) {
- if ( isset( $cacheVal[$field] )
- && strlen( $cacheVal[$field] ) > 100 * 1024
- ) {
- unset( $cacheVal[$field] ); // don't let the value get too big
- }
- }
- if ( $this->fileExists ) {
- $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
- } else {
- $ttl = $cache::TTL_DAY;
- }
- return $cacheVal;
- },
- [ 'version' => self::VERSION ]
- );
- $this->fileExists = $cachedValues['fileExists'];
- if ( $this->fileExists ) {
- $this->setProps( $cachedValues );
- }
- $this->dataLoaded = true;
- $this->extraDataLoaded = true;
- foreach ( $this->getLazyCacheFields( '' ) as $field ) {
- $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
- }
- }
- /**
- * Purge the file object/metadata cache
- */
- public function invalidateCache() {
- $key = $this->getCacheKey();
- if ( !$key ) {
- return;
- }
- $this->repo->getMasterDB()->onTransactionPreCommitOrIdle(
- function () use ( $key ) {
- MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
- },
- __METHOD__
- );
- }
- /**
- * Load metadata from the file itself
- */
- function loadFromFile() {
- $props = $this->repo->getFileProps( $this->getVirtualUrl() );
- $this->setProps( $props );
- }
- /**
- * Returns the list of object properties that are included as-is in the cache.
- * @param string $prefix Must be the empty string
- * @return string[]
- * @since 1.31 No longer accepts a non-empty $prefix
- */
- protected function getCacheFields( $prefix = 'img_' ) {
- if ( $prefix !== '' ) {
- throw new InvalidArgumentException(
- __METHOD__ . ' with a non-empty prefix is no longer supported.'
- );
- }
- // See self::getQueryInfo() for the fetching of the data from the DB,
- // self::loadFromRow() for the loading of the object from the DB row,
- // and self::loadFromCache() for the caching, and self::setProps() for
- // populating the object from an array of data.
- return [ 'size', 'width', 'height', 'bits', 'media_type',
- 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'description' ];
- }
- /**
- * Returns the list of object properties that are included as-is in the
- * cache, only when they're not too big, and are lazily loaded by self::loadExtraFromDB().
- * @param string $prefix Must be the empty string
- * @return string[]
- * @since 1.31 No longer accepts a non-empty $prefix
- */
- protected function getLazyCacheFields( $prefix = 'img_' ) {
- if ( $prefix !== '' ) {
- throw new InvalidArgumentException(
- __METHOD__ . ' with a non-empty prefix is no longer supported.'
- );
- }
- // Keep this in sync with the omit-lazy option in self::getQueryInfo().
- return [ 'metadata' ];
- }
- /**
- * Load file metadata from the DB
- * @param int $flags
- */
- function loadFromDB( $flags = 0 ) {
- $fname = static::class . '::' . __FUNCTION__;
- # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
- $this->dataLoaded = true;
- $this->extraDataLoaded = true;
- $dbr = ( $flags & self::READ_LATEST )
- ? $this->repo->getMasterDB()
- : $this->repo->getReplicaDB();
- $fileQuery = static::getQueryInfo();
- $row = $dbr->selectRow(
- $fileQuery['tables'],
- $fileQuery['fields'],
- [ 'img_name' => $this->getName() ],
- $fname,
- [],
- $fileQuery['joins']
- );
- if ( $row ) {
- $this->loadFromRow( $row );
- } else {
- $this->fileExists = false;
- }
- }
- /**
- * Load lazy file metadata from the DB.
- * This covers fields that are sometimes not cached.
- */
- protected function loadExtraFromDB() {
- $fname = static::class . '::' . __FUNCTION__;
- # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
- $this->extraDataLoaded = true;
- $fieldMap = $this->loadExtraFieldsWithTimestamp( $this->repo->getReplicaDB(), $fname );
- if ( !$fieldMap ) {
- $fieldMap = $this->loadExtraFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
- }
- if ( $fieldMap ) {
- foreach ( $fieldMap as $name => $value ) {
- $this->$name = $value;
- }
- } else {
- throw new MWException( "Could not find data for image '{$this->getName()}'." );
- }
- }
- /**
- * @param IDatabase $dbr
- * @param string $fname
- * @return string[]|bool
- */
- private function loadExtraFieldsWithTimestamp( $dbr, $fname ) {
- $fieldMap = false;
- $fileQuery = self::getQueryInfo( [ 'omit-nonlazy' ] );
- $row = $dbr->selectRow(
- $fileQuery['tables'],
- $fileQuery['fields'],
- [
- 'img_name' => $this->getName(),
- 'img_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
- ],
- $fname,
- [],
- $fileQuery['joins']
- );
- if ( $row ) {
- $fieldMap = $this->unprefixRow( $row, 'img_' );
- } else {
- # File may have been uploaded over in the meantime; check the old versions
- $fileQuery = OldLocalFile::getQueryInfo( [ 'omit-nonlazy' ] );
- $row = $dbr->selectRow(
- $fileQuery['tables'],
- $fileQuery['fields'],
- [
- 'oi_name' => $this->getName(),
- 'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
- ],
- $fname,
- [],
- $fileQuery['joins']
- );
- if ( $row ) {
- $fieldMap = $this->unprefixRow( $row, 'oi_' );
- }
- }
- if ( isset( $fieldMap['metadata'] ) ) {
- $fieldMap['metadata'] = $this->repo->getReplicaDB()->decodeBlob( $fieldMap['metadata'] );
- }
- return $fieldMap;
- }
- /**
- * @param array|object $row
- * @param string $prefix
- * @throws MWException
- * @return array
- */
- protected function unprefixRow( $row, $prefix = 'img_' ) {
- $array = (array)$row;
- $prefixLength = strlen( $prefix );
- // Sanity check prefix once
- if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
- throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
- }
- $decoded = [];
- foreach ( $array as $name => $value ) {
- $decoded[substr( $name, $prefixLength )] = $value;
- }
- return $decoded;
- }
- /**
- * Decode a row from the database (either object or array) to an array
- * with timestamps and MIME types decoded, and the field prefix removed.
- * @param object $row
- * @param string $prefix
- * @throws MWException
- * @return array
- */
- function decodeRow( $row, $prefix = 'img_' ) {
- $decoded = $this->unprefixRow( $row, $prefix );
- $decoded['description'] = MediaWikiServices::getInstance()->getCommentStore()
- ->getComment( 'description', (object)$decoded )->text;
- $decoded['user'] = User::newFromAnyId(
- $decoded['user'] ?? null,
- $decoded['user_text'] ?? null,
- $decoded['actor'] ?? null
- );
- unset( $decoded['user_text'], $decoded['actor'] );
- $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
- $decoded['metadata'] = $this->repo->getReplicaDB()->decodeBlob( $decoded['metadata'] );
- if ( empty( $decoded['major_mime'] ) ) {
- $decoded['mime'] = 'unknown/unknown';
- } else {
- if ( !$decoded['minor_mime'] ) {
- $decoded['minor_mime'] = 'unknown';
- }
- $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
- }
- // Trim zero padding from char/binary field
- $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
- // Normalize some fields to integer type, per their database definition.
- // Use unary + so that overflows will be upgraded to double instead of
- // being trucated as with intval(). This is important to allow >2GB
- // files on 32-bit systems.
- foreach ( [ 'size', 'width', 'height', 'bits' ] as $field ) {
- $decoded[$field] = +$decoded[$field];
- }
- return $decoded;
- }
- /**
- * Load file metadata from a DB result row
- *
- * @param object $row
- * @param string $prefix
- */
- function loadFromRow( $row, $prefix = 'img_' ) {
- $this->dataLoaded = true;
- $this->extraDataLoaded = true;
- $array = $this->decodeRow( $row, $prefix );
- foreach ( $array as $name => $value ) {
- $this->$name = $value;
- }
- $this->fileExists = true;
- $this->maybeUpgradeRow();
- }
- /**
- * Load file metadata from cache or DB, unless already loaded
- * @param int $flags
- */
- function load( $flags = 0 ) {
- if ( !$this->dataLoaded ) {
- if ( $flags & self::READ_LATEST ) {
- $this->loadFromDB( $flags );
- } else {
- $this->loadFromCache();
- }
- }
- if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
- // @note: loads on name/timestamp to reduce race condition problems
- $this->loadExtraFromDB();
- }
- }
- /**
- * Upgrade a row if it needs it
- */
- function maybeUpgradeRow() {
- global $wgUpdateCompatibleMetadata;
- if ( wfReadOnly() || $this->upgrading ) {
- return;
- }
- $upgrade = false;
- if ( is_null( $this->media_type ) || $this->mime == 'image/svg' ) {
- $upgrade = true;
- } else {
- $handler = $this->getHandler();
- if ( $handler ) {
- $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
- if ( $validity === MediaHandler::METADATA_BAD ) {
- $upgrade = true;
- } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE ) {
- $upgrade = $wgUpdateCompatibleMetadata;
- }
- }
- }
- if ( $upgrade ) {
- $this->upgrading = true;
- // Defer updates unless in auto-commit CLI mode
- DeferredUpdates::addCallableUpdate( function () {
- $this->upgrading = false; // avoid duplicate updates
- try {
- $this->upgradeRow();
- } catch ( LocalFileLockError $e ) {
- // let the other process handle it (or do it next time)
- }
- } );
- }
- }
- /**
- * @return bool Whether upgradeRow() ran for this object
- */
- function getUpgraded() {
- return $this->upgraded;
- }
- /**
- * Fix assorted version-related problems with the image row by reloading it from the file
- */
- function upgradeRow() {
- $this->lock(); // begin
- $this->loadFromFile();
- # Don't destroy file info of missing files
- if ( !$this->fileExists ) {
- $this->unlock();
- wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
- return;
- }
- $dbw = $this->repo->getMasterDB();
- list( $major, $minor ) = self::splitMime( $this->mime );
- if ( wfReadOnly() ) {
- $this->unlock();
- return;
- }
- wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
- $dbw->update( 'image',
- [
- 'img_size' => $this->size, // sanity
- 'img_width' => $this->width,
- 'img_height' => $this->height,
- 'img_bits' => $this->bits,
- 'img_media_type' => $this->media_type,
- 'img_major_mime' => $major,
- 'img_minor_mime' => $minor,
- 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
- 'img_sha1' => $this->sha1,
- ],
- [ 'img_name' => $this->getName() ],
- __METHOD__
- );
- $this->invalidateCache();
- $this->unlock(); // done
- $this->upgraded = true; // avoid rework/retries
- }
- /**
- * Set properties in this object to be equal to those given in the
- * associative array $info. Only cacheable fields can be set.
- * All fields *must* be set in $info except for getLazyCacheFields().
- *
- * If 'mime' is given, it will be split into major_mime/minor_mime.
- * If major_mime/minor_mime are given, $this->mime will also be set.
- *
- * @param array $info
- */
- function setProps( $info ) {
- $this->dataLoaded = true;
- $fields = $this->getCacheFields( '' );
- $fields[] = 'fileExists';
- foreach ( $fields as $field ) {
- if ( isset( $info[$field] ) ) {
- $this->$field = $info[$field];
- }
- }
- if ( isset( $info['user'] ) || isset( $info['user_text'] ) || isset( $info['actor'] ) ) {
- $this->user = User::newFromAnyId(
- $info['user'] ?? null,
- $info['user_text'] ?? null,
- $info['actor'] ?? null
- );
- }
- // Fix up mime fields
- if ( isset( $info['major_mime'] ) ) {
- $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
- } elseif ( isset( $info['mime'] ) ) {
- $this->mime = $info['mime'];
- list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
- }
- }
- /** splitMime inherited */
- /** getName inherited */
- /** getTitle inherited */
- /** getURL inherited */
- /** getViewURL inherited */
- /** getPath inherited */
- /** isVisible inherited */
- /**
- * @return bool
- */
- function isMissing() {
- if ( $this->missing === null ) {
- list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
- $this->missing = !$fileExists;
- }
- return $this->missing;
- }
- /**
- * Return the width of the image
- *
- * @param int $page
- * @return int
- */
- public function getWidth( $page = 1 ) {
- $page = (int)$page;
- if ( $page < 1 ) {
- $page = 1;
- }
- $this->load();
- if ( $this->isMultipage() ) {
- $handler = $this->getHandler();
- if ( !$handler ) {
- return 0;
- }
- $dim = $handler->getPageDimensions( $this, $page );
- if ( $dim ) {
- return $dim['width'];
- } else {
- // For non-paged media, the false goes through an
- // intval, turning failure into 0, so do same here.
- return 0;
- }
- } else {
- return $this->width;
- }
- }
- /**
- * Return the height of the image
- *
- * @param int $page
- * @return int
- */
- public function getHeight( $page = 1 ) {
- $page = (int)$page;
- if ( $page < 1 ) {
- $page = 1;
- }
- $this->load();
- if ( $this->isMultipage() ) {
- $handler = $this->getHandler();
- if ( !$handler ) {
- return 0;
- }
- $dim = $handler->getPageDimensions( $this, $page );
- if ( $dim ) {
- return $dim['height'];
- } else {
- // For non-paged media, the false goes through an
- // intval, turning failure into 0, so do same here.
- return 0;
- }
- } else {
- return $this->height;
- }
- }
- /**
- * Returns user who uploaded the file
- *
- * @param string $type 'text', 'id', or 'object'
- * @return int|string|User
- * @since 1.31 Added 'object'
- */
- function getUser( $type = 'text' ) {
- $this->load();
- if ( $type === 'object' ) {
- return $this->user;
- } elseif ( $type === 'text' ) {
- return $this->user->getName();
- } elseif ( $type === 'id' ) {
- return $this->user->getId();
- }
- throw new MWException( "Unknown type '$type'." );
- }
- /**
- * Get short description URL for a file based on the page ID.
- *
- * @return string|null
- * @throws MWException
- * @since 1.27
- */
- public function getDescriptionShortUrl() {
- $pageId = $this->title->getArticleID();
- if ( $pageId !== null ) {
- $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
- if ( $url !== false ) {
- return $url;
- }
- }
- return null;
- }
- /**
- * Get handler-specific metadata
- * @return string
- */
- function getMetadata() {
- $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
- return $this->metadata;
- }
- /**
- * @return int
- */
- function getBitDepth() {
- $this->load();
- return (int)$this->bits;
- }
- /**
- * Returns the size of the image file, in bytes
- * @return int
- */
- public function getSize() {
- $this->load();
- return $this->size;
- }
- /**
- * Returns the MIME type of the file.
- * @return string
- */
- function getMimeType() {
- $this->load();
- return $this->mime;
- }
- /**
- * Returns the type of the media in the file.
- * Use the value returned by this function with the MEDIATYPE_xxx constants.
- * @return string
- */
- function getMediaType() {
- $this->load();
- return $this->media_type;
- }
- /** canRender inherited */
- /** mustRender inherited */
- /** allowInlineDisplay inherited */
- /** isSafeFile inherited */
- /** isTrustedFile inherited */
- /**
- * Returns true if the file exists on disk.
- * @return bool Whether file exist on disk.
- */
- public function exists() {
- $this->load();
- return $this->fileExists;
- }
- /** getTransformScript inherited */
- /** getUnscaledThumb inherited */
- /** thumbName inherited */
- /** createThumb inherited */
- /** transform inherited */
- /** getHandler inherited */
- /** iconThumb inherited */
- /** getLastError inherited */
- /**
- * Get all thumbnail names previously generated for this file
- * @param string|bool $archiveName Name of an archive file, default false
- * @return array First element is the base dir, then files in that base dir.
- */
- function getThumbnails( $archiveName = false ) {
- if ( $archiveName ) {
- $dir = $this->getArchiveThumbPath( $archiveName );
- } else {
- $dir = $this->getThumbPath();
- }
- $backend = $this->repo->getBackend();
- $files = [ $dir ];
- try {
- $iterator = $backend->getFileList( [ 'dir' => $dir ] );
- foreach ( $iterator as $file ) {
- $files[] = $file;
- }
- } catch ( FileBackendError $e ) {
- } // suppress (T56674)
- return $files;
- }
- /**
- * Refresh metadata in memcached, but don't touch thumbnails or CDN
- */
- function purgeMetadataCache() {
- $this->invalidateCache();
- }
- /**
- * Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
- *
- * @param array $options An array potentially with the key forThumbRefresh.
- *
- * @note This used to purge old thumbnails by default as well, but doesn't anymore.
- */
- function purgeCache( $options = [] ) {
- // Refresh metadata cache
- $this->purgeMetadataCache();
- // Delete thumbnails
- $this->purgeThumbnails( $options );
- // Purge CDN cache for this file
- DeferredUpdates::addUpdate(
- new CdnCacheUpdate( [ $this->getUrl() ] ),
- DeferredUpdates::PRESEND
- );
- }
- /**
- * Delete cached transformed files for an archived version only.
- * @param string $archiveName Name of the archived file
- */
- function purgeOldThumbnails( $archiveName ) {
- // Get a list of old thumbnails and URLs
- $files = $this->getThumbnails( $archiveName );
- // Purge any custom thumbnail caches
- Hooks::run( 'LocalFilePurgeThumbnails', [ $this, $archiveName ] );
- // Delete thumbnails
- $dir = array_shift( $files );
- $this->purgeThumbList( $dir, $files );
- // Purge the CDN
- $urls = [];
- foreach ( $files as $file ) {
- $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
- }
- DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
- }
- /**
- * Delete cached transformed files for the current version only.
- * @param array $options
- */
- public function purgeThumbnails( $options = [] ) {
- $files = $this->getThumbnails();
- // Always purge all files from CDN regardless of handler filters
- $urls = [];
- foreach ( $files as $file ) {
- $urls[] = $this->getThumbUrl( $file );
- }
- array_shift( $urls ); // don't purge directory
- // Give media handler a chance to filter the file purge list
- if ( !empty( $options['forThumbRefresh'] ) ) {
- $handler = $this->getHandler();
- if ( $handler ) {
- $handler->filterThumbnailPurgeList( $files, $options );
- }
- }
- // Purge any custom thumbnail caches
- Hooks::run( 'LocalFilePurgeThumbnails', [ $this, false ] );
- // Delete thumbnails
- $dir = array_shift( $files );
- $this->purgeThumbList( $dir, $files );
- // Purge the CDN
- DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
- }
- /**
- * Prerenders a configurable set of thumbnails
- *
- * @since 1.28
- */
- public function prerenderThumbnails() {
- global $wgUploadThumbnailRenderMap;
- $jobs = [];
- $sizes = $wgUploadThumbnailRenderMap;
- rsort( $sizes );
- foreach ( $sizes as $size ) {
- if ( $this->isVectorized() || $this->getWidth() > $size ) {
- $jobs[] = new ThumbnailRenderJob(
- $this->getTitle(),
- [ 'transformParams' => [ 'width' => $size ] ]
- );
- }
- }
- if ( $jobs ) {
- JobQueueGroup::singleton()->lazyPush( $jobs );
- }
- }
- /**
- * Delete a list of thumbnails visible at urls
- * @param string $dir Base dir of the files.
- * @param array $files Array of strings: relative filenames (to $dir)
- */
- protected function purgeThumbList( $dir, $files ) {
- $fileListDebug = strtr(
- var_export( $files, true ),
- [ "\n" => '' ]
- );
- wfDebug( __METHOD__ . ": $fileListDebug\n" );
- $purgeList = [];
- foreach ( $files as $file ) {
- if ( $this->repo->supportsSha1URLs() ) {
- $reference = $this->getSha1();
- } else {
- $reference = $this->getName();
- }
- # Check that the reference (filename or sha1) is part of the thumb name
- # This is a basic sanity check to avoid erasing unrelated directories
- if ( strpos( $file, $reference ) !== false
- || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
- ) {
- $purgeList[] = "{$dir}/{$file}";
- }
- }
- # Delete the thumbnails
- $this->repo->quickPurgeBatch( $purgeList );
- # Clear out the thumbnail directory if empty
- $this->repo->quickCleanDir( $dir );
- }
- /** purgeDescription inherited */
- /** purgeEverything inherited */
- /**
- * @param int|null $limit Optional: Limit to number of results
- * @param string|int|null $start Optional: Timestamp, start from
- * @param string|int|null $end Optional: Timestamp, end at
- * @param bool $inc
- * @return OldLocalFile[]
- */
- function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
- $dbr = $this->repo->getReplicaDB();
- $oldFileQuery = OldLocalFile::getQueryInfo();
- $tables = $oldFileQuery['tables'];
- $fields = $oldFileQuery['fields'];
- $join_conds = $oldFileQuery['joins'];
- $conds = $opts = [];
- $eq = $inc ? '=' : '';
- $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
- if ( $start ) {
- $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
- }
- if ( $end ) {
- $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
- }
- if ( $limit ) {
- $opts['LIMIT'] = $limit;
- }
- // Search backwards for time > x queries
- $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
- $opts['ORDER BY'] = "oi_timestamp $order";
- $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
- // Avoid PHP 7.1 warning from passing $this by reference
- $localFile = $this;
- Hooks::run( 'LocalFile::getHistory', [ &$localFile, &$tables, &$fields,
- &$conds, &$opts, &$join_conds ] );
- $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
- $r = [];
- foreach ( $res as $row ) {
- $r[] = $this->repo->newFileFromRow( $row );
- }
- if ( $order == 'ASC' ) {
- $r = array_reverse( $r ); // make sure it ends up descending
- }
- return $r;
- }
- /**
- * Returns the history of this file, line by line.
- * starts with current version, then old versions.
- * uses $this->historyLine to check which line to return:
- * 0 return line for current version
- * 1 query for old versions, return first one
- * 2, ... return next old version from above query
- * @return bool
- */
- public function nextHistoryLine() {
- # Polymorphic function name to distinguish foreign and local fetches
- $fname = static::class . '::' . __FUNCTION__;
- $dbr = $this->repo->getReplicaDB();
- if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
- $fileQuery = self::getQueryInfo();
- $this->historyRes = $dbr->select( $fileQuery['tables'],
- $fileQuery['fields'] + [
- 'oi_archive_name' => $dbr->addQuotes( '' ),
- 'oi_deleted' => 0,
- ],
- [ 'img_name' => $this->title->getDBkey() ],
- $fname,
- [],
- $fileQuery['joins']
- );
- if ( 0 == $dbr->numRows( $this->historyRes ) ) {
- $this->historyRes = null;
- return false;
- }
- } elseif ( $this->historyLine == 1 ) {
- $fileQuery = OldLocalFile::getQueryInfo();
- $this->historyRes = $dbr->select(
- $fileQuery['tables'],
- $fileQuery['fields'],
- [ 'oi_name' => $this->title->getDBkey() ],
- $fname,
- [ 'ORDER BY' => 'oi_timestamp DESC' ],
- $fileQuery['joins']
- );
- }
- $this->historyLine++;
- return $dbr->fetchObject( $this->historyRes );
- }
- /**
- * Reset the history pointer to the first element of the history
- */
- public function resetHistory() {
- $this->historyLine = 0;
- if ( !is_null( $this->historyRes ) ) {
- $this->historyRes = null;
- }
- }
- /** getHashPath inherited */
- /** getRel inherited */
- /** getUrlRel inherited */
- /** getArchiveRel inherited */
- /** getArchivePath inherited */
- /** getThumbPath inherited */
- /** getArchiveUrl inherited */
- /** getThumbUrl inherited */
- /** getArchiveVirtualUrl inherited */
- /** getThumbVirtualUrl inherited */
- /** isHashed inherited */
- /**
- * Upload a file and record it in the DB
- * @param string|FSFile $src Source storage path, virtual URL, or filesystem path
- * @param string $comment Upload description
- * @param string $pageText Text to use for the new description page,
- * if a new description page is created
- * @param int|bool $flags Flags for publish()
- * @param array|bool $props File properties, if known. This can be used to
- * reduce the upload time when uploading virtual URLs for which the file
- * info is already known
- * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the
- * current time
- * @param User|null $user User object or null to use $wgUser
- * @param string[] $tags Change tags to add to the log entry and page revision.
- * (This doesn't check $user's permissions.)
- * @param bool $createNullRevision Set to false to avoid creation of a null revision on file
- * upload, see T193621
- * @return Status On success, the value member contains the
- * archive name, or an empty string if it was a new file.
- */
- function upload( $src, $comment, $pageText, $flags = 0, $props = false,
- $timestamp = false, $user = null, $tags = [],
- $createNullRevision = true
- ) {
- if ( $this->getRepo()->getReadOnlyReason() !== false ) {
- return $this->readOnlyFatalStatus();
- } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
- // Check this in advance to avoid writing to FileBackend and the file tables,
- // only to fail on insert the revision due to the text store being unavailable.
- return $this->readOnlyFatalStatus();
- }
- $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
- if ( !$props ) {
- if ( $this->repo->isVirtualUrl( $srcPath )
- || FileBackend::isStoragePath( $srcPath )
- ) {
- $props = $this->repo->getFileProps( $srcPath );
- } else {
- $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
- $props = $mwProps->getPropsFromPath( $srcPath, true );
- }
- }
- $options = [];
- $handler = MediaHandler::getHandler( $props['mime'] );
- if ( $handler ) {
- $metadata = Wikimedia\quietCall( 'unserialize', $props['metadata'] );
- if ( !is_array( $metadata ) ) {
- $metadata = [];
- }
- $options['headers'] = $handler->getContentHeaders( $metadata );
- } else {
- $options['headers'] = [];
- }
- // Trim spaces on user supplied text
- $comment = trim( $comment );
- $this->lock(); // begin
- $status = $this->publish( $src, $flags, $options );
- if ( $status->successCount >= 2 ) {
- // There will be a copy+(one of move,copy,store).
- // The first succeeding does not commit us to updating the DB
- // since it simply copied the current version to a timestamped file name.
- // It is only *preferable* to avoid leaving such files orphaned.
- // Once the second operation goes through, then the current version was
- // updated and we must therefore update the DB too.
- $oldver = $status->value;
- $uploadStatus = $this->recordUpload2(
- $oldver,
- $comment,
- $pageText,
- $props,
- $timestamp,
- $user,
- $tags,
- $createNullRevision
- );
- if ( !$uploadStatus->isOK() ) {
- if ( $uploadStatus->hasMessage( 'filenotfound' ) ) {
- // update filenotfound error with more specific path
- $status->fatal( 'filenotfound', $srcPath );
- } else {
- $status->merge( $uploadStatus );
- }
- }
- }
- $this->unlock(); // done
- return $status;
- }
- /**
- * Record a file upload in the upload log and the image table
- * @param string $oldver
- * @param string $desc
- * @param string $license
- * @param string $copyStatus
- * @param string $source
- * @param bool $watch
- * @param string|bool $timestamp
- * @param User|null $user User object or null to use $wgUser
- * @return bool
- */
- function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
- $watch = false, $timestamp = false, User $user = null ) {
- if ( !$user ) {
- global $wgUser;
- $user = $wgUser;
- }
- $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
- if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user )->isOK() ) {
- return false;
- }
- if ( $watch ) {
- $user->addWatch( $this->getTitle() );
- }
- return true;
- }
- /**
- * Record a file upload in the upload log and the image table
- * @param string $oldver
- * @param string $comment
- * @param string $pageText
- * @param bool|array $props
- * @param string|bool $timestamp
- * @param null|User $user
- * @param string[] $tags
- * @param bool $createNullRevision Set to false to avoid creation of a null revision on file
- * upload, see T193621
- * @return Status
- */
- function recordUpload2(
- $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = [],
- $createNullRevision = true
- ) {
- global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
- if ( is_null( $user ) ) {
- global $wgUser;
- $user = $wgUser;
- }
- $dbw = $this->repo->getMasterDB();
- # Imports or such might force a certain timestamp; otherwise we generate
- # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
- if ( $timestamp === false ) {
- $timestamp = $dbw->timestamp();
- $allowTimeKludge = true;
- } else {
- $allowTimeKludge = false;
- }
- $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
- $props['description'] = $comment;
- $props['user'] = $user->getId();
- $props['user_text'] = $user->getName();
- $props['actor'] = $user->getActorId( $dbw );
- $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
- $this->setProps( $props );
- # Fail now if the file isn't there
- if ( !$this->fileExists ) {
- wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
- return Status::newFatal( 'filenotfound', $this->getRel() );
- }
- $dbw->startAtomic( __METHOD__ );
- # Test to see if the row exists using INSERT IGNORE
- # This avoids race conditions by locking the row until the commit, and also
- # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
- $commentStore = MediaWikiServices::getInstance()->getCommentStore();
- list( $commentFields, $commentCallback ) =
- $commentStore->insertWithTempTable( $dbw, 'img_description', $comment );
- $actorMigration = ActorMigration::newMigration();
- $actorFields = $actorMigration->getInsertValues( $dbw, 'img_user', $user );
- $dbw->insert( 'image',
- [
- 'img_name' => $this->getName(),
- 'img_size' => $this->size,
- 'img_width' => intval( $this->width ),
- 'img_height' => intval( $this->height ),
- 'img_bits' => $this->bits,
- 'img_media_type' => $this->media_type,
- 'img_major_mime' => $this->major_mime,
- 'img_minor_mime' => $this->minor_mime,
- 'img_timestamp' => $timestamp,
- 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
- 'img_sha1' => $this->sha1
- ] + $commentFields + $actorFields,
- __METHOD__,
- 'IGNORE'
- );
- $reupload = ( $dbw->affectedRows() == 0 );
- if ( $reupload ) {
- $row = $dbw->selectRow(
- 'image',
- [ 'img_timestamp', 'img_sha1' ],
- [ 'img_name' => $this->getName() ],
- __METHOD__,
- [ 'LOCK IN SHARE MODE' ]
- );
- if ( $row && $row->img_sha1 === $this->sha1 ) {
- $dbw->endAtomic( __METHOD__ );
- wfDebug( __METHOD__ . ": File " . $this->getRel() . " already exists!\n" );
- $title = Title::newFromText( $this->getName(), NS_FILE );
- return Status::newFatal( 'fileexists-no-change', $title->getPrefixedText() );
- }
- if ( $allowTimeKludge ) {
- # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
- $lUnixtime = $row ? wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
- # Avoid a timestamp that is not newer than the last version
- # TODO: the image/oldimage tables should be like page/revision with an ID field
- if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
- sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
- $timestamp = $dbw->timestamp( $lUnixtime + 1 );
- $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
- }
- }
- $tables = [ 'image' ];
- $fields = [
- 'oi_name' => 'img_name',
- 'oi_archive_name' => $dbw->addQuotes( $oldver ),
- 'oi_size' => 'img_size',
- 'oi_width' => 'img_width',
- 'oi_height' => 'img_height',
- 'oi_bits' => 'img_bits',
- 'oi_timestamp' => 'img_timestamp',
- 'oi_metadata' => 'img_metadata',
- 'oi_media_type' => 'img_media_type',
- 'oi_major_mime' => 'img_major_mime',
- 'oi_minor_mime' => 'img_minor_mime',
- 'oi_sha1' => 'img_sha1',
- ];
- $joins = [];
- if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
- $fields['oi_description'] = 'img_description';
- }
- if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
- $tables[] = 'image_comment_temp';
- $fields['oi_description_id'] = 'imgcomment_description_id';
- $joins['image_comment_temp'] = [
- $wgCommentTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
- [ 'imgcomment_name = img_name' ]
- ];
- }
- if ( $wgCommentTableSchemaMigrationStage !== MIGRATION_OLD &&
- $wgCommentTableSchemaMigrationStage !== MIGRATION_NEW
- ) {
- // Upgrade any rows that are still old-style. Otherwise an upgrade
- // might be missed if a deletion happens while the migration script
- // is running.
- $res = $dbw->select(
- [ 'image', 'image_comment_temp' ],
- [ 'img_name', 'img_description' ],
- [ 'img_name' => $this->getName(), 'imgcomment_name' => null ],
- __METHOD__,
- [],
- [ 'image_comment_temp' => [ 'LEFT JOIN', [ 'imgcomment_name = img_name' ] ] ]
- );
- foreach ( $res as $row ) {
- list( , $callback ) = $commentStore->insertWithTempTable(
- $dbw, 'img_description', $row->img_description
- );
- $callback( $row->img_name );
- }
- }
- if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
- $fields['oi_user'] = 'img_user';
- $fields['oi_user_text'] = 'img_user_text';
- }
- if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
- $fields['oi_actor'] = 'img_actor';
- }
- if ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
- $wgActorTableSchemaMigrationStage !== MIGRATION_NEW
- ) {
- // Upgrade any rows that are still old-style. Otherwise an upgrade
- // might be missed if a deletion happens while the migration script
- // is running.
- $res = $dbw->select(
- [ 'image' ],
- [ 'img_name', 'img_user', 'img_user_text' ],
- [ 'img_name' => $this->getName(), 'img_actor' => 0 ],
- __METHOD__
- );
- foreach ( $res as $row ) {
- $actorId = User::newFromAnyId( $row->img_user, $row->img_user_text, null )->getActorId( $dbw );
- $dbw->update(
- 'image',
- [ 'img_actor' => $actorId ],
- [ 'img_name' => $row->img_name, 'img_actor' => 0 ],
- __METHOD__
- );
- }
- }
- # (T36993) Note: $oldver can be empty here, if the previous
- # version of the file was broken. Allow registration of the new
- # version to continue anyway, because that's better than having
- # an image that's not fixable by user operations.
- # Collision, this is an update of a file
- # Insert previous contents into oldimage
- $dbw->insertSelect( 'oldimage', $tables, $fields,
- [ 'img_name' => $this->getName() ], __METHOD__, [], [], $joins );
- # Update the current image row
- $dbw->update( 'image',
- [
- 'img_size' => $this->size,
- 'img_width' => intval( $this->width ),
- 'img_height' => intval( $this->height ),
- 'img_bits' => $this->bits,
- 'img_media_type' => $this->media_type,
- 'img_major_mime' => $this->major_mime,
- 'img_minor_mime' => $this->minor_mime,
- 'img_timestamp' => $timestamp,
- 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
- 'img_sha1' => $this->sha1
- ] + $commentFields + $actorFields,
- [ 'img_name' => $this->getName() ],
- __METHOD__
- );
- if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
- // So $commentCallback can insert the new row
- $dbw->delete( 'image_comment_temp', [ 'imgcomment_name' => $this->getName() ], __METHOD__ );
- }
- }
- $commentCallback( $this->getName() );
- $descTitle = $this->getTitle();
- $descId = $descTitle->getArticleID();
- $wikiPage = new WikiFilePage( $descTitle );
- $wikiPage->setFile( $this );
- // Add the log entry...
- $logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
- $logEntry->setTimestamp( $this->timestamp );
- $logEntry->setPerformer( $user );
- $logEntry->setComment( $comment );
- $logEntry->setTarget( $descTitle );
- // Allow people using the api to associate log entries with the upload.
- // Log has a timestamp, but sometimes different from upload timestamp.
- $logEntry->setParameters(
- [
- 'img_sha1' => $this->sha1,
- 'img_timestamp' => $timestamp,
- ]
- );
- // Note we keep $logId around since during new image
- // creation, page doesn't exist yet, so log_page = 0
- // but we want it to point to the page we're making,
- // so we later modify the log entry.
- // For a similar reason, we avoid making an RC entry
- // now and wait until the page exists.
- $logId = $logEntry->insert();
- if ( $descTitle->exists() ) {
- // Use own context to get the action text in content language
- $formatter = LogFormatter::newFromEntry( $logEntry );
- $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
- $editSummary = $formatter->getPlainActionText();
- $nullRevision = $createNullRevision === false ? null : Revision::newNullRevision(
- $dbw,
- $descId,
- $editSummary,
- false,
- $user
- );
- if ( $nullRevision ) {
- $nullRevision->insertOn( $dbw );
- Hooks::run(
- 'NewRevisionFromEditComplete',
- [ $wikiPage, $nullRevision, $nullRevision->getParentId(), $user ]
- );
- $wikiPage->updateRevisionOn( $dbw, $nullRevision );
- // Associate null revision id
- $logEntry->setAssociatedRevId( $nullRevision->getId() );
- }
- $newPageContent = null;
- } else {
- // Make the description page and RC log entry post-commit
- $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
- }
- # Defer purges, page creation, and link updates in case they error out.
- # The most important thing is that files and the DB registry stay synced.
- $dbw->endAtomic( __METHOD__ );
- # Do some cache purges after final commit so that:
- # a) Changes are more likely to be seen post-purge
- # b) They won't cause rollback of the log publish/update above
- DeferredUpdates::addUpdate(
- new AutoCommitUpdate(
- $dbw,
- __METHOD__,
- function () use (
- $reupload, $wikiPage, $newPageContent, $comment, $user,
- $logEntry, $logId, $descId, $tags
- ) {
- # Update memcache after the commit
- $this->invalidateCache();
- $updateLogPage = false;
- if ( $newPageContent ) {
- # New file page; create the description page.
- # There's already a log entry, so don't make a second RC entry
- # CDN and file cache for the description page are purged by doEditContent.
- $status = $wikiPage->doEditContent(
- $newPageContent,
- $comment,
- EDIT_NEW | EDIT_SUPPRESS_RC,
- false,
- $user
- );
- if ( isset( $status->value['revision'] ) ) {
- /** @var Revision $rev */
- $rev = $status->value['revision'];
- // Associate new page revision id
- $logEntry->setAssociatedRevId( $rev->getId() );
- }
- // This relies on the resetArticleID() call in WikiPage::insertOn(),
- // which is triggered on $descTitle by doEditContent() above.
- if ( isset( $status->value['revision'] ) ) {
- /** @var Revision $rev */
- $rev = $status->value['revision'];
- $updateLogPage = $rev->getPage();
- }
- } else {
- # Existing file page: invalidate description page cache
- $wikiPage->getTitle()->invalidateCache();
- $wikiPage->getTitle()->purgeSquid();
- # Allow the new file version to be patrolled from the page footer
- Article::purgePatrolFooterCache( $descId );
- }
- # Update associated rev id. This should be done by $logEntry->insert() earlier,
- # but setAssociatedRevId() wasn't called at that point yet...
- $logParams = $logEntry->getParameters();
- $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
- $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
- if ( $updateLogPage ) {
- # Also log page, in case where we just created it above
- $update['log_page'] = $updateLogPage;
- }
- $this->getRepo()->getMasterDB()->update(
- 'logging',
- $update,
- [ 'log_id' => $logId ],
- __METHOD__
- );
- $this->getRepo()->getMasterDB()->insert(
- 'log_search',
- [
- 'ls_field' => 'associated_rev_id',
- 'ls_value' => $logEntry->getAssociatedRevId(),
- 'ls_log_id' => $logId,
- ],
- __METHOD__
- );
- # Add change tags, if any
- if ( $tags ) {
- $logEntry->setTags( $tags );
- }
- # Uploads can be patrolled
- $logEntry->setIsPatrollable( true );
- # Now that the log entry is up-to-date, make an RC entry.
- $logEntry->publish( $logId );
- # Run hook for other updates (typically more cache purging)
- Hooks::run( 'FileUpload', [ $this, $reupload, !$newPageContent ] );
- if ( $reupload ) {
- # Delete old thumbnails
- $this->purgeThumbnails();
- # Remove the old file from the CDN cache
- DeferredUpdates::addUpdate(
- new CdnCacheUpdate( [ $this->getUrl() ] ),
- DeferredUpdates::PRESEND
- );
- } else {
- # Update backlink pages pointing to this title if created
- LinksUpdate::queueRecursiveJobsForTable(
- $this->getTitle(),
- 'imagelinks',
- 'upload-image',
- $user->getName()
- );
- }
- $this->prerenderThumbnails();
- }
- ),
- DeferredUpdates::PRESEND
- );
- if ( !$reupload ) {
- # This is a new file, so update the image count
- DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
- }
- # Invalidate cache for all pages using this file
- DeferredUpdates::addUpdate(
- new HTMLCacheUpdate( $this->getTitle(), 'imagelinks', 'file-upload' )
- );
- return Status::newGood();
- }
- /**
- * Move or copy a file to its public location. If a file exists at the
- * destination, move it to an archive. Returns a Status object with
- * the archive name in the "value" member on success.
- *
- * The archive name should be passed through to recordUpload for database
- * registration.
- *
- * @param string|FSFile $src Local filesystem path or virtual URL to the source image
- * @param int $flags A bitwise combination of:
- * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
- * @param array $options Optional additional parameters
- * @return Status On success, the value member contains the
- * archive name, or an empty string if it was a new file.
- */
- function publish( $src, $flags = 0, array $options = [] ) {
- return $this->publishTo( $src, $this->getRel(), $flags, $options );
- }
- /**
- * Move or copy a file to a specified location. Returns a Status
- * object with the archive name in the "value" member on success.
- *
- * The archive name should be passed through to recordUpload for database
- * registration.
- *
- * @param string|FSFile $src Local filesystem path or virtual URL to the source image
- * @param string $dstRel Target relative path
- * @param int $flags A bitwise combination of:
- * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
- * @param array $options Optional additional parameters
- * @return Status On success, the value member contains the
- * archive name, or an empty string if it was a new file.
- */
- function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
- $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
- $repo = $this->getRepo();
- if ( $repo->getReadOnlyReason() !== false ) {
- return $this->readOnlyFatalStatus();
- }
- $this->lock(); // begin
- $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
- $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
- if ( $repo->hasSha1Storage() ) {
- $sha1 = $repo->isVirtualUrl( $srcPath )
- ? $repo->getFileSha1( $srcPath )
- : FSFile::getSha1Base36FromPath( $srcPath );
- /** @var FileBackendDBRepoWrapper $wrapperBackend */
- $wrapperBackend = $repo->getBackend();
- $dst = $wrapperBackend->getPathForSHA1( $sha1 );
- $status = $repo->quickImport( $src, $dst );
- if ( $flags & File::DELETE_SOURCE ) {
- unlink( $srcPath );
- }
- if ( $this->exists() ) {
- $status->value = $archiveName;
- }
- } else {
- $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
- $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
- if ( $status->value == 'new' ) {
- $status->value = '';
- } else {
- $status->value = $archiveName;
- }
- }
- $this->unlock(); // done
- return $status;
- }
- /** getLinksTo inherited */
- /** getExifData inherited */
- /** isLocal inherited */
- /** wasDeleted inherited */
- /**
- * Move file to the new title
- *
- * Move current, old version and all thumbnails
- * to the new filename. Old file is deleted.
- *
- * Cache purging is done; checks for validity
- * and logging are caller's responsibility
- *
- * @param Title $target New file name
- * @return Status
- */
- function move( $target ) {
- if ( $this->getRepo()->getReadOnlyReason() !== false ) {
- return $this->readOnlyFatalStatus();
- }
- wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
- $batch = new LocalFileMoveBatch( $this, $target );
- $this->lock(); // begin
- $batch->addCurrent();
- $archiveNames = $batch->addOlds();
- $status = $batch->execute();
- $this->unlock(); // done
- wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
- // Purge the source and target files...
- $oldTitleFile = wfLocalFile( $this->title );
- $newTitleFile = wfLocalFile( $target );
- // To avoid slow purges in the transaction, move them outside...
- DeferredUpdates::addUpdate(
- new AutoCommitUpdate(
- $this->getRepo()->getMasterDB(),
- __METHOD__,
- function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
- $oldTitleFile->purgeEverything();
- foreach ( $archiveNames as $archiveName ) {
- $oldTitleFile->purgeOldThumbnails( $archiveName );
- }
- $newTitleFile->purgeEverything();
- }
- ),
- DeferredUpdates::PRESEND
- );
- if ( $status->isOK() ) {
- // Now switch the object
- $this->title = $target;
- // Force regeneration of the name and hashpath
- unset( $this->name );
- unset( $this->hashPath );
- }
- return $status;
- }
- /**
- * Delete all versions of the file.
- *
- * Moves the files into an archive directory (or deletes them)
- * and removes the database rows.
- *
- * Cache purging is done; logging is caller's responsibility.
- *
- * @param string $reason
- * @param bool $suppress
- * @param User|null $user
- * @return Status
- */
- function delete( $reason, $suppress = false, $user = null ) {
- if ( $this->getRepo()->getReadOnlyReason() !== false ) {
- return $this->readOnlyFatalStatus();
- }
- $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
- $this->lock(); // begin
- $batch->addCurrent();
- // Get old version relative paths
- $archiveNames = $batch->addOlds();
- $status = $batch->execute();
- $this->unlock(); // done
- if ( $status->isOK() ) {
- DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
- }
- // To avoid slow purges in the transaction, move them outside...
- DeferredUpdates::addUpdate(
- new AutoCommitUpdate(
- $this->getRepo()->getMasterDB(),
- __METHOD__,
- function () use ( $archiveNames ) {
- $this->purgeEverything();
- foreach ( $archiveNames as $archiveName ) {
- $this->purgeOldThumbnails( $archiveName );
- }
- }
- ),
- DeferredUpdates::PRESEND
- );
- // Purge the CDN
- $purgeUrls = [];
- foreach ( $archiveNames as $archiveName ) {
- $purgeUrls[] = $this->getArchiveUrl( $archiveName );
- }
- DeferredUpdates::addUpdate( new CdnCacheUpdate( $purgeUrls ), DeferredUpdates::PRESEND );
- return $status;
- }
- /**
- * Delete an old version of the file.
- *
- * Moves the file into an archive directory (or deletes it)
- * and removes the database row.
- *
- * Cache purging is done; logging is caller's responsibility.
- *
- * @param string $archiveName
- * @param string $reason
- * @param bool $suppress
- * @param User|null $user
- * @throws MWException Exception on database or file store failure
- * @return Status
- */
- function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
- if ( $this->getRepo()->getReadOnlyReason() !== false ) {
- return $this->readOnlyFatalStatus();
- }
- $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
- $this->lock(); // begin
- $batch->addOld( $archiveName );
- $status = $batch->execute();
- $this->unlock(); // done
- $this->purgeOldThumbnails( $archiveName );
- if ( $status->isOK() ) {
- $this->purgeDescription();
- }
- DeferredUpdates::addUpdate(
- new CdnCacheUpdate( [ $this->getArchiveUrl( $archiveName ) ] ),
- DeferredUpdates::PRESEND
- );
- return $status;
- }
- /**
- * Restore all or specified deleted revisions to the given file.
- * Permissions and logging are left to the caller.
- *
- * May throw database exceptions on error.
- *
- * @param array $versions Set of record ids of deleted items to restore,
- * or empty to restore all revisions.
- * @param bool $unsuppress
- * @return Status
- */
- function restore( $versions = [], $unsuppress = false ) {
- if ( $this->getRepo()->getReadOnlyReason() !== false ) {
- return $this->readOnlyFatalStatus();
- }
- $batch = new LocalFileRestoreBatch( $this, $unsuppress );
- $this->lock(); // begin
- if ( !$versions ) {
- $batch->addAll();
- } else {
- $batch->addIds( $versions );
- }
- $status = $batch->execute();
- if ( $status->isGood() ) {
- $cleanupStatus = $batch->cleanup();
- $cleanupStatus->successCount = 0;
- $cleanupStatus->failCount = 0;
- $status->merge( $cleanupStatus );
- }
- $this->unlock(); // done
- return $status;
- }
- /** isMultipage inherited */
- /** pageCount inherited */
- /** scaleHeight inherited */
- /** getImageSize inherited */
- /**
- * Get the URL of the file description page.
- * @return string
- */
- function getDescriptionUrl() {
- return $this->title->getLocalURL();
- }
- /**
- * Get the HTML text of the description page
- * This is not used by ImagePage for local files, since (among other things)
- * it skips the parser cache.
- *
- * @param Language|null $lang What language to get description in (Optional)
- * @return string|false
- */
- function getDescriptionText( Language $lang = null ) {
- $store = MediaWikiServices::getInstance()->getRevisionStore();
- $revision = $store->getRevisionByTitle( $this->title, 0, Revision::READ_NORMAL );
- if ( !$revision ) {
- return false;
- }
- $renderer = MediaWikiServices::getInstance()->getRevisionRenderer();
- $rendered = $renderer->getRenderedRevision( $revision, new ParserOptions( null, $lang ) );
- if ( !$rendered ) {
- // audience check failed
- return false;
- }
- $pout = $rendered->getRevisionParserOutput();
- return $pout->getText();
- }
- /**
- * @param int $audience
- * @param User|null $user
- * @return string
- */
- function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
- $this->load();
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
- return '';
- } elseif ( $audience == self::FOR_THIS_USER
- && !$this->userCan( self::DELETED_COMMENT, $user )
- ) {
- return '';
- } else {
- return $this->description;
- }
- }
- /**
- * @return bool|string
- */
- function getTimestamp() {
- $this->load();
- return $this->timestamp;
- }
- /**
- * @return bool|string
- */
- public function getDescriptionTouched() {
- // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
- // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
- // need to differentiate between null (uninitialized) and false (failed to load).
- if ( $this->descriptionTouched === null ) {
- $cond = [
- 'page_namespace' => $this->title->getNamespace(),
- 'page_title' => $this->title->getDBkey()
- ];
- $touched = $this->repo->getReplicaDB()->selectField( 'page', 'page_touched', $cond, __METHOD__ );
- $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
- }
- return $this->descriptionTouched;
- }
- /**
- * @return string
- */
- function getSha1() {
- $this->load();
- // Initialise now if necessary
- if ( $this->sha1 == '' && $this->fileExists ) {
- $this->lock(); // begin
- $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
- if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
- $dbw = $this->repo->getMasterDB();
- $dbw->update( 'image',
- [ 'img_sha1' => $this->sha1 ],
- [ 'img_name' => $this->getName() ],
- __METHOD__ );
- $this->invalidateCache();
- }
- $this->unlock(); // done
- }
- return $this->sha1;
- }
- /**
- * @return bool Whether to cache in RepoGroup (this avoids OOMs)
- */
- function isCacheable() {
- $this->load();
- // If extra data (metadata) was not loaded then it must have been large
- return $this->extraDataLoaded
- && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
- }
- /**
- * @return Status
- * @since 1.28
- */
- public function acquireFileLock() {
- return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
- [ $this->getPath() ], LockManager::LOCK_EX, 10
- ) );
- }
- /**
- * @return Status
- * @since 1.28
- */
- public function releaseFileLock() {
- return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
- [ $this->getPath() ], LockManager::LOCK_EX
- ) );
- }
- /**
- * Start an atomic DB section and lock the image for update
- * or increments a reference counter if the lock is already held
- *
- * This method should not be used outside of LocalFile/LocalFile*Batch
- *
- * @throws LocalFileLockError Throws an error if the lock was not acquired
- * @return bool Whether the file lock owns/spawned the DB transaction
- */
- public function lock() {
- if ( !$this->locked ) {
- $logger = LoggerFactory::getInstance( 'LocalFile' );
- $dbw = $this->repo->getMasterDB();
- $makesTransaction = !$dbw->trxLevel();
- $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
- // T56736: use simple lock to handle when the file does not exist.
- // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
- // Also, that would cause contention on INSERT of similarly named rows.
- $status = $this->acquireFileLock(); // represents all versions of the file
- if ( !$status->isGood() ) {
- $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
- $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
- throw new LocalFileLockError( $status );
- }
- // Release the lock *after* commit to avoid row-level contention.
- // Make sure it triggers on rollback() as well as commit() (T132921).
- $dbw->onTransactionResolution(
- function () use ( $logger ) {
- $status = $this->releaseFileLock();
- if ( !$status->isGood() ) {
- $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
- }
- },
- __METHOD__
- );
- // Callers might care if the SELECT snapshot is safely fresh
- $this->lockedOwnTrx = $makesTransaction;
- }
- $this->locked++;
- return $this->lockedOwnTrx;
- }
- /**
- * Decrement the lock reference count and end the atomic section if it reaches zero
- *
- * This method should not be used outside of LocalFile/LocalFile*Batch
- *
- * The commit and loc release will happen when no atomic sections are active, which
- * may happen immediately or at some point after calling this
- */
- public function unlock() {
- if ( $this->locked ) {
- --$this->locked;
- if ( !$this->locked ) {
- $dbw = $this->repo->getMasterDB();
- $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
- $this->lockedOwnTrx = false;
- }
- }
- }
- /**
- * @return Status
- */
- protected function readOnlyFatalStatus() {
- return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
- $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
- }
- /**
- * Clean up any dangling locks
- */
- function __destruct() {
- $this->unlock();
- }
- } // LocalFile class
- # ------------------------------------------------------------------------------
- /**
- * Helper class for file deletion
- * @ingroup FileAbstraction
- */
- class LocalFileDeleteBatch {
- /** @var LocalFile */
- private $file;
- /** @var string */
- private $reason;
- /** @var array */
- private $srcRels = [];
- /** @var array */
- private $archiveUrls = [];
- /** @var array Items to be processed in the deletion batch */
- private $deletionBatch;
- /** @var bool Whether to suppress all suppressable fields when deleting */
- private $suppress;
- /** @var Status */
- private $status;
- /** @var User */
- private $user;
- /**
- * @param File $file
- * @param string $reason
- * @param bool $suppress
- * @param User|null $user
- */
- function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
- $this->file = $file;
- $this->reason = $reason;
- $this->suppress = $suppress;
- if ( $user ) {
- $this->user = $user;
- } else {
- global $wgUser;
- $this->user = $wgUser;
- }
- $this->status = $file->repo->newGood();
- }
- public function addCurrent() {
- $this->srcRels['.'] = $this->file->getRel();
- }
- /**
- * @param string $oldName
- */
- public function addOld( $oldName ) {
- $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
- $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
- }
- /**
- * Add the old versions of the image to the batch
- * @return string[] List of archive names from old versions
- */
- public function addOlds() {
- $archiveNames = [];
- $dbw = $this->file->repo->getMasterDB();
- $result = $dbw->select( 'oldimage',
- [ 'oi_archive_name' ],
- [ 'oi_name' => $this->file->getName() ],
- __METHOD__
- );
- foreach ( $result as $row ) {
- $this->addOld( $row->oi_archive_name );
- $archiveNames[] = $row->oi_archive_name;
- }
- return $archiveNames;
- }
- /**
- * @return array
- */
- protected function getOldRels() {
- if ( !isset( $this->srcRels['.'] ) ) {
- $oldRels =& $this->srcRels;
- $deleteCurrent = false;
- } else {
- $oldRels = $this->srcRels;
- unset( $oldRels['.'] );
- $deleteCurrent = true;
- }
- return [ $oldRels, $deleteCurrent ];
- }
- /**
- * @return array
- */
- protected function getHashes() {
- $hashes = [];
- list( $oldRels, $deleteCurrent ) = $this->getOldRels();
- if ( $deleteCurrent ) {
- $hashes['.'] = $this->file->getSha1();
- }
- if ( count( $oldRels ) ) {
- $dbw = $this->file->repo->getMasterDB();
- $res = $dbw->select(
- 'oldimage',
- [ 'oi_archive_name', 'oi_sha1' ],
- [ 'oi_archive_name' => array_keys( $oldRels ),
- 'oi_name' => $this->file->getName() ], // performance
- __METHOD__
- );
- foreach ( $res as $row ) {
- if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
- // Get the hash from the file
- $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
- $props = $this->file->repo->getFileProps( $oldUrl );
- if ( $props['fileExists'] ) {
- // Upgrade the oldimage row
- $dbw->update( 'oldimage',
- [ 'oi_sha1' => $props['sha1'] ],
- [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
- __METHOD__ );
- $hashes[$row->oi_archive_name] = $props['sha1'];
- } else {
- $hashes[$row->oi_archive_name] = false;
- }
- } else {
- $hashes[$row->oi_archive_name] = $row->oi_sha1;
- }
- }
- }
- $missing = array_diff_key( $this->srcRels, $hashes );
- foreach ( $missing as $name => $rel ) {
- $this->status->error( 'filedelete-old-unregistered', $name );
- }
- foreach ( $hashes as $name => $hash ) {
- if ( !$hash ) {
- $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
- unset( $hashes[$name] );
- }
- }
- return $hashes;
- }
- protected function doDBInserts() {
- global $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage;
- $now = time();
- $dbw = $this->file->repo->getMasterDB();
- $commentStore = MediaWikiServices::getInstance()->getCommentStore();
- $actorMigration = ActorMigration::newMigration();
- $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
- $encUserId = $dbw->addQuotes( $this->user->getId() );
- $encGroup = $dbw->addQuotes( 'deleted' );
- $ext = $this->file->getExtension();
- $dotExt = $ext === '' ? '' : ".$ext";
- $encExt = $dbw->addQuotes( $dotExt );
- list( $oldRels, $deleteCurrent ) = $this->getOldRels();
- // Bitfields to further suppress the content
- if ( $this->suppress ) {
- $bitfield = Revision::SUPPRESSED_ALL;
- } else {
- $bitfield = 'oi_deleted';
- }
- if ( $deleteCurrent ) {
- $tables = [ 'image' ];
- $fields = [
- 'fa_storage_group' => $encGroup,
- 'fa_storage_key' => $dbw->conditional(
- [ 'img_sha1' => '' ],
- $dbw->addQuotes( '' ),
- $dbw->buildConcat( [ "img_sha1", $encExt ] )
- ),
- 'fa_deleted_user' => $encUserId,
- 'fa_deleted_timestamp' => $encTimestamp,
- 'fa_deleted' => $this->suppress ? $bitfield : 0,
- 'fa_name' => 'img_name',
- 'fa_archive_name' => 'NULL',
- 'fa_size' => 'img_size',
- 'fa_width' => 'img_width',
- 'fa_height' => 'img_height',
- 'fa_metadata' => 'img_metadata',
- 'fa_bits' => 'img_bits',
- 'fa_media_type' => 'img_media_type',
- 'fa_major_mime' => 'img_major_mime',
- 'fa_minor_mime' => 'img_minor_mime',
- 'fa_timestamp' => 'img_timestamp',
- 'fa_sha1' => 'img_sha1'
- ];
- $joins = [];
- $fields += array_map(
- [ $dbw, 'addQuotes' ],
- $commentStore->insert( $dbw, 'fa_deleted_reason', $this->reason )
- );
- if ( $wgCommentTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
- $fields['fa_description'] = 'img_description';
- }
- if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
- $tables[] = 'image_comment_temp';
- $fields['fa_description_id'] = 'imgcomment_description_id';
- $joins['image_comment_temp'] = [
- $wgCommentTableSchemaMigrationStage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN',
- [ 'imgcomment_name = img_name' ]
- ];
- }
- if ( $wgCommentTableSchemaMigrationStage !== MIGRATION_OLD &&
- $wgCommentTableSchemaMigrationStage !== MIGRATION_NEW
- ) {
- // Upgrade any rows that are still old-style. Otherwise an upgrade
- // might be missed if a deletion happens while the migration script
- // is running.
- $res = $dbw->select(
- [ 'image', 'image_comment_temp' ],
- [ 'img_name', 'img_description' ],
- [ 'img_name' => $this->file->getName(), 'imgcomment_name' => null ],
- __METHOD__,
- [],
- [ 'image_comment_temp' => [ 'LEFT JOIN', [ 'imgcomment_name = img_name' ] ] ]
- );
- foreach ( $res as $row ) {
- list( , $callback ) = $commentStore->insertWithTempTable(
- $dbw, 'img_description', $row->img_description
- );
- $callback( $row->img_name );
- }
- }
- if ( $wgActorTableSchemaMigrationStage <= MIGRATION_WRITE_BOTH ) {
- $fields['fa_user'] = 'img_user';
- $fields['fa_user_text'] = 'img_user_text';
- }
- if ( $wgActorTableSchemaMigrationStage >= MIGRATION_WRITE_BOTH ) {
- $fields['fa_actor'] = 'img_actor';
- }
- if ( $wgActorTableSchemaMigrationStage !== MIGRATION_OLD &&
- $wgActorTableSchemaMigrationStage !== MIGRATION_NEW
- ) {
- // Upgrade any rows that are still old-style. Otherwise an upgrade
- // might be missed if a deletion happens while the migration script
- // is running.
- $res = $dbw->select(
- [ 'image' ],
- [ 'img_name', 'img_user', 'img_user_text' ],
- [ 'img_name' => $this->file->getName(), 'img_actor' => 0 ],
- __METHOD__
- );
- foreach ( $res as $row ) {
- $actorId = User::newFromAnyId( $row->img_user, $row->img_user_text, null )->getActorId( $dbw );
- $dbw->update(
- 'image',
- [ 'img_actor' => $actorId ],
- [ 'img_name' => $row->img_name, 'img_actor' => 0 ],
- __METHOD__
- );
- }
- }
- $dbw->insertSelect( 'filearchive', $tables, $fields,
- [ 'img_name' => $this->file->getName() ], __METHOD__, [], [], $joins );
- }
- if ( count( $oldRels ) ) {
- $fileQuery = OldLocalFile::getQueryInfo();
- $res = $dbw->select(
- $fileQuery['tables'],
- $fileQuery['fields'],
- [
- 'oi_name' => $this->file->getName(),
- 'oi_archive_name' => array_keys( $oldRels )
- ],
- __METHOD__,
- [ 'FOR UPDATE' ],
- $fileQuery['joins']
- );
- $rowsInsert = [];
- if ( $res->numRows() ) {
- $reason = $commentStore->createComment( $dbw, $this->reason );
- foreach ( $res as $row ) {
- $comment = $commentStore->getComment( 'oi_description', $row );
- $user = User::newFromAnyId( $row->oi_user, $row->oi_user_text, $row->oi_actor );
- $rowsInsert[] = [
- // Deletion-specific fields
- 'fa_storage_group' => 'deleted',
- 'fa_storage_key' => ( $row->oi_sha1 === '' )
- ? ''
- : "{$row->oi_sha1}{$dotExt}",
- 'fa_deleted_user' => $this->user->getId(),
- 'fa_deleted_timestamp' => $dbw->timestamp( $now ),
- // Counterpart fields
- 'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
- 'fa_name' => $row->oi_name,
- 'fa_archive_name' => $row->oi_archive_name,
- 'fa_size' => $row->oi_size,
- 'fa_width' => $row->oi_width,
- 'fa_height' => $row->oi_height,
- 'fa_metadata' => $row->oi_metadata,
- 'fa_bits' => $row->oi_bits,
- 'fa_media_type' => $row->oi_media_type,
- 'fa_major_mime' => $row->oi_major_mime,
- 'fa_minor_mime' => $row->oi_minor_mime,
- 'fa_timestamp' => $row->oi_timestamp,
- 'fa_sha1' => $row->oi_sha1
- ] + $commentStore->insert( $dbw, 'fa_deleted_reason', $reason )
- + $commentStore->insert( $dbw, 'fa_description', $comment )
- + $actorMigration->getInsertValues( $dbw, 'fa_user', $user );
- }
- }
- $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
- }
- }
- function doDBDeletes() {
- global $wgCommentTableSchemaMigrationStage;
- $dbw = $this->file->repo->getMasterDB();
- list( $oldRels, $deleteCurrent ) = $this->getOldRels();
- if ( count( $oldRels ) ) {
- $dbw->delete( 'oldimage',
- [
- 'oi_name' => $this->file->getName(),
- 'oi_archive_name' => array_keys( $oldRels )
- ], __METHOD__ );
- }
- if ( $deleteCurrent ) {
- $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
- if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
- $dbw->delete(
- 'image_comment_temp', [ 'imgcomment_name' => $this->file->getName() ], __METHOD__
- );
- }
- }
- }
- /**
- * Run the transaction
- * @return Status
- */
- public function execute() {
- $repo = $this->file->getRepo();
- $this->file->lock();
- // Prepare deletion batch
- $hashes = $this->getHashes();
- $this->deletionBatch = [];
- $ext = $this->file->getExtension();
- $dotExt = $ext === '' ? '' : ".$ext";
- foreach ( $this->srcRels as $name => $srcRel ) {
- // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
- if ( isset( $hashes[$name] ) ) {
- $hash = $hashes[$name];
- $key = $hash . $dotExt;
- $dstRel = $repo->getDeletedHashPath( $key ) . $key;
- $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
- }
- }
- if ( !$repo->hasSha1Storage() ) {
- // Removes non-existent file from the batch, so we don't get errors.
- // This also handles files in the 'deleted' zone deleted via revision deletion.
- $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
- if ( !$checkStatus->isGood() ) {
- $this->status->merge( $checkStatus );
- return $this->status;
- }
- $this->deletionBatch = $checkStatus->value;
- // Execute the file deletion batch
- $status = $this->file->repo->deleteBatch( $this->deletionBatch );
- if ( !$status->isGood() ) {
- $this->status->merge( $status );
- }
- }
- if ( !$this->status->isOK() ) {
- // Critical file deletion error; abort
- $this->file->unlock();
- return $this->status;
- }
- // Copy the image/oldimage rows to filearchive
- $this->doDBInserts();
- // Delete image/oldimage rows
- $this->doDBDeletes();
- // Commit and return
- $this->file->unlock();
- return $this->status;
- }
- /**
- * Removes non-existent files from a deletion batch.
- * @param array $batch
- * @return Status
- */
- protected function removeNonexistentFiles( $batch ) {
- $files = $newBatch = [];
- foreach ( $batch as $batchItem ) {
- list( $src, ) = $batchItem;
- $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
- }
- $result = $this->file->repo->fileExistsBatch( $files );
- if ( in_array( null, $result, true ) ) {
- return Status::newFatal( 'backend-fail-internal',
- $this->file->repo->getBackend()->getName() );
- }
- foreach ( $batch as $batchItem ) {
- if ( $result[$batchItem[0]] ) {
- $newBatch[] = $batchItem;
- }
- }
- return Status::newGood( $newBatch );
- }
- }
- # ------------------------------------------------------------------------------
- /**
- * Helper class for file undeletion
- * @ingroup FileAbstraction
- */
- class LocalFileRestoreBatch {
- /** @var LocalFile */
- private $file;
- /** @var string[] List of file IDs to restore */
- private $cleanupBatch;
- /** @var string[] List of file IDs to restore */
- private $ids;
- /** @var bool Add all revisions of the file */
- private $all;
- /** @var bool Whether to remove all settings for suppressed fields */
- private $unsuppress = false;
- /**
- * @param File $file
- * @param bool $unsuppress
- */
- function __construct( File $file, $unsuppress = false ) {
- $this->file = $file;
- $this->cleanupBatch = [];
- $this->ids = [];
- $this->unsuppress = $unsuppress;
- }
- /**
- * Add a file by ID
- * @param int $fa_id
- */
- public function addId( $fa_id ) {
- $this->ids[] = $fa_id;
- }
- /**
- * Add a whole lot of files by ID
- * @param int[] $ids
- */
- public function addIds( $ids ) {
- $this->ids = array_merge( $this->ids, $ids );
- }
- /**
- * Add all revisions of the file
- */
- public function addAll() {
- $this->all = true;
- }
- /**
- * Run the transaction, except the cleanup batch.
- * The cleanup batch should be run in a separate transaction, because it locks different
- * rows and there's no need to keep the image row locked while it's acquiring those locks
- * The caller may have its own transaction open.
- * So we save the batch and let the caller call cleanup()
- * @return Status
- */
- public function execute() {
- /** @var Language */
- global $wgLang;
- $repo = $this->file->getRepo();
- if ( !$this->all && !$this->ids ) {
- // Do nothing
- return $repo->newGood();
- }
- $lockOwnsTrx = $this->file->lock();
- $dbw = $this->file->repo->getMasterDB();
- $commentStore = MediaWikiServices::getInstance()->getCommentStore();
- $actorMigration = ActorMigration::newMigration();
- $status = $this->file->repo->newGood();
- $exists = (bool)$dbw->selectField( 'image', '1',
- [ 'img_name' => $this->file->getName() ],
- __METHOD__,
- // The lock() should already prevents changes, but this still may need
- // to bypass any transaction snapshot. However, if lock() started the
- // trx (which it probably did) then snapshot is post-lock and up-to-date.
- $lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
- );
- // Fetch all or selected archived revisions for the file,
- // sorted from the most recent to the oldest.
- $conditions = [ 'fa_name' => $this->file->getName() ];
- if ( !$this->all ) {
- $conditions['fa_id'] = $this->ids;
- }
- $arFileQuery = ArchivedFile::getQueryInfo();
- $result = $dbw->select(
- $arFileQuery['tables'],
- $arFileQuery['fields'],
- $conditions,
- __METHOD__,
- [ 'ORDER BY' => 'fa_timestamp DESC' ],
- $arFileQuery['joins']
- );
- $idsPresent = [];
- $storeBatch = [];
- $insertBatch = [];
- $insertCurrent = false;
- $deleteIds = [];
- $first = true;
- $archiveNames = [];
- foreach ( $result as $row ) {
- $idsPresent[] = $row->fa_id;
- if ( $row->fa_name != $this->file->getName() ) {
- $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
- $status->failCount++;
- continue;
- }
- if ( $row->fa_storage_key == '' ) {
- // Revision was missing pre-deletion
- $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
- $status->failCount++;
- continue;
- }
- $deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
- $row->fa_storage_key;
- $deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
- if ( isset( $row->fa_sha1 ) ) {
- $sha1 = $row->fa_sha1;
- } else {
- // old row, populate from key
- $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
- }
- # Fix leading zero
- if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
- $sha1 = substr( $sha1, 1 );
- }
- if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
- || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
- || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
- || is_null( $row->fa_metadata )
- ) {
- // Refresh our metadata
- // Required for a new current revision; nice for older ones too. :)
- $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
- } else {
- $props = [
- 'minor_mime' => $row->fa_minor_mime,
- 'major_mime' => $row->fa_major_mime,
- 'media_type' => $row->fa_media_type,
- 'metadata' => $row->fa_metadata
- ];
- }
- $comment = $commentStore->getComment( 'fa_description', $row );
- $user = User::newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
- if ( $first && !$exists ) {
- // This revision will be published as the new current version
- $destRel = $this->file->getRel();
- list( $commentFields, $commentCallback ) =
- $commentStore->insertWithTempTable( $dbw, 'img_description', $comment );
- $actorFields = $actorMigration->getInsertValues( $dbw, 'img_user', $user );
- $insertCurrent = [
- 'img_name' => $row->fa_name,
- 'img_size' => $row->fa_size,
- 'img_width' => $row->fa_width,
- 'img_height' => $row->fa_height,
- 'img_metadata' => $props['metadata'],
- 'img_bits' => $row->fa_bits,
- 'img_media_type' => $props['media_type'],
- 'img_major_mime' => $props['major_mime'],
- 'img_minor_mime' => $props['minor_mime'],
- 'img_timestamp' => $row->fa_timestamp,
- 'img_sha1' => $sha1
- ] + $commentFields + $actorFields;
- // The live (current) version cannot be hidden!
- if ( !$this->unsuppress && $row->fa_deleted ) {
- $status->fatal( 'undeleterevdel' );
- $this->file->unlock();
- return $status;
- }
- } else {
- $archiveName = $row->fa_archive_name;
- if ( $archiveName == '' ) {
- // This was originally a current version; we
- // have to devise a new archive name for it.
- // Format is <timestamp of archiving>!<name>
- $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
- do {
- $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
- $timestamp++;
- } while ( isset( $archiveNames[$archiveName] ) );
- }
- $archiveNames[$archiveName] = true;
- $destRel = $this->file->getArchiveRel( $archiveName );
- $insertBatch[] = [
- 'oi_name' => $row->fa_name,
- 'oi_archive_name' => $archiveName,
- 'oi_size' => $row->fa_size,
- 'oi_width' => $row->fa_width,
- 'oi_height' => $row->fa_height,
- 'oi_bits' => $row->fa_bits,
- 'oi_timestamp' => $row->fa_timestamp,
- 'oi_metadata' => $props['metadata'],
- 'oi_media_type' => $props['media_type'],
- 'oi_major_mime' => $props['major_mime'],
- 'oi_minor_mime' => $props['minor_mime'],
- 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
- 'oi_sha1' => $sha1
- ] + $commentStore->insert( $dbw, 'oi_description', $comment )
- + $actorMigration->getInsertValues( $dbw, 'oi_user', $user );
- }
- $deleteIds[] = $row->fa_id;
- if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
- // private files can stay where they are
- $status->successCount++;
- } else {
- $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
- $this->cleanupBatch[] = $row->fa_storage_key;
- }
- $first = false;
- }
- unset( $result );
- // Add a warning to the status object for missing IDs
- $missingIds = array_diff( $this->ids, $idsPresent );
- foreach ( $missingIds as $id ) {
- $status->error( 'undelete-missing-filearchive', $id );
- }
- if ( !$repo->hasSha1Storage() ) {
- // Remove missing files from batch, so we don't get errors when undeleting them
- $checkStatus = $this->removeNonexistentFiles( $storeBatch );
- if ( !$checkStatus->isGood() ) {
- $status->merge( $checkStatus );
- return $status;
- }
- $storeBatch = $checkStatus->value;
- // Run the store batch
- // Use the OVERWRITE_SAME flag to smooth over a common error
- $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
- $status->merge( $storeStatus );
- if ( !$status->isGood() ) {
- // Even if some files could be copied, fail entirely as that is the
- // easiest thing to do without data loss
- $this->cleanupFailedBatch( $storeStatus, $storeBatch );
- $status->setOK( false );
- $this->file->unlock();
- return $status;
- }
- }
- // Run the DB updates
- // Because we have locked the image row, key conflicts should be rare.
- // If they do occur, we can roll back the transaction at this time with
- // no data loss, but leaving unregistered files scattered throughout the
- // public zone.
- // This is not ideal, which is why it's important to lock the image row.
- if ( $insertCurrent ) {
- $dbw->insert( 'image', $insertCurrent, __METHOD__ );
- $commentCallback( $insertCurrent['img_name'] );
- }
- if ( $insertBatch ) {
- $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
- }
- if ( $deleteIds ) {
- $dbw->delete( 'filearchive',
- [ 'fa_id' => $deleteIds ],
- __METHOD__ );
- }
- // If store batch is empty (all files are missing), deletion is to be considered successful
- if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
- if ( !$exists ) {
- wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
- DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
- $this->file->purgeEverything();
- } else {
- wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
- $this->file->purgeDescription();
- }
- }
- $this->file->unlock();
- return $status;
- }
- /**
- * Removes non-existent files from a store batch.
- * @param array $triplets
- * @return Status
- */
- protected function removeNonexistentFiles( $triplets ) {
- $files = $filteredTriplets = [];
- foreach ( $triplets as $file ) {
- $files[$file[0]] = $file[0];
- }
- $result = $this->file->repo->fileExistsBatch( $files );
- if ( in_array( null, $result, true ) ) {
- return Status::newFatal( 'backend-fail-internal',
- $this->file->repo->getBackend()->getName() );
- }
- foreach ( $triplets as $file ) {
- if ( $result[$file[0]] ) {
- $filteredTriplets[] = $file;
- }
- }
- return Status::newGood( $filteredTriplets );
- }
- /**
- * Removes non-existent files from a cleanup batch.
- * @param string[] $batch
- * @return string[]
- */
- protected function removeNonexistentFromCleanup( $batch ) {
- $files = $newBatch = [];
- $repo = $this->file->repo;
- foreach ( $batch as $file ) {
- $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
- rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
- }
- $result = $repo->fileExistsBatch( $files );
- foreach ( $batch as $file ) {
- if ( $result[$file] ) {
- $newBatch[] = $file;
- }
- }
- return $newBatch;
- }
- /**
- * Delete unused files in the deleted zone.
- * This should be called from outside the transaction in which execute() was called.
- * @return Status
- */
- public function cleanup() {
- if ( !$this->cleanupBatch ) {
- return $this->file->repo->newGood();
- }
- $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
- $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
- return $status;
- }
- /**
- * Cleanup a failed batch. The batch was only partially successful, so
- * rollback by removing all items that were successfully copied.
- *
- * @param Status $storeStatus
- * @param array[] $storeBatch
- */
- protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
- $cleanupBatch = [];
- foreach ( $storeStatus->success as $i => $success ) {
- // Check if this item of the batch was successfully copied
- if ( $success ) {
- // Item was successfully copied and needs to be removed again
- // Extract ($dstZone, $dstRel) from the batch
- $cleanupBatch[] = [ $storeBatch[$i][1], $storeBatch[$i][2] ];
- }
- }
- $this->file->repo->cleanupBatch( $cleanupBatch );
- }
- }
- # ------------------------------------------------------------------------------
- /**
- * Helper class for file movement
- * @ingroup FileAbstraction
- */
- class LocalFileMoveBatch {
- /** @var LocalFile */
- protected $file;
- /** @var Title */
- protected $target;
- protected $cur;
- protected $olds;
- protected $oldCount;
- protected $archive;
- /** @var IDatabase */
- protected $db;
- /**
- * @param File $file
- * @param Title $target
- */
- function __construct( File $file, Title $target ) {
- $this->file = $file;
- $this->target = $target;
- $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
- $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
- $this->oldName = $this->file->getName();
- $this->newName = $this->file->repo->getNameFromTitle( $this->target );
- $this->oldRel = $this->oldHash . $this->oldName;
- $this->newRel = $this->newHash . $this->newName;
- $this->db = $file->getRepo()->getMasterDB();
- }
- /**
- * Add the current image to the batch
- */
- public function addCurrent() {
- $this->cur = [ $this->oldRel, $this->newRel ];
- }
- /**
- * Add the old versions of the image to the batch
- * @return string[] List of archive names from old versions
- */
- public function addOlds() {
- $archiveBase = 'archive';
- $this->olds = [];
- $this->oldCount = 0;
- $archiveNames = [];
- $result = $this->db->select( 'oldimage',
- [ 'oi_archive_name', 'oi_deleted' ],
- [ 'oi_name' => $this->oldName ],
- __METHOD__,
- [ 'LOCK IN SHARE MODE' ] // ignore snapshot
- );
- foreach ( $result as $row ) {
- $archiveNames[] = $row->oi_archive_name;
- $oldName = $row->oi_archive_name;
- $bits = explode( '!', $oldName, 2 );
- if ( count( $bits ) != 2 ) {
- wfDebug( "Old file name missing !: '$oldName' \n" );
- continue;
- }
- list( $timestamp, $filename ) = $bits;
- if ( $this->oldName != $filename ) {
- wfDebug( "Old file name doesn't match: '$oldName' \n" );
- continue;
- }
- $this->oldCount++;
- // Do we want to add those to oldCount?
- if ( $row->oi_deleted & File::DELETED_FILE ) {
- continue;
- }
- $this->olds[] = [
- "{$archiveBase}/{$this->oldHash}{$oldName}",
- "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
- ];
- }
- return $archiveNames;
- }
- /**
- * Perform the move.
- * @return Status
- */
- public function execute() {
- $repo = $this->file->repo;
- $status = $repo->newGood();
- $destFile = wfLocalFile( $this->target );
- $this->file->lock(); // begin
- $destFile->lock(); // quickly fail if destination is not available
- $triplets = $this->getMoveTriplets();
- $checkStatus = $this->removeNonexistentFiles( $triplets );
- if ( !$checkStatus->isGood() ) {
- $destFile->unlock();
- $this->file->unlock();
- $status->merge( $checkStatus ); // couldn't talk to file backend
- return $status;
- }
- $triplets = $checkStatus->value;
- // Verify the file versions metadata in the DB.
- $statusDb = $this->verifyDBUpdates();
- if ( !$statusDb->isGood() ) {
- $destFile->unlock();
- $this->file->unlock();
- $statusDb->setOK( false );
- return $statusDb;
- }
- if ( !$repo->hasSha1Storage() ) {
- // Copy the files into their new location.
- // If a prior process fataled copying or cleaning up files we tolerate any
- // of the existing files if they are identical to the ones being stored.
- $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
- wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
- "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
- if ( !$statusMove->isGood() ) {
- // Delete any files copied over (while the destination is still locked)
- $this->cleanupTarget( $triplets );
- $destFile->unlock();
- $this->file->unlock();
- wfDebugLog( 'imagemove', "Error in moving files: "
- . $statusMove->getWikiText( false, false, 'en' ) );
- $statusMove->setOK( false );
- return $statusMove;
- }
- $status->merge( $statusMove );
- }
- // Rename the file versions metadata in the DB.
- $this->doDBUpdates();
- wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
- "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
- $destFile->unlock();
- $this->file->unlock(); // done
- // Everything went ok, remove the source files
- $this->cleanupSource( $triplets );
- $status->merge( $statusDb );
- return $status;
- }
- /**
- * Verify the database updates and return a new Status indicating how
- * many rows would be updated.
- *
- * @return Status
- */
- protected function verifyDBUpdates() {
- $repo = $this->file->repo;
- $status = $repo->newGood();
- $dbw = $this->db;
- $hasCurrent = $dbw->lockForUpdate(
- 'image',
- [ 'img_name' => $this->oldName ],
- __METHOD__
- );
- $oldRowCount = $dbw->lockForUpdate(
- 'oldimage',
- [ 'oi_name' => $this->oldName ],
- __METHOD__
- );
- if ( $hasCurrent ) {
- $status->successCount++;
- } else {
- $status->failCount++;
- }
- $status->successCount += $oldRowCount;
- // T36934: oldCount is based on files that actually exist.
- // There may be more DB rows than such files, in which case $affected
- // can be greater than $total. We use max() to avoid negatives here.
- $status->failCount += max( 0, $this->oldCount - $oldRowCount );
- if ( $status->failCount ) {
- $status->error( 'imageinvalidfilename' );
- }
- return $status;
- }
- /**
- * Do the database updates and return a new Status indicating how
- * many rows where updated.
- */
- protected function doDBUpdates() {
- global $wgCommentTableSchemaMigrationStage;
- $dbw = $this->db;
- // Update current image
- $dbw->update(
- 'image',
- [ 'img_name' => $this->newName ],
- [ 'img_name' => $this->oldName ],
- __METHOD__
- );
- if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
- $dbw->update(
- 'image_comment_temp',
- [ 'imgcomment_name' => $this->newName ],
- [ 'imgcomment_name' => $this->oldName ],
- __METHOD__
- );
- }
- // Update old images
- $dbw->update(
- 'oldimage',
- [
- 'oi_name' => $this->newName,
- 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
- $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
- ],
- [ 'oi_name' => $this->oldName ],
- __METHOD__
- );
- }
- /**
- * Generate triplets for FileRepo::storeBatch().
- * @return array[]
- */
- protected function getMoveTriplets() {
- $moves = array_merge( [ $this->cur ], $this->olds );
- $triplets = []; // The format is: (srcUrl, destZone, destUrl)
- foreach ( $moves as $move ) {
- // $move: (oldRelativePath, newRelativePath)
- $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
- $triplets[] = [ $srcUrl, 'public', $move[1] ];
- wfDebugLog(
- 'imagemove',
- "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
- );
- }
- return $triplets;
- }
- /**
- * Removes non-existent files from move batch.
- * @param array $triplets
- * @return Status
- */
- protected function removeNonexistentFiles( $triplets ) {
- $files = [];
- foreach ( $triplets as $file ) {
- $files[$file[0]] = $file[0];
- }
- $result = $this->file->repo->fileExistsBatch( $files );
- if ( in_array( null, $result, true ) ) {
- return Status::newFatal( 'backend-fail-internal',
- $this->file->repo->getBackend()->getName() );
- }
- $filteredTriplets = [];
- foreach ( $triplets as $file ) {
- if ( $result[$file[0]] ) {
- $filteredTriplets[] = $file;
- } else {
- wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
- }
- }
- return Status::newGood( $filteredTriplets );
- }
- /**
- * Cleanup a partially moved array of triplets by deleting the target
- * files. Called if something went wrong half way.
- * @param array[] $triplets
- */
- protected function cleanupTarget( $triplets ) {
- // Create dest pairs from the triplets
- $pairs = [];
- foreach ( $triplets as $triplet ) {
- // $triplet: (old source virtual URL, dst zone, dest rel)
- $pairs[] = [ $triplet[1], $triplet[2] ];
- }
- $this->file->repo->cleanupBatch( $pairs );
- }
- /**
- * Cleanup a fully moved array of triplets by deleting the source files.
- * Called at the end of the move process if everything else went ok.
- * @param array[] $triplets
- */
- protected function cleanupSource( $triplets ) {
- // Create source file names from the triplets
- $files = [];
- foreach ( $triplets as $triplet ) {
- $files[] = $triplet[0];
- }
- $this->file->repo->cleanupBatch( $files );
- }
- }
- class LocalFileLockError extends ErrorPageError {
- public function __construct( Status $status ) {
- parent::__construct(
- 'actionfailed',
- $status->getMessage()
- );
- }
- public function report() {
- global $wgOut;
- $wgOut->setStatusCode( 429 );
- parent::report();
- }
- }
|