gplx/mediawiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Property.php

585 lines
15 KiB
PHP

<?php
namespace SMW;
use MediaWiki\Json\JsonUnserializer;
use RuntimeException;
use SMW\Exception\DataItemException;
use SMW\Exception\DataTypeLookupException;
use SMW\Exception\PredefinedPropertyLabelMismatchException;
use SMW\Exception\PropertyLabelNotResolvedException;
use SMW\Localizer\Localizer;
use SMW\Services\ServicesFactory as ApplicationFactory;
use SMWDataItem;
use SMWDIUri;
/**
* This class implements Property item
*
* @note PropertyRegistry class manages global registrations of predefined
* (built-in) properties, and maintains an association of property IDs, localized
* labels, and aliases.
*
* @since 1.6
*
* @author Markus Krötzsch
* @author Jeroen De Dauw
* @author mwjames
*/
class DIProperty extends SMWDataItem {
/**
* @see PropertyRegistry::registerPredefinedProperties
*/
const TYPE_SUBOBJECT = '_SOBJ';
const TYPE_ERROR = '_ERRP';
const TYPE_CATEGORY = '_INST';
const TYPE_SUBCATEGORY = '_SUBC';
const TYPE_SORTKEY = '_SKEY';
const TYPE_MODIFICATION_DATE = '_MDAT';
const TYPE_CREATION_DATE = '_CDAT';
const TYPE_LAST_EDITOR = '_LEDT';
const TYPE_NEW_PAGE = '_NEWP';
const TYPE_HAS_TYPE = '_TYPE';
const TYPE_CONVERSION = '_CONV';
const TYPE_ASKQUERY = '_ASK';
const TYPE_MEDIA = '_MEDIA';
const TYPE_MIME = '_MIME';
const TYPE_DISPLAYTITLE = '_DTITLE';
/**
* Change propagation
*/
const TYPE_CHANGE_PROP = '_CHGPRO';
/**
* Either an internal SMW property key (starting with "_") or the DB
* key of a property page in the wiki.
* @var string|null
*/
private $m_key;
/**
* Whether to take the inverse of this property or not.
* @var bool
*/
private $m_inverse;
/**
* @var string
*/
private $propertyValueType;
/**
* Interwiki prefix for when a property represents a non-local entity
*
* @var string
*/
private $interwiki = '';
public int $id;
/**
* Initialise a property. This constructor checks that keys of
* predefined properties do really exist (in the current configuration
* of the wiki). No check is performed to see if a user label is in
* fact the label or alias of a predefined property. If this should be
* done, the function self::newFromUserLabel() can be used.
*
* @param string|bool|null $key Key for the property (internal SMW key or wikipage DB key)
* @param bool $inverse States if the inverse of the property is constructed
*/
public function __construct( $key, $inverse = false ) {
$key = (string)$key;
if ( $key === '' || $key[0] == '-' ) {
throw new PropertyLabelNotResolvedException( "Illegal property key \"$key\"." );
}
if ( $key[0] == '_' && !PropertyRegistry::getInstance()->isRegistered( $key ) ) {
throw new PredefinedPropertyLabelMismatchException( "There is no predefined property with \"$key\"." );
}
$this->m_key = $key;
$this->m_inverse = $inverse;
}
/**
* @since 1.6
*
* @return int
*/
public function getDIType(): int {
return SMWDataItem::TYPE_PROPERTY;
}
/**
* @since 1.6
*
* @return string|null
*/
public function getKey(): ?string {
return $this->m_key;
}
/**
* @since 1.6
*
* @return bool
*/
public function isInverse(): bool {
return $this->m_inverse;
}
/**
* @since 3.1
*
* @return string
*/
public function getSha1(): string {
return sha1( json_encode( [ $this->m_key, SMW_NS_PROPERTY, '', '' ] ) );
}
/**
* @since 1.6
*
* @return string|null
*/
public function getSortKey(): ?string {
return $this->m_key;
}
/**
* Specifies whether values of this property should be shown in the
* Factbox. A property may wish to prevent this if either
* (1) its information is really dull, e.g. being a mere copy of
* information that is obvious from other things that are shown, or
* (2) the property is set in a hook after parsing, so that it is not
* reliably available when Factboxes are displayed. If a property is
* internal so it should never be observed by users, then it is better
* to just not associate any translated label with it, so it never
* appears anywhere.
*
* Examples of properties that are not shown include Modification date
* (not available in time), and Has improper value for (errors are
* shown directly on the page anyway).
*
* @since 1.6
*
* @return int
*/
public function isShown(): bool {
if ( $this->isUserDefined() ) {
return true;
}
return PropertyRegistry::getInstance()->isVisible( $this->m_key );
}
/**
* Return true if this is a usual wiki property that is defined by a
* wiki page, and not a property that is pre-defined in the wiki.
*
* @since 1.6
*
* @return bool
*/
public function isUserDefined(): bool {
return $this->m_key[0] != '_';
}
/**
* Whether a user can freely use this property for value annotation or
* not.
*
* @since 3.0
*
* @return bool
*/
public function isUserAnnotable(): bool {
// A user defined property is generally assumed to be unrestricted for
// usage
if ( $this->isUserDefined() ) {
return true;
}
return PropertyRegistry::getInstance()->isAnnotable( $this->m_key );
}
/**
* Find a user-readable label for this property, or return '' if it is
* a predefined property that has no label. For inverse properties, the
* label starts with a "-".
*
* @since 1.6
*
* @return string
*/
public function getLabel(): string {
$prefix = $this->m_inverse ? '-' : '';
if ( $this->isUserDefined() ) {
return $prefix . str_replace( '_', ' ', $this->m_key );
}
return $prefix . PropertyRegistry::getInstance()->findPropertyLabelById( $this->m_key );
}
/**
* @since 2.4
*
* @return string|null
*/
public function getCanonicalLabel(): ?string {
$prefix = $this->m_inverse ? '-' : '';
if ( $this->isUserDefined() ) {
return $prefix . str_replace( '_', ' ', $this->m_key );
}
return $prefix . PropertyRegistry::getInstance()->findCanonicalPropertyLabelById( $this->m_key );
}
/**
* Borrowing the skos:prefLabel definition where a preferred label is expected
* to have only one label per given language (skos:altLabel can have many
* alternative labels)
*
* An empty string signals that no preferred label is available in the current
* user language.
*
* @since 2.5
*
* @param string $languageCode
*
* @return string
*/
public function getPreferredLabel( string $languageCode = '' ): string {
$label = PropertyRegistry::getInstance()->findPreferredPropertyLabelFromIdByLanguageCode(
$this->m_key,
$languageCode
);
if ( $label !== '' ) {
return ( $this->m_inverse ? '-' : '' ) . $label;
}
return '';
}
/**
* @since 2.4
*
* @param string $interwiki
*/
public function setInterwiki( string $interwiki ) {
$this->interwiki = $interwiki;
}
/**
* Get an object of type DIWikiPage that represents the page which
* relates to this property, or null if no such page exists. The latter
* can happen for special properties without user-readable label.
*
* It is possible to construct subobjects of the property's wikipage by
* providing an optional subobject name.
*
* @since 1.6
*
* @param string $subobjectName
*
* @return DIWikiPage|null
*/
public function getDiWikiPage( string $subobjectName = '' ): ?DIWikiPage {
$dbkey = $this->m_key;
if ( !$this->isUserDefined() ) {
$dbkey = $this->getLabel();
}
return $this->newDIWikiPage( $dbkey, $subobjectName );
}
/**
* @since 2.4
*
* @param string $subobjectName
*
* @return DIWikiPage|null
*/
public function getCanonicalDiWikiPage( string $subobjectName = '' ): ?DIWikiPage {
if ( $this->isUserDefined() ) {
$dbkey = $this->m_key;
} elseif ( $this->m_key === $this->findPropertyTypeID() ) {
// If _dat as property [[Date::...]] refers directly to its _dat type
// then use the en-label as canonical representation
$dbkey = PropertyRegistry::getInstance()->findPropertyLabelFromIdByLanguageCode( $this->m_key, 'en' );
} else {
$dbkey = PropertyRegistry::getInstance()->findCanonicalPropertyLabelById( $this->m_key );
}
if ( $dbkey === false ) {
$dbkey = $this->m_key;
}
if ( $dbkey === null ) {
return null;
}
return $this->newDIWikiPage( $dbkey, $subobjectName );
}
/**
* @since 2.4
*
* @return DIProperty
*/
public function getRedirectTarget(): self {
if ( $this->m_inverse ) {
return $this;
}
return ApplicationFactory::getInstance()->getStore()->getRedirectTarget( $this );
}
/**
* @deprecated since 3.0, use DIProperty::setPropertyValueType
*/
public function setPropertyTypeId( $valueType ) {
return $this->setPropertyValueType( $valueType );
}
/**
* @since 3.0
*
* @param string $valueType
*
* @return self
* @throws DataTypeLookupException
* @throws RuntimeException
*/
public function setPropertyValueType( string $valueType ): self {
if ( !DataTypeRegistry::getInstance()->isRegistered( $valueType ) ) {
throw new DataTypeLookupException( "{$valueType} is an unknown type id" );
}
if ( $this->isUserDefined() && $this->propertyValueType === null ) {
$this->propertyValueType = $valueType;
return $this;
}
if ( !$this->isUserDefined() && $valueType === PropertyRegistry::getInstance()->getPropertyValueTypeById( $this->m_key ) ) {
$this->propertyValueType = $valueType;
return $this;
}
throw new RuntimeException( 'DataType cannot be altered for a predefined property' );
}
/**
* @deprecated since 3.0, use DIProperty::findPropertyValueType
*/
public function findPropertyTypeId() {
return $this->findPropertyValueType();
}
/**
* Find the property's type ID, either by looking up its predefined ID
* (if any) or by retrieving the relevant information from the store.
* If no type is stored for a user defined property, the global default
* type will be used.
*
* @since 3.0
*
* @return string type ID
*/
public function findPropertyValueType(): string {
if ( isset( $this->propertyValueType ) ) {
return $this->propertyValueType;
}
if ( !$this->isUserDefined() ) {
return $this->propertyValueType = PropertyRegistry::getInstance()->getPropertyValueTypeById( $this->m_key );
}
$diWikiPage = new DIWikiPage( $this->getKey(), SMW_NS_PROPERTY, $this->interwiki );
$applicationFactory = ApplicationFactory::getInstance();
$typearray = $applicationFactory->getPropertySpecificationLookup()->getSpecification(
$this,
new self( '_TYPE' )
);
if ( is_array( $typearray ) && count( $typearray ) >= 1 ) { // some types given, pick one (hopefully unique)
$typeDataItem = reset( $typearray );
if ( $typeDataItem instanceof SMWDIUri ) {
$this->propertyValueType = $typeDataItem->getFragment();
} else {
// This is important. If a page has an invalid assignment to "has type", no
// value will be stored, so the elseif case below occurs. But if the value
// is retrieved within the same run, then the error value for "has type" is
// cached and thus this case occurs. This is why it is important to tolerate
// this case -- it is not necessarily a DB error.
$this->propertyValueType = $applicationFactory->getSettings()->get( 'smwgPDefaultType' );
}
} else { // no type given
$this->propertyValueType = $applicationFactory->getSettings()->get( 'smwgPDefaultType' );
}
return $this->propertyValueType;
}
/**
* @see DataItem::getSerialization
*
* @since 1.6
*
* @return string|null
*/
public function getSerialization(): ?string {
return ( $this->m_inverse ? '-' : '' ) . $this->m_key;
}
/**
* Create a data item from the provided serialization string and type
* ID.
*
* @since 1.6
*
* @param ?string $serialization
*
* @return DIProperty
*/
public static function doUnserialize( ?string $serialization ): self {
$inverse = false;
if ( is_string( $serialization ) && $serialization[0] == '-' ) {
$serialization = substr( $serialization, 1 );
$inverse = true;
}
return new self( $serialization, $inverse );
}
/**
* @see DataItem::equals
*
* @since 1.6
*
* @param SMWDataItem $di
*
* @return bool
*/
public function equals( SMWDataItem $di ): bool {
if ( $di->getDIType() !== SMWDataItem::TYPE_PROPERTY ) {
return false;
}
return $di->getKey() === $this->m_key;
}
/**
* Construct a property from a user-supplied label. The main difference
* to the normal constructor of DIProperty is that it is checked
* whether the label refers to a known predefined property.
* Note that this function only gives access to the registry data that
* DIProperty stores, but does not do further parsing of user input.
*
* To process wiki input, PropertyValue should be used.
*
* @since 1.6
*
* @param string $label
* @param bool $inverse = false
* @param $languageCode = false
*
* @return DIProperty
*/
public static function newFromUserLabel( string $label, bool $inverse = false, $languageCode = false ): self {
// Explicitly cast to a string so we are able to return an object from
// any user label
$label = (string)$label;
if ( $label !== '' && $label[0] == '-' ) {
$label = substr( $label, 1 );
$inverse = true;
}
// Special handling for when the user value contains a @LCODE marker
if ( ( $annotatedLanguageCode = Localizer::getAnnotatedLanguageCodeFrom( $label ) ) !== false ) {
$languageCode = $annotatedLanguageCode;
}
$id = false;
$label = str_replace( '_', ' ', $label );
if ( $languageCode ) {
$id = PropertyRegistry::getInstance()->findPropertyIdFromLabelByLanguageCode(
$label,
$languageCode
);
}
if ( $id !== false ) {
return new self( $id, $inverse );
}
$id = PropertyRegistry::getInstance()->findPropertyIdByLabel(
$label
);
if ( $id === false ) {
return new self( str_replace( ' ', '_', $label ), $inverse );
}
return new self( $id, $inverse );
}
private function newDIWikiPage( string $dbkey, string $subobjectName ): ?DIWikiPage {
// If an inverse marker is present just omit the marker so a normal
// property page link can be produced independent of its directionality
if ( $dbkey !== '' && $dbkey[0] == '-' ) {
$dbkey = substr( $dbkey, 1 );
}
try {
return new DIWikiPage( str_replace( ' ', '_', $dbkey ), SMW_NS_PROPERTY, $this->interwiki, $subobjectName );
} catch ( DataItemException $e ) {
return null;
}
}
/**
* Implements \JsonSerializable.
*
* @since 4.0.0
*
* @return array
*/
public function jsonSerialize(): array {
$json = parent::jsonSerialize();
$json['propertyValueType'] = $this->propertyValueType;
$json['interwiki'] = $this->interwiki;
return $json;
}
/**
* Implements JsonUnserializable.
*
* @since 4.0.0
*
* @param JsonUnserializer $unserializer Unserializer
* @param array $json JSON to be unserialized
*
* @return self
*/
public static function newFromJsonArray( JsonUnserializer $unserializer, array $json ) {
$obj = parent::newFromJsonArray( $unserializer, $json );
$obj->propertyValueType = $json['propertyValueType'];
$obj->interwiki = $json['interwiki'];
return $obj;
}
}