| Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/Import/LiquidThreadsApi/Source.php |
<?php
namespace Flow\Import\LiquidThreadsApi;
use ApiBase;
use ApiMain;
use Exception;
use FauxRequest;
use Flow\Import\ImportException;
use Flow\Import\IImportSource;
use Http;
use RequestContext;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use UsageException;
use User;
class ImportSource implements IImportSource {
// Thread types defined by LQT which are returned via api
const THREAD_TYPE_NORMAL = 0;
const THREAD_TYPE_MOVED = 1;
const THREAD_TYPE_DELETED = 2;
const THREAD_TYPE_HIDDEN = 4;
/**
* @var ApiBackend
*/
protected $api;
/**
* @var string
*/
protected $pageName;
/**
* @var CachedThreadData
*/
protected $threadData;
/**
* @var CachedPageData
*/
protected $pageData;
/**
* @var int
*/
protected $cachedTopics = 0;
/**
* @var User Used for scripted actions and occurances (such as suppression)
* where the original user is not available.
*/
protected $scriptUser;
/**
* @param ApiBackend $apiBackend
* @param string $pageName
* @param User $scriptUser
*/
public function __construct( ApiBackend $apiBackend, $pageName, User $scriptUser ) {
$this->api = $apiBackend;
$this->pageName = $pageName;
$this->scriptUser = $scriptUser;
$this->threadData = new CachedThreadData( $this->api );
$this->pageData = new CachedPageData( $this->api );
}
/**
* Returns a system user suitable for assigning programatic actions to.
*
* @return User
*/
public function getScriptUser() {
return $this->scriptUser;
}
/**
* {@inheritDoc}
*/
public function getHeader() {
return new ImportHeader( $this->api, $this, $this->pageName );
}
/**
* {@inheritDoc}
*/
public function getTopics() {
return new TopicIterator( $this, $this->threadData, $this->pageName );
}
/**
* @param integer $id
* @return ImportTopic|null
*/
public function getTopic( $id ) {
// reset our internal cached data every 100 topics. Otherwise imports
// of any considerable size will take up large amounts of memory for
// no reason, running into swap on smaller machines.
$this->cachedTopics++;
if ( $this->cachedTopics > 100 ) {
$this->threadData->reset();
$this->pageData->reset();
$this->cachedTopics = 0;
}
$data = $this->threadData->get( $id );
switch ( $data['type'] ) {
// Standard thread
case self::THREAD_TYPE_NORMAL:
return new ImportTopic( $this, $data );
// The topic no longer exists at the queried location, but
// a stub was left behind pointing to it. This modified
// version of ImportTopic gracefully adjusts the #REDIRECT
// into a template to keep a similar output to lqt.
case self::THREAD_TYPE_MOVED:
return new MovedImportTopic( $this, $data );
// To get these back from the api we would have to send the `showdeleted`
// query param. As we are not requesting them, just ignore for now.
case self::THREAD_TYPE_DELETED:
return null;
// Was assigned but never used by LQT.
case self::THREAD_TYPE_HIDDEN:
return null;
}
}
/**
* @param integer $id
* @return ImportPost
*/
public function getPost( $id ) {
return new ImportPost( $this, $this->threadData->get( $id ) );
}
/**
* @param integer $id
* @return array
*/
public function getThreadData( $id ) {
if ( is_array( $id ) ) {
return $this->threadData->getMulti( $id );
} else {
return $this->threadData->get( $id );
}
}
/**
* @param integer[]|integer $pageIds
* @return array
*/
public function getPageData( $pageIds ) {
if ( is_array( $pageIds ) ) {
return $this->pageData->getMulti( $pageIds );
} else {
return $this->pageData->get( $pageIds );
}
}
/**
* @param string $pageName
* @param integer $startId
* @return array
*/
public function getFromPage( $pageName, $startId = 0 ) {
return $this->threadData->getFromPage( $pageName, $startId );
}
/**
* Gets a unique identifier for the wiki being imported
* @return string Usually either a string 'local' or an API URL
*/
public function getApiKey() {
return $this->api->getKey();
}
/**
* Returns a key uniquely representing an object determined by arguments.
* Parameters: Zero or more strings that uniquely represent the object
* for this ImportSource
*
* @return string Unique key
*/
public function getObjectKey( /* $args */ ) {
$components = array_merge(
array( 'lqt-api', $this->getApiKey() ),
func_get_args()
);
return implode( ':', $components );
}
}
abstract class ApiBackend implements LoggerAwareInterface {
/**
* @var LoggerInterface
*/
protected $logger;
public function __construct() {
$this->logger = new NullLogger;
}
public function setLogger( LoggerInterface $logger ) {
$this->logger = $logger;
}
/**
* Retrieves LiquidThreads data from the API
*
* @param array $conditions The parameters to pass to select the threads. Usually used in two ways: with thstartid/thpage, or with ththreadid
* @return array Data as returned under query.threads by the API
* @throws ApiNotFoundException Thrown when the remote api reports that the provided conditions
* have no matching records.
* @throws ImportException When an error is received from the remote api. This is often either
* a bad request or lqt threw an exception trying to respond to a valid request.
*/
public function retrieveThreadData( array $conditions ) {
$params = array(
'action' => 'query',
'list' => 'threads',
'thprop' => 'id|subject|page|parent|ancestor|created|modified|author|summaryid|type|rootid|replies|signature',
'rawcontinue' => 1, // We're doing continuation a different way, but this avoids a warning.
'format' => 'json',
'limit' => ApiBase::LIMIT_BIG1,
);
$data = $this->apiCall( $params + $conditions );
if ( ! isset( $data['query']['threads'] ) ) {
if ( $this->isNotFoundError( $data ) ) {
$message = "Did not find thread with conditions: " . json_encode( $conditions );
$this->logger->debug( __METHOD__ . ": $message" );
throw new ApiNotFoundException( $message );
} else {
$this->logger->error( __METHOD__ . ': Failed API call against ' . $this->getKey() . ' with conditions : ' . json_encode( $conditions ) );
throw new ImportException( "Null response from API module:" . json_encode( $data ) );
}
}
$firstThread = reset( $data['query']['threads'] );
if ( ! isset( $firstThread['replies'] ) ) {
throw new ImportException( "Foreign API does not support reply exporting:" . json_encode( $data ) );
}
return $data['query']['threads'];
}
/**
* Retrieves data about a set of pages from the API
*
* @param array $pageIds Page IDs to return data for.
* @return array The query.pages part of the API response.
* @throws \MWException
*/
public function retrievePageDataById( array $pageIds ) {
if ( !$pageIds ) {
throw new \MWException( 'At least one page id must be provided' );
}
return $this->retrievePageData( array(
'pageids' => implode( '|', $pageIds ),
) );
}
/**
* Retrieves data about the latest revision of the titles
* from the API
*
* @param string[] $titles Titles to return data for
* @return array The query.pages prt of the API response.
* @throws \MWException
* @throws ImportException
*/
public function retrieveTopRevisionByTitle( array $titles ) {
if ( !$titles ) {
throw new \MWException( 'At least one title must be provided' );
}
return $this->retrievePageData( array(
'titles' => implode( '|', $titles ),
'rvlimit' => 1,
'rvdir' => 'older',
), true );
}
/**
* Retrieves data about a set of pages from the API
*
* @param array $conditions Conditions to retrieve pages by; to be sent to the API.
* @param bool $expectContinue Pass true here when caller expects more revisions to exist than
* they are requesting information about.
* @return array The query.pages part of the API response.
* @throws ApiNotFoundException Thrown when the remote api reports that the provided conditions
* have no matching records.
* @throws ImportException When an error is received from the remote api. This is often either
* a bad request or lqt threw an exception trying to respond to a valid request.
* @throws ImportException When more revisions are available than can be returned in a single
* query and the calling code does not set $expectContinue to true.
*/
public function retrievePageData( array $conditions, $expectContinue = false ) {
$conditions += array(
'action' => 'query',
'prop' => 'revisions',
'rvprop' => 'timestamp|user|content|ids',
'format' => 'json',
'rvlimit' => 5000,
'rvdir' => 'newer',
'continue' => '',
);
$data = $this->apiCall( $conditions );
if ( ! isset( $data['query'] ) ) {
if ( $this->isNotFoundError( $data ) ) {
$message = "Did not find pages: " . json_encode( $conditions );
$this->logger->debug( __METHOD__ . ": $message" );
throw new ApiNotFoundException( $message );
} else {
$this->logger->error( __METHOD__ . ': Failed API call against ' . $this->getKey() . ' with conditions : ' . json_encode( $conditions ) );
throw new ImportException( "Null response from API module: " . json_encode( $data ) );
}
} elseif ( !$expectContinue && isset( $data['continue'] ) ) {
throw new ImportException( "More revisions than can be retrieved for conditions, import would be incomplete: " . json_encode( $conditions ) );
}
return $data['query']['pages'];
}
/**
* Calls the remote API
*
* @param array $params The API request to send
* @param int $retry Retry the request on failure this many times
* @return array API return value, decoded from JSON into an array.
*/
abstract function apiCall( array $params, $retry = 1 );
/**
* @return string A unique identifier for this backend.
*/
abstract function getKey();
/**
* @param array $apiResponse
* @return bool
*/
protected function isNotFoundError( $apiResponse ) {
// LQT has some bugs where not finding the requested item in the database throws
// returns this exception.
$expect = 'Exception Caught: DatabaseBase::makeList: empty input for field thread_parent';
return false !== strpos( $apiResponse['error']['info'], $expect );
}
}
class RemoteApiBackend extends ApiBackend {
/**
* @param string
*/
protected $apiUrl;
/**
* @param string|null
*/
protected $cacheDir;
/**
* @param string $apiUrl
* @param string|null $cacheDir
*/
public function __construct( $apiUrl, $cacheDir = null ) {
parent::__construct();
$this->apiUrl = $apiUrl;
$this->cacheDir = $cacheDir;
}
public function getKey() {
return $this->apiUrl;
}
public function apiCall( array $params, $retry = 1 ) {
$params['format'] = 'json';
$url = wfAppendQuery( $this->apiUrl, $params );
$file = $this->cacheDir . '/' . md5( $url ) . '.cache';
$this->logger->debug( __METHOD__ . ": $url" );
if ( $this->cacheDir && file_exists( $file ) ) {
$result = file_get_contents( $file );
} else {
do {
$result = Http::get( $url );
} while ( $result === false && --$retry >= 0 );
if ( $this->cacheDir && file_put_contents( $file, $result ) === false ) {
$this->logger->warning( "Failed writing cached api result to $file" );
}
}
return json_decode( $result, true );
}
}
class LocalApiBackend extends ApiBackend {
/**
* @var User|null
*/
protected $user;
public function __construct( User $user = null ) {
parent::__construct();
$this->user = $user;
}
public function getKey() {
return 'local';
}
public function apiCall( array $params, $retry = 1 ) {
try {
$context = new RequestContext;
$context->setRequest( new FauxRequest( $params ) );
if ( $this->user ) {
$context->setUser( $this->user );
}
$api = new ApiMain( $context );
$api->execute();
return $api->getResult()->getResultData( null, array( 'Strip' => 'all' ) );
} catch ( UsageException $exception ) {
// Mimic the behaviour when called remotely
return array( 'error' => $exception->getMessageArray() );
} catch ( Exception $exception ) {
// Mimic behaviour when called remotely
return array(
'error' => array(
'code' => 'internal_api_error_' . get_class( $exception ),
'info' => 'Exception Caught: ' . $exception->getMessage(),
),
);
}
}
}