/Wikimate.php
PHP | 2470 lines | 1084 code | 288 blank | 1098 comment | 191 complexity | 5ada9f65ebcdcc20fc762db946c09f69 MD5 | raw file
Possible License(s): MIT
Large files files are truncated, but you can click here to view the full file
- <?php
- /**
- * Wikimate is a wrapper for the MediaWiki API that aims to be very easy to use.
- *
- * @package Wikimate
- * @version 1.0.0
- * @copyright SPDX-License-Identifier: MIT
- */
- /**
- * Provides an interface over wiki API objects such as pages and files.
- *
- * All requests to the API can throw WikimateException if the server is lagged
- * and a finite number of retries is exhausted. By default requests are
- * retried indefinitely. See {@see Wikimate::request()} for more information.
- *
- * @author Robert McLeod & Frans P. de Vries
- * @since 0.2 December 2010
- */
- class Wikimate
- {
- /**
- * The current version number (conforms to Semantic Versioning)
- *
- * @var string
- * @link https://semver.org/
- */
- const VERSION = '1.0.0';
- /**
- * Identifier for CSRF token
- *
- * @var string
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tokens
- */
- const TOKEN_DEFAULT = 'csrf';
- /**
- * Identifier for Login token
- *
- * @var string
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tokens
- */
- const TOKEN_LOGIN = 'login';
- /**
- * Default lag value in seconds
- *
- * @var integer
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Maxlag_parameter
- */
- const MAXLAG_DEFAULT = 5;
- /**
- * Base URL for API requests
- *
- * @var string
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Main_page#Endpoint
- */
- protected $api;
- /**
- * Default headers for Requests_Session
- *
- * @var array
- */
- protected $headers;
- /**
- * Default data for Requests_Session
- *
- * @var array
- */
- protected $data;
- /**
- * Default options for Requests_Session
- *
- * @var array
- */
- protected $options;
- /**
- * Username for API requests
- *
- * @var string
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Login#Method_1._login
- */
- protected $username;
- /**
- * Password for API requests
- *
- * @var string
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Login#Method_1._login
- */
- protected $password;
- /**
- * Session object for HTTP requests
- *
- * @var Requests_Session
- * @link https://requests.ryanmccue.info/
- */
- protected $session;
- /**
- * User agent string for Requests_Session
- *
- * @var string
- * @link https://requests.ryanmccue.info/docs/usage-advanced.html#session-handling
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Etiquette#The_User-Agent_header
- */
- protected $useragent;
- /**
- * Error array with API and Wikimate errors
- *
- * @var array|null
- */
- protected $error = null;
- /**
- * Whether to output debug logging
- *
- * @var boolean
- */
- protected $debugMode = false;
- /**
- * Maximum lag in seconds to accept in requests
- *
- * @var integer
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Maxlag_parameter
- */
- protected $maxlag = self::MAXLAG_DEFAULT;
- /**
- * Maximum number of retries for lagged requests (-1 = retry indefinitely)
- *
- * @var integer
- */
- protected $maxretries = -1;
- /**
- * Stored CSRF token for API requests
- *
- * @var string|null
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tokens
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit#Additional_notes
- */
- private $csrfToken = null;
- /**
- * Creates a new Wikimate object.
- *
- * @param string $api Base URL for the API
- * @param array $headers Default headers for API requests
- * @param array $data Default data for API requests
- * @param array $options Default options for API requests
- * @return Wikimate
- */
- public function __construct($api, $headers = array(), $data = array(), $options = array())
- {
- $this->api = $api;
- $this->headers = $headers;
- $this->data = $data;
- $this->options = $options;
- $this->initRequests();
- }
- /**
- * Sets up a Requests_Session with appropriate user agent.
- *
- * @return void
- * @link https://requests.ryanmccue.info/docs/usage-advanced.html#session-handling
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Etiquette#The_User-Agent_header
- */
- protected function initRequests()
- {
- $this->useragent = 'Wikimate/' . self::VERSION . ' (https://github.com/hamstar/Wikimate)';
- $this->session = new Requests_Session($this->api, $this->headers, $this->data, $this->options);
- $this->session->useragent = $this->useragent;
- }
- /**
- * Sends a GET or POST request in JSON format to the API.
- *
- * This method handles maxlag errors as advised at:
- * {@see https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Maxlag_parameter}
- * The request is sent with the current maxlag value
- * (default: 5 seconds, per MAXLAG_DEFAULT).
- * If a lag error is received, the method waits (sleeps) for the
- * recommended time (per the Retry-After header), then tries again.
- * It will do this indefinitely unless the number of retries is limited,
- * in which case WikimateException is thrown once the limit is reached.
- *
- * The string type for $data is used only for upload POST requests,
- * and must contain the complete multipart body, including maxlag.
- *
- * @param array|string $data Data for the request
- * @param array $headers Optional extra headers to send with the request
- * @param boolean $post True to send a POST request, otherwise GET
- * @return array The API response
- * @throw WikimateException If lagged and ran out of retries,
- * or got an unexpected API response
- */
- private function request($data, $headers = array(), $post = false)
- {
- $retries = 0;
- // Add format & maxlag parameter to request
- if (is_array($data)) {
- $data['format'] = 'json';
- $data['maxlag'] = $this->getMaxlag();
- $action = $data['action'];
- } else {
- $action = 'upload';
- }
- // Define type of HTTP request for messages
- $httptype = $post ? 'POST' : 'GET';
- // Send appropriate type of request, once or multiple times
- do {
- if ($post) {
- // Debug logging of POST requests, except for upload string
- if ($this->debugMode && is_array($data)) {
- echo "$action $httptype parameters:\n";
- print_r($data);
- }
- $response = $this->session->post($this->api, $headers, $data);
- } else {
- // Debug logging of GET requests as a query string
- if ($this->debugMode) {
- echo "$action $httptype parameters:\n";
- echo http_build_query($data) . "\n";
- }
- $response = $this->session->get($this->api . '?' . http_build_query($data), $headers);
- }
- // Check for replication lag error
- $serverLagged = ($response->headers->offsetGet('X-Database-Lag') !== null);
- if ($serverLagged) {
- // Determine recommended or default delay
- if ($response->headers->offsetGet('Retry-After') !== null) {
- $sleep = (int)$response->headers->offsetGet('Retry-After');
- } else {
- $sleep = $this->getMaxlag();
- }
- if ($this->debugMode) {
- preg_match('/Waiting for [^ ]*: ([0-9.-]+) seconds? lagged/', $response->body, $match);
- echo "Server lagged for {$match[1]} seconds; will retry in {$sleep} seconds\n";
- }
- sleep($sleep);
- // Check retries limit
- if ($this->getMaxretries() >= 0) {
- $retries++;
- } else {
- $retries = -1; // continue indefinitely
- }
- }
- } while ($serverLagged && $retries <= $this->getMaxretries());
- // Throw exception if we ran out of retries
- if ($serverLagged) {
- throw new WikimateException("Server lagged ($retries consecutive maxlag responses)");
- }
- // Check if we got the API doc page (invalid request)
- if (strpos($response->body, "This is an auto-generated MediaWiki API documentation page") !== false) {
- throw new WikimateException("The API could not understand the $action $httptype request");
- }
- // Check if we got a JSON result
- $result = json_decode($response->body, true);
- if ($result === null) {
- throw new WikimateException("The API did not return the $action JSON response");
- }
- if ($this->debugMode) {
- echo "$action $httptype response:\n";
- print_r($result);
- }
- return $result;
- }
- /**
- * Obtains a wiki token for logging in or data-modifying actions.
- *
- * If a CSRF (default) token is requested, it is stored and returned
- * upon further such requests, instead of making another API call.
- * The stored token is discarded via {@see Wikimate::logout()}.
- *
- * For now this method, in Wikimate tradition, is kept simple and supports
- * only the two token types needed elsewhere in the library. It also
- * doesn't support the option to request multiple tokens at once.
- * See {@see https://www.mediawiki.org/wiki/Special:MyLanguage/API:Tokens}
- * for more information.
- *
- * @param string $type The token type
- * @return mixed The requested token (string), or null if error
- */
- protected function token($type = self::TOKEN_DEFAULT)
- {
- // Check for supported token types
- if ($type != self::TOKEN_DEFAULT && $type != self::TOKEN_LOGIN) {
- $this->error = array();
- $this->error['token'] = 'The API does not support the token type';
- return null;
- }
- // Check for existing CSRF token for this login session
- if ($type == self::TOKEN_DEFAULT && $this->csrfToken !== null) {
- return $this->csrfToken;
- }
- $details = array(
- 'action' => 'query',
- 'meta' => 'tokens',
- 'type' => $type,
- );
- // Send the token request
- $tokenResult = $this->request($details, array(), true);
- // Check for errors
- if (isset($tokenResult['error'])) {
- $this->error = $tokenResult['error']; // Set the error if there was one
- return null;
- } else {
- $this->error = null; // Reset the error status
- }
- if ($type == self::TOKEN_LOGIN) {
- return $tokenResult['query']['tokens']['logintoken'];
- } else {
- // Store CSRF token for this login session
- $this->csrfToken = $tokenResult['query']['tokens']['csrftoken'];
- return $this->csrfToken;
- }
- }
- /**
- * Logs in to the wiki.
- *
- * @param string $username The user name
- * @param string $password The user password
- * @param string $domain The domain (optional)
- * @return boolean True if logged in
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Login#Method_1._login
- */
- public function login($username, $password, $domain = null)
- {
- // Obtain login token first
- if (($logintoken = $this->token(self::TOKEN_LOGIN)) === null) {
- return false;
- }
- $details = array(
- 'action' => 'login',
- 'lgname' => $username,
- 'lgpassword' => $password,
- 'lgtoken' => $logintoken,
- );
- // If $domain is provided, set the corresponding detail in the request information array
- if (is_string($domain)) {
- $details['lgdomain'] = $domain;
- }
- // Send the login request
- $loginResult = $this->request($details, array(), true);
- // Check for errors
- if (isset($loginResult['error'])) {
- $this->error = $loginResult['error']; // Set the error if there was one
- return false;
- } else {
- $this->error = null; // Reset the error status
- }
- if (isset($loginResult['login']['result']) && $loginResult['login']['result'] != 'Success') {
- // Some more comprehensive error checking
- $this->error = array();
- switch ($loginResult['login']['result']) {
- case 'Failed':
- $this->error['auth'] = 'Incorrect username or password';
- break;
- default:
- $this->error['auth'] = 'The API result was: ' . $loginResult['login']['result'];
- break;
- }
- return false;
- }
- return true;
- }
- /**
- * Logs out of the wiki and discard CSRF token.
- *
- * @return boolean True if logged out
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Logout
- */
- public function logout()
- {
- // Obtain logout token first
- if (($logouttoken = $this->token()) === null) {
- return false;
- }
- // Token is needed in MediaWiki v1.34+, older versions produce an
- // 'Unrecognized parameter' warning which can be ignored
- $details = array(
- 'action' => 'logout',
- 'token' => $logouttoken,
- );
- // Send the logout request
- $logoutResult = $this->request($details, array(), true);
- // Check for errors
- if (isset($logoutResult['error'])) {
- $this->error = $logoutResult['error']; // Set the error if there was one
- return false;
- } else {
- $this->error = null; // Reset the error status
- }
- // Discard CSRF token for this login session
- $this->csrfToken = null;
- return true;
- }
- /**
- * Gets the current value of the maxlag parameter.
- *
- * @return integer The maxlag value in seconds
- */
- public function getMaxlag()
- {
- return $this->maxlag;
- }
- /**
- * Sets the new value of the maxlag parameter.
- *
- * @param integer $ml The new maxlag value in seconds
- * @return Wikimate This object
- */
- public function setMaxlag($ml)
- {
- $this->maxlag = (int)$ml;
- return $this;
- }
- /**
- * Gets the current value of the max retries limit.
- *
- * @return integer The max retries limit
- */
- public function getMaxretries()
- {
- return $this->maxretries;
- }
- /**
- * Sets the new value of the max retries limit.
- *
- * @param integer $mr The new max retries limit
- * @return Wikimate This object
- */
- public function setMaxretries($mr)
- {
- $this->maxretries = (int)$mr;
- return $this;
- }
- /**
- * Gets the user agent for API requests.
- *
- * @return string The default user agent, or the current one defined
- * by {@see Wikimate::setUserAgent()}
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Etiquette#The_User-Agent_header
- */
- public function getUserAgent()
- {
- return $this->useragent;
- }
- /**
- * Sets the user agent for API requests.
- *
- * In order to use a custom user agent for all requests in the session,
- * call this method before invoking {@see Wikimate::login()}.
- *
- * @param string $ua The new user agent
- * @return Wikimate This object
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Etiquette#The_User-Agent_header
- */
- public function setUserAgent($ua)
- {
- $this->useragent = (string)$ua;
- // Update the session
- $this->session->useragent = $this->useragent;
- return $this;
- }
- /**
- * Sets the debug mode.
- *
- * @param boolean $b True to turn debugging on
- * @return Wikimate This object
- */
- public function setDebugMode($b)
- {
- $this->debugMode = $b;
- return $this;
- }
- /**
- * Gets or prints the Requests configuration.
- *
- * @param boolean $echo Whether to echo the session options and headers
- * @return mixed Options array if $echo is false, or
- * True if options/headers have been echoed to STDOUT
- */
- public function debugRequestsConfig($echo = false)
- {
- if ($echo) {
- echo "<pre>Requests options:\n";
- print_r($this->session->options);
- echo "Requests headers:\n";
- print_r($this->session->headers);
- echo "</pre>";
- return true;
- }
- return $this->session->options;
- }
- /**
- * Returns a WikiPage object populated with the page data.
- *
- * @param string $title The name of the wiki article
- * @return WikiPage The page object
- */
- public function getPage($title)
- {
- return new WikiPage($title, $this);
- }
- /**
- * Returns a WikiFile object populated with the file data.
- *
- * @param string $filename The name of the wiki file
- * @return WikiFile The file object
- */
- public function getFile($filename)
- {
- return new WikiFile($filename, $this);
- }
- /**
- * Performs a query to the wiki API with the given details.
- *
- * @param array $array Array of details to be passed in the query
- * @return array Decoded JSON output from the wiki API
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Query
- */
- public function query($array)
- {
- $array['action'] = 'query';
- return $this->request($array);
- }
- /**
- * Performs a parse query to the wiki API.
- *
- * @param array $array Array of details to be passed in the query
- * @return array Decoded JSON output from the wiki API
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext
- */
- public function parse($array)
- {
- $array['action'] = 'parse';
- return $this->request($array);
- }
- /**
- * Perfoms an edit query to the wiki API.
- *
- * @param array $array Array of details to be passed in the query
- * @return array|boolean Decoded JSON output from the wiki API
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit
- */
- public function edit($array)
- {
- // Obtain default token first
- if (($edittoken = $this->token()) === null) {
- return false;
- }
- $headers = array(
- 'Content-Type' => "application/x-www-form-urlencoded"
- );
- $array['action'] = 'edit';
- $array['token'] = $edittoken;
- return $this->request($array, $headers, true);
- }
- /**
- * Perfoms a delete query to the wiki API.
- *
- * @param array $array Array of details to be passed in the query
- * @return array|boolean Decoded JSON output from the wiki API
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Delete
- */
- public function delete($array)
- {
- // Obtain default token first
- if (($deletetoken = $this->token()) === null) {
- return false;
- }
- $headers = array(
- 'Content-Type' => "application/x-www-form-urlencoded"
- );
- $array['action'] = 'delete';
- $array['token'] = $deletetoken;
- return $this->request($array, $headers, true);
- }
- /**
- * Downloads data from the given URL.
- *
- * @param string $url The URL to download from
- * @return mixed The downloaded data (string), or null if error
- */
- public function download($url)
- {
- $getResult = $this->session->get($url);
- if (!$getResult->success) {
- // Debug logging of Requests_Response only on failed download
- if ($this->debugMode) {
- echo "download GET response:\n";
- print_r($getResult);
- }
- $this->error = array();
- $this->error['file'] = 'Download error (HTTP status: ' . $getResult->status_code . ')';
- $this->error['http'] = $getResult->status_code;
- return null;
- }
- return $getResult->body;
- }
- /**
- * Uploads a file to the wiki API.
- *
- * @param array $array Array of details to be used in the upload
- * @return array|boolean Decoded JSON output from the wiki API
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Upload
- */
- public function upload($array)
- {
- // Obtain default token first
- if (($uploadtoken = $this->token()) === null) {
- return false;
- }
- $array['action'] = 'upload';
- $array['format'] = 'json';
- $array['maxlag'] = $this->getMaxlag();
- $array['token'] = $uploadtoken;
- // Construct multipart body:
- // https://www.mediawiki.org/w/index.php?title=API:Upload&oldid=2293685#Sample_Raw_Upload
- // https://www.mediawiki.org/w/index.php?title=API:Upload&oldid=2339771#Sample_Raw_POST_of_a_single_chunk
- $boundary = '---Wikimate-' . md5(microtime());
- $body = '';
- foreach ($array as $fieldName => $fieldData) {
- $body .= "--{$boundary}\r\n";
- $body .= 'Content-Disposition: form-data; name="' . $fieldName . '"';
- // Process the (binary) file
- if ($fieldName == 'file') {
- $body .= '; filename="' . $array['filename'] . '"' . "\r\n";
- $body .= "Content-Type: application/octet-stream; charset=UTF-8\r\n";
- $body .= "Content-Transfer-Encoding: binary\r\n";
- // Process text parameters
- } else {
- $body .= "\r\n";
- $body .= "Content-Type: text/plain; charset=UTF-8\r\n";
- $body .= "Content-Transfer-Encoding: 8bit\r\n";
- }
- $body .= "\r\n{$fieldData}\r\n";
- }
- $body .= "--{$boundary}--\r\n";
- // Construct multipart headers
- $headers = array(
- 'Content-Type' => "multipart/form-data; boundary={$boundary}",
- 'Content-Length' => strlen($body),
- );
- return $this->request($body, $headers, true);
- }
- /**
- * Performs a file revert query to the wiki API.
- *
- * @param array $array Array of details to be passed in the query
- * @return array|boolean Decoded JSON output from the wiki API
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Filerevert
- */
- public function filerevert($array)
- {
- // Obtain default token first
- if (($reverttoken = $this->token()) === null) {
- return false;
- }
- $array['action'] = 'filerevert';
- $array['token'] = $reverttoken;
- $headers = array(
- 'Content-Type' => "application/x-www-form-urlencoded"
- );
- return $this->request($array, $headers, true);
- }
- /**
- * Returns the latest error if there is one.
- *
- * @return mixed The error array, or null if no error
- */
- public function getError()
- {
- return $this->error;
- }
- }
- /**
- * Defines Wikimate's exception for unexpected run-time errors
- * while communicating with the API.
- * WikimateException can be thrown only from Wikimate::request(),
- * and is propagated to callers of this library.
- *
- * @author Frans P. de Vries
- * @since 1.0.0 August 2021
- * @link https://www.php.net/manual/en/class.runtimeexception.php
- */
- class WikimateException extends RuntimeException
- {
- }
- /**
- * Models a wiki article page that can have its text altered and retrieved.
- *
- * @author Robert McLeod & Frans P. de Vries
- * @since 0.2 December 2010
- */
- class WikiPage
- {
- /**
- * Use section indexes as keys in return array of {@see WikiPage::getAllSections()}
- *
- * @var integer
- */
- const SECTIONLIST_BY_INDEX = 1;
- /**
- * Use section names as keys in return array of {@see WikiPage::getAllSections()}
- *
- * @var integer
- */
- const SECTIONLIST_BY_NAME = 2;
- /**
- * The title of the page
- *
- * @var string|null
- */
- protected $title = null;
- /**
- * Wikimate object for API requests
- *
- * @var Wikimate|null
- */
- protected $wikimate = null;
- /**
- * Whether the page exists
- *
- * @var boolean
- */
- protected $exists = false;
- /**
- * Whether the page is invalid
- *
- * @var boolean
- */
- protected $invalid = false;
- /**
- * Error array with API and WikiPage errors
- *
- * @var array|null
- */
- protected $error = null;
- /**
- * Stores the timestamp for detection of edit conflicts
- *
- * @var integer|null
- */
- protected $starttimestamp = null;
- /**
- * The complete text of the page
- *
- * @var string|null
- */
- protected $text = null;
- /**
- * The sections object for the page
- *
- * @var stdClass|null
- */
- protected $sections = null;
- /*
- *
- * Magic methods
- *
- */
- /**
- * Constructs a WikiPage object from the title given
- * and associate with the passed Wikimate object.
- *
- * @param string $title Name of the wiki article
- * @param Wikimate $wikimate Wikimate object
- */
- public function __construct($title, $wikimate)
- {
- $this->wikimate = $wikimate;
- $this->title = $title;
- $this->sections = new \stdClass();
- $this->text = $this->getText(true);
- if ($this->invalid) {
- $this->error['page'] = 'Invalid page title - cannot create WikiPage';
- }
- }
- /**
- * Forgets all object properties.
- */
- public function __destruct()
- {
- $this->title = null;
- $this->wikimate = null;
- $this->exists = false;
- $this->invalid = false;
- $this->error = null;
- $this->starttimestamp = null;
- $this->text = null;
- $this->sections = null;
- }
- /**
- * Returns the wikicode of the page.
- *
- * @return string String of wikicode
- */
- public function __toString()
- {
- return $this->text;
- }
- /**
- * Returns an array sections with the section name as the key
- * and the text as the element, e.g.
- *
- * array(
- * 'intro' => 'this text is the introduction',
- * 'History' => 'this is text under the history section'
- *)
- *
- * @return array Array of sections
- */
- public function __invoke()
- {
- return $this->getAllSections(false, self::SECTIONLIST_BY_NAME);
- }
- /**
- * Returns the page existence status.
- *
- * @return boolean True if page exists
- */
- public function exists()
- {
- return $this->exists;
- }
- /**
- * Alias of self::__destruct().
- *
- * @return void
- */
- public function destroy()
- {
- $this->__destruct();
- }
- /*
- *
- * Page meta methods
- *
- */
- /**
- * Returns the latest error if there is one.
- *
- * @return mixed The error array, or null if no error
- */
- public function getError()
- {
- return $this->error;
- }
- /**
- * Returns the title of this page.
- *
- * @return string The title of this page
- */
- public function getTitle()
- {
- return $this->title;
- }
- /**
- * Returns the number of sections in this page.
- *
- * @return integer The number of sections in this page
- */
- public function getNumSections()
- {
- return count($this->sections->byIndex);
- }
- /**
- * Returns the sections offsets and lengths.
- *
- * @return stdClass Section class
- */
- public function getSectionOffsets()
- {
- return $this->sections;
- }
- /*
- *
- * Getter methods
- *
- */
- /**
- * Gets the text of the page. If refresh is true,
- * then this method will query the wiki API again for the page details.
- *
- * @param boolean $refresh True to query the wiki API again
- * @return mixed The text of the page (string), or null if error
- */
- public function getText($refresh = false)
- {
- if ($refresh) { // We want to query the API
- // Specify relevant page properties to retrieve
- $data = array(
- 'titles' => $this->title,
- 'prop' => 'info|revisions',
- 'rvprop' => 'content', // Need to get page text
- 'curtimestamp' => 1,
- );
- $r = $this->wikimate->query($data); // Run the query
- // Check for errors
- if (isset($r['error'])) {
- $this->error = $r['error']; // Set the error if there was one
- return null;
- } else {
- $this->error = null; // Reset the error status
- }
- // Get the page (there should only be one)
- $page = array_pop($r['query']['pages']);
- // Abort if invalid page title
- if (isset($page['invalid'])) {
- $this->invalid = true;
- return null;
- }
- $this->starttimestamp = $r['curtimestamp'];
- unset($r, $data);
- if (!isset($page['missing'])) {
- // Update the existence if the page is there
- $this->exists = true;
- // Put the content into text
- $this->text = $page['revisions'][0]['*'];
- }
- unset($page);
- // Now we need to get the section headers, if any
- preg_match_all('/(={1,6}).*?\1 *(?:\n|$)/', $this->text, $matches);
- // Set the intro section (between title and first section)
- $this->sections->byIndex[0]['offset'] = 0;
- $this->sections->byName['intro']['offset'] = 0;
- // Check for section header matches
- if (empty($matches[0])) {
- // Define lengths for page consisting only of intro section
- $this->sections->byIndex[0]['length'] = strlen($this->text);
- $this->sections->byName['intro']['length'] = strlen($this->text);
- } else {
- // Array of section header matches
- $sections = $matches[0];
- // Set up the current section
- $currIndex = 0;
- $currName = 'intro';
- // Collect offsets and lengths from section header matches
- foreach ($sections as $section) {
- // Get the current offset
- $currOffset = strpos($this->text, $section, $this->sections->byIndex[$currIndex]['offset']);
- // Are we still on the first section?
- if ($currIndex == 0) {
- $this->sections->byIndex[$currIndex]['length'] = $currOffset;
- $this->sections->byIndex[$currIndex]['depth'] = 0;
- $this->sections->byName[$currName]['length'] = $currOffset;
- $this->sections->byName[$currName]['depth'] = 0;
- }
- // Get the current name and index
- $currName = trim(str_replace('=', '', $section));
- $currIndex++;
- // Search for existing name and create unique one
- $cName = $currName;
- for ($seq = 2; array_key_exists($cName, $this->sections->byName); $seq++) {
- $cName = $currName . '_' . $seq;
- }
- if ($seq > 2) {
- $currName = $cName;
- }
- // Set the offset and depth (from the matched ='s) for the current section
- $this->sections->byIndex[$currIndex]['offset'] = $currOffset;
- $this->sections->byIndex[$currIndex]['depth'] = strlen($matches[1][$currIndex - 1]);
- $this->sections->byName[$currName]['offset'] = $currOffset;
- $this->sections->byName[$currName]['depth'] = strlen($matches[1][$currIndex - 1]);
- // If there is a section after this, set the length of this one
- if (isset($sections[$currIndex])) {
- // Get the offset of the next section
- $nextOffset = strpos($this->text, $sections[$currIndex], $currOffset);
- // Calculate the length of this one
- $length = $nextOffset - $currOffset;
- // Set the length of this section
- $this->sections->byIndex[$currIndex]['length'] = $length;
- $this->sections->byName[$currName]['length'] = $length;
- } else {
- // Set the length of last section
- $this->sections->byIndex[$currIndex]['length'] = strlen($this->text) - $currOffset;
- $this->sections->byName[$currName]['length'] = strlen($this->text) - $currOffset;
- }
- }
- }
- }
- return $this->text; // Return the text in any case
- }
- /**
- * Returns the requested section, with its subsections, if any.
- *
- * Section can be the following:
- * - section name (string, e.g. "History")
- * - section index (int, e.g. 3)
- *
- * @param mixed $section The section to get
- * @param boolean $includeHeading False to get section text only,
- * true to include heading too
- * @param boolean $includeSubsections False to get section text only,
- * true to include subsections too
- * @return mixed Wikitext of the section on the page,
- * or null if section is undefined
- */
- public function getSection($section, $includeHeading = false, $includeSubsections = true)
- {
- // Check if we have a section name or index
- if (is_int($section)) {
- if (!isset($this->sections->byIndex[$section])) {
- return null;
- }
- $coords = $this->sections->byIndex[$section];
- } elseif (is_string($section)) {
- if (!isset($this->sections->byName[$section])) {
- return null;
- }
- $coords = $this->sections->byName[$section];
- } else {
- $coords = array();
- }
- // Extract the offset, depth and (initial) length
- @extract($coords);
- // Find subsections if requested, and not the intro
- if ($includeSubsections && $offset > 0) {
- $found = false;
- foreach ($this->sections->byName as $section) {
- if ($found) {
- // Include length of this subsection
- if ($depth < $section['depth']) {
- $length += $section['length'];
- // Done if not a subsection
- } else {
- break;
- }
- } else {
- // Found our section if same offset
- if ($offset == $section['offset']) {
- $found = true;
- }
- }
- }
- }
- // Extract text of section, and its subsections if requested
- $text = substr($this->text, $offset, $length);
- // Whack off the heading if requested, and not the intro
- if (!$includeHeading && $offset > 0) {
- // Chop off the first line
- $text = substr($text, strpos($text, "\n"));
- }
- return $text;
- }
- /**
- * Returns all the sections of the page in an array - the key names can be
- * set to name or index by using the following for the second param:
- * - self::SECTIONLIST_BY_NAME
- * - self::SECTIONLIST_BY_INDEX
- *
- * @param boolean $includeHeading False to get section text only
- * @param integer $keyNames Modifier for the array key names
- * @return array Array of sections
- * @throw UnexpectedValueException If $keyNames is not a supported constant
- */
- public function getAllSections($includeHeading = false, $keyNames = self::SECTIONLIST_BY_INDEX)
- {
- $sections = array();
- switch ($keyNames) {
- case self::SECTIONLIST_BY_INDEX:
- $array = array_keys($this->sections->byIndex);
- break;
- case self::SECTIONLIST_BY_NAME:
- $array = array_keys($this->sections->byName);
- break;
- default:
- throw new \UnexpectedValueException("Unexpected keyNames parameter " .
- "($keyNames) passed to WikiPage::getAllSections()");
- }
- foreach ($array as $key) {
- $sections[$key] = $this->getSection($key, $includeHeading);
- }
- return $sections;
- }
- /*
- *
- * Setter methods
- *
- */
- /**
- * Sets the text in the page. Updates the starttimestamp to the timestamp
- * after the page edit (if the edit is successful).
- *
- * Section can be the following:
- * - section name (string, e.g. "History")
- * - section index (int, e.g. 3)
- * - a new section (the string "new")
- * - the whole page (null)
- *
- * @param string $text The article text
- * @param string $section The section to edit (whole page by default)
- * @param boolean $minor True for minor edit
- * @param string $summary Summary text, and section header in case
- * of new section
- * @return boolean True if page was edited successfully
- */
- public function setText($text, $section = null, $minor = false, $summary = null)
- {
- $data = array(
- 'title' => $this->title,
- 'text' => $text,
- 'md5' => md5($text),
- 'bot' => "true",
- 'starttimestamp' => $this->starttimestamp,
- );
- // Set options from arguments
- if (!is_null($section)) {
- // Obtain section index in case it is a name
- $data['section'] = $this->findSection($section);
- if ($data['section'] == -1) {
- return false;
- }
- }
- if ($minor) {
- $data['minor'] = $minor;
- }
- if (!is_null($summary)) {
- $data['summary'] = $summary;
- }
- // Make sure we don't create a page by accident or overwrite another one
- if (!$this->exists) {
- $data['createonly'] = "true"; // createonly if not exists
- } else {
- $data['nocreate'] = "true"; // Don't create, it should exist
- }
- $r = $this->wikimate->edit($data); // The edit query
- // Check if it worked
- if (isset($r['edit']['result']) && $r['edit']['result'] == 'Success') {
- $this->exists = true;
- if (is_null($section)) {
- $this->text = $text;
- }
- // Get the new starttimestamp
- $data = array(
- 'titles' => $this->title,
- 'prop' => 'info',
- 'curtimestamp' => 1,
- );
- $r = $this->wikimate->query($data);
- // Check for errors
- if (isset($r['error'])) {
- $this->error = $r['error']; // Set the error if there was one
- return false;
- } else {
- $this->error = null; // Reset the error status
- }
- $this->starttimestamp = $r['curtimestamp']; // Update the starttimestamp
- return true;
- }
- // Return error response
- if (isset($r['error'])) {
- $this->error = $r['error'];
- } else {
- $this->error = array();
- $this->error['page'] = 'Unexpected edit response: ' . $r['edit']['result'];
- }
- return false;
- }
- /**
- * Sets the text of the given section.
- * Essentially an alias of WikiPage:setText()
- * with the summary and minor parameters switched.
- *
- * Section can be the following:
- * - section name (string, e.g. "History")
- * - section index (int, e.g. 3)
- * - a new section (the string "new")
- * - the whole page (null)
- *
- * @param string $text The text of the section
- * @param mixed $section The section to edit (intro by default)
- * @param string $summary Summary text, and section header in case
- * of new section
- * @param boolean $minor True for minor edit
- * @return boolean True if the section was saved
- */
- public function setSection($text, $section = 0, $summary = null, $minor = false)
- {
- return $this->setText($text, $section, $minor, $summary);
- }
- /**
- * Alias of WikiPage::setSection() specifically for creating new sections.
- *
- * @param string $name The heading name for the new section
- * @param string $text The text of the new section
- * @return boolean True if the section was saved
- */
- public function newSection($name, $text)
- {
- return $this->setSection($text, 'new', $name, false);
- }
- /**
- * Deletes the page.
- *
- * @param string $reason Reason for the deletion
- * @return boolean True if page was deleted successfully
- */
- public function delete($reason = null)
- {
- $data = array(
- 'title' => $this->title,
- );
- // Set options from arguments
- if (!is_null($reason)) {
- $data['reason'] = $reason;
- }
- $r = $this->wikimate->delete($data); // The delete query
- // Check if it worked
- if (isset($r['delete'])) {
- $this->exists = false; // The page was deleted
- $this->error = null; // Reset the error status
- return true;
- }
- $this->error = $r['error']; // Return error response
- return false;
- }
- /*
- *
- * Private methods
- *
- */
- /**
- * Finds a section's index by name.
- * If a section index or 'new' is passed, it is returned directly.
- *
- * @param mixed $section The section name or index to find
- * @return mixed The section index, or -1 if not found
- */
- private function findSection($section)
- {
- // Check section type
- if (is_int($section) || $section === 'new') {
- return $section;
- } elseif (is_string($section)) {
- // Search section names for related index
- $sections = array_keys($this->sections->byName);
- $index = array_search($section, $sections);
- // Return index if found
- if ($index !== false) {
- return $index;
- }
- }
- // Return error message and value
- $this->error = array();
- $this->error['page'] = "Section '$section' was not found on this page";
- return -1;
- }
- }
- /**
- * Models a wiki file that can have its properties retrieved and
- * its contents downloaded and uploaded.
- * All properties pertain to the current revision of the file.
- *
- * @author Robert McLeod & Frans P. de Vries
- * @since 0.12.0 October 2016
- */
- class WikiFile
- {
- /**
- * The name of the file
- *
- * @var string|null
- */
- protected $filename = null;
- /**
- * Wikimate object for API requests
- *
- * @var Wikimate|null
- */
- protected $wikimate = null;
- /**
- * Whether the file exists
- *
- * @var boolean
- */
- protected $exists = false;
- /**
- * Whether the file is invalid
- *
- * @var boolean
- */
- protected $invalid = false;
- /**
- * Error array with API and WikiFile errors
- *
- * @var array|null
- */
- protected $error = null;
- /**
- * Image info for the current file revision
- *
- * @var array|null
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Imageinfo
- */
- protected $info = null;
- /**
- * Image info for all file revisions
- *
- * @var array|null
- * @link https://www.mediawiki.org/wiki/Special:MyLanguage/API:Imageinfo
- */
- protected $history = null;
- /*
- *
- * Magic methods
- *
- */
- /**
- * Constructs a WikiFile object from the filename given
- * and associate with the passed Wikimate object.
- *
- * @param string $filename Name of the wiki file
- * @param Wikimate $wikimate Wikimate object
- */
- public function __construct($filename, $wikimate)
- {
- $this->wikimate = $wikimate;
- $this->filename = $filename;
- $this->info = $this->getInfo(true);
- if ($this->invalid) {
- $this->error['file'] = 'Invalid filename - cannot create WikiFile';
- }
- }
- /**
- * Forgets all object properties.
- */
- public function __destruct()
- {
- $this->filename = null;
- $this->wikimate = null;
- $this->exists = false;
- $this->invalid = false;
- $this->error = null;
- $this->info = null;
- $this->history = null;
- }
- /**
- * Returns the file existence status.
- *
- * @return boolean True if file exists
- */
- public function exists()
- {
- return $this->exists;
- }
- /**
- * Alias of self::__destruct().
- *
- * @return void
- */
- public function destroy()
- {
- $this->__destruct();
- }
- /*
- *
- * File meta methods
- *
- */
- /**
- * Returns the latest error if there is one.
- *
- * @return mixed The error array, or null if no error
- */
- public function getError()
- {
- return $this->error;
- }
- /**
- * Returns the name of this file.
- *
- * @return string The name of this file
- */
- public function getFilename()
- {
- return $this->filename;
- }
- /*
- *
- * Getter methods
- *
- */
- /**
- * Gets the information of the file. If refresh is true,
- * then this method will query the wiki API again for the file details.
- *
- * @param boolean $refresh True to query the wiki API again
- * @param array $history An optional array of revision history parameters
- * @return mixed The info of the file (array), or null if error
- */
- public function getInfo($refresh = false, $history = null)
- {
- if ($refresh) { // We want to query the API
- // Specify relevant file properties to retrieve
- $data = array(
- 'titles' => 'File:' . $this->filename,
- 'prop' => 'info|imageinfo',
- 'iiprop' => 'bitdepth|canonicaltitle|comment|parsedcomment|'
- . 'commonmetadata|metadata|extmetadata|mediatype|'
- . 'mime|thumbmime|sha1|size|timestamp|url|user|userid',
- );
- // Add optional history parameters
- if (is_array($history)) {
- foreach ($history as $key => $val) {
- $data[$key] = $val;
- }
- // Retrieve archive name property as well
- $data['iiprop'] .= '|archivename';
- }
- $r = $this->wikimate->query($data); // Run the query
- // Check for errors
- if (isset($r['error'])) {
- $this->error = $r['error']; // Set the error if there was one
- return null;
- } else {
- $this->error = null; // Reset the error status
- }
- // Get the page (there should only be one)
- $page = array_pop($r['query']['pages']);
- unset($r, $data);
- // Abort if invalid file title
- if (isset($page['invalid'])) {
- $this->invalid = true;
- return null;
- }
- // Check that file is present and has info
- if (!isset($page['missing']) && isset($page['imageinfo'])) {
- // Update the existence if the file is there
- $this->exists = true;
- // Put the content into info & history
- $this->info = $page['imageinfo'][0];
- $this->history = $page['imageinfo'];
- }
- unset($page);
- }
- return $this->info; // Return the info in any case
- }
- /**
- * Returns the anonymous flag of this file,
- * or of its specified revision.
- * If true, then getUser()'s value represents an anonymous IP address.
- *
- * @param mixed $revision The index or timestamp of the revision (optional)
- * @return mixed The anonymous flag of this file (boolean),
- …
Large files files are truncated, but you can click here to view the full file