PageRenderTime 100ms CodeModel.GetById 19ms app.highlight 69ms RepoModel.GetById 1ms app.codeStats 1ms

/Wikimate.php

http://github.com/hamstar/Wikimate
PHP | 1937 lines | 933 code | 233 blank | 771 comment | 152 complexity | a0fed427d3b549055c1df384650a311a MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2/// =============================================================================
   3/// Wikimate is a wrapper for the MediaWiki API that aims to be very easy to use.
   4///
   5/// @version    0.12.0
   6/// @copyright  SPDX-License-Identifier: MIT
   7/// =============================================================================
   8
   9/**
  10 * Provides an interface over wiki API objects such as pages.
  11 *
  12 * @author  Robert McLeod
  13 * @since   December 2010
  14 */
  15class Wikimate
  16{
  17	/**
  18	 * @var  string  The current version number (conforms to http://semver.org/).
  19	 */
  20	const VERSION = '0.12.0';
  21
  22	protected $api;
  23	protected $username;
  24	protected $password;
  25
  26	/** @var  Requests_Session */
  27	protected $session;
  28	protected $useragent;
  29
  30	protected $error     = null;
  31	protected $debugMode = false;
  32
  33	/**
  34	 * Create a new Wikimate object.
  35	 *
  36	 * @return  Wikimate
  37	 */
  38	public function __construct($api, $headers = array(), $data = array(), $options = array())
  39	{
  40		$this->api     = $api;
  41		$this->headers = $headers;
  42		$this->data    = $data;
  43		$this->options = $options;
  44
  45		$this->initRequests();
  46	}
  47
  48	/**
  49	 * Set up a Requests_Session with appropriate user agent.
  50	 *
  51	 * @return  void
  52	 */
  53	protected function initRequests()
  54	{
  55		$this->useragent = 'Wikimate '.self::VERSION.' (https://github.com/hamstar/Wikimate)';
  56
  57		$this->session = new Requests_Session($this->api, $this->headers, $this->data, $this->options);
  58		$this->session->useragent = $this->useragent;
  59	}
  60
  61	/**
  62	 * Logs in to the wiki.
  63	 *
  64	 * @param   string   $username  The user name
  65	 * @param   string   $password  The user password
  66	 * @param   string   $domain    The domain (optional)
  67	 * @return  boolean             True if logged in
  68	 */
  69	public function login($username, $password, $domain = null)
  70	{
  71		//Logger::log("Logging in");
  72
  73		$details = array(
  74			'action' => 'login',
  75			'lgname' => $username,
  76			'lgpassword' => $password,
  77			'format' => 'json'
  78		);
  79
  80		// If $domain is provided, set the corresponding detail in the request information array
  81		if (is_string($domain)) {
  82			$details['lgdomain'] = $domain;
  83		}
  84
  85		// Send the login request
  86		$response = $this->session->post($this->api, array(), $details);
  87		// Check if we got an API result or the API doc page (invalid request)
  88		if (strpos($response->body, "This is an auto-generated MediaWiki API documentation page") !== false) {
  89			$this->error = array();
  90			$this->error['login'] = 'The API could not understand the first login request';
  91			return false;
  92		}
  93
  94		$loginResult = json_decode($response->body);
  95
  96		if ($this->debugMode) {
  97			echo "Login request:\n";
  98			print_r($details);
  99			echo "Login request response:\n";
 100			print_r($loginResult);
 101		}
 102
 103		if (isset($loginResult->login->result) && $loginResult->login->result == 'NeedToken') {
 104			//Logger::log("Sending token {$loginResult->login->token}");
 105			$details['lgtoken'] = strtolower(trim($loginResult->login->token));
 106
 107			// Send the confirm token request
 108			$loginResult = $this->session->post($this->api, array(), $details)->body;
 109
 110			// Check if we got an API result or the API doc page (invalid request)
 111			if (strpos($loginResult, "This is an auto-generated MediaWiki API documentation page") !== false) {
 112				$this->error = array();
 113				$this->error['login'] = 'The API could not understand the confirm token request';
 114				return false;
 115			}
 116
 117			$loginResult = json_decode($loginResult);
 118
 119			if ($this->debugMode) {
 120				echo "Confirm token request:\n";
 121				print_r($details);
 122				echo "Confirm token response:\n";
 123				print_r($loginResult);
 124			}
 125
 126			if ($loginResult->login->result != 'Success') {
 127				// Some more comprehensive error checking
 128				$this->error = array();
 129				switch ($loginResult->login->result) {
 130					case 'NotExists':
 131						$this->error['login'] = 'The username does not exist';
 132						break;
 133					default:
 134						$this->error['login'] = 'The API result was: ' . $loginResult->login->result;
 135						break;
 136				}
 137				return false;
 138			}
 139		}
 140
 141		//Logger::log("Logged in");
 142		return true;
 143	}
 144
 145	/**
 146	 * Sets the debug mode.
 147	 *
 148	 * @param   boolean   $b  True to turn debugging on
 149	 * @return  Wikimate      This object
 150	 */
 151	public function setDebugMode($b)
 152	{
 153		$this->debugMode = $b;
 154		return $this;
 155	}
 156
 157	/**
 158	 * Used to return or print the curl settings, but now prints an error and
 159	 * returns Wikimate::debugRequestsConfig().
 160	 *
 161	 * @deprecated                  Since version 0.10.0
 162	 * @param       boolean  $echo  True to echo the configuration
 163	 * @return      mixed           Array of config if $echo is false, (boolean) true if echo is true
 164	 */
 165	public function debugCurlConfig($echo = false)
 166	{
 167		if ($echo) {
 168			echo "ERROR: Curl is no longer used by Wikimate.\n";
 169		}
 170		return $this->debugRequestsConfig($echo);
 171	}
 172
 173	/**
 174	 * Get or print the Requests configuration.
 175	 *
 176	 * @param   boolean  $echo  Whether to echo the options
 177	 * @return  array           Options if $echo is false
 178	 * @return  boolean         True if options have been echoed to STDOUT
 179	 */
 180	public function debugRequestsConfig($echo = false)
 181	{
 182		if ($echo) {
 183			echo "<pre>Requests options:\n";
 184			print_r($this->session->options);
 185			echo "Requests headers:\n";
 186			print_r($this->session->headers);
 187			echo "</pre>";
 188			return true;
 189		}
 190		return $this->session->options;
 191	}
 192
 193	/**
 194	 * Returns a WikiPage object populated with the page data.
 195	 *
 196	 * @param   string    $title  The name of the wiki article
 197	 * @return  WikiPage          The page object
 198	 */
 199	public function getPage($title)
 200	{
 201		return new WikiPage($title, $this);
 202	}
 203
 204	/**
 205	 * Returns a WikiFile object populated with the file data.
 206	 *
 207	 * @param   string    $filename  The name of the wiki file
 208	 * @return  WikiFile             The file object
 209	 */
 210	public function getFile($filename)
 211	{
 212		return new WikiFile($filename, $this);
 213	}
 214
 215	/**
 216	 * Performs a query to the wiki API with the given details.
 217	 *
 218	 * @param   array  $array  Array of details to be passed in the query
 219	 * @return  array          Unserialized php output from the wiki API
 220	 */
 221	public function query($array)
 222	{
 223		$array['action'] = 'query';
 224		$array['format'] = 'php';
 225
 226		$apiResult = $this->session->get($this->api.'?'.http_build_query($array));
 227
 228		return unserialize($apiResult->body);
 229	}
 230
 231	/**
 232	 * Performs a parse query to the wiki API.
 233	 *
 234	 * @param   array  $array  Array of details to be passed in the query
 235	 * @return  array          Unserialized php output from the wiki API
 236	 */
 237	public function parse($array)
 238	{
 239		$array['action'] = 'parse';
 240		$array['format'] = 'php';
 241
 242		$apiResult = $this->session->get($this->api.'?'.http_build_query($array));
 243
 244		return unserialize($apiResult->body);
 245	}
 246
 247	/**
 248	 * Perfoms an edit query to the wiki API.
 249	 *
 250	 * @param   array  $array  Array of details to be passed in the query
 251	 * @return  array          Unserialized php output from the wiki API
 252	 */
 253	public function edit($array)
 254	{
 255		$headers = array(
 256			'Content-Type' => "application/x-www-form-urlencoded"
 257		);
 258
 259		$array['action'] = 'edit';
 260		$array['format'] = 'php';
 261
 262		$apiResult = $this->session->post($this->api, $headers, $array);
 263
 264		return unserialize($apiResult->body);
 265	}
 266
 267	/**
 268	 * Perfoms a delete query to the wiki API.
 269	 *
 270	 * @param   array  $array  Array of details to be passed in the query
 271	 * @return  array          Unserialized php output from the wiki API
 272	 */
 273	public function delete($array)
 274	{
 275		$headers = array(
 276			'Content-Type' => "application/x-www-form-urlencoded"
 277		);
 278
 279		$array['action'] = 'delete';
 280		$array['format'] = 'php';
 281
 282		$apiResult = $this->session->post($this->api, $headers, $array);
 283
 284		return unserialize($apiResult->body);
 285	}
 286
 287	/**
 288	 * Downloads data from the given URL.
 289	 *
 290	 * @param   string  $url  The URL to download from
 291	 * @return  mixed         The downloaded data (string), or null if error
 292	 */
 293	public function download($url)
 294	{
 295		$getResult = $this->session->get($url);
 296
 297		if (!$getResult->success) {
 298			$this->error = array();
 299			$this->error['file'] = 'Download error (HTTP status: ' . $getResult->status_code . ')';
 300			$this->error['http'] = $getResult->status_code;
 301			return null;
 302		}
 303		return $getResult->body;
 304	}
 305
 306	/**
 307	 * Uploads a file to the wiki API.
 308	 *
 309	 * @param   array    $array  Array of details to be used in the upload
 310	 * @return  array            Unserialized php output from the wiki API
 311	 */
 312	public function upload($array)
 313	{
 314		$array['action'] = 'upload';
 315		$array['format'] = 'php';
 316
 317		// Construct multipart body: https://www.mediawiki.org/wiki/API:Upload#Sample_Raw_Upload
 318		$boundary = '---Wikimate-' . md5(microtime());
 319		$body = '';
 320		foreach ($array as $fieldName => $fieldData) {
 321			$body .= "--{$boundary}\r\n";
 322			$body .= 'Content-Disposition: form-data; name="' . $fieldName . '"';
 323			// Process the (binary) file
 324			if ($fieldName == 'file') {
 325				$body .= '; filename="' . $array['filename'] . '"' . "\r\n";
 326				$body .= "Content-Type: application/octet-stream; charset=UTF-8\r\n";
 327				$body .= "Content-Transfer-Encoding: binary\r\n";
 328			// Process text parameters
 329			} else {
 330				$body .= "\r\n";
 331				$body .= "Content-Type: text/plain; charset=UTF-8\r\n";
 332				$body .= "Content-Transfer-Encoding: 8bit\r\n";
 333			}
 334			$body .= "\r\n{$fieldData}\r\n";
 335		}
 336		$body .= "--{$boundary}--\r\n";
 337
 338		// Construct multipart headers
 339		$headers = array(
 340			'Content-Type' => "multipart/form-data; boundary={$boundary}",
 341			'Content-Length' => strlen($body),
 342		);
 343
 344		$apiResult = $this->session->post($this->api, $headers, $body);
 345
 346		return unserialize($apiResult->body);
 347	}
 348
 349	/**
 350	 * Returns the latest error if there is one.
 351	 *
 352	 * @return  mixed  The error array, or null if no error
 353	 */
 354	public function getError()
 355	{
 356		return $this->error;
 357	}
 358}
 359
 360
 361/**
 362 * Models a wiki article page that can have its text altered and retrieved.
 363 *
 364 * @author  Robert McLeod
 365 * @since   December 2010
 366 */
 367class WikiPage
 368{
 369	const SECTIONLIST_BY_INDEX = 1;
 370	const SECTIONLIST_BY_NAME = 2;
 371	const SECTIONLIST_BY_NUMBER = 3;
 372
 373	protected $title          = null;
 374	protected $wikimate       = null;
 375	protected $exists         = false;
 376	protected $invalid        = false;
 377	protected $error          = null;
 378	protected $edittoken      = null;
 379	protected $starttimestamp = null;
 380	protected $text           = null;
 381	protected $sections       = null;
 382
 383	/*
 384	 *
 385	 * Magic methods
 386	 *
 387	 */
 388
 389	/**
 390	 * Constructs a WikiPage object from the title given
 391	 * and associate with the passed Wikimate object.
 392	 *
 393	 * @param  string    $title     Name of the wiki article
 394	 * @param  Wikimate  $wikimate  Wikimate object
 395	 */
 396	public function __construct($title, $wikimate)
 397	{
 398		$this->wikimate = $wikimate;
 399		$this->title    = $title;
 400		$this->text     = $this->getText(true);
 401
 402		if ($this->invalid) {
 403			$this->error['page'] = 'Invalid page title - cannot create WikiPage';
 404		}
 405	}
 406
 407	/**
 408	 * Forget all object properties.
 409	 *
 410	 * @return  <type>  Destructor
 411	 */
 412	public function __destruct()
 413	{
 414		$this->title          = null;
 415		$this->wikimate       = null;
 416		$this->exists         = false;
 417		$this->invalid        = false;
 418		$this->error          = null;
 419		$this->edittoken      = null;
 420		$this->starttimestamp = null;
 421		$this->text           = null;
 422		$this->sections       = null;
 423		return null;
 424	}
 425
 426	/**
 427	 * Returns the wikicode of the page.
 428	 *
 429	 * @return  string  String of wikicode
 430	 */
 431	public function __toString()
 432	{
 433		return $this->text;
 434	}
 435
 436	/**
 437	 * Returns an array sections with the section name as the key
 438	 * and the text as the element, e.g.
 439	 *
 440	 * array(
 441	 *   'intro' => 'this text is the introduction',
 442	 *   'History' => 'this is text under the history section'
 443	 *)
 444	 *
 445	 * @return  array  Array of sections
 446	 */
 447	public function __invoke()
 448	{
 449		return $this->getAllSections(false, self::SECTIONLIST_BY_NAME);
 450	}
 451
 452	/**
 453	 * Returns the page existence status.
 454	 *
 455	 * @return  boolean  True if page exists
 456	 */
 457	public function exists()
 458	{
 459		return $this->exists;
 460	}
 461
 462	/**
 463	 * Alias of self::__destruct().
 464	 */
 465	public function destroy()
 466	{
 467		$this->__destruct();
 468	}
 469
 470	/*
 471	 *
 472	 * Page meta methods
 473	 *
 474	 */
 475
 476	/**
 477	 * Returns the latest error if there is one.
 478	 *
 479	 * @return  mixed  The error array, or null if no error
 480	 */
 481	public function getError()
 482	{
 483		return $this->error;
 484	}
 485
 486	/**
 487	 * Returns the title of this page.
 488	 *
 489	 * @return  string  The title of this page
 490	 */
 491	public function getTitle()
 492	{
 493		return $this->title;
 494	}
 495
 496	/**
 497	 * Returns the number of sections in this page.
 498	 *
 499	 * @return  integer  The number of sections in this page
 500	 */
 501	public function getNumSections()
 502	{
 503		return count($this->sections->byIndex);
 504	}
 505
 506	/**
 507	 * Returns the sections offsets and lengths.
 508	 *
 509	 * @return  StdClass  Section class
 510	 */
 511	public function getSectionOffsets()
 512	{
 513		return $this->sections;
 514	}
 515
 516	/*
 517	 *
 518	 * Getter methods
 519	 *
 520	 */
 521
 522	/**
 523	 * Gets the text of the page. If refresh is true,
 524	 * then this method will query the wiki API again for the page details.
 525	 *
 526	 * @param   boolean  $refresh  True to query the wiki API again
 527	 * @return  mixed              The text of the page (string), or null if error
 528	 */
 529	public function getText($refresh = false)
 530	{
 531		if ($refresh) { // We want to query the API
 532			// Specify relevant page properties to retrieve
 533			$data = array(
 534				'titles' => $this->title,
 535				'prop' => 'info|revisions',
 536				'rvprop' => 'content', // Need to get page text
 537				'intoken' => 'edit',
 538			);
 539
 540			$r = $this->wikimate->query($data); // Run the query
 541
 542			// Check for errors
 543			if (isset($r['error'])) {
 544				$this->error = $r['error']; // Set the error if there was one
 545				return null;
 546			} else {
 547				$this->error = null; // Reset the error status
 548			}
 549
 550			// Get the page (there should only be one)
 551			$page = array_pop($r['query']['pages']);
 552			unset($r, $data);
 553
 554			// Abort if invalid page title
 555			if (isset($page['invalid'])) {
 556				$this->invalid = true;
 557				return null;
 558			}
 559
 560			$this->edittoken      = $page['edittoken'];
 561			$this->starttimestamp = $page['starttimestamp'];
 562
 563			if (!isset($page['missing'])) {
 564				// Update the existence if the page is there
 565				$this->exists = true;
 566				// Put the content into text
 567				$this->text   = $page['revisions'][0]['*'];
 568			}
 569			unset($page);
 570
 571			// Now we need to get the section headers, if any
 572			preg_match_all('/(={1,6}).*?\1 *(?:\n|$)/', $this->text, $matches);
 573
 574			// Set the intro section (between title and first section)
 575			$this->sections->byIndex[0]['offset']      = 0;
 576			$this->sections->byName['intro']['offset'] = 0;
 577
 578			// Check for section header matches
 579			if (empty($matches[0])) {
 580				// Define lengths for page consisting only of intro section
 581				$this->sections->byIndex[0]['length']      = strlen($this->text);
 582				$this->sections->byName['intro']['length'] = strlen($this->text);
 583			} else {
 584				// Array of section header matches
 585				$sections = $matches[0];
 586
 587				// Set up the current section
 588				$currIndex = 0;
 589				$currName  = 'intro';
 590
 591				// Collect offsets and lengths from section header matches
 592				foreach ($sections as $section) {
 593					// Get the current offset
 594					$currOffset = strpos($this->text, $section, $this->sections->byIndex[$currIndex]['offset']);
 595
 596					// Are we still on the first section?
 597					if ($currIndex == 0) {
 598						$this->sections->byIndex[$currIndex]['length'] = $currOffset;
 599						$this->sections->byIndex[$currIndex]['depth']  = 0;
 600						$this->sections->byName[$currName]['length']   = $currOffset;
 601						$this->sections->byName[$currName]['depth']    = 0;
 602					}
 603
 604					// Get the current name and index
 605					$currName = trim(str_replace('=', '', $section));
 606					$currIndex++;
 607
 608					// Search for existing name and create unique one
 609					$cName = $currName;
 610					for ($seq = 2; array_key_exists($cName, $this->sections->byName); $seq++) {
 611						$cName = $currName . '_' . $seq;
 612					}
 613					if ($seq > 2) {
 614						$currName = $cName;
 615					}
 616
 617					// Set the offset and depth (from the matched ='s) for the current section
 618					$this->sections->byIndex[$currIndex]['offset'] = $currOffset;
 619					$this->sections->byIndex[$currIndex]['depth']  = strlen($matches[1][$currIndex-1]);
 620					$this->sections->byName[$currName]['offset']   = $currOffset;
 621					$this->sections->byName[$currName]['depth']    = strlen($matches[1][$currIndex-1]);
 622
 623					// If there is a section after this, set the length of this one
 624					if (isset($sections[$currIndex])) {
 625						// Get the offset of the next section
 626						$nextOffset = strpos($this->text, $sections[$currIndex], $currOffset);
 627						// Calculate the length of this one
 628						$length     = $nextOffset - $currOffset;
 629
 630						// Set the length of this section
 631						$this->sections->byIndex[$currIndex]['length'] = $length;
 632						$this->sections->byName[$currName]['length']   = $length;
 633					}
 634					else {
 635						// Set the length of last section
 636						$this->sections->byIndex[$currIndex]['length'] = strlen($this->text) - $currOffset;
 637						$this->sections->byName[$currName]['length']   = strlen($this->text) - $currOffset;
 638					}
 639				}
 640			}
 641		}
 642
 643		return $this->text; // Return the text in any case
 644	}
 645
 646	/**
 647	 * Returns the requested section, with its subsections, if any.
 648	 *
 649	 * Section can be the following:
 650	 * - section name (string, e.g. "History")
 651	 * - section index (int, e.g. 3)
 652	 *
 653	 * @param   mixed    $section             The section to get
 654	 * @param   boolean  $includeHeading      False to get section text only,
 655	 *                                        true to include heading too
 656	 * @param   boolean  $includeSubsections  False to get section text only,
 657	 *                                        true to include subsections too
 658	 * @return  string                        Wikitext of the section on the page,
 659	 *                                        or false if section is undefined
 660	 */
 661	public function getSection($section, $includeHeading = false, $includeSubsections = true)
 662	{
 663		// Check if we have a section name or index
 664		if (is_int($section)) {
 665			if (!isset($this->sections->byIndex[$section])) {
 666				return false;
 667			}
 668			$coords = $this->sections->byIndex[$section];
 669		} else if (is_string($section)) {
 670			if (!isset($this->sections->byName[$section])) {
 671				return false;
 672			}
 673			$coords = $this->sections->byName[$section];
 674		}
 675
 676		// Extract the offset, depth and (initial) length
 677		@extract($coords);
 678		// Find subsections if requested, and not the intro
 679		if ($includeSubsections && $offset > 0) {
 680			$found = false;
 681			foreach ($this->sections->byName as $section) {
 682				if ($found) {
 683					// Include length of this subsection
 684					if ($depth < $section['depth']) {
 685						$length += $section['length'];
 686					// Done if not a subsection
 687					} else {
 688						break;
 689					}
 690				} else {
 691					// Found our section if same offset
 692					if ($offset == $section['offset']) {
 693						$found = true;
 694					}
 695				}
 696			}
 697		}
 698		// Extract text of section, and its subsections if requested
 699		$text = substr($this->text, $offset, $length);
 700
 701		// Whack off the heading if requested, and not the intro
 702		if (!$includeHeading && $offset > 0) {
 703			// Chop off the first line
 704			$text = substr($text, strpos($text, "\n"));
 705		}
 706
 707		return $text;
 708	}
 709
 710	/**
 711	 * Return all the sections of the page in an array - the key names can be
 712	 * set to name or index by using the following for the second param:
 713	 * - self::SECTIONLIST_BY_NAME
 714	 * - self::SECTIONLIST_BY_INDEX
 715	 *
 716	 * @param   boolean  $includeHeading  False to get section text only
 717	 * @param   integer  $keyNames        Modifier for the array key names
 718	 * @return  array                     Array of sections
 719	 * @throw   Exception                 If $keyNames is not a supported constant
 720	 */
 721	public function getAllSections($includeHeading = false, $keyNames = self::SECTIONLIST_BY_INDEX)
 722	{
 723		$sections = array();
 724
 725		switch ($keyNames) {
 726			case self::SECTIONLIST_BY_INDEX:
 727				$array = array_keys($this->sections->byIndex);
 728				break;
 729			case self::SECTIONLIST_BY_NAME:
 730				$array = array_keys($this->sections->byName);
 731				break;
 732			default:
 733				throw new Exception('Unexpected parameter $keyNames given to WikiPage::getAllSections()');
 734				break;
 735		}
 736
 737		foreach ($array as $key) {
 738			$sections[$key] = $this->getSection($key, $includeHeading);
 739		}
 740
 741		return $sections;
 742	}
 743
 744	/*
 745	 *
 746	 * Setter methods
 747	 *
 748	 */
 749
 750	/**
 751	 * Sets the text in the page.  Updates the starttimestamp to the timestamp
 752	 * after the page edit (if the edit is successful).
 753	 *
 754	 * Section can be the following:
 755	 * - section name (string, e.g. "History")
 756	 * - section index (int, e.g. 3)
 757	 * - a new section (the string "new")
 758	 * - the whole page (null)
 759	 *
 760	 * @param   string   $text     The article text
 761	 * @param   string   $section  The section to edit (whole page by default)
 762	 * @param   boolean  $minor    True for minor edit
 763	 * @param   string   $summary  Summary text, and section header in case
 764	 *                             of new section
 765	 * @return  boolean            True if page was edited successfully
 766	 */
 767	public function setText($text, $section = null, $minor = false, $summary = null)
 768	{
 769		$data = array(
 770			'title' => $this->title,
 771			'text' => $text,
 772			'md5' => md5($text),
 773			'bot' => "true",
 774			'token' => $this->edittoken,
 775			'starttimestamp' => $this->starttimestamp,
 776		);
 777
 778		// Set options from arguments
 779		if (!is_null($section)) {
 780			// Obtain section index in case it is a name
 781			$data['section'] = $this->findSection($section);
 782			if ($data['section'] == -1) {
 783				return false;
 784			}
 785		}
 786		if ($minor) {
 787			$data['minor'] = $minor;
 788		}
 789		if (!is_null($summary)) {
 790			$data['summary'] = $summary;
 791		}
 792
 793		// Make sure we don't create a page by accident or overwrite another one
 794		if (!$this->exists) {
 795			$data['createonly'] = "true"; // createonly if not exists
 796		} else {
 797			$data['nocreate'] = "true"; // Don't create, it should exist
 798		}
 799
 800		$r = $this->wikimate->edit($data); // The edit query
 801
 802		// Check if it worked
 803		if (isset($r['edit']['result']) && $r['edit']['result'] == 'Success') {
 804			$this->exists = true;
 805
 806			if (is_null($section)) {
 807				$this->text = $text;
 808			}
 809
 810			// Get the new starttimestamp
 811			$data = array(
 812				'titles' => $this->title,
 813				'prop' => 'info',
 814				'intoken' => 'edit',
 815			);
 816
 817			$r = $this->wikimate->query($data);
 818
 819			$page = array_pop($r['query']['pages']); // Get the page
 820
 821			$this->starttimestamp = $page['starttimestamp']; // Update the starttimestamp
 822
 823			$this->error = null; // Reset the error status
 824			return true;
 825		}
 826
 827		// Return error response
 828		if (isset($r['error'])) {
 829			$this->error = $r['error'];
 830		} else {
 831			$this->error = array();
 832			$this->error['page'] = 'Unexpected edit response: '.$r['edit']['result'];
 833		}
 834		return false;
 835	}
 836
 837	/**
 838	 * Sets the text of the given section.
 839	 * Essentially an alias of WikiPage:setText()
 840	 * with the summary and minor parameters switched.
 841	 *
 842	 * Section can be the following:
 843	 * - section name (string, e.g. "History")
 844	 * - section index (int, e.g. 3)
 845	 * - a new section (the string "new")
 846	 * - the whole page (null)
 847	 *
 848	 * @param   string   $text     The text of the section
 849	 * @param   mixed    $section  The section to edit (intro by default)
 850	 * @param   string   $summary  Summary text, and section header in case
 851	 *                             of new section
 852	 * @param   boolean  $minor    True for minor edit
 853	 * @return  boolean            True if the section was saved
 854	 */
 855	public function setSection($text, $section = 0, $summary = null, $minor = false)
 856	{
 857		return $this->setText($text, $section, $minor, $summary);
 858	}
 859
 860	/**
 861	 * Alias of WikiPage::setSection() specifically for creating new sections.
 862	 *
 863	 * @param   string   $name  The heading name for the new section
 864	 * @param   string   $text  The text of the new section
 865	 * @return  boolean         True if the section was saved
 866	 */
 867	public function newSection($name, $text)
 868	{
 869		return $this->setSection($text, $section = 'new', $summary = $name, $minor = false);
 870	}
 871
 872	/**
 873	 * Delete the page.
 874	 *
 875	 * @param   string   $reason  Reason for the deletion
 876	 * @return  boolean           True if page was deleted successfully
 877	 */
 878	public function delete($reason = null)
 879	{
 880		$data = array(
 881			'title' => $this->title,
 882			'token' => $this->edittoken,
 883		);
 884
 885		// Set options from arguments
 886		if (!is_null($reason)) {
 887			$data['reason'] = $reason;
 888		}
 889
 890		$r = $this->wikimate->delete($data); // The delete query
 891
 892		// Check if it worked
 893		if (isset($r['delete'])) {
 894			$this->exists = false; // The page was deleted
 895
 896			$this->error = null; // Reset the error status
 897			return true;
 898		}
 899
 900		$this->error = $r['error']; // Return error response
 901		return false;
 902	}
 903
 904	/*
 905	 *
 906	 * Private methods
 907	 *
 908	 */
 909
 910	/**
 911	 * Find a section's index by name.
 912	 * If a section index or 'new' is passed, it is returned directly.
 913	 *
 914	 * @param   mixed  $section  The section name or index to find
 915	 * @return  mixed            The section index, or -1 if not found
 916	 */
 917	private function findSection($section)
 918	{
 919		// Check section type
 920		if (is_int($section) || $section === 'new') {
 921			return $section;
 922		} else if (is_string($section)) {
 923			// Search section names for related index
 924			$sections = array_keys($this->sections->byName);
 925			$index = array_search($section, $sections);
 926
 927			// Return index if found
 928			if ($index !== false) {
 929				return $index;
 930			}
 931		}
 932
 933		// Return error message and value
 934		$this->error = array();
 935		$this->error['page'] = "Section '$section' was not found on this page";
 936		return -1;
 937	}
 938}
 939
 940
 941/**
 942 * Models a wiki file that can have its properties retrieved and
 943 * its contents downloaded and uploaded.
 944 * All properties pertain to the current revision of the file.
 945 *
 946 * @author  Robert McLeod & Frans P. de Vries
 947 * @since   October 2016
 948 */
 949class WikiFile
 950{
 951	protected $filename  = null;
 952	protected $wikimate  = null;
 953	protected $exists    = false;
 954	protected $invalid   = false;
 955	protected $error     = null;
 956	protected $edittoken = null;
 957	protected $info      = null;
 958	protected $history   = null;
 959
 960	/*
 961	 *
 962	 * Magic methods
 963	 *
 964	 */
 965
 966	/**
 967	 * Constructs a WikiFile object from the filename given
 968	 * and associate with the passed Wikimate object.
 969	 *
 970	 * @param  string    $filename  Name of the wiki file
 971	 * @param  Wikimate  $wikimate  Wikimate object
 972	 */
 973	public function __construct($filename, $wikimate)
 974	{
 975		$this->wikimate = $wikimate;
 976		$this->filename = $filename;
 977		$this->info     = $this->getInfo(true);
 978
 979		if ($this->invalid) {
 980			$this->error['file'] = 'Invalid filename - cannot create WikiFile';
 981		}
 982	}
 983
 984	/**
 985	 * Forget all object properties.
 986	 *
 987	 * @return  <type>  Destructor
 988	 */
 989	public function __destruct()
 990	{
 991		$this->filename   = null;
 992		$this->wikimate   = null;
 993		$this->exists     = false;
 994		$this->invalid    = false;
 995		$this->error      = null;
 996		$this->edittoken  = null;
 997		$this->info       = null;
 998		$this->history    = null;
 999		return null;
1000	}
1001
1002	/**
1003	 * Returns the file existence status.
1004	 *
1005	 * @return  boolean  True if file exists
1006	 */
1007	public function exists()
1008	{
1009		return $this->exists;
1010	}
1011
1012	/**
1013	 * Alias of self::__destruct().
1014	 */
1015	public function destroy()
1016	{
1017		$this->__destruct();
1018	}
1019
1020	/*
1021	 *
1022	 * File meta methods
1023	 *
1024	 */
1025
1026	/**
1027	 * Returns the latest error if there is one.
1028	 *
1029	 * @return  mixed  The error array, or null if no error
1030	 */
1031	public function getError()
1032	{
1033		return $this->error;
1034	}
1035
1036	/**
1037	 * Returns the name of this file.
1038	 *
1039	 * @return  string  The name of this file
1040	 */
1041	public function getFilename()
1042	{
1043		return $this->filename;
1044	}
1045
1046	/*
1047	 *
1048	 * Getter methods
1049	 *
1050	 */
1051
1052	/**
1053	 * Gets the information of the file. If refresh is true,
1054	 * then this method will query the wiki API again for the file details.
1055	 *
1056	 * @param   boolean  $refresh  True to query the wiki API again
1057	 * @param   array    $history  An optional array of revision history parameters
1058	 * @return  mixed              The info of the file (array), or null if error
1059	 */
1060	public function getInfo($refresh = false, $history = null)
1061	{
1062		if ($refresh) { // We want to query the API
1063			// Specify relevant file properties to retrieve
1064			$data = array(
1065				'titles' => 'File:' . $this->filename,
1066				'prop' => 'info|imageinfo',
1067				'iiprop' => 'bitdepth|canonicaltitle|comment|parsedcomment|'
1068				          . 'commonmetadata|metadata|extmetadata|mediatype|'
1069				          . 'mime|thumbmime|sha1|size|timestamp|url|user|userid',
1070				'intoken' => 'edit',
1071			);
1072			// Add optional history parameters
1073			if (is_array($history)) {
1074				foreach ($history as $key => $val) {
1075					$data[$key] = $val;
1076				}
1077				// Retrieve archive name property as well
1078				$data['iiprop'] .= '|archivename';
1079			}
1080
1081			$r = $this->wikimate->query($data); // Run the query
1082
1083			// Check for errors
1084			if (isset($r['error'])) {
1085				$this->error = $r['error']; // Set the error if there was one
1086				return null;
1087			} else {
1088				$this->error = null; // Reset the error status
1089			}
1090
1091			// Get the page (there should only be one)
1092			$page = array_pop($r['query']['pages']);
1093			unset($r, $data);
1094
1095			// Abort if invalid file title
1096			if (isset($page['invalid'])) {
1097				$this->invalid = true;
1098				return null;
1099			}
1100
1101			$this->edittoken = $page['edittoken'];
1102
1103			// Check that file is present and has info
1104			if (!isset($page['missing']) && isset($page['imageinfo'])) {
1105				// Update the existence if the file is there
1106				$this->exists = true;
1107				// Put the content into info & history
1108				$this->info    = $page['imageinfo'][0];
1109				$this->history = $page['imageinfo'];
1110			}
1111			unset($page);
1112		}
1113
1114		return $this->info; // Return the info in any case
1115	}
1116
1117	/**
1118	 * Returns the anonymous flag of this file,
1119	 * or of its specified revision.
1120	 * If true, then getUser()'s value represents an anonymous IP address.
1121	 *
1122	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1123	 * @return  mixed             The anonymous flag of this file (boolean),
1124	 *                            or null if revision not found
1125	 */
1126	public function getAnon($revision = null)
1127	{
1128		// Without revision, use current info
1129		if (!isset($revision)) {
1130			// Check for anon flag
1131			return isset($this->info['anon']) ? true : false;
1132		}
1133
1134		// Obtain the properties of the revision
1135		if (($info = $this->getRevision($revision)) === null) {
1136			return null;
1137		}
1138
1139		// Check for anon flag
1140		return isset($info['anon']) ? true : false;
1141	}
1142
1143	/**
1144	 * Returns the aspect ratio of this image,
1145	 * or of its specified revision.
1146	 * Returns 0 if file is not an image (and thus has no dimensions).
1147	 *
1148	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1149	 * @return  float             The aspect ratio of this image, or 0 if no dimensions,
1150	 *                            or -1 if revision not found
1151	 */
1152	public function getAspectRatio($revision = null)
1153	{
1154		// Without revision, use current info
1155		if (!isset($revision)) {
1156			// Check for dimensions
1157			if ($this->info['height'] > 0) {
1158				return $this->info['width'] / $this->info['height'];
1159			} else {
1160				return 0;
1161			}
1162		}
1163
1164		// Obtain the properties of the revision
1165		if (($info = $this->getRevision($revision)) === null) {
1166			return -1;
1167		}
1168
1169		// Check for dimensions
1170		if (isset($info['height'])) {
1171			return $info['width'] / $info['height'];
1172		} else {
1173			return 0;
1174		}
1175	}
1176
1177	/**
1178	 * Returns the bit depth of this file,
1179	 * or of its specified revision.
1180	 *
1181	 * @param   mixed    $revision  The index or timestamp of the revision (optional)
1182	 * @return  integer             The bit depth of this file,
1183	 *                              or -1 if revision not found
1184	 */
1185	public function getBitDepth($revision = null)
1186	{
1187		// Without revision, use current info
1188		if (!isset($revision)) {
1189			return (int)$this->info['bitdepth'];
1190		}
1191
1192		// Obtain the properties of the revision
1193		if (($info = $this->getRevision($revision)) === null) {
1194			return -1;
1195		}
1196
1197		return (int)$info['bitdepth'];
1198	}
1199
1200	/**
1201	 * Returns the canonical title of this file,
1202	 * or of its specified revision.
1203	 *
1204	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1205	 * @return  mixed             The canonical title of this file (string),
1206	 *                            or null if revision not found
1207	 */
1208	public function getCanonicalTitle($revision = null)
1209	{
1210		// Without revision, use current info
1211		if (!isset($revision)) {
1212			return $this->info['canonicaltitle'];
1213		}
1214
1215		// Obtain the properties of the revision
1216		if (($info = $this->getRevision($revision)) === null) {
1217			return null;
1218		}
1219
1220		return $info['canonicaltitle'];
1221	}
1222
1223	/**
1224	 * Returns the edit comment of this file,
1225	 * or of its specified revision.
1226	 *
1227	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1228	 * @return  mixed             The edit comment of this file (string),
1229	 *                            or null if revision not found
1230	 */
1231	public function getComment($revision = null)
1232	{
1233		// Without revision, use current info
1234		if (!isset($revision)) {
1235			return $this->info['comment'];
1236		}
1237
1238		// Obtain the properties of the revision
1239		if (($info = $this->getRevision($revision)) === null) {
1240			return null;
1241		}
1242
1243		return $info['comment'];
1244	}
1245
1246	/**
1247	 * Returns the common metadata of this file,
1248	 * or of its specified revision.
1249	 *
1250	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1251	 * @return  mixed             The common metadata of this file (array),
1252	 *                            or null if revision not found
1253	 */
1254	public function getCommonMetadata($revision = null)
1255	{
1256		// Without revision, use current info
1257		if (!isset($revision)) {
1258			return $this->info['commonmetadata'];
1259		}
1260
1261		// Obtain the properties of the revision
1262		if (($info = $this->getRevision($revision)) === null) {
1263			return null;
1264		}
1265
1266		return $info['commonmetadata'];
1267	}
1268
1269	/**
1270	 * Returns the description URL of this file,
1271	 * or of its specified revision.
1272	 *
1273	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1274	 * @return  mixed             The description URL of this file (string),
1275	 *                            or null if revision not found
1276	 */
1277	public function getDescriptionUrl($revision = null)
1278	{
1279		// Without revision, use current info
1280		if (!isset($revision)) {
1281			return $this->info['descriptionurl'];
1282		}
1283
1284		// Obtain the properties of the revision
1285		if (($info = $this->getRevision($revision)) === null) {
1286			return null;
1287		}
1288
1289		return $info['descriptionurl'];
1290	}
1291
1292	/**
1293	 * Returns the extended metadata of this file,
1294	 * or of its specified revision.
1295	 *
1296	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1297	 * @return  mixed             The extended metadata of this file (array),
1298	 *                            or null if revision not found
1299	 */
1300	public function getExtendedMetadata($revision = null)
1301	{
1302		// Without revision, use current info
1303		if (!isset($revision)) {
1304			return $this->info['extmetadata'];
1305		}
1306
1307		// Obtain the properties of the revision
1308		if (($info = $this->getRevision($revision)) === null) {
1309			return null;
1310		}
1311
1312		return $info['extmetadata'];
1313	}
1314
1315	/**
1316	 * Returns the height of this file,
1317	 * or of its specified revision.
1318	 *
1319	 * @param   mixed    $revision  The index or timestamp of the revision (optional)
1320	 * @return  integer             The height of this file, or -1 if revision not found
1321	 */
1322	public function getHeight($revision = null)
1323	{
1324		// Without revision, use current info
1325		if (!isset($revision)) {
1326			return (int)$this->info['height'];
1327		}
1328
1329		// Obtain the properties of the revision
1330		if (($info = $this->getRevision($revision)) === null) {
1331			return -1;
1332		}
1333
1334		return (int)$info['height'];
1335	}
1336
1337	/**
1338	 * Returns the media type of this file,
1339	 * or of its specified revision.
1340	 *
1341	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1342	 * @return  mixed             The media type of this file (string),
1343	 *                            or null if revision not found
1344	 */
1345	public function getMediaType($revision = null)
1346	{
1347		// Without revision, use current info
1348		if (!isset($revision)) {
1349			return $this->info['mediatype'];
1350		}
1351
1352		// Obtain the properties of the revision
1353		if (($info = $this->getRevision($revision)) === null) {
1354			return null;
1355		}
1356
1357		return $info['mediatype'];
1358	}
1359
1360	/**
1361	 * Returns the Exif metadata of this file,
1362	 * or of its specified revision.
1363	 *
1364	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1365	 * @return  mixed             The metadata of this file (array),
1366	 *                            or null if revision not found
1367	 */
1368	public function getMetadata($revision = null)
1369	{
1370		// Without revision, use current info
1371		if (!isset($revision)) {
1372			return $this->info['metadata'];
1373		}
1374
1375		// Obtain the properties of the revision
1376		if (($info = $this->getRevision($revision)) === null) {
1377			return null;
1378		}
1379
1380		return $info['metadata'];
1381	}
1382
1383	/**
1384	 * Returns the MIME type of this file,
1385	 * or of its specified revision.
1386	 *
1387	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1388	 * @return  mixed             The MIME type of this file (string),
1389	 *                            or null if revision not found
1390	 */
1391	public function getMime($revision = null)
1392	{
1393		// Without revision, use current info
1394		if (!isset($revision)) {
1395			return $this->info['mime'];
1396		}
1397
1398		// Obtain the properties of the revision
1399		if (($info = $this->getRevision($revision)) === null) {
1400			return null;
1401		}
1402
1403		return $info['mime'];
1404	}
1405
1406	/**
1407	 * Returns the parsed edit comment of this file,
1408	 * or of its specified revision.
1409	 *
1410	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1411	 * @return  mixed             The parsed edit comment of this file (string),
1412	 *                            or null if revision not found
1413	 */
1414	public function getParsedComment($revision = null)
1415	{
1416		// Without revision, use current info
1417		if (!isset($revision)) {
1418			return $this->info['parsedcomment'];
1419		}
1420
1421		// Obtain the properties of the revision
1422		if (($info = $this->getRevision($revision)) === null) {
1423			return null;
1424		}
1425
1426		return $info['parsedcomment'];
1427	}
1428
1429	/**
1430	 * Returns the SHA-1 hash of this file,
1431	 * or of its specified revision.
1432	 *
1433	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1434	 * @return  mixed             The SHA-1 hash of this file (string),
1435	 *                            or null if revision not found
1436	 */
1437	public function getSha1($revision = null)
1438	{
1439		// Without revision, use current info
1440		if (!isset($revision)) {
1441			return $this->info['sha1'];
1442		}
1443
1444		// Obtain the properties of the revision
1445		if (($info = $this->getRevision($revision)) === null) {
1446			return null;
1447		}
1448
1449		return $info['sha1'];
1450	}
1451
1452	/**
1453	 * Returns the size of this file,
1454	 * or of its specified revision.
1455	 *
1456	 * @param   mixed    $revision  The index or timestamp of the revision (optional)
1457	 * @return  integer             The size of this file, or -1 if revision not found
1458	 */
1459	public function getSize($revision = null)
1460	{
1461		// Without revision, use current info
1462		if (!isset($revision)) {
1463			return (int)$this->info['size'];
1464		}
1465
1466		// Obtain the properties of the revision
1467		if (($info = $this->getRevision($revision)) === null) {
1468			return -1;
1469		}
1470
1471		return (int)$info['size'];
1472	}
1473
1474	/**
1475	 * Returns the MIME type of this file's thumbnail,
1476	 * or of its specified revision.
1477	 * Returns empty string if property not available for this file type.
1478	 *
1479	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1480	 * @return  mixed             The MIME type of this file's thumbnail (string),
1481	 *                            or '' if unavailable, or null if revision not found
1482	 */
1483	public function getThumbMime($revision = null)
1484	{
1485		// Without revision, use current info
1486		if (!isset($revision)) {
1487			return (isset($this->info['thumbmime']) ? $this->info['thumbmime'] : '');
1488		}
1489
1490		// Obtain the properties of the revision
1491		if (($info = $this->getRevision($revision)) === null) {
1492			return null;
1493		}
1494
1495		// Check for thumbnail MIME type
1496		return (isset($info['thumbmime']) ? $info['thumbmime'] : '');
1497	}
1498
1499	/**
1500	 * Returns the timestamp of this file,
1501	 * or of its specified revision.
1502	 *
1503	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1504	 * @return  mixed             The timestamp of this file (string),
1505	 *                            or null if revision not found
1506	 */
1507	public function getTimestamp($revision = null)
1508	{
1509		// Without revision, use current info
1510		if (!isset($revision)) {
1511			return $this->info['timestamp'];
1512		}
1513
1514		// Obtain the properties of the revision
1515		if (($info = $this->getRevision($revision)) === null) {
1516			return null;
1517		}
1518
1519		return $info['timestamp'];
1520	}
1521
1522	/**
1523	 * Returns the URL of this file,
1524	 * or of its specified revision.
1525	 *
1526	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1527	 * @return  mixed             The URL of this file (string),
1528	 *                            or null if revision not found
1529	 */
1530	public function getUrl($revision = null)
1531	{
1532		// Without revision, use current info
1533		if (!isset($revision)) {
1534			return $this->info['url'];
1535		}
1536
1537		// Obtain the properties of the revision
1538		if (($info = $this->getRevision($revision)) === null) {
1539			return null;
1540		}
1541
1542		return $info['url'];
1543	}
1544
1545	/**
1546	 * Returns the user who uploaded this file,
1547	 * or of its specified revision.
1548	 *
1549	 * @param   mixed  $revision  The index or timestamp of the revision (optional)
1550	 * @return  mixed             The user of this file (string),
1551	 *                            or null if revision not found
1552	 */
1553	public function getUser($revision = null)
1554	{
1555		// Without revision, use current info
1556		if (!isset($revision)) {
1557			return $this->info['user'];
1558		}
1559
1560		// Obtain the properties of the revision
1561		if (($info = $this->getRevision($revision)) === null) {
1562			return null;
1563		}
1564
1565		return $info['user'];
1566	}
1567
1568	/**
1569	 * Returns the ID of the user who uploaded this file,
1570	 * or of its specified revision.
1571	 *
1572	 * @param   mixed    $revision  The index or timestamp of the revision (optional)
1573	 * @return  integer             The user ID of this file,
1574	 *                              or -1 if revision not found
1575	 */
1576	public function getUserId($revision = null)
1577	{
1578		// Without revision, use current info
1579		if (!isset($revision)) {
1580			return (int)$this->info['userid'];
1581		}
1582
1583		// Obtain the properties of the revision
1584		if (($info = $this->getRevision($revision)) === null) {
1585			return -1;
1586		}
1587
1588		return (int)$info['userid'];
1589	}
1590
1591	/**
1592	 * Returns the width of this file,
1593	 * or of its specified revision.
1594	 *
1595	 * @param   mixed    $revision  The index or timestamp of the revision (optional)
1596	 * @return  integer             The width of this file, or -1 if revision not found
1597	 */
1598	public function getWidth($revision = null)
1599	{
1600		// Without revision, use current info
1601		if (!isset($revision)) {
1602			return (int)$this->info['width'];
1603		}
1604
1605		// Obtain the properties of the revision
1606		if (($info = $this->getRevision($revision)) === null) {
1607			return -1;
1608		}
1609
1610		return (int)$info['width'];
1611	}
1612
1613	/*
1614	 *
1615	 * File history & deletion methods
1616	 *
1617	 */
1618
1619	/**
1620	 * Returns the revision history of this file with all properties.
1621	 * The initial history at object creation contains only the
1622	 * current revision of the file. To obtain more revisions,
1623	 * set $refresh to true and also optionally set $limit and
1624	 * the timestamps.
1625	 *
1626	 * The maximum limit is 500 for user accounts and 5000 for bot accounts.
1627	 *
1628	 * Timestamps can be in several formats as described here:
1629	 * https://www.mediawiki.org/w/api.php?action=help&modules=main#main.2Fdatatypes
1630	 *
1631	 * @param   boolean  $refresh  True to query the wiki API again
1632	 * @param   integer  $limit    The number of file revisions to return
1633	 *                             (the maximum number by default)
1634	 * @param   string   $startts  The start timestamp of the listing (optional)
1635	 * @param   string   $endts    The end timestamp of the listing (optional)
1636	 * @return  mixed              The array of selected file revisions, or null if error
1637	 */
1638	public function getHistory($refresh = false, $limit = null, $startts = null, $endts = null)
1639	{
1640		if ($refresh) { // We want to query the API
1641			// Collect optional history parameters
1642			$history = array();
1643			if (!is_null($limit)) {
1644				$history['iilimit'] = $limit;
1645			} else {
1646				$history['iilimit'] = 'max';
1647			}
1648			if (!is_null($startts)) {
1649				$history['iistart'] = $startts;
1650			}
1651			if (!is_null($endts)) {
1652				$history['iiend'] = $endts;
1653			}
1654
1655			// Get file revision history
1656			if ($this->getInfo($refresh, $history) === null) {
1657				return null;
1658			}
1659		}
1660
1661		return $this->history;
1662	}
1663
1664	/**
1665	 * Returns the properties of the specified file revision.
1666	 *
1667	 * Revision can be the following:
1668	 * - revision timestamp (string, e.g. "2001-01-15T14:56:00Z")
1669	 * - revision index (int, e.g. 3)
1670	 * The most recent revision has index 0,
1671	 * and it increments towards older revisions.
1672	 * A timestamp must be in ISO 8601 format.
1673	 *
1674	 * @param   mixed  $revision  The index or timestamp of the revision
1675	 * @return  mixed             The properties (array), or null if not found
1676	 */
1677	public function getRevision($revision)
1678	{
1679		// Select revision by index
1680		if (is_int($revision)) {
1681			if (isset($this->history[$revision])) {
1682				return $this->history[$revision];
1683			}
1684		// Search revision by timestamp
1685		} else {
1686			foreach ($this->history as $history) {
1687				if ($history['timestamp'] == $revision) {
1688					return $history;
1689				}
1690			}
1691		}
1692
1693		// Return error message
1694		$this->error = array();
1695		$this->error['file'] = "Revision '$revision' was not found for this file";
1696		return null;
1697	}
1698
1699	/**
1700	 * Returns the archive name of the specified file revision.
1701	 *
1702	 * Revision can be the following:
1703	 * - revision timestamp (string, e.g. "2001-01-15T14:56:00Z")
1704	 * - revision index (int, e.g. 3)
1705	 * The most recent revision has index 0,
1706	 * and it increments towards older revisions.
1707	 * A timestamp must be in ISO 8601 format.
1708	 *
1709	 * @param   mixed  $revision  The index or timestamp of the revision
1710	 * @return  mixed             The archive name (string), or null if not found
1711	 */
1712	public function getArchivename($revision)
1713	{
1714		// Obtain the properties of the revision
1715		if (($info = $this->getRevision($revision)) === null) {
1716			return null;
1717		}
1718
1719		// Check for archive name
1720		if (!isset($info['archivename'])) {
1721			// Return error message
1722			$this->error = array();
1723			$this->error['file'] = 'This revision contains no archive name';
1724			return null;
1725		}
1726
1727		return $info['archivename'];
1728	}
1729
1730	/**
1731	 * Delete the file, or only an older revision of it.
1732	 *
1733	 * @param   string   $reason       Reason for the deletion
1734	 * @param   string   $archivename  The archive name of the older revision
1735	 * @return  boolean                True if file (revision) was deleted successfully
1736	 */
1737	public function delete($reason = null, $archivename = null)
1738	{
1739		$data = array(
1740			'title' => 'File:' . $this->filename,
1741			'token' => $this->edittoken,
1742		);
1743
1744		// Set options from arguments
1745		if (!is_null($reason)) {
1746			$data['reason'] = $reason;
1747		}
1748		if (!is_null($archivename)) {
1749			$data['oldimage'] = $archivename;
1750		}
1751
1752		$r = $this->wikimate->delete($data); // The delete query
1753
1754		// Check if it worked
1755		if (isset($r['delete'])) {
1756			if (is_null($archivename)) {
1757				$this->exists = false; // The file was deleted altogether
1758			}
1759
1760			$this->error = null; // Reset the error status
1761			return true;
1762		}
1763
1764		$this->error = $r['error']; // Return error response
1765		return false;
1766	}
1767
1768	/*
1769	 *
1770	 * File contents methods
1771	 *
1772	 */
1773
1774	/**
1775	 * Downloads and returns the current file's contents,
1776	 * or null if an error occurs.
1777	 *
1778	 * @return  mixed  Contents (string), or null if error
1779	 */
1780	public function downloadData()
1781	{
1782		// Download file, or handle error
1783		$data = $this->wikimate->download($this->getUrl());
1784		if ($data === null) {
1785			$this->error = $this->wikimate->getError(); // Copy error if there was one
1786		} else {
1787			$this->error = null; // Reset the error status
1788		}
1789
1790		return $data;
1791	}
1792
1793	/**
1794	 * Downloads the current file's contents and writes it to the given path.
1795	 *
1796	 * @param   string   $path  The file path to write to
1797	 * @return  boolean         True if path was written successfully
1798	 */
1799	public function downloadFile($path)
1800	{
1801		// Download contents of current file
1802		if (($data = $this->downloadData()) === null) {
1803			return false;
1804		}
1805
1806		// Write contents to specified path
1807		if (@file_put_contents($path, $data) === false) {
1808			$this->error = array();
1809			$this->error['file'] = "Unable to write file '$path'";
1810			return false;
1811		}
1812
1813		return true;
1814	}
1815
1816	/**
1817	 * Uploads to the current file using the given parameters.
1818	 * $text is only used for the page contents of a new file,
1819	 * not an existing one (update that via WikiPage::setText()).
1820	 * If no $text is specified, $comment will be used as new page text.
1821	 *
1822	 * @param   array    $params     The upload parameters
1823	 * @param   string   $comment    Upload comment for the file
1824	 * @param   string   $text       The article text for the file page
1825	 * @param   boolean  $overwrite  True to overwrite existing file
1826	 * @return  boolean              True if uploading was successful
1827	 */
1828	private function uploadCommon(array $params, $comment, $text = null, $overwrite = false)
1829	{
1830		// Check whether to overwrite existing file
1831		if ($this->exists && !$overwrite) {
1832			$this->error = array();
1833			$this->error['file'] = 'Cannot overwrite existing file';
1834			return false;
1835		}
1836
1837		// Collect upload parameters
1838		$params['filename']       = $this->filename;
1839		$params['comment']        = $comment;
1840		$params['ignorewarnings'] = $overwrite;
1841		$params['token']          = $this->edittoken;
1842		if (!is_null($text)) {
1843			$params['text']   = $text;
1844		}
1845
1846		// Upload file, or handle error
1847		$r = $this->wikimate->upload($params);
1848
1849		if (isset($r['upload']['result']) && $r['upload']['result'] == 'Success') {
1850			// Update the file's properties
1851			$this->info = $r['upload']['imageinfo'];
1852
1853			$this->error = null; // Reset the error status
1854			return true;
1855		}
1856
1857		// Return error response
1858		if (isset($r['error'])) {
1859			$this->err…

Large files files are truncated, but you can click here to view the full file