/* XOWA: the XOWA Offline Wiki Application Copyright (C) 2012 gnosygnu@gmail.com This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package gplx.xowa.mediawiki.includes.filerepo; import gplx.*; import gplx.xowa.*; import gplx.xowa.mediawiki.*; import gplx.xowa.mediawiki.includes.*; import gplx.xowa.mediawiki.includes.filerepo.file.*; import gplx.xowa.mediawiki.includes.utls.*; /* TODO.XO: * getZoneUrl */ public class XomwFileRepo { // static final DELETE_SOURCE = 1; // static final OVERWRITE = 2; // static final OVERWRITE_SAME = 4; // static final SKIP_LOCKING = 8; // // static final NAME_AND_TIME_ONLY = 1; // // /** @var boolean Whether to fetch commons image description pages and display // * them on the local wiki */ // public fetchDescription; // // /** @var int */ // public descriptionCacheExpiry; // // /** @var boolean */ // protected hasSha1Storage = false; // // /** @var boolean */ // protected supportsSha1URLs = false; // // /** @var FileBackend */ // protected backend; // // /** @var array Map of zones to config */ // protected zones = []; // // /** @var String URL of thumb.php */ // protected thumbScriptUrl; // // /** @var boolean Whether to skip media file transformation on parse and rely // * on a 404 handler instead. */ // protected transformVia404; // // /** @var String URL of image description pages, e.g. // * https://en.wikipedia.org/wiki/File: // */ // protected descBaseUrl; // // /** @var String URL of the MediaWiki installation, equivalent to // * wgScriptPath, e.g. https://en.wikipedia.org/w // */ // protected scriptDirUrl; // // /** @var String Script extension of the MediaWiki installation, equivalent // * to the old wgScriptExtension, e.g. .php5 defaults to .php */ // protected scriptExtension; // // /** @var String Equivalent to wgArticlePath, e.g. https://en.wikipedia.org/wiki/1 */ // protected articleUrl; // // /** @var boolean Equivalent to wgCapitalLinks (or wgCapitalLinkOverrides[NS_FILE], // * determines whether filenames implicitly start with a capital letter. // * The current implementation may give incorrect description page links // * when the local wgCapitalLinks and initialCapital are mismatched. // */ // protected initialCapital; // // /** @var String May be 'paranoid' to remove all parameters from error // * messages, 'none' to leave the paths in unchanged, or 'simple' to // * replace paths with placeholders. Default for LocalRepo is // * 'simple'. // */ // protected pathDisclosureProtection = 'simple'; /** @var String|false Public zone URL. */ public byte[] url; /** @var String The super thumbnail URL. Defaults to "/thumb". */ public byte[] thumbUrl; /** @var int The number of directory levels for hash-based division of files */ private int hashLevels = 2; // /** @var int The number of directory levels for hash-based division of deleted files */ // protected deletedHashLevels; // // /** @var int File names over this size will use the short form of thumbnail // * names. Short thumbnail names only have the width, parameters, and the // * extension. // */ // protected abbrvThreshold; // // /** @var String The URL of the repo's favicon, if any */ // protected favicon; // // /** @var boolean Whether all zones should be private (e.g. private wiki repo) */ // protected isPrivate; // // /** @var array callable Override these in the super class */ // protected fileFactory = [ 'UnregisteredLocalFile', 'newFromTitle' ]; // /** @var array callable|boolean Override these in the super class */ // protected oldFileFactory = false; // /** @var array callable|boolean Override these in the super class */ // protected fileFactoryKey = false; // /** @var array callable|boolean Override these in the super class */ // protected oldFileFactoryKey = false; public XomwFileRepo(byte[] url, byte[] thumbUrl) { this.url = url; this.thumbUrl = thumbUrl; } // /** // * @param array|null info // * @throws MWException // */ // public function __construct(array info = null) { // // Verify required settings presence // if ( // info === null // || !array_key_exists('name', info) // || !array_key_exists('backend', info) // ) { // throw new MWException(__CLASS__ . // " requires an array of options having both 'name' and 'backend' keys.\n"); // } // // // Required settings // this.name = info['name']; // if (info['backend'] instanceof FileBackend) { // this.backend = info['backend']; // useful for testing // } else { // this.backend = FileBackendGroup::singleton().get(info['backend']); // } // // // Optional settings that can have no value // optionalSettings = [ // 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', // 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry', // 'scriptExtension', 'favicon' // ]; // foreach (optionalSettings as var) { // if (isset(info[var])) { // this.var = info[var]; // } // } // // // Optional settings that have a default // this.initialCapital = isset(info['initialCapital']) // ? info['initialCapital'] // : XomwNamespace::isCapitalized(NS_FILE); // this.url = isset(info['url']) // ? info['url'] // : false; // a subclass may set the URL (e.g. ForeignAPIRepo) // if (isset(info['thumbUrl'])) { // this.thumbUrl = info['thumbUrl']; // } else { // this.thumbUrl = this.url ? "{this.url}/thumb" : false; // } // this.hashLevels = isset(info['hashLevels']) // ? info['hashLevels'] // : 2; // this.deletedHashLevels = isset(info['deletedHashLevels']) // ? info['deletedHashLevels'] // : this.hashLevels; // this.transformVia404 = !empty(info['transformVia404']); // this.abbrvThreshold = isset(info['abbrvThreshold']) // ? info['abbrvThreshold'] // : 255; // this.isPrivate = !empty(info['isPrivate']); // // Give defaults for the basic zones... // this.zones = isset(info['zones']) ? info['zones'] : []; // foreach ([ 'public', 'thumb', 'transcoded', 'temp', 'deleted' ] as zone) { // if (!isset(this.zones[zone]['container'])) { // this.zones[zone]['container'] = "{this.name}-{zone}"; // } // if (!isset(this.zones[zone]['directory'])) { // this.zones[zone]['directory'] = ''; // } // if (!isset(this.zones[zone]['urlsByExt'])) { // this.zones[zone]['urlsByExt'] = []; // } // } // // this.supportsSha1URLs = !empty(info['supportsSha1URLs']); // } // // /** // * Get the file backend instance. Use this function wisely. // * // * @return FileBackend // */ // public function getBackend() { // return this.backend; // } // // /** // * Get an explanatory message if this repo is read-only. // * This checks if an administrator disabled writes to the backend. // * // * @return String|boolean Returns false if the repo is not read-only // */ // public function getReadOnlyReason() { // return this.backend.getReadOnlyReason(); // } // // /** // * Check if a single zone or list of zones is defined for usage // * // * @param array doZones Only do a particular zones // * @throws MWException // * @return Status // */ // protected function initZones(doZones = []) { // status = this.newGood(); // foreach ((array)doZones as zone) { // root = this.getZonePath(zone); // if (root === null) { // throw new MWException("No 'zone' zone defined in the {this.name} repo."); // } // } // // return status; // } // // /** // * Determine if a String is an mwrepo:// URL // * // * @param String url // * @return boolean // */ // public static function isVirtualUrl(url) { // return substr(url, 0, 9) == 'mwrepo://'; // } // // /** // * Get a URL referring to this repository, with the private mwrepo protocol. // * The suffix, if supplied, is considered to be unencoded, and will be // * URL-encoded before being returned. // * // * @param String|boolean suffix // * @return String // */ // public function getVirtualUrl(suffix = false) { // path = 'mwrepo://' . this.name; // if (suffix !== false) { // path .= '/' . rawurlencode(suffix); // } // // return path; // } /** * Get the URL corresponding to one of the four basic zones * * @param String zone One of: public, deleted, temp, thumb * @param String|null ext Optional file extension * @return String|boolean */ public byte[] getZoneUrl(int zone, byte[] ext) { // XO.MW.UNSUPPORTED.CUSTOM:ignore customized zones by ext //if (in_array(zone, [ 'public', 'thumb', 'transcoded' ])) { // // standard public zones // if (ext !== null && isset(this.zones[zone]['urlsByExt'][ext])) { // // custom URL for extension/zone // return this.zones[zone]['urlsByExt'][ext]; // } elseif (isset(this.zones[zone]['url'])) { // // custom URL for zone // return this.zones[zone]['url']; // } //} switch (zone) { case Zone__public: return this.url; //case 'temp': //case 'deleted': // return false; // no public URL case Zone__thumb: return this.thumbUrl; //case 'transcoded': // return "{this.url}/transcoded"; //default: // return false; } return url; } // /** // * @return boolean Whether non-ASCII path characters are allowed // */ // public function backendSupportsUnicodePaths() { // return (boolean)(this.getBackend().getFeatures() & FileBackend::ATTR_UNICODE_PATHS); // } // // /** // * Get the backend storage path corresponding to a virtual URL. // * Use this function wisely. // * // * @param String url // * @throws MWException // * @return String // */ // public function resolveVirtualUrl(url) { // if (substr(url, 0, 9) != 'mwrepo://') { // throw new MWException(__METHOD__ . ': unknown protocol'); // } // bits = explode('/', substr(url, 9), 3); // if (count(bits) != 3) { // throw new MWException(__METHOD__ . ": invalid mwrepo URL: url"); // } // list(repo, zone, rel) = bits; // if (repo !== this.name) { // throw new MWException(__METHOD__ . ": fetching from a foreign repo is not supported"); // } // super = this.getZonePath(zone); // if (!super) { // throw new MWException(__METHOD__ . ": invalid zone: zone"); // } // // return super . '/' . rawurldecode(rel); // } // // /** // * The the storage container and super path of a zone // * // * @param String zone // * @return array (container, super path) or (null, null) // */ // protected function getZoneLocation(zone) { // if (!isset(this.zones[zone])) { // return [ null, null ]; // bogus // } // // return [ this.zones[zone]['container'], this.zones[zone]['directory'] ]; // } /** * Get the storage path corresponding to one of the zones * * @param String zone * @return String|null Returns null if the zone is not defined */ public byte[] getZonePath(int zone) { // list(container, super) = this.getZoneLocation(zone); // if (container === null || super === null) { // return null; // } // backendName = this.backend.getName(); // if (super != '') { // may not be set // super = "/{super}"; // } // // return "mwstore://backendName/{container}{super}"; return Bry_.Empty; } // /** // * Create a new File Object from the local repository // * // * @param Title|String title Title Object or String // * @param boolean|String time Time at which the image was uploaded. If this // * is specified, the returned Object will be an instance of the // * repository's old file class instead of a current file. Repositories // * not supporting version control should return false if this parameter // * is set. // * @return File|null A File, or null if passed an invalid Title // */ // public function newFile(title, time = false) { // title = File::normalizeTitle(title); // if (!title) { // return null; // } // if (time) { // if (this.oldFileFactory) { // return call_user_func(this.oldFileFactory, title, this, time); // } else { // return null; // } // } else { // return call_user_func(this.fileFactory, title, this); // } // } // // /** // * Find an instance of the named file created at the specified time // * Returns false if the file does not exist. Repositories not supporting // * version control should return false if the time is specified. // * // * @param Title|String title Title Object or String // * @param array options Associative array of options: // * time: requested time for a specific file version, or false for the // * current version. An image Object will be returned which was // * created at the specified time (which may be archived or current). // * ignoreRedirect: If true, do not follow file redirects // * private: If true, return restricted (deleted) files if the current // * user is allowed to view them. Otherwise, such files will not // * be found. If a User Object, use that user instead of the current. // * latest: If true, load from the latest available data into File objects // * @return File|boolean False on failure // */ // public function findFile(title, options = []) { // title = File::normalizeTitle(title); // if (!title) { // return false; // } // if (isset(options['bypassCache'])) { // options['latest'] = options['bypassCache']; // b/c // } // time = isset(options['time']) ? options['time'] : false; // flags = !empty(options['latest']) ? File::READ_LATEST : 0; // # First try the current version of the file to see if it precedes the timestamp // img = this.newFile(title); // if (!img) { // return false; // } // img.load(flags); // if (img.exists() && (!time || img.getTimestamp() == time)) { // return img; // } // # Now try an old version of the file // if (time !== false) { // img = this.newFile(title, time); // if (img) { // img.load(flags); // if (img.exists()) { // if (!img.isDeleted(File::DELETED_FILE)) { // return img; // always OK // } elseif (!empty(options['private']) && // img.userCan(File::DELETED_FILE, // options['private'] instanceof User ? options['private'] : null // ) // ) { // return img; // } // } // } // } // // # Now try redirects // if (!empty(options['ignoreRedirect'])) { // return false; // } // redir = this.checkRedirect(title); // if (redir && title.getNamespace() == NS_FILE) { // img = this.newFile(redir); // if (!img) { // return false; // } // img.load(flags); // if (img.exists()) { // img.redirectedFrom(title.getDBkey()); // // return img; // } // } // // return false; // } // // /** // * Find many files at once. // * // * @param array items An array of titles, or an array of findFile() options with // * the "title" option giving the title. Example: // * // * findItem = [ 'title' => title, 'private' => true ]; // * findBatch = [ findItem ]; // * repo.findFiles(findBatch); // * // * No title should appear in items twice, as the result use titles as keys // * @param int flags Supports: // * - FileRepo::NAME_AND_TIME_ONLY : return a (search title => (title,timestamp)) map. // * The search title uses the input titles; the other is the final post-redirect title. // * All titles are returned as String DB keys and the inner array is associative. // * @return array Map of (file name => File objects) for matches // */ // public function findFiles(array items, flags = 0) { // result = []; // foreach (items as item) { // if (is_array(item)) { // title = item['title']; // options = item; // unset(options['title']); // } else { // title = item; // options = []; // } // file = this.findFile(title, options); // if (file) { // searchName = File::normalizeTitle(title).getDBkey(); // must be valid // if (flags & self::NAME_AND_TIME_ONLY) { // result[searchName] = [ // 'title' => file.getTitle().getDBkey(), // 'timestamp' => file.getTimestamp() // ]; // } else { // result[searchName] = file; // } // } // } // // return result; // } // // /** // * Find an instance of the file with this key, created at the specified time // * Returns false if the file does not exist. Repositories not supporting // * version control should return false if the time is specified. // * // * @param String sha1 Base 36 SHA-1 hash // * @param array options Option array, same as findFile(). // * @return File|boolean False on failure // */ // public function findFileFromKey(sha1, options = []) { // time = isset(options['time']) ? options['time'] : false; // # First try to find a matching current version of a file... // if (!this.fileFactoryKey) { // return false; // find-by-sha1 not supported // } // img = call_user_func(this.fileFactoryKey, sha1, this, time); // if (img && img.exists()) { // return img; // } // # Now try to find a matching old version of a file... // if (time !== false && this.oldFileFactoryKey) { // find-by-sha1 supported? // img = call_user_func(this.oldFileFactoryKey, sha1, this, time); // if (img && img.exists()) { // if (!img.isDeleted(File::DELETED_FILE)) { // return img; // always OK // } elseif (!empty(options['private']) && // img.userCan(File::DELETED_FILE, // options['private'] instanceof User ? options['private'] : null // ) // ) { // return img; // } // } // } // // return false; // } // // /** // * Get an array or iterator of file objects for files that have a given // * SHA-1 content hash. // * // * STUB // * @param String hash SHA-1 hash // * @return File[] // */ // public function findBySha1(hash) { // return []; // } // // /** // * Get an array of arrays or iterators of file objects for files that // * have the given SHA-1 content hashes. // * // * @param array hashes An array of hashes // * @return array An Array of arrays or iterators of file objects and the hash as key // */ // public function findBySha1s(array hashes) { // result = []; // foreach (hashes as hash) { // files = this.findBySha1(hash); // if (count(files)) { // result[hash] = files; // } // } // // return result; // } // // /** // * Return an array of files where the name starts with prefix. // * // * STUB // * @param String prefix The prefix to search for // * @param int limit The maximum amount of files to return // * @return array // */ // public function findFilesByPrefix(prefix, limit) { // return []; // } // // /** // * Get the URL of thumb.php // * // * @return String // */ // public function getThumbScriptUrl() { // return this.thumbScriptUrl; // } // // /** // * Returns true if the repository can transform files via a 404 handler // * // * @return boolean // */ // public function canTransformVia404() { // return this.transformVia404; // } /** * Get the name of a file from its title Object * * @param Title title * @return String */ public byte[] getNameFromTitle(XomwTitle title) { // global wgContLang; // if (this.initialCapital != XomwNamespace::isCapitalized(NS_FILE)) { // name = title.getUserCaseDBKey(); // if (this.initialCapital) { // name = wgContLang.ucfirst(name); // } // } else { // name = title.getDBkey(); // } // // return name; return title.getDBkey(); } // // /** // * Get the public zone root storage directory of the repository // * // * @return String // */ // public function getRootDirectory() { // return this.getZonePath('public'); // } /** * Get a relative path including trailing slash, e.g. f/fa/ * If the repo is not hashed, returns an empty String * * @param String name Name of file * @return String */ public byte[] getHashPath(byte[] name) { return getHashPathForLevel(name, this.hashLevels); } // /** // * Get a relative path including trailing slash, e.g. f/fa/ // * If the repo is not hashed, returns an empty String // * // * @param String suffix Basename of file from FileRepo::storeTemp() // * @return String // */ // public function getTempHashPath(suffix) { // parts = explode('!', suffix, 2); // format is ! or just // name = isset(parts[1]) ? parts[1] : suffix; // hash path is not based on timestamp // return self::getHashPathForLevel(name, this.hashLevels); // } /** * @param String name * @param int levels * @return String */ public static byte[] getHashPathForLevel(byte[] name, int levels) { if (levels == 0) { return Bry_.Empty; } else { byte[] hash = gplx.xowa.files.Xof_file_wkr_.Md5(name); // XO.MW: assume 2 if (levels != 2) throw Err_.new_wo_type("levels must be 2", "levels", levels); byte[] path = new byte[5]; path[0] = path[2] = hash[0]; path[3] = hash[1]; path[1] = path[4] = Byte_ascii.Slash; return path; } } // /** // * Get the number of hash directory levels // * // * @return int // */ // public function getHashLevels() { // return this.hashLevels; // } // // /** // * Get the name of this repository, as specified by info['name]' to the constructor // * // * @return String // */ // public function getName() { // return this.name; // } // // /** // * Make an url to this repo // * // * @param String query Query String to append // * @param String entry Entry point; defaults to index // * @return String|boolean False on failure // */ // public function makeUrl(query = '', entry = 'index') { // if (isset(this.scriptDirUrl)) { // ext = isset(this.scriptExtension) ? this.scriptExtension : '.php'; // // return wfAppendQuery("{this.scriptDirUrl}/{entry}{ext}", query); // } // // return false; // } // // /** // * Get the URL of an image description page. May return false if it is // * unknown or not applicable. In general this should only be called by the // * File class, since it may return invalid results for certain kinds of // * repositories. Use File::getDescriptionUrl() in user code. // * // * In particular, it uses the article paths as specified to the repository // * constructor, whereas local repositories use the local Title functions. // * // * @param String name // * @return String|false // */ // public function getDescriptionUrl(name) { // encName = wfUrlencode(name); // if (!is_null(this.descBaseUrl)) { // # "http://example.com/wiki/File:" // return this.descBaseUrl . encName; // } // if (!is_null(this.articleUrl)) { // # "http://example.com/wiki/1" // # We use "Image:" as the canonical namespace for // # compatibility across all MediaWiki versions. // return str_replace('1', // "Image:encName", this.articleUrl); // } // if (!is_null(this.scriptDirUrl)) { // # "http://example.com/w" // # We use "Image:" as the canonical namespace for // # compatibility across all MediaWiki versions, // # and just sort of hope index.php is right. ;) // return this.makeUrl("title=Image:encName"); // } // // return false; // } // // /** // * Get the URL of the content-only fragment of the description page. For // * MediaWiki this means action=render. This should only be called by the // * repository's file class, since it may return invalid results. User code // * should use File::getDescriptionText(). // * // * @param String name Name of image to fetch // * @param String lang Language to fetch it in, if any. // * @return String|false // */ // public function getDescriptionRenderUrl(name, lang = null) { // query = 'action=render'; // if (!is_null(lang)) { // query .= '&uselang=' . lang; // } // if (isset(this.scriptDirUrl)) { // return this.makeUrl( // 'title=' . // wfUrlencode('Image:' . name) . // "&query"); // } else { // descUrl = this.getDescriptionUrl(name); // if (descUrl) { // return wfAppendQuery(descUrl, query); // } else { // return false; // } // } // } // // /** // * Get the URL of the stylesheet to apply to description pages // * // * @return String|boolean False on failure // */ // public function getDescriptionStylesheetUrl() { // if (isset(this.scriptDirUrl)) { // return this.makeUrl('title=MediaWiki:Filepage.css&' . // wfArrayToCgi(Skin::getDynamicStylesheetQuery())); // } // // return false; // } // // /** // * Store a file to a given destination. // * // * @param String srcPath Source file system path, storage path, or virtual URL // * @param String dstZone Destination zone // * @param String dstRel Destination relative path // * @param int flags Bitwise combination of the following flags: // * self::OVERWRITE Overwrite an existing destination file instead of failing // * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the // * same contents as the source // * self::SKIP_LOCKING Skip any file locking when doing the store // * @return Status // */ // public function store(srcPath, dstZone, dstRel, flags = 0) { // this.assertWritableRepo(); // fail out if read-only // // status = this.storeBatch([ [ srcPath, dstZone, dstRel ] ], flags); // if (status.successCount == 0) { // status.setOK(false); // } // // return status; // } // // /** // * Store a batch of files // * // * @param array triplets (src, dest zone, dest rel) triplets as per store() // * @param int flags Bitwise combination of the following flags: // * self::OVERWRITE Overwrite an existing destination file instead of failing // * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the // * same contents as the source // * self::SKIP_LOCKING Skip any file locking when doing the store // * @throws MWException // * @return Status // */ // public function storeBatch(array triplets, flags = 0) { // this.assertWritableRepo(); // fail out if read-only // // if (flags & self::DELETE_SOURCE) { // throw new InvalidArgumentException("DELETE_SOURCE not supported in " . __METHOD__); // } // // status = this.newGood(); // backend = this.backend; // convenience // // operations = []; // // Validate each triplet and get the store operation... // foreach (triplets as triplet) { // list(srcPath, dstZone, dstRel) = triplet; // wfDebug(__METHOD__ // . "(\src='srcPath', \dstZone='dstZone', \dstRel='dstRel')\n" // ); // // // Resolve destination path // root = this.getZonePath(dstZone); // if (!root) { // throw new MWException("Invalid zone: dstZone"); // } // if (!this.validateFilename(dstRel)) { // throw new MWException('Validation error in dstRel'); // } // dstPath = "root/dstRel"; // dstDir = dirname(dstPath); // // Create destination directories for this triplet // if (!this.initDirectory(dstDir).isOK()) { // return this.newFatal('directorycreateerror', dstDir); // } // // // Resolve source to a storage path if virtual // srcPath = this.resolveToStoragePath(srcPath); // // // Get the appropriate file operation // if (FileBackend::isStoragePath(srcPath)) { // opName = 'copy'; // } else { // opName = 'store'; // } // operations[] = [ // 'op' => opName, // 'src' => srcPath, // 'dst' => dstPath, // 'overwrite' => flags & self::OVERWRITE, // 'overwriteSame' => flags & self::OVERWRITE_SAME, // ]; // } // // // Execute the store operation for each triplet // opts = [ 'force' => true ]; // if (flags & self::SKIP_LOCKING) { // opts['nonLocking'] = true; // } // status.merge(backend.doOperations(operations, opts)); // // return status; // } // // /** // * Deletes a batch of files. // * Each file can be a (zone, rel) pair, virtual url, storage path. // * It will try to delete each file, but ignores any errors that may occur. // * // * @param array files List of files to delete // * @param int flags Bitwise combination of the following flags: // * self::SKIP_LOCKING Skip any file locking when doing the deletions // * @return Status // */ // public function cleanupBatch(array files, flags = 0) { // this.assertWritableRepo(); // fail out if read-only // // status = this.newGood(); // // operations = []; // foreach (files as path) { // if (is_array(path)) { // // This is a pair, extract it // list(zone, rel) = path; // path = this.getZonePath(zone) . "/rel"; // } else { // // Resolve source to a storage path if virtual // path = this.resolveToStoragePath(path); // } // operations[] = [ 'op' => 'delete', 'src' => path ]; // } // // Actually delete files from storage... // opts = [ 'force' => true ]; // if (flags & self::SKIP_LOCKING) { // opts['nonLocking'] = true; // } // status.merge(this.backend.doOperations(operations, opts)); // // return status; // } // // /** // * Import a file from the local file system into the repo. // * This does no locking nor journaling and overrides existing files. // * This function can be used to write to otherwise read-only foreign repos. // * This is intended for copying generated thumbnails into the repo. // * // * @param String|FSFile src Source file system path, storage path, or virtual URL // * @param String dst Virtual URL or storage path // * @param array|String|null options An array consisting of a key named headers // * listing extra headers. If a String, taken as content-disposition header. // * (Support for array of options new in 1.23) // * @return Status // */ // final public function quickImport(src, dst, options = null) { // return this.quickImportBatch([ [ src, dst, options ] ]); // } // // /** // * Purge a file from the repo. This does no locking nor journaling. // * This function can be used to write to otherwise read-only foreign repos. // * This is intended for purging thumbnails. // * // * @param String path Virtual URL or storage path // * @return Status // */ // final public function quickPurge(path) { // return this.quickPurgeBatch([ path ]); // } // // /** // * Deletes a directory if empty. // * This function can be used to write to otherwise read-only foreign repos. // * // * @param String dir Virtual URL (or storage path) of directory to clean // * @return Status // */ // public function quickCleanDir(dir) { // status = this.newGood(); // status.merge(this.backend.clean( // [ 'dir' => this.resolveToStoragePath(dir) ])); // // return status; // } // // /** // * Import a batch of files from the local file system into the repo. // * This does no locking nor journaling and overrides existing files. // * This function can be used to write to otherwise read-only foreign repos. // * This is intended for copying generated thumbnails into the repo. // * // * All path parameters may be a file system path, storage path, or virtual URL. // * When "headers" are given they are used as HTTP headers if supported. // * // * @param array triples List of (source path or FSFile, destination path, disposition) // * @return Status // */ // public function quickImportBatch(array triples) { // status = this.newGood(); // operations = []; // foreach (triples as triple) { // list(src, dst) = triple; // if (src instanceof FSFile) { // op = 'store'; // } else { // src = this.resolveToStoragePath(src); // op = FileBackend::isStoragePath(src) ? 'copy' : 'store'; // } // dst = this.resolveToStoragePath(dst); // // if (!isset(triple[2])) { // headers = []; // } elseif (is_string(triple[2])) { // // back-compat // headers = [ 'Content-Disposition' => triple[2] ]; // } elseif (is_array(triple[2]) && isset(triple[2]['headers'])) { // headers = triple[2]['headers']; // } else { // headers = []; // } // // operations[] = [ // 'op' => op, // 'src' => src, // 'dst' => dst, // 'headers' => headers // ]; // status.merge(this.initDirectory(dirname(dst))); // } // status.merge(this.backend.doQuickOperations(operations)); // // return status; // } // // /** // * Purge a batch of files from the repo. // * This function can be used to write to otherwise read-only foreign repos. // * This does no locking nor journaling and is intended for purging thumbnails. // * // * @param array paths List of virtual URLs or storage paths // * @return Status // */ // public function quickPurgeBatch(array paths) { // status = this.newGood(); // operations = []; // foreach (paths as path) { // operations[] = [ // 'op' => 'delete', // 'src' => this.resolveToStoragePath(path), // 'ignoreMissingSource' => true // ]; // } // status.merge(this.backend.doQuickOperations(operations)); // // return status; // } // // /** // * Pick a random name in the temp zone and store a file to it. // * Returns a Status Object with the file Virtual URL in the value, // * file can later be disposed using FileRepo::freeTemp(). // * // * @param String originalName The super name of the file as specified // * by the user. The file extension will be maintained. // * @param String srcPath The current location of the file. // * @return Status Object with the URL in the value. // */ // public function storeTemp(originalName, srcPath) { // this.assertWritableRepo(); // fail out if read-only // // date = MWTimestamp::getInstance().format('YmdHis'); // hashPath = this.getHashPath(originalName); // dstUrlRel = hashPath . date . '!' . rawurlencode(originalName); // virtualUrl = this.getVirtualUrl('temp') . '/' . dstUrlRel; // // result = this.quickImport(srcPath, virtualUrl); // result.value = virtualUrl; // // return result; // } // // /** // * Remove a temporary file or mark it for garbage collection // * // * @param String virtualUrl The virtual URL returned by FileRepo::storeTemp() // * @return boolean True on success, false on failure // */ // public function freeTemp(virtualUrl) { // this.assertWritableRepo(); // fail out if read-only // // temp = this.getVirtualUrl('temp'); // if (substr(virtualUrl, 0, strlen(temp)) != temp) { // wfDebug(__METHOD__ . ": Invalid temp virtual URL\n"); // // return false; // } // // return this.quickPurge(virtualUrl).isOK(); // } // // /** // * Concatenate a list of temporary files into a target file location. // * // * @param array srcPaths Ordered list of source virtual URLs/storage paths // * @param String dstPath Target file system path // * @param int flags Bitwise combination of the following flags: // * self::DELETE_SOURCE Delete the source files on success // * @return Status // */ // public function concatenate(array srcPaths, dstPath, flags = 0) { // this.assertWritableRepo(); // fail out if read-only // // status = this.newGood(); // // sources = []; // foreach (srcPaths as srcPath) { // // Resolve source to a storage path if virtual // source = this.resolveToStoragePath(srcPath); // sources[] = source; // chunk to merge // } // // // Concatenate the chunks into one FS file // params = [ 'srcs' => sources, 'dst' => dstPath ]; // status.merge(this.backend.concatenate(params)); // if (!status.isOK()) { // return status; // } // // // Delete the sources if required // if (flags & self::DELETE_SOURCE) { // status.merge(this.quickPurgeBatch(srcPaths)); // } // // // Make sure status is OK, despite any quickPurgeBatch() fatals // status.setResult(true); // // return status; // } // // /** // * Copy or move a file either from a storage path, virtual URL, // * or file system path, into this repository at the specified destination location. // * // * Returns a Status Object. On success, the value contains "new" or // * "archived", to indicate whether the file was new with that name. // * // * Options to options include: // * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests // * // * @param String|FSFile src The source file system path, storage path, or URL // * @param String dstRel The destination relative path // * @param String archiveRel The relative path where the existing file is to // * be archived, if there is one. Relative to the public zone root. // * @param int flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate // * that the source file should be deleted if possible // * @param array options Optional additional parameters // * @return Status // */ // public function publish( // src, dstRel, archiveRel, flags = 0, array options = [] // ) { // this.assertWritableRepo(); // fail out if read-only // // status = this.publishBatch( // [ [ src, dstRel, archiveRel, options ] ], flags); // if (status.successCount == 0) { // status.setOK(false); // } // if (isset(status.value[0])) { // status.value = status.value[0]; // } else { // status.value = false; // } // // return status; // } // // /** // * Publish a batch of files // * // * @param array ntuples (source, dest, archive) triplets or // * (source, dest, archive, options) 4-tuples as per publish(). // * @param int flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate // * that the source files should be deleted if possible // * @throws MWException // * @return Status // */ // public function publishBatch(array ntuples, flags = 0) { // this.assertWritableRepo(); // fail out if read-only // // backend = this.backend; // convenience // // Try creating directories // status = this.initZones('public'); // if (!status.isOK()) { // return status; // } // // status = this.newGood([]); // // operations = []; // sourceFSFilesToDelete = []; // cleanup for disk source files // // Validate each triplet and get the store operation... // foreach (ntuples as ntuple) { // list(src, dstRel, archiveRel) = ntuple; // srcPath = (src instanceof FSFile) ? src.getPath() : src; // // options = isset(ntuple[3]) ? ntuple[3] : []; // // Resolve source to a storage path if virtual // srcPath = this.resolveToStoragePath(srcPath); // if (!this.validateFilename(dstRel)) { // throw new MWException('Validation error in dstRel'); // } // if (!this.validateFilename(archiveRel)) { // throw new MWException('Validation error in archiveRel'); // } // // publicRoot = this.getZonePath('public'); // dstPath = "publicRoot/dstRel"; // archivePath = "publicRoot/archiveRel"; // // dstDir = dirname(dstPath); // archiveDir = dirname(archivePath); // // Abort immediately on directory creation errors since they're likely to be repetitive // if (!this.initDirectory(dstDir).isOK()) { // return this.newFatal('directorycreateerror', dstDir); // } // if (!this.initDirectory(archiveDir).isOK()) { // return this.newFatal('directorycreateerror', archiveDir); // } // // // Set any desired headers to be use in GET/HEAD responses // headers = isset(options['headers']) ? options['headers'] : []; // // // Archive destination file if it exists. // // This will check if the archive file also exists and fail if does. // // This is a sanity check to avoid data loss. On Windows and Linux, // // copy() will overwrite, so the existence check is vulnerable to // // race conditions unless a functioning LockManager is used. // // LocalFile also uses SELECT FOR UPDATE for synchronization. // operations[] = [ // 'op' => 'copy', // 'src' => dstPath, // 'dst' => archivePath, // 'ignoreMissingSource' => true // ]; // // // Copy (or move) the source file to the destination // if (FileBackend::isStoragePath(srcPath)) { // if (flags & self::DELETE_SOURCE) { // operations[] = [ // 'op' => 'move', // 'src' => srcPath, // 'dst' => dstPath, // 'overwrite' => true, // replace current // 'headers' => headers // ]; // } else { // operations[] = [ // 'op' => 'copy', // 'src' => srcPath, // 'dst' => dstPath, // 'overwrite' => true, // replace current // 'headers' => headers // ]; // } // } else { // FS source path // operations[] = [ // 'op' => 'store', // 'src' => src, // prefer FSFile objects // 'dst' => dstPath, // 'overwrite' => true, // replace current // 'headers' => headers // ]; // if (flags & self::DELETE_SOURCE) { // sourceFSFilesToDelete[] = srcPath; // } // } // } // // // Execute the operations for each triplet // status.merge(backend.doOperations(operations)); // // Find out which files were archived... // foreach (ntuples as i => ntuple) { // list(, , archiveRel) = ntuple; // archivePath = this.getZonePath('public') . "/archiveRel"; // if (this.fileExists(archivePath)) { // status.value[i] = 'archived'; // } else { // status.value[i] = 'new'; // } // } // // Cleanup for disk source files... // foreach (sourceFSFilesToDelete as file) { // MediaWiki\suppressWarnings(); // unlink(file); // FS cleanup // MediaWiki\restoreWarnings(); // } // // return status; // } // // /** // * Creates a directory with the appropriate zone permissions. // * Callers are responsible for doing read-only and "writable repo" checks. // * // * @param String dir Virtual URL (or storage path) of directory to clean // * @return Status // */ // protected function initDirectory(dir) { // path = this.resolveToStoragePath(dir); // list(, container,) = FileBackend::splitStoragePath(path); // // params = [ 'dir' => path ]; // if (this.isPrivate // || container === this.zones['deleted']['container'] // || container === this.zones['temp']['container'] // ) { // # Take all available measures to prevent web accessibility of new deleted // # directories, in case the user has not configured offline storage // params = [ 'noAccess' => true, 'noListing' => true ] + params; // } // // status = this.newGood(); // status.merge(this.backend.prepare(params)); // // return status; // } // // /** // * Deletes a directory if empty. // * // * @param String dir Virtual URL (or storage path) of directory to clean // * @return Status // */ // public function cleanDir(dir) { // this.assertWritableRepo(); // fail out if read-only // // status = this.newGood(); // status.merge(this.backend.clean( // [ 'dir' => this.resolveToStoragePath(dir) ])); // // return status; // } /** * Checks existence of a a file * * @param String file Virtual URL (or storage path) of file to check * @return boolean */ public boolean fileExists(XomwFile file) { // result = this.fileExistsBatch(file); // // return result[0]; return file.exists(); } // /** // * Checks existence of an array of files. // * // * @param array files Virtual URLs (or storage paths) of files to check // * @return array Map of files and existence flags, or false // */ // public function fileExistsBatch(array files) { // paths = array_map([ this, 'resolveToStoragePath' ], files); // this.backend.preloadFileStat([ 'srcs' => paths ]); // // result = []; // foreach (files as key => file) { // path = this.resolveToStoragePath(file); // result[key] = this.backend.fileExists([ 'src' => path ]); // } // // return result; // } // // /** // * Move a file to the deletion archive. // * If no valid deletion archive exists, this may either delete the file // * or throw an exception, depending on the preference of the repository // * // * @param mixed srcRel Relative path for the file to be deleted // * @param mixed archiveRel Relative path for the archive location. // * Relative to a private archive directory. // * @return Status // */ // public function delete(srcRel, archiveRel) { // this.assertWritableRepo(); // fail out if read-only // // return this.deleteBatch([ [ srcRel, archiveRel ] ]); // } // // /** // * Move a group of files to the deletion archive. // * // * If no valid deletion archive is configured, this may either delete the // * file or throw an exception, depending on the preference of the repository. // * // * The overwrite policy is determined by the repository -- currently LocalRepo // * assumes a naming scheme in the deleted zone based on content hash, as // * opposed to the public zone which is assumed to be unique. // * // * @param array sourceDestPairs Array of source/destination pairs. Each element // * is a two-element array containing the source file path relative to the // * public root in the first element, and the archive file path relative // * to the deleted zone root in the second element. // * @throws MWException // * @return Status // */ // public function deleteBatch(array sourceDestPairs) { // this.assertWritableRepo(); // fail out if read-only // // // Try creating directories // status = this.initZones([ 'public', 'deleted' ]); // if (!status.isOK()) { // return status; // } // // status = this.newGood(); // // backend = this.backend; // convenience // operations = []; // // Validate filenames and create archive directories // foreach (sourceDestPairs as pair) { // list(srcRel, archiveRel) = pair; // if (!this.validateFilename(srcRel)) { // throw new MWException(__METHOD__ . ':Validation error in srcRel'); // } elseif (!this.validateFilename(archiveRel)) { // throw new MWException(__METHOD__ . ':Validation error in archiveRel'); // } // // publicRoot = this.getZonePath('public'); // srcPath = "{publicRoot}/srcRel"; // // deletedRoot = this.getZonePath('deleted'); // archivePath = "{deletedRoot}/{archiveRel}"; // archiveDir = dirname(archivePath); // does not touch FS // // // Create destination directories // if (!this.initDirectory(archiveDir).isOK()) { // return this.newFatal('directorycreateerror', archiveDir); // } // // operations[] = [ // 'op' => 'move', // 'src' => srcPath, // 'dst' => archivePath, // // We may have 2+ identical files being deleted, // // all of which will map to the same destination file // 'overwriteSame' => true // also see bug 31792 // ]; // } // // // Move the files by execute the operations for each pair. // // We're now committed to returning an OK result, which will // // lead to the files being moved in the DB also. // opts = [ 'force' => true ]; // status.merge(backend.doOperations(operations, opts)); // // return status; // } // // /** // * Delete files in the deleted directory if they are not referenced in the filearchive table // * // * STUB // * @param array storageKeys // */ // public function cleanupDeletedBatch(array storageKeys) { // this.assertWritableRepo(); // } // // /** // * Get a relative path for a deletion archive key, // * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg // * // * @param String key // * @throws MWException // * @return String // */ // public function getDeletedHashPath(key) { // if (strlen(key) < 31) { // throw new MWException("Invalid storage key 'key'."); // } // path = ''; // for (i = 0; i < this.deletedHashLevels; i++) { // path .= key[i] . '/'; // } // // return path; // } // // /** // * If a path is a virtual URL, resolve it to a storage path. // * Otherwise, just return the path as it is. // * // * @param String path // * @return String // * @throws MWException // */ // protected function resolveToStoragePath(path) { // if (this.isVirtualUrl(path)) { // return this.resolveVirtualUrl(path); // } // // return path; // } // // /** // * Get a local FS copy of a file with a given virtual URL/storage path. // * Temporary files may be purged when the file Object falls out of scope. // * // * @param String virtualUrl // * @return TempFSFile|null Returns null on failure // */ // public function getLocalCopy(virtualUrl) { // path = this.resolveToStoragePath(virtualUrl); // // return this.backend.getLocalCopy([ 'src' => path ]); // } // // /** // * Get a local FS file with a given virtual URL/storage path. // * The file is either an original or a copy. It should not be changed. // * Temporary files may be purged when the file Object falls out of scope. // * // * @param String virtualUrl // * @return FSFile|null Returns null on failure. // */ // public function getLocalReference(virtualUrl) { // path = this.resolveToStoragePath(virtualUrl); // // return this.backend.getLocalReference([ 'src' => path ]); // } // // /** // * Get properties of a file with a given virtual URL/storage path. // * Properties should ultimately be obtained via FSFile::getProps(). // * // * @param String virtualUrl // * @return array // */ // public function getFileProps(virtualUrl) { // fsFile = this.getLocalReference(virtualUrl); // mwProps = new MWFileProps(MimeMagic::singleton()); // if (fsFile) { // props = mwProps.getPropsFromPath(fsFile.getPath(), true); // } else { // props = mwProps.newPlaceholderProps(); // } // // return props; // } // // /** // * Get the timestamp of a file with a given virtual URL/storage path // * // * @param String virtualUrl // * @return String|boolean False on failure // */ // public function getFileTimestamp(virtualUrl) { // path = this.resolveToStoragePath(virtualUrl); // // return this.backend.getFileTimestamp([ 'src' => path ]); // } // // /** // * Get the size of a file with a given virtual URL/storage path // * // * @param String virtualUrl // * @return int|boolean False on failure // */ // public function getFileSize(virtualUrl) { // path = this.resolveToStoragePath(virtualUrl); // // return this.backend.getFileSize([ 'src' => path ]); // } // // /** // * Get the sha1 (super 36) of a file with a given virtual URL/storage path // * // * @param String virtualUrl // * @return String|boolean // */ // public function getFileSha1(virtualUrl) { // path = this.resolveToStoragePath(virtualUrl); // // return this.backend.getFileSha1Base36([ 'src' => path ]); // } // // /** // * Attempt to stream a file with the given virtual URL/storage path // * // * @param String virtualUrl // * @param array headers Additional HTTP headers to send on success // * @param array optHeaders HTTP request headers (if-modified-since, range, ...) // * @return Status // * @since 1.27 // */ // public function streamFileWithStatus(virtualUrl, headers = [], optHeaders = []) { // path = this.resolveToStoragePath(virtualUrl); // params = [ 'src' => path, 'headers' => headers, 'options' => optHeaders ]; // // status = this.newGood(); // status.merge(this.backend.streamFile(params)); // // return status; // } // // /** // * Attempt to stream a file with the given virtual URL/storage path // * // * @deprecated since 1.26, use streamFileWithStatus // * @param String virtualUrl // * @param array headers Additional HTTP headers to send on success // * @return boolean Success // */ // public function streamFile(virtualUrl, headers = []) { // return this.streamFileWithStatus(virtualUrl, headers).isOK(); // } // // /** // * Call a callback function for every public regular file in the repository. // * This only acts on the current version of files, not any old versions. // * May use either the database or the filesystem. // * // * @param callable callback // * @return void // */ // public function enumFiles(callback) { // this.enumFilesInStorage(callback); // } // // /** // * Call a callback function for every public file in the repository. // * May use either the database or the filesystem. // * // * @param callable callback // * @return void // */ // protected function enumFilesInStorage(callback) { // publicRoot = this.getZonePath('public'); // numDirs = 1 << (this.hashLevels * 4); // // Use a priori assumptions about directory structure // // to reduce the tree height of the scanning process. // for (flatIndex = 0; flatIndex < numDirs; flatIndex++) { // hexString = sprintf("%0{this.hashLevels}x", flatIndex); // path = publicRoot; // for (hexPos = 0; hexPos < this.hashLevels; hexPos++) { // path .= '/' . substr(hexString, 0, hexPos + 1); // } // iterator = this.backend.getFileList([ 'dir' => path ]); // foreach (iterator as name) { // // Each item returned is a public file // call_user_func(callback, "{path}/{name}"); // } // } // } // // /** // * Determine if a relative path is valid, i.e. not blank or involving directory traveral // * // * @param String filename // * @return boolean // */ // public function validateFilename(filename) { // if (strval(filename) == '') { // return false; // } // // return FileBackend::isPathTraversalFree(filename); // } // // /** // * Get a callback function to use for cleaning error message parameters // * // * @return array // */ // function getErrorCleanupFunction() { // switch (this.pathDisclosureProtection) { // case 'none': // case 'simple': // b/c // callback = [ this, 'passThrough' ]; // break; // default: // 'paranoid' // callback = [ this, 'paranoidClean' ]; // } // return callback; // } // // /** // * Path disclosure protection function // * // * @param String param // * @return String // */ // function paranoidClean(param) { // return '[hidden]'; // } // // /** // * Path disclosure protection function // * // * @param String param // * @return String // */ // function passThrough(param) { // return param; // } // // /** // * Create a new fatal error // * // * @param String message // * @return Status // */ // public function newFatal(message /*, parameters...*/) { // status = call_user_func_array([ 'Status', 'newFatal' ], func_get_args()); // status.cleanCallback = this.getErrorCleanupFunction(); // // return status; // } // // /** // * Create a new good result // * // * @param null|String value // * @return Status // */ // public function newGood(value = null) { // status = Status::newGood(value); // status.cleanCallback = this.getErrorCleanupFunction(); // // return status; // } // // /** // * Checks if there is a redirect named as title. If there is, return the // * title Object. If not, return false. // * STUB // * // * @param Title title Title of image // * @return boolean // */ // public function checkRedirect(Title title) { // return false; // } // // /** // * Invalidates image redirect cache related to that image // * Doesn't do anything for repositories that don't support image redirects. // * // * STUB // * @param Title title Title of image // */ // public function invalidateImageRedirect(Title title) { // } // // /** // * Get the human-readable name of the repo // * // * @return String // */ // public function getDisplayName() { // global wgSitename; // // if (this.isLocal()) { // return wgSitename; // } // // // 'shared-repo-name-wikimediacommons' is used when wgUseInstantCommons = true // return wfMessageFallback('shared-repo-name-' . this.name, 'shared-repo').text(); // } // // /** // * Get the portion of the file that contains the origin file name. // * If that name is too long, then the name "thumbnail." will be given. // * // * @param String name // * @return String // */ // public function nameForThumb(name) { // if (strlen(name) > this.abbrvThreshold) { // ext = FileBackend::extensionFromPath(name); // name = (ext == '') ? 'thumbnail' : "thumbnail.ext"; // } // // return name; // } // // /** // * Returns true if this the local file repository. // * // * @return boolean // */ // public function isLocal() { // return this.getName() == 'local'; // } // // /** // * Get a key on the primary cache for this repository. // * Returns false if the repository's cache is not accessible at this site. // * The parameters are the parts of the key, as for wfMemcKey(). // * // * STUB // * @return boolean // */ // public function getSharedCacheKey(/*...*/) { // return false; // } // // /** // * Get a key for this repo in the local cache domain. These cache keys are // * not shared with remote instances of the repo. // * The parameters are the parts of the key, as for wfMemcKey(). // * // * @return String // */ // public function getLocalCacheKey(/*...*/) { // args = func_get_args(); // array_unshift(args, 'filerepo', this.getName()); // // return call_user_func_array('wfMemcKey', args); // } // // /** // * Get a temporary private FileRepo associated with this repo. // * // * Files will be created in the temp zone of this repo. // * It will have the same backend as this repo. // * // * @return TempFileRepo // */ // public function getTempRepo() { // return new TempFileRepo([ // 'name' => "{this.name}-temp", // 'backend' => this.backend, // 'zones' => [ // 'public' => [ // // Same place storeTemp() uses in the super repo, though // // the path hashing is mismatched, which is annoying. // 'container' => this.zones['temp']['container'], // 'directory' => this.zones['temp']['directory'] // ], // 'thumb' => [ // 'container' => this.zones['temp']['container'], // 'directory' => this.zones['temp']['directory'] == '' // ? 'thumb' // : this.zones['temp']['directory'] . '/thumb' // ], // 'transcoded' => [ // 'container' => this.zones['temp']['container'], // 'directory' => this.zones['temp']['directory'] == '' // ? 'transcoded' // : this.zones['temp']['directory'] . '/transcoded' // ] // ], // 'hashLevels' => this.hashLevels, // performance // 'isPrivate' => true // all in temp zone // ]); // } // // /** // * Get an UploadStash associated with this repo. // * // * @param User user // * @return UploadStash // */ // public function getUploadStash(User user = null) { // return new UploadStash(this, user); // } // // /** // * Throw an exception if this repo is read-only by design. // * This does not and should not check getReadOnlyReason(). // * // * @return void // * @throws MWException // */ // protected function assertWritableRepo() { // } // // /** // * Return information about the repository. // * // * @return array // * @since 1.22 // */ // public function getInfo() { // ret = [ // 'name' => this.getName(), // 'displayname' => this.getDisplayName(), // 'rootUrl' => this.getZoneUrl('public'), // 'local' => this.isLocal(), // ]; // // optionalSettings = [ // 'url', 'thumbUrl', 'initialCapital', 'descBaseUrl', 'scriptDirUrl', 'articleUrl', // 'fetchDescription', 'descriptionCacheExpiry', 'scriptExtension', 'favicon' // ]; // foreach (optionalSettings as k) { // if (isset(this.k)) { // ret[k] = this.k; // } // } // // return ret; // } // // /** // * Returns whether or not storage is SHA-1 based // * @return boolean // */ // public function hasSha1Storage() { // return this.hasSha1Storage; // } // // /** // * Returns whether or not repo supports having originals SHA-1s in the thumb URLs // * @return boolean // */ // public function supportsSha1URLs() { // return this.supportsSha1URLs; // } public static final int Zone__public = 0, Zone__thumb = 1; }