diff options
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_WikiPage.php')
-rw-r--r-- | www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_WikiPage.php | 751 |
1 files changed, 751 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_WikiPage.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_WikiPage.php new file mode 100644 index 00000000..c1c1579e --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_WikiPage.php @@ -0,0 +1,751 @@ +<?php + +use SMW\ApplicationFactory; +use SMW\DIProperty; +use SMW\Localizer; +use SMW\Message; +use SMW\Utils\Image; + +/** + * @ingroup SMWDataValues + */ + +/** + * This datavalue implements special processing suitable for defining + * wikipages as values of properties. + * + * The class can support general wiki pages, or pages of a fixed + * namespace, Whether a namespace is fixed is decided based on the + * type ID when the object is constructed. + * + * The short display simulates the behavior of the MediaWiki "pipe trick" + * but always includes fragments. This can be overwritten by setting a + * caption, which is also done by default when generating a value from user + * input. The long display always includes all relevant information. Only if a + * fixed namespace is used for the datatype, the namespace prefix is omitted. + * This behavior has changed in SMW 1.7: up to this time, short displays have + * always included the namespace and long displays used the pipe trick, leading + * to a paradoxical confusion of "long" and "short". + * + * @author Nikolas Iwan + * @author Markus Krötzsch + * @ingroup SMWDataValues + */ +class SMWWikiPageValue extends SMWDataValue { + + /** + * Whether text transformation should be suppressed or not. + */ + const NO_TEXT_TRANSFORMATION = 'no.text.transformation'; + + /** + * Whether to use the short form or not. + */ + const SHORT_FORM = 'short.form'; + + /** + * Fragment text for user-specified title. Not stored, but kept for + * printout on page. + * @var string + */ + protected $m_fragment = ''; + + /** + * Full titletext with prefixes, including interwiki prefix. + * Set to empty string if not computed yet. + * @var string + */ + protected $m_prefixedtext = ''; + + /** + * Cache for the related MW page ID. + * Set to -1 if not computed yet. + * @var integer + */ + protected $m_id = -1; + + /** + * Cache for the related MW title object. + * Set to null if not computed yet. + * @var Title + */ + protected $m_title = null; + + /** + * If this has a value other than NS_MAIN, the datavalue will only + * accept pages in this namespace. This field is initialized when + * creating the object (based on the type id or base on the preference + * of some subclass); it is not usually changed afterwards. + * @var integer + */ + protected $m_fixNamespace = NS_MAIN; + + /** + * @var array + */ + protected $linkAttributes = []; + + /** + * @var array + */ + protected $queryParameters = []; + + public function __construct( $typeid ) { + parent::__construct( $typeid ); + switch ( $typeid ) { + case '_wpp' : case '__sup': + $this->m_fixNamespace = SMW_NS_PROPERTY; + break; + case '_wpc' : case '__suc': case '__sin': + $this->m_fixNamespace = NS_CATEGORY; + break; + case '_wpf' : case '__spf': + $this->m_fixNamespace = SF_NS_FORM; + break; + case '_wps' : + $this->m_fixNamespace = SMW_NS_SCHEMA; + break; + default: // case '_wpg': + $this->m_fixNamespace = NS_MAIN; + } + } + + protected function parseUserValue( $value ) { + global $wgContLang; + + // support inputs like " [[Test]] "; + // note that this only works when SMW_PARSER_LINV is set + $value = ltrim( rtrim( $value, ' ]' ), ' [' ); + + // #1066, Manipulate the output only for when the value has no caption + // assigned and only if a single :Foo is being present, ::Foo is not permitted + if ( $this->m_caption === false && isset( $value[2] ) && $value[0] === ':' && $value[1] !== ':' ) { + $value = substr( $value, 1 ); + } + + if ( $this->m_caption === false ) { + $this->m_caption = $value; + } + + if ( $value === '' && !$this->getOption( self::OPT_QUERY_CONTEXT ) ) { + $this->addErrorMsg( [ 'smw-datavalue-wikipage-empty' ], Message::ESCAPED ); + return; + } + + // #1701 If the DV is part of a Description and an approximate search + // (e.g. ~foo* / ~Foo*) then use the value as-is and avoid being + // transformed by the Title object + // If the vaue contains a valid NS then use the Title to create a correct + // instance to distinguish [[~Foo*]] from [[Help:~Foo*]] + if ( $this->getOption( self::OPT_QUERY_COMP_CONTEXT ) || $this->getOption( self::OPT_QUERY_CONTEXT ) ) { + + $title = Title::newFromText( $value, $this->m_fixNamespace ); + + // T:P0427 If the user value says `ab c*` then make sure to use this one + // instead of the transformed DBKey which would be `Ab c*` + if ( $title !== null && $title->getNamespace() === NS_MAIN && $this->getOption( 'isCapitalLinks' ) === false ) { + return $this->m_dataitem = new SMWDIWikiPage( $value, NS_MAIN ); + // If we know that it is a wikipage in a query context and the wiki + // requires `isCapitalLinks` then use the standard transformation so + // they appear as standard links even though the user input was `abc`. + // T:P0902 (`[[Help:]]`) + } elseif ( $title !== null ) { + return $this->m_dataitem = SMWDIWikiPage::newFromTitle( $title ); + } elseif ( !Localizer::getInstance()->getNamespaceIndexByName( substr( $value, 0, -1 ) ) ) { + return $this->m_dataitem = new SMWDIWikiPage( $value, NS_MAIN ); + } + } + + if ( $value[0] == '#' ) { + if ( is_null( $this->m_contextPage ) ) { + $this->addErrorMsg( [ 'smw-datavalue-wikipage-missing-fragment-context', $value ] ); + return; + } else { + $this->m_title = Title::makeTitle( $this->m_contextPage->getNamespace(), + $this->m_contextPage->getDBkey(), substr( $value, 1 ), + $this->m_contextPage->getInterwiki() ); + } + } else { + $this->m_title = Title::newFromText( $value, $this->m_fixNamespace ); + } + + /// TODO: Escape the text so users can see punctuation problems (bug 11666). + if ( $this->m_title === null && $this->getProperty() !== null ) { + $this->addErrorMsg( [ 'smw-datavalue-wikipage-property-invalid-title', $this->getProperty()->getLabel(), $value ] ); + } elseif ( $this->m_title === null ) { + $this->addErrorMsg( [ 'smw-datavalue-wikipage-invalid-title', $value ] ); + } elseif ( ( $this->m_fixNamespace != NS_MAIN ) && + ( $this->m_fixNamespace != $this->m_title->getNamespace() ) ) { + $this->addErrorMsg( [ 'smw_wrong_namespace', $wgContLang->getNsText( $this->m_fixNamespace ) ] ); + } else { + $this->m_fragment = str_replace( ' ', '_', $this->m_title->getFragment() ); + $this->m_prefixedtext = ''; + $this->m_id = -1; // unset id + $this->m_dataitem = SMWDIWikiPage::newFromTitle( $this->m_title, $this->m_typeid ); + } + } + + /** + * @see SMWDataValue::loadDataItem() + * @param $dataitem SMWDataItem + * @return boolean + */ + protected function loadDataItem( SMWDataItem $dataItem ) { + + if ( $dataItem->getDIType() == SMWDataItem::TYPE_CONTAINER ) { + // might throw an exception, we just pass it through + $dataItem = $dataItem->getSemanticData()->getSubject(); + } + + if ( $dataItem->getDIType() !== SMWDataItem::TYPE_WIKIPAGE ) { + return false; + } + + $this->m_dataitem = $dataItem; + $this->m_id = -1; + $this->m_title = null; + $this->m_fragment = $dataItem->getSubobjectName(); + $this->m_prefixedtext = ''; + $this->m_caption = false; // this class can handle this + $this->linkAttributes = []; + + if ( ( $this->m_fixNamespace != NS_MAIN ) && + ( $this->m_fixNamespace != $dataItem->getNamespace() ) ) { + $this->addErrorMsg( + [ + 'smw_wrong_namespace', + Localizer::getInstance()->getNamespaceTextById( $this->m_fixNamespace ) + ] + ); + } + + return true; + } + + /** + * @since 2.4 + * + * @param array $linkAttributes + */ + public function setLinkAttributes( array $linkAttributes ) { + $this->linkAttributes = $linkAttributes; + } + + /** + * @since 2.5 + * + * @param array $queryParameters + */ + public function setQueryParameters( array $queryParameters ) { + $this->queryParameters = $queryParameters; + } + + /** + * Display the value on a wiki page. This is used to display the value + * in the place where it was annotated on a wiki page. The desired + * behavior is that the display in this case looks as if no property + * annotation had been given, i.e. an annotation [[property::page|foo]] + * should display like [[page|foo]] in MediaWiki. But this should lead + * to a link, not to a category assignment. This means that: + * + * (1) If Image: is used (instead of Media:) then let MediaWiki embed + * the image. + * + * (2) If Category: is used, treat it as a page and link to it (do not + * categorize the page) + * + * (3) Preserve everything given after "|" for display (caption, image + * parameters, ...) + * + * (4) Use the (default) caption for display. When the value comes from + * user input, this includes the full value that one would also see in + * MediaWiki. + * + * @param $linked mixed generate links if not null or false + * @return string + */ + public function getShortWikiText( $linked = null ) { + + if ( is_null( $linked ) || $linked === false || + $this->m_outformat == '-' || !$this->isValid() || + $this->m_caption === '' ) { + return $this->m_caption !== false ? $this->m_caption : $this->getWikiValue(); + } + + if ( Image::isImage( $this->m_dataitem ) && $this->m_dataitem->getInterwiki() === '' ) { + $linkEscape = ''; + $options = $this->m_outformat === false ? 'frameless|border|text-top|' : str_replace( ';', '|', \Sanitizer::removeHTMLtags( $this->m_outformat ) ); + $defaultCaption = '|' . $this->getShortCaptionText() . '|' . $options; + } else { + $linkEscape = ':'; + $defaultCaption = '|' . $this->getShortCaptionText(); + } + + if ( $this->m_caption === false ) { + $link = '[[' . $linkEscape . $this->getWikiLinkTarget() . $defaultCaption . ']]'; + } else { + $link = '[[' . $linkEscape . $this->getWikiLinkTarget() . '|' . $this->m_caption . ']]'; + } + + if ( $this->m_fragment !== '' ) { + $this->linkAttributes['class'] = 'smw-subobject-entity'; + } + + if ( $this->linkAttributes !== [] ) { + $link = \Html::rawElement( + 'span', + $this->linkAttributes, + $link + ); + } + + return $link; + } + + /** + * Display the value as in getShortWikiText() but create HTML. + * The only difference is that images are not embedded. + * + * @param Linker $linker mixed the Linker object to use or null if no linking is desired + * @return string + */ + public function getShortHTMLText( $linker = null ) { + + if ( $this->m_fragment !== '' ) { + $this->linkAttributes['class'] = 'smw-subobject-entity'; + } + + // init the Title object, may reveal hitherto unnoticed errors: + if ( !is_null( $linker ) && $linker !== false && + $this->m_caption !== '' && $this->m_outformat != '-' ) { + $this->getTitle(); + } + + if ( is_null( $linker ) || $linker === false || !$this->isValid() || + $this->m_outformat == '-' || $this->m_caption === '' ) { + + $caption = $this->m_caption === false ? $this->getWikiValue() : $this->m_caption; + return \Sanitizer::removeHTMLtags( $caption ); + } + + $caption = $this->m_caption === false ? $this->getShortCaptionText() : $this->m_caption; + $caption = \Sanitizer::removeHTMLtags( $caption ); + + if ( $this->getNamespace() == NS_MEDIA ) { // this extra case *is* needed + return $linker->makeMediaLinkObj( $this->getTitle(), $caption ); + } + + return $linker->link( + $this->getTitle(), + $caption, + $this->linkAttributes, + $this->queryParameters + ); + } + + /** + * Display the "long" value on a wiki page. This behaves largely like + * getShortWikiText() but does not use the caption. Instead, it always + * takes the long display form (wiki value). + * + * @param $linked mixed if true the result will be linked + * @return string + */ + public function getLongWikiText( $linked = null ) { + if ( !$this->isValid() ) { + return $this->getErrorText(); + } + + if ( is_null( $linked ) || $linked === false || $this->m_outformat == '-' ) { + return $this->getWikiValue(); + } elseif ( Image::isImage( $this->m_dataitem ) && $this->m_dataitem->getInterwiki() === '' ) { + // Embed images and other files + // Note that the embedded file links to the image, hence needs no additional link text. + // There should not be a linebreak after an impage, just like there is no linebreak after + // other values (whether formatted or not). + return '[[' . $this->getWikiLinkTarget() . '|' . + $this->getLongCaptionText() . '|frameless|border|text-top]]'; + } + + $link = '[[:' . $this->getWikiLinkTarget() . '|' . $this->getLongCaptionText() . ']]'; + + if ( $this->m_fragment !== '' ) { + $this->linkAttributes['class'] = 'smw-subobject-entity'; + } + + if ( $this->linkAttributes !== [] ) { + $link = \Html::rawElement( + 'span', + $this->linkAttributes, + $link + ); + } + + return $link; + } + + /** + * Display the "long" value in HTML. This behaves largely like + * getLongWikiText() but does not embed images. + * + * @param $linker mixed if a Linker is given, the result will be linked + * @return string + */ + public function getLongHTMLText( $linker = null ) { + + if ( $this->m_fragment !== '' ) { + $this->linkAttributes['class'] = 'smw-subobject-entity'; + } + + // init the Title object, may reveal hitherto unnoticed errors: + if ( !is_null( $linker ) && ( $this->m_outformat != '-' ) ) { + $this->getTitle(); + } + + if ( !$this->isValid() ) { + return $this->getErrorText(); + } + + if ( $linker === null || $linker === false || $this->m_outformat == '-' ) { + return \Sanitizer::removeHTMLtags( $this->getWikiValue() ); + } elseif ( $this->getNamespace() == NS_MEDIA ) { // this extra case is really needed + return $linker->makeMediaLinkObj( + $this->getTitle(), + \Sanitizer::removeHTMLtags( $this->getLongCaptionText() ) + ); + } + + // all others use default linking, no embedding of images here + return $linker->link( + $this->getTitle(), + \Sanitizer::removeHTMLtags( $this->getLongCaptionText() ), + $this->linkAttributes, + $this->queryParameters + ); + } + + /** + * Return a string that could be used in an in-page property assignment + * for setting this value. This does not include initial ":" for + * escaping things like Category: links since the property value does + * not include such escapes either. Fragment information is included. + * Namespaces are omitted if a fixed namespace is used, since they are + * not needed in this case when making a property assignment. + * + * @return string + */ + public function getWikiValue() { + + if ( $this->getOption( self::SHORT_FORM, false ) ) { + $text = $this->getText(); + } elseif ( $this->getTypeID() === '_wpp' || $this->m_fixNamespace == NS_MAIN ) { + $text = $this->getPrefixedText(); + } else { + $text = $this->getText(); + } + + return $text . ( $this->m_fragment !== '' ? "#{$this->m_fragment}" : '' ); + } + + public function getHash() { + return $this->isValid() ? $this->getPrefixedText() : implode( "\t", $this->getErrors() ); + } + + /** + * Create links to mapping services based on a wiki-editable message. + * The parameters available to the message are: + * $1: urlencoded article name (no namespace) + * + * @return array + */ + protected function getServiceLinkParams() { + if ( $this->isValid() ) { + return [ rawurlencode( str_replace( '_', ' ', $this->m_dataitem->getDBkey() ) ) ]; + } else { + return []; + } + } + +///// special interface for wiki page values + + /** + * Return according Title object or null if no valid value was set. + * null can be returned even if this object returns true for isValid(), + * since the latter function does not check whether MediaWiki can really + * make a Title out of the given data. + * However, isValid() will return false *after* this function failed in + * trying to create a title. + * + * @return Title + */ + public function getTitle() { + + if ( $this->m_title !== null ) { + return $this->m_title; + } + + if ( $this->isValid() ) { + + if ( ( $title = $this->m_dataitem->getTitle() ) !== null ) { + return $this->m_title = $title; + } + + // #3278, Special handling of `>` in the user namespace, MW (1.31+) + // added a prefix to users that originate from imported content + if ( + $this->m_dataitem->getNamespace() === NS_USER && + strpos( $this->m_dataitem->getDBkey(), '>' ) !== false ) { + + $this->setOption( self::OPT_DISABLE_INFOLINKS, true ); + + $this->m_title = Title::newFromText( + $this->m_dataitem->getDBkey() + ); + + return $this->m_title; + } + } + + $errArg = $this->m_caption; + + if ( $this->isValid() ) { + $ns = Localizer::getInstance()->getNamespaceTextById( + $this->m_dataitem->getNamespace() + ); + + $errArg = "$ns:" . $this->m_dataitem->getDBkey(); + } + + // Should not normally happen, but anyway ... + $this->addErrorMsg( [ 'smw_notitle', $errArg ] ); + } + + /** + * Get MediaWiki's ID for this value or 0 if not available. + * + * @return integer + */ + public function getArticleID() { + if ( $this->m_id === false ) { + $this->m_id = !is_null( $this->getTitle() ) ? $this->m_title->getArticleID() : 0; + } + + return $this->m_id; + } + + /** + * Get namespace constant for this value. + * + * @return integer + */ + public function getNamespace() { + return $this->m_dataitem->getNamespace(); + } + + /** + * Get DBKey for this value. Subclasses that allow for values that do not + * correspond to wiki pages may choose a DB key that is not a legal title + * DB key but rather another suitable internal ID. Thus it is not suitable + * to use this method in places where only MediaWiki Title keys are allowed. + * + * @return string + */ + public function getDBkey() { + return $this->m_dataitem->getDBkey(); + } + + /** + * Get text label for this value, just like Title::getText(). + * + * @return string + */ + public function getText() { + + if ( $this->getOption( self::NO_TEXT_TRANSFORMATION, false ) ) { + return $this->m_dataitem->getDBkey(); + } + + return str_replace( '_', ' ', $this->m_dataitem->getDBkey() ); + } + + /** + * Get the prefixed text for this value, including a localized namespace + * prefix. + * + * @return string + */ + public function getPrefixedText() { + + if ( $this->m_prefixedtext !== '' ) { + return $this->m_prefixedtext; + } + + // In case something went wrong (invalid NS etc.), hint the ID to aid the + // investigation + if ( $this->m_dataitem->getId() > 0 ) { + $this->m_prefixedtext = 'NO_VALID_VALUE (ID: ' . $this->m_dataitem->getId() . ')'; + } else { + $this->m_prefixedtext = 'NO_VALID_VALUE'; + } + + if ( $this->isValid() ) { + $nstext = Localizer::getInstance()->getNamespaceTextById( $this->m_dataitem->getNamespace() ); + $this->m_prefixedtext = + ( $this->m_dataitem->getInterwiki() !== '' ? $this->m_dataitem->getInterwiki() . ':' : '' ) . + ( $nstext !== '' ? "$nstext:" : '' ) . $this->getText(); + } + + return $this->m_prefixedtext; + } + + /** + * Get interwiki prefix or empty string. + * + * @return string + */ + public function getInterwiki() { + return $this->m_dataitem->getInterwiki(); + } + + /** + * DataValue::getPreferredCaption + * + * @since 2.4 + * + * @return string + */ + public function getPreferredCaption() { + + if ( ( $preferredCaption = parent::getPreferredCaption() ) !== '' && $preferredCaption !== false ) { + return $preferredCaption; + } + + $preferredCaption = $this->getDisplayTitle(); + + if ( $preferredCaption === '' && $this->getOption( 'prefixed.preferred.caption' ) ) { + $preferredCaption = $this->getPrefixedText(); + } elseif ( $preferredCaption === '' ) { + $preferredCaption = $this->getText(); + } + + return $preferredCaption; + } + + /** + * Get a short caption used to label this value. In particular, this + * omits namespace and interwiki prefixes (similar to the MediaWiki + * "pipe trick"). Fragments are included unless they start with an + * underscore (used for generated fragment names that are not helpful + * for users and that might change easily). + * + * @since 1.7 + * @return string + */ + protected function getShortCaptionText() { + if ( $this->m_fragment !== '' && $this->m_fragment[0] != '_' ) { + $fragmentText = '#' . str_replace( '_', ' ', $this->m_fragment ); + } else { + $fragmentText = ''; + } + + if ( $this->m_caption && $this->m_caption !== '' ) { + return $this->m_caption; + } + + $displayTitle = $this->getDisplayTitle(); + + if ( $displayTitle === '' ) { + $displayTitle = $this->getText(); + } + + return $displayTitle . $fragmentText; + } + + /** + * Get a long caption used to label this value. In particular, this + * includes namespace and interwiki prefixes, while fragments are only + * included if they do not start with an underscore (used for generated + * fragment names that are not helpful for users and that might change + * easily). + * + * @since 1.7 + * @return string + */ + protected function getLongCaptionText() { + if ( $this->m_fragment !== '' && $this->m_fragment[0] != '_' ) { + $fragmentText = '#' . str_replace( '_', ' ', $this->m_fragment ); + } else { + $fragmentText = ''; + } + + if ( $this->m_caption && $this->m_caption !== '' ) { + return $this->m_caption; + } + + $displayTitle = $this->getDisplayTitle(); + + if ( $displayTitle === '' ) { + $displayTitle = $this->m_fixNamespace == NS_MAIN ? $this->getPrefixedText() : $this->getText(); + } + + return $displayTitle . $fragmentText; + } + + /** + * Compute a text that can be used in wiki text to link to this + * datavalue. Processing includes some escaping and adding the + * fragment. + * + * @since 1.7 + * @return string + */ + protected function getWikiLinkTarget() { + return str_replace( "'", ''', $this->getPrefixedText() ) . + ( $this->m_fragment !== '' ? "#{$this->m_fragment}" : '' ); + } + + /** + * Find the sortkey for this object. + * + * @deprecated Use SMWStore::getWikiPageSortKey(). Will vanish before SMW 1.7 + * + * @return string sortkey + */ + public function getSortKey() { + return ApplicationFactory::getInstance()->getStore()->getWikiPageSortKey( $this->m_dataitem ); + } + + /** + * @since 2.4 + * + * @return string + */ + public function getDisplayTitle() { + + if ( $this->m_dataitem === null || !$this->isEnabledFeature( SMW_DV_WPV_DTITLE ) ) { + return ''; + } + + return $this->findDisplayTitleFor( $this->m_dataitem ); + } + + private function findDisplayTitleFor( $subject ) { + + $displayTitle = ''; + + $dataItems = ApplicationFactory::getInstance()->getCachedPropertyValuesPrefetcher()->getPropertyValues( + $subject, + new DIProperty( '_DTITLE' ) + ); + + if ( $dataItems !== null && $dataItems !== [] ) { + $displayTitle = end( $dataItems )->getString(); + } elseif ( $subject->getSubobjectName() !== '' ) { + // Check whether the base subject has a DISPLAYTITLE + return $this->findDisplayTitleFor( $subject->asBase() ); + } + + return $displayTitle; + } + +} |