Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 359 |
PFAutocompleteAPI | |
0.00% |
0 / 1 |
|
0.00% |
0 / 12 |
4692 | |
0.00% |
0 / 359 |
__construct | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
execute | |
0.00% |
0 / 1 |
552 | |
0.00% |
0 / 78 |
|||
getAllowedParams | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 23 |
|||
getParamDescription | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 11 |
|||
getDescription | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
getExamples | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 6 |
|||
getAllValuesForProperty | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 27 |
|||
computeAllValuesForProperty | |
0.00% |
0 / 1 |
156 | |
0.00% |
0 / 89 |
|||
getAllValuesForCargoField | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 19 |
|||
computeAllValuesForCargoField | |
0.00% |
0 / 1 |
240 | |
0.00% |
0 / 80 |
|||
cargoFieldIsList | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 15 |
|||
shiftExactMatch | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 7 |
<?php | |
/** | |
* @file | |
* @ingroup PF | |
*/ | |
/** | |
* Adds and handles the 'pfautocomplete' action to the MediaWiki API. | |
* | |
* @ingroup PF | |
* | |
* @author Sergey Chernyshev | |
* @author Yaron Koren | |
*/ | |
class PFAutocompleteAPI extends ApiBase { | |
public function __construct( $query, $moduleName ) { | |
parent::__construct( $query, $moduleName ); | |
} | |
public function execute() { | |
$params = $this->extractRequestParams(); | |
$substr = $params['substr']; | |
$namespace = $params['namespace']; | |
$property = $params['property']; | |
$category = $params['category']; | |
$concept = $params['concept']; | |
$cargo_table = $params['cargo_table']; | |
$cargo_field = $params['cargo_field']; | |
$cargo_where = $params['cargo_where']; | |
$external_url = $params['external_url']; | |
$baseprop = $params['baseprop']; | |
$base_cargo_table = $params['base_cargo_table']; | |
$base_cargo_field = $params['base_cargo_field']; | |
$basevalue = $params['basevalue']; | |
// $limit = $params['limit']; | |
if ( $baseprop === null && $base_cargo_table === null && strlen( $substr ) == 0 ) { | |
$this->dieWithError( [ 'apierror-missingparam', 'substr' ], 'param_substr' ); | |
} | |
global $wgPageFormsUseDisplayTitle; | |
$map = false; | |
if ( $baseprop !== null ) { | |
if ( $property !== null ) { | |
$data = $this->getAllValuesForProperty( $property, null, $baseprop, $basevalue ); | |
} | |
} elseif ( $property !== null ) { | |
$data = $this->getAllValuesForProperty( $property, $substr ); | |
} elseif ( $category !== null ) { | |
$data = PFValuesUtils::getAllPagesForCategory( $category, 3, $substr ); | |
$map = $wgPageFormsUseDisplayTitle; | |
if ( $map ) { | |
$data = PFValuesUtils::disambiguateLabels( $data ); | |
} | |
} elseif ( $concept !== null ) { | |
$data = PFValuesUtils::getAllPagesForConcept( $concept, $substr ); | |
$map = $wgPageFormsUseDisplayTitle; | |
if ( $map ) { | |
$data = PFValuesUtils::disambiguateLabels( $data ); | |
} | |
} elseif ( $cargo_table !== null && $cargo_field !== null ) { | |
$data = self::getAllValuesForCargoField( $cargo_table, $cargo_field, $cargo_where, $substr, $base_cargo_table, $base_cargo_field, $basevalue ); | |
} elseif ( $namespace !== null ) { | |
$data = PFValuesUtils::getAllPagesForNamespace( $namespace, $substr ); | |
$map = $wgPageFormsUseDisplayTitle; | |
} elseif ( $external_url !== null ) { | |
$data = PFValuesUtils::getValuesFromExternalURL( $external_url, $substr ); | |
} else { | |
$data = []; | |
} | |
// If we got back an error message, exit with that message. | |
if ( !is_array( $data ) ) { | |
if ( is_callable( [ $this, 'dieWithError' ] ) ) { | |
if ( !$data instanceof Message ) { | |
$data = ApiMessage::create( new RawMessage( '$1', [ $data ] ), 'unknownerror' ); | |
} | |
$this->dieWithError( $data ); | |
} else { | |
$code = 'unknownerror'; | |
if ( $data instanceof Message ) { | |
$code = $data instanceof IApiMessage ? $data->getApiCode() : $data->getKey(); | |
$data = $data->inLanguage( 'en' )->useDatabase( false )->text(); | |
} | |
$this->dieWithError( $data, $code ); | |
} | |
} | |
// to prevent JS parsing problems, display should be the same | |
// even if there are no results | |
/* | |
if ( count( $data ) <= 0 ) { | |
return; | |
} | |
*/ | |
// Format data as the API requires it - this is not needed | |
// for "values from url", where the data is already formatted | |
// correctly. | |
if ( $external_url === null ) { | |
$formattedData = []; | |
foreach ( $data as $index => $value ) { | |
if ( $map ) { | |
$formattedData[] = [ 'title' => $index, 'displaytitle' => $value ]; | |
} else { | |
$formattedData[] = [ 'title' => $value ]; | |
} | |
} | |
} else { | |
$formattedData = $data; | |
} | |
// Set top-level elements. | |
$result = $this->getResult(); | |
$result->setIndexedTagName( $formattedData, 'p' ); | |
$result->addValue( null, $this->getModuleName(), $formattedData ); | |
} | |
protected function getAllowedParams() { | |
return [ | |
'limit' => [ | |
ApiBase::PARAM_TYPE => 'limit', | |
ApiBase::PARAM_DFLT => 10, | |
ApiBase::PARAM_MIN => 1, | |
ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, | |
ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 | |
], | |
'substr' => null, | |
'property' => null, | |
'category' => null, | |
'concept' => null, | |
'cargo_table' => null, | |
'cargo_field' => null, | |
'cargo_where' => null, | |
'namespace' => null, | |
'external_url' => null, | |
'baseprop' => null, | |
'base_cargo_table' => null, | |
'base_cargo_field' => null, | |
'basevalue' => null, | |
]; | |
} | |
protected function getParamDescription() { | |
return [ | |
'substr' => 'Search substring', | |
'property' => 'Semantic property for which to search values', | |
'category' => 'Category for which to search values', | |
'concept' => 'Concept for which to search values', | |
'namespace' => 'Namespace for which to search values', | |
'external_url' => 'Alias for external URL from which to get values', | |
'baseprop' => 'A previous property in the form to check against', | |
'basevalue' => 'The value to check for the previous property', | |
// 'limit' => 'Limit how many entries to return', | |
]; | |
} | |
protected function getDescription() { | |
return 'Autocompletion call used by the Page Forms extension (https://www.mediawiki.org/Extension:Page_Forms)'; | |
} | |
protected function getExamples() { | |
return [ | |
'api.php?action=pfautocomplete&substr=te', | |
'api.php?action=pfautocomplete&substr=te&property=Has_author', | |
'api.php?action=pfautocomplete&substr=te&category=Authors', | |
]; | |
} | |
private function getAllValuesForProperty( | |
$property_name, | |
$substring, | |
$basePropertyName = null, | |
$baseValue = null | |
) { | |
global $wgPageFormsCacheAutocompleteValues, $wgPageFormsAutocompleteCacheTimeout; | |
global $smwgDefaultStore; | |
if ( $smwgDefaultStore == null ) { | |
$this->dieWithError( 'Semantic MediaWiki must be installed to query on "property"', 'param_property' ); | |
} | |
$property_name = str_replace( ' ', '_', $property_name ); | |
// Use cache if allowed | |
if ( !$wgPageFormsCacheAutocompleteValues ) { | |
return $this->computeAllValuesForProperty( $property_name, $substring, $basePropertyName, $baseValue ); | |
} | |
$cache = PFFormUtils::getFormCache(); | |
// Remove trailing whitespace to avoid unnecessary database selects | |
$cacheKeyString = $property_name . '::' . rtrim( $substring ); | |
if ( $basePropertyName !== null ) { | |
$cacheKeyString .= ',' . $basePropertyName . ',' . $baseValue; | |
} | |
$cacheKey = $cache->makeKey( 'pf-autocomplete', md5( $cacheKeyString ) ); | |
return $cache->getWithSetCallback( | |
$cacheKey, | |
$wgPageFormsAutocompleteCacheTimeout, | |
function () use ( $property_name, $substring, $basePropertyName, $baseValue ) { | |
return $this->computeAllValuesForProperty( $property_name, $substring, $basePropertyName, $baseValue ); | |
} | |
); | |
} | |
/** | |
* @param string $property_name | |
* @param string $substring | |
* @param string|null $basePropertyName | |
* @param mixed $baseValue | |
* @return array | |
*/ | |
private function computeAllValuesForProperty( | |
$property_name, | |
$substring, | |
$basePropertyName = null, | |
$baseValue = null | |
) { | |
global $wgPageFormsMaxAutocompleteValues; | |
global $smwgDefaultStore; | |
$db = wfGetDB( DB_REPLICA ); | |
$sqlOptions = []; | |
$sqlOptions['LIMIT'] = $wgPageFormsMaxAutocompleteValues; | |
if ( method_exists( 'SMW\DataValueFactory', 'newPropertyValueByLabel' ) ) { | |
// SMW 3.0+ | |
$property = SMW\DataValueFactory::getInstance()->newPropertyValueByLabel( $property_name ); | |
} else { | |
$property = SMWPropertyValue::makeUserProperty( $property_name ); | |
} | |
$propertyHasTypePage = ( $property->getPropertyTypeID() == '_wpg' ); | |
$conditions = [ 'p_ids.smw_title' => $property_name ]; | |
if ( $propertyHasTypePage ) { | |
$valueField = 'o_ids.smw_title'; | |
if ( $smwgDefaultStore === 'SMWSQLStore2' ) { | |
$idsTable = $db->tableName( 'smw_ids' ); | |
$propsTable = $db->tableName( 'smw_rels2' ); | |
} else { | |
// SMWSQLStore3 - also the backup for SMWSPARQLStore | |
$idsTable = $db->tableName( 'smw_object_ids' ); | |
$propsTable = $db->tableName( 'smw_di_wikipage' ); | |
} | |
$fromClause = "$propsTable p JOIN $idsTable p_ids ON p.p_id = p_ids.smw_id JOIN $idsTable o_ids ON p.o_id = o_ids.smw_id"; | |
} else { | |
if ( $smwgDefaultStore === 'SMWSQLStore2' ) { | |
$valueField = 'p.value_xsd'; | |
$idsTable = $db->tableName( 'smw_ids' ); | |
$propsTable = $db->tableName( 'smw_atts2' ); | |
} else { | |
// SMWSQLStore3 - also the backup for SMWSPARQLStore | |
$valueField = 'p.o_hash'; | |
$idsTable = $db->tableName( 'smw_object_ids' ); | |
$propsTable = $db->tableName( 'smw_di_blob' ); | |
} | |
$fromClause = "$propsTable p JOIN $idsTable p_ids ON p.p_id = p_ids.smw_id"; | |
} | |
if ( $basePropertyName !== null ) { | |
if ( method_exists( 'SMW\DataValueFactory', 'newPropertyValueByLabel' ) ) { | |
$baseProperty = SMW\DataValueFactory::getInstance()->newPropertyValueByLabel( $basePropertyName ); | |
} else { | |
// SMW 3.0+ | |
$baseProperty = SMWPropertyValue::makeUserProperty( $basePropertyName ); | |
} | |
$basePropertyHasTypePage = ( $baseProperty->getPropertyTypeID() == '_wpg' ); | |
$basePropertyName = str_replace( ' ', '_', $basePropertyName ); | |
$conditions['base_p_ids.smw_title'] = $basePropertyName; | |
if ( $basePropertyHasTypePage ) { | |
if ( $smwgDefaultStore === 'SMWSQLStore2' ) { | |
$idsTable = $db->tableName( 'smw_ids' ); | |
$propsTable = $db->tableName( 'smw_rels2' ); | |
} else { | |
$idsTable = $db->tableName( 'smw_object_ids' ); | |
$propsTable = $db->tableName( 'smw_di_wikipage' ); | |
} | |
$fromClause .= " JOIN $propsTable p_base ON p.s_id = p_base.s_id"; | |
$fromClause .= " JOIN $idsTable base_p_ids ON p_base.p_id = base_p_ids.smw_id JOIN $idsTable base_o_ids ON p_base.o_id = base_o_ids.smw_id"; | |
$baseValue = str_replace( ' ', '_', $baseValue ); | |
$conditions['base_o_ids.smw_title'] = $baseValue; | |
} else { | |
if ( $smwgDefaultStore === 'SMWSQLStore2' ) { | |
$baseValueField = 'p_base.value_xsd'; | |
$idsTable = $db->tableName( 'smw_ids' ); | |
$propsTable = $db->tableName( 'smw_atts2' ); | |
} else { | |
$baseValueField = 'p_base.o_hash'; | |
$idsTable = $db->tableName( 'smw_object_ids' ); | |
$propsTable = $db->tableName( 'smw_di_blob' ); | |
} | |
$fromClause .= " JOIN $propsTable p_base ON p.s_id = p_base.s_id"; | |
$fromClause .= " JOIN $idsTable base_p_ids ON p_base.p_id = base_p_ids.smw_id"; | |
$conditions[$baseValueField] = $baseValue; | |
} | |
} | |
if ( $substring !== null ) { | |
// "Page" type property valeus are stored differently | |
// in the DB, i.e. underlines instead of spaces. | |
$conditions[] = PFValuesUtils::getSQLConditionForAutocompleteInColumn( $valueField, $substring, $propertyHasTypePage ); | |
} | |
$sqlOptions['ORDER BY'] = $valueField; | |
$res = $db->select( $fromClause, "DISTINCT $valueField", | |
$conditions, __METHOD__, $sqlOptions ); | |
$values = []; | |
while ( $row = $res->fetchRow() ) { | |
$values[] = str_replace( '_', ' ', $row[0] ); | |
} | |
$res->free(); | |
$values = self::shiftExactMatch( $substring, $values ); | |
return $values; | |
} | |
private static function getAllValuesForCargoField( $cargoTable, $cargoField, $cargoWhere, $substring, $baseCargoTable = null, $baseCargoField = null, $baseValue = null ) { | |
global $wgPageFormsCacheAutocompleteValues, $wgPageFormsAutocompleteCacheTimeout; | |
if ( !$wgPageFormsCacheAutocompleteValues ) { | |
return self::computeAllValuesForCargoField( | |
$cargoTable, $cargoField, $cargoWhere, $substring, $baseCargoTable, $baseCargoField, $baseValue ); | |
} | |
$cache = PFFormUtils::getFormCache(); | |
// Remove trailing whitespace to avoid unnecessary database selects | |
$cacheKeyString = $cargoTable . '|' . $cargoField . '|' . rtrim( $substring ); | |
if ( $baseCargoTable !== null ) { | |
$cacheKeyString .= '|' . $baseCargoTable . '|' . $baseCargoField . '|' . $baseValue; | |
} | |
$cacheKey = $cache->makeKey( 'pf-autocomplete', md5( $cacheKeyString ) ); | |
return $cache->getWithSetCallback( | |
$cacheKey, | |
$wgPageFormsAutocompleteCacheTimeout, | |
function () use ( $cargoTable, $cargoField, $cargoWhere, $substring, $baseCargoTable, $baseCargoField, $baseValue ) { | |
return self::computeAllValuesForCargoField( | |
$cargoTable, $cargoField, $cargoWhere, $substring, $baseCargoTable, $baseCargoField, $baseValue ); | |
} | |
); | |
} | |
private static function computeAllValuesForCargoField( | |
$cargoTable, | |
$cargoField, | |
$cargoWhere, | |
$substring, | |
$baseCargoTable, | |
$baseCargoField, | |
$baseValue | |
) { | |
global $wgPageFormsMaxAutocompleteValues, $wgPageFormsAutocompleteOnAllChars; | |
$tablesStr = $cargoTable; | |
$fieldsStr = $cargoField; | |
$joinOnStr = ''; | |
$whereStr = ''; | |
if ( $cargoWhere !== null ) { | |
$whereStr = '(' . stripslashes( $cargoWhere ) . ')'; | |
} | |
if ( $baseCargoTable !== null && $baseCargoField !== null ) { | |
if ( $whereStr != '' ) { | |
$whereStr .= " AND "; | |
} | |
if ( $baseCargoTable != $cargoTable ) { | |
$tablesStr .= ", $baseCargoTable"; | |
$joinOnStr = "$cargoTable._pageName = $baseCargoTable._pageName"; | |
} | |
$whereStr .= "$baseCargoTable.$baseCargoField = \"$baseValue\""; | |
} | |
if ( $substring !== null ) { | |
if ( $whereStr != '' ) { | |
$whereStr .= " AND "; | |
} | |
$fieldIsList = self::cargoFieldIsList( $cargoTable, $cargoField ); | |
if ( $fieldIsList ) { | |
// If it's a list field, we query directly on | |
// the "helper table" for that field. We could | |
// instead use "HOLDS LIKE", but this would | |
// return false positives - other values that | |
// have been listed alongside the values we're | |
// looking for - at least for Cargo >= 2.6. | |
$fieldTableName = $cargoTable . '__' . $cargoField; | |
// Because of the way Cargo querying works, the | |
// field table has to be listed first for only | |
// the right values to show up. | |
$tablesStr = $fieldTableName . ', ' . $tablesStr; | |
if ( $joinOnStr != '' ) { | |
$joinOnStr = ', ' . $joinOnStr; | |
} | |
$joinOnStr = $fieldTableName . '._rowID=' . | |
$cargoTable . '._ID' . $joinOnStr; | |
$fieldsStr = $cargoField = '_value'; | |
} | |
// LIKE is almost always case-insensitive for MySQL, | |
// usually (?) case-sensitive for PostgreSQL, and | |
// case-insensitive (though only for ASCII characters) | |
// in SQLite. In order to make this check consistenly | |
// case-sensitive everywhere, we call LOWER() on all | |
// the fields. There are other ways to accomplish this, | |
// but this one works consistently across the different | |
// DB systems. | |
if ( $wgPageFormsAutocompleteOnAllChars ) { | |
$whereStr .= "(LOWER($cargoField) LIKE LOWER(\"%$substring%\"))"; | |
} else { | |
$whereStr .= "(LOWER($cargoField) LIKE LOWER(\"$substring%\")"; | |
// Also look for the substring after any word | |
// separator (most commonly, a space). In theory, | |
// any punctuation can be a word separator, | |
// but we will just look for the most common | |
// ones. | |
// This would be much easier to do with the | |
// REGEXP operator, but its presence is | |
// inconsistent between MySQL, PostgreSQL and | |
// SQLite. | |
$wordSeparators = [ ' ', '/', '(', ')', '-', '|', '\'', '\"' ]; | |
foreach ( $wordSeparators as $wordSeparator ) { | |
$whereStr .= " OR LOWER($cargoField) LIKE LOWER(\"%$wordSeparator$substring%\")"; | |
} | |
$whereStr .= ')'; | |
} | |
} | |
$sqlQuery = CargoSQLQuery::newFromValues( | |
$tablesStr, | |
$fieldsStr, | |
$whereStr, | |
$joinOnStr, | |
$cargoField, | |
$havingStr = null, | |
$cargoField, | |
$wgPageFormsMaxAutocompleteValues, | |
$offsetStr = 0 | |
); | |
$queryResults = $sqlQuery->run(); | |
if ( $cargoField[0] != '_' ) { | |
$cargoFieldAlias = str_replace( '_', ' ', $cargoField ); | |
} else { | |
$cargoFieldAlias = $cargoField; | |
} | |
$values = []; | |
foreach ( $queryResults as $row ) { | |
$value = $row[$cargoFieldAlias]; | |
// @TODO - this check should not be necessary. | |
if ( $value == '' ) { | |
continue; | |
} | |
// Cargo HTML-encodes everything - let's decode double | |
// quotes, at least. | |
$values[] = str_replace( '"', '"', $value ); | |
} | |
$values = self::shiftExactMatch( $substring, $values ); | |
return $values; | |
} | |
static function cargoFieldIsList( $cargoTable, $cargoField ) { | |
// @TODO - this is duplicate work; the schema is retrieved | |
// again when the CargoSQLQuery object is created. There should | |
// be some way of avoiding that duplicate retrieval. | |
try { | |
$tableSchemas = CargoUtils::getTableSchemas( [ $cargoTable ] ); | |
} catch ( MWException $e ) { | |
return false; | |
} | |
if ( !array_key_exists( $cargoTable, $tableSchemas ) ) { | |
return false; | |
} | |
$tableSchema = $tableSchemas[$cargoTable]; | |
if ( !array_key_exists( $cargoField, $tableSchema->mFieldDescriptions ) ) { | |
return false; | |
} | |
$fieldDesc = $tableSchema->mFieldDescriptions[$cargoField]; | |
return $fieldDesc->mIsList; | |
} | |
/** | |
* Move the exact match to the top for better autocompletion | |
* @param string $substring | |
* @param array $values | |
* @return array $values | |
*/ | |
static function shiftExactMatch( $substring, $values ) { | |
$firstMatchIdx = array_search( $substring, $values ); | |
if ( $firstMatchIdx ) { | |
unset( $values[ $firstMatchIdx ] ); | |
array_unshift( $values, $substring ); | |
} | |
return $values; | |
} | |
} |