diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/SemanticMediaWiki/includes |
first commit
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/includes')
69 files changed, 23106 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/ContentParser.php b/www/wiki/extensions/SemanticMediaWiki/includes/ContentParser.php new file mode 100644 index 00000000..3fda57a9 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/ContentParser.php @@ -0,0 +1,251 @@ +<?php + +namespace SMW; + +use Parser; +use ParserOptions; +use Revision; +use Title; +use User; + +/** + * Fetches the ParserOutput either by parsing an invoked text component, + * re-parsing a text revision, or accessing the ContentHandler to generate a + * ParserOutput object + * + * @ingroup SMW + * + * @licence GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ +class ContentParser { + + /** @var Title */ + protected $title; + + /** @var Parser */ + protected $parser = null; + + /** @var ParserOutput */ + protected $parserOutput = null; + + /** @var Revision */ + protected $revision = null; + + /** @var array */ + protected $errors = []; + + /** + * @var boolean + */ + private $enabledToUseContentHandler = true; + + /** + * @var boolean + */ + private $skipInTextAnnotationParser = false; + + /** + * @note Injecting new Parser() alone will not yield an expected result and + * doing new Parser( $GLOBALS['wgParserConf'] brings no benefits therefore + * we stick to the GLOBAL as fallback if no parser is injected. + * + * @since 1.9 + * + * @param Title $title + * @param Parser|null $parser + */ + public function __construct( Title $title, Parser $parser = null ) { + $this->title = $title; + $this->parser = $parser; + + if ( $this->parser === null ) { + $this->parser = $GLOBALS['wgParser']; + } + } + + /** + * @since 2.3 + * + * @return Parser $parser + */ + public function setParser( Parser $parser ) { + $this->parser = $parser; + } + + /** + * @since 1.9.1 + * + * @return ContentParser + */ + public function setRevision( Revision $revision = null ) { + $this->revision = $revision; + return $this; + } + + /** + * @bug 62856 and #212 + * + * @since 2.0 + */ + public function forceToUseParser() { + $this->enabledToUseContentHandler = false; + } + + /** + * @since 1.9 + * + * @return Title + */ + public function getTitle() { + return $this->title; + } + + /** + * @since 1.9 + * + * @return ParserOutput|null + */ + public function getOutput() { + return $this->parserOutput; + } + + /** + * @since 1.9 + * + * @return array + */ + public function getErrors() { + return $this->errors; + } + + public function skipInTextAnnotationParser() { + return $this->skipInTextAnnotationParser = true; + } + + /** + * Generates or fetches the ParserOutput object from an appropriate source + * + * @since 1.9 + * + * @param string|null $text + * + * @return ContentParser + */ + public function parse( $text = null ) { + + if ( $text !== null ) { + return $this->parseText( $text ); + } + + if ( $this->hasContentHandler() && $this->enabledToUseContentHandler ) { + return $this->fetchFromContent(); + } + + return $this->fetchFromParser(); + } + + protected function parseText( $text ) { + + $this->parserOutput = $this->parser->parse( + $text, + $this->getTitle(), + $this->makeParserOptions() + ); + + return $this; + } + + /** + * @note Revision ID must be passed to the parser output to + * get revision variables correct + * + * @note If no content is available create an empty object + */ + protected function fetchFromContent() { + + if ( $this->getRevision() === null ) { + return $this->msgForNullRevision(); + } + + $content = $this->getRevision()->getContent( Revision::RAW ); + + if ( !$content ) { + $content = $this->getRevision()->getContentHandler()->makeEmptyContent(); + } + + $this->parserOutput = $content->getParserOutput( + $this->getTitle(), + $this->getRevision()->getId(), + null, + true + ); + + return $this; + } + + protected function fetchFromParser() { + + if ( $this->getRevision() === null ) { + return $this->msgForNullRevision(); + } + + $this->parserOutput = $this->parser->parse( + $this->getRevision()->getText(), + $this->getTitle(), + $this->makeParserOptions(), + true, + true, + $this->getRevision()->getID() + ); + + return $this; + } + + protected function msgForNullRevision( $fname = __METHOD__ ) { + $this->errors = [ $fname . " No revision available for {$this->getTitle()->getPrefixedDBkey()}" ]; + return $this; + } + + protected function makeParserOptions() { + + $user = null; + + if ( $this->getRevision() !== null ) { + $user = User::newFromId( $this->getRevision()->getUser() ); + } + + $parserOptions = new ParserOptions( $user ); + + // Use the InterfaceMessage marker to skip InTextAnnotationParser + // processing + $parserOptions->setInterfaceMessage( $this->skipInTextAnnotationParser ); + + return $parserOptions; + } + + protected function getRevision() { + + if ( $this->revision instanceof Revision ) { + return $this->revision; + } + + // Revision::READ_NORMAL is not specified in MW 1.19 + if ( defined( 'Revision::READ_NORMAL' ) ) { + $this->revision = Revision::newFromTitle( $this->getTitle(), false, Revision::READ_NORMAL ); + } else { + $this->revision = Revision::newFromTitle( $this->getTitle() ); + } + + \Hooks::run( 'SMW::Parser::ChangeRevision', [ $this->getTitle(), &$this->revision ] ); + + return $this->revision; + } + + protected function hasContentHandler() { + return defined( 'CONTENT_MODEL_WIKITEXT' ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/ExternalHooks.php b/www/wiki/extensions/SemanticMediaWiki/includes/ExternalHooks.php new file mode 100644 index 00000000..317a6df5 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/ExternalHooks.php @@ -0,0 +1,83 @@ +<?php + +/** + * Static class for hooks handled by the Semantic MediaWiki extension. + * + * @since 1.7 + * + * @file SemanticMediaWiki.hooks.php + * @ingroup SMW + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author mwjames + */ +final class SMWExternalHooks { + + /** + * TODO + * + * @since 1.7 + * + * @return boolean + */ + public static function onPageSchemasRegistration() { + // @codeCoverageIgnoreStart + $GLOBALS['wgPageSchemasHandlerClasses'][] = 'SMWPageSchemas'; + + return true; + // @codeCoverageIgnoreEnd + } + + /** + * Adds links to Admin Links page. + * + * @since 1.7 + * + * @param ALTree $admin_links_tree + * + * @return boolean + */ + public static function addToAdminLinks( ALTree $admin_links_tree ) { + // @codeCoverageIgnoreStart + $data_structure_section = new ALSection( wfMessage( 'smw_adminlinks_datastructure' )->text() ); + + $smw_row = new ALRow( 'smw' ); + $smw_row->addItem( ALItem::newFromSpecialPage( 'Categories' ) ); + $smw_row->addItem( ALItem::newFromSpecialPage( 'Properties' ) ); + $smw_row->addItem( ALItem::newFromSpecialPage( 'UnusedProperties' ) ); + + $data_structure_section->addRow( $smw_row ); + $smw_admin_row = new ALRow( 'smw_admin' ); + $smw_admin_row->addItem( ALItem::newFromSpecialPage( 'SMWAdmin' ) ); + + $data_structure_section->addRow( $smw_admin_row ); + $smw_docu_row = new ALRow( 'smw_docu' ); + $smw_name = wfMessage( 'specialpages-group-smw_group' )->text(); + $smw_docu_label = wfMessage( 'adminlinks_documentation', $smw_name )->text(); + $smw_docu_row->addItem( AlItem::newFromExternalLink( 'http://semantic-mediawiki.org/wiki/Help:User_manual', $smw_docu_label ) ); + + $data_structure_section->addRow( $smw_docu_row ); + $admin_links_tree->addSection( $data_structure_section, wfMessage( 'adminlinks_browsesearch' )->text() ); + $smw_row = new ALRow( 'smw' ); + $displaying_data_section = new ALSection( wfMessage( 'smw_adminlinks_displayingdata' )->text() ); + $smw_row->addItem( AlItem::newFromExternalLink( + 'http://semantic-mediawiki.org/wiki/Help:Inline_queries', + wfMessage( 'smw_adminlinks_inlinequerieshelp' )->text() + ) ); + + $displaying_data_section->addRow( $smw_row ); + $admin_links_tree->addSection( $displaying_data_section, wfMessage( 'adminlinks_browsesearch' )->text() ); + $browse_search_section = $admin_links_tree->getSection( wfMessage( 'adminlinks_browsesearch' )->text() ); + + $smw_row = new ALRow( 'smw' ); + $smw_row->addItem( ALItem::newFromSpecialPage( 'Browse' ) ); + $smw_row->addItem( ALItem::newFromSpecialPage( 'Ask' ) ); + $smw_row->addItem( ALItem::newFromSpecialPage( 'SearchByProperty' ) ); + $browse_search_section->addRow( $smw_row ); + + return true; + // @codeCoverageIgnoreEnd + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/Highlighter.php b/www/wiki/extensions/SemanticMediaWiki/includes/Highlighter.php new file mode 100644 index 00000000..6b659346 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/Highlighter.php @@ -0,0 +1,370 @@ +<?php + +namespace SMW; + +use Html; +use SMWOutputs; + +/** + * Highlighter utility function for Semantic MediaWiki + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ +class Highlighter { + + /** + * Highlighter ID for no types + */ + const TYPE_NOTYPE = 0; + + /** + * Highlighter ID for properties + */ + const TYPE_PROPERTY = 1; + + /** + * Highlighter ID for text + */ + const TYPE_TEXT = 2; + + /** + * Highlighter ID for quantities + */ + const TYPE_QUANTITY = 3; + + /** + * Highlighter ID for warnings + */ + const TYPE_WARNING = 4; + + /** + * Highlighter ID for error + */ + const TYPE_ERROR = 5; + + /** + * Highlighter ID for information + */ + const TYPE_INFO = 6; + + /** + * Highlighter ID for help + */ + const TYPE_HELP = 7; + + /** + * Highlighter ID for notes + */ + const TYPE_NOTE = 8; + + /** + * Highlighter ID for service links + */ + const TYPE_SERVICE = 9; + + /** + * Highlighter ID for reference links + */ + const TYPE_REFERENCE = 10; + + /** + * @var array $options + */ + private $options; + + /** + * @var int $type + */ + private $type; + + /** + * @var string|null + */ + private $language = null; + + /** + * @since 1.9 + * + * @param int $type + * @param string|null $language + */ + public function __construct( $type, $language = null ) { + $this->type = $type; + $this->language = $language; + } + + /** + * @since 1.9 + * + * @param string|int $type + * @param string|null $language + * + * @return Highlighter + */ + public static function factory( $type, $language = null ) { + if ( $type === '' || !is_int( $type ) ) { + $type = self::getTypeId( $type ); + } + + return new Highlighter( $type, $language ); + } + + /** + * @since 3.0 + * + * @param string $text + * @param string|null $type + * + * @return booelan + */ + public static function hasHighlighterClass( $text, $type = null ) { + + if ( strpos( $text, 'smw-highlighter' ) === false ) { + return false; + } + + if ( $type !== null ) { + return strpos( $text, 'data-type="' . self::getTypeId( $type ) . '"' ) !== false; + } + + return true; + } + + /** + * @since 3.0 + * + * @param string $text + * + * @return string + */ + public static function decode( $text ) { + // #2347, '[' is handled by the MediaWiki parser/sanitizer itself + return str_replace( + [ '&', '<', '>', ' ', '<nowiki>', '</nowiki>' ], + [ '&', '<', '>', ' ', '', '' ], + $text + ); + } + + /** + * Returns html output + * + * @since 1.9 + * + * @return string + */ + public function getHtml() { + SMWOutputs::requireResource( 'ext.smw.tooltips' ); + return $this->getContainer(); + } + + /** + * Set content + * + * An external class that invokes content via setContent has to ensure + * that all data are appropriately escaped. + * + * @since 1.9 + * + * @param array $content + * + * @return array + */ + public function setContent( array $content ) { + /** + * @var $content + * $content['caption'] = a text or null + * $content['context'] = a text or null + */ + return $this->options = array_merge( $this->getTypeConfiguration( $this->type ), $content ); + } + + /** + * Returns type id + * + * @since 1.9 + * + * @param string $type + * + * @return integer + */ + public static function getTypeId( $type ) { + // TODO: why do we have a htmlspecialchars here?! + switch ( strtolower ( htmlspecialchars ( $type ) ) ) { + case 'property': + return self::TYPE_PROPERTY; + case 'text': + return self::TYPE_TEXT; + case 'quantity': + return self::TYPE_QUANTITY; + case 'warning': + return self::TYPE_WARNING; + case 'error': + return self::TYPE_ERROR; + case 'info': + return self::TYPE_INFO; + case 'help': + return self::TYPE_HELP; + case 'note': + return self::TYPE_NOTE; + case 'service': + return self::TYPE_SERVICE; + case 'reference': + return self::TYPE_REFERENCE; + default: + return self::TYPE_NOTYPE; + } + } + + /** + * Builds Html container + * + * Content that is being invoked has to be escaped + * @see Highlighter::setContent + * + * @since 1.9 + * + * @return string + */ + private function getContainer() { + + $captionclass = $this->options['captionclass']; + + // 2.4+ can display context for user-defined properties, here we ensure + // to keep the style otherwise it displays italic which is by convention + // reserved for predefined properties + if ( $this->type === self::TYPE_PROPERTY && isset( $this->options['userDefined'] ) ) { + $captionclass = $this->options['userDefined'] ? 'smwtext' : $captionclass; + } + + $language = is_string( $this->language ) ? $this->language : Message::USER_LANGUAGE; + $style = []; + + if ( isset( $this->options['style'] ) ) { + $style = [ 'style' => $this->options['style'] ]; + } + + // #1875 + // title attribute contains stripped content to allow for a display in + // no-js environments, the tooltip will remove the element once it is + // loaded + $title = $this->title( $this->options['content'], $language ); + + $html = Html::rawElement( + 'span', + [ + 'class' => 'smw-highlighter', + 'data-type' => $this->options['type'], + 'data-content' => isset( $this->options['data-content'] ) ? $this->options['data-content'] : null, + 'data-state' => $this->options['state'], + 'data-title' => Message::get( $this->options['title'], Message::TEXT, $language ), + 'title' => $title + ] + $style, + Html::rawElement( + 'span', + [ + 'class' => $captionclass + ], + $this->options['caption'] + ) . Html::rawElement( + 'span', + [ + 'class' => 'smwttcontent' + ], + htmlspecialchars_decode( $this->options['content'] ) + ) + ); + + return $html; + } + + /** + * Returns initial configuration settings + * + * @note You could create a class per entity type but does this + * really make sense just to get some configuration parameters? + * + * @since 1.9 + * + * @param string $type + * + * @return array + */ + private function getTypeConfiguration( $type ) { + $settings = []; + $settings['type'] = $type; + $settings['caption'] = ''; + $settings['content'] = ''; + + switch ( $type ) { + case self::TYPE_PROPERTY: + $settings['state'] = 'inline'; + $settings['title'] = 'smw-ui-tooltip-title-property'; + $settings['captionclass'] = 'smwbuiltin'; + break; + case self::TYPE_TEXT: + $settings['state'] = 'persistent'; + $settings['title'] = 'smw-ui-tooltip-title-info'; + $settings['captionclass'] = 'smwtext'; + break; + case self::TYPE_QUANTITY: + $settings['state'] = 'inline'; + $settings['title'] = 'smw-ui-tooltip-title-quantity'; + $settings['captionclass'] = 'smwtext'; + break; + case self::TYPE_NOTE: + $settings['state'] = 'inline'; + $settings['title'] = 'smw-ui-tooltip-title-note'; + $settings['captionclass'] = 'smwtticon note'; + break; + case self::TYPE_WARNING: + $settings['state'] = 'inline'; + $settings['title'] = 'smw-ui-tooltip-title-warning'; + $settings['captionclass'] = 'smwtticon warning'; + break; + case self::TYPE_ERROR: + $settings['state'] = 'inline'; + $settings['title'] = 'smw-ui-tooltip-title-error'; + $settings['captionclass'] = 'smwtticon error'; + break; + case self::TYPE_SERVICE: + $settings['state'] = 'persistent'; + $settings['title'] = 'smw-ui-tooltip-title-service'; + $settings['captionclass'] = 'smwtticon service'; + break; + case self::TYPE_REFERENCE: + $settings['state'] = 'persistent'; + $settings['title'] = 'smw-ui-tooltip-title-reference'; + $settings['captionclass'] = 'smwtext'; + break; + case self::TYPE_HELP: + case self::TYPE_INFO: + $settings['state'] = 'persistent'; + $settings['title'] = 'smw-ui-tooltip-title-info'; + $settings['captionclass'] = 'smwtticon info'; + break; + case self::TYPE_NOTYPE: + default: + $settings['state'] = 'persistent'; + $settings['title'] = 'smw-ui-tooltip-title-info'; + $settings['captionclass'] = 'smwbuiltin'; + }; + + return $settings; + } + + private function title( $content, $language ) { + + // Pre-process the content when used as title to avoid breaking elements + // (URLs etc.) + if ( strpos( $content, '[' ) !== false || strpos( $content, '//' ) !== false ) { + $content = Message::get( [ 'smw-parse', $content ], Message::PARSE, $language ); + } + + return strip_tags( htmlspecialchars_decode( str_replace( [ "[", ' ' ], [ "[", ' ' ], $content ) ) ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/IdeAliases.php b/www/wiki/extensions/SemanticMediaWiki/includes/IdeAliases.php new file mode 100644 index 00000000..3c83dd78 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/IdeAliases.php @@ -0,0 +1,75 @@ +<?php + +/** + * Indicate class aliases in a way PHPStorm and Eclipse understand. + * This is purely an IDE helper file, and is not loaded by the extension. + * + * @since 1.9 + * + * @ingroup SMW + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +throw new Exception( 'Not an actual source file' ); + +class SMWDataItemException extends SMW\Exception\DataItemException { +} + +abstract class SMWStore extends SMW\Store { +} + +class SMWSemanticData extends SMW\SemanticData { +} + +class SMWDIWikiPage extends SMW\DIWikiPage { +} + +class SMWDIConcept extends SMW\DIConcept { +} + +class SMWDIProperty extends SMW\DIProperty { +} + +class SMWDISerializer extends SMW\Serializers\QueryResultSerializer { +} + +class SMWUpdateJob extends SMW\UpdateJob { +} + +class SMWRefreshJob extends SMW\RefreshJob { +} + +abstract class SMWResultPrinter extends SMW\ResultPrinter { +} + +class SMWCategoryResultPrinter extends SMW\Query\ResultPrinters\CategoryResultPrinter { +} + +class SMWDSVResultPrinter extends SMW\DsvResultPrinter { +} + +class SMWEmbeddedResultPrinter extends SMW\EmbeddedResultPrinter { +} + +class SMWRDFResultPrinter extends SMW\RdfResultPrinter { +} + +class SMWListResultPrinter extends SMW\Query\ResultPrinters\ListResultPrinter { +} + +interface SMWIResultPrinter extends SMW\QueryResultPrinter { +} + +class SMWSparqlDatabase4Store extends SMW\SPARQLStore\FourstoreHttpDatabaseConnector { +} + +class SMWSparqlDatabaseVirtuoso extends SMW\SPARQLStore\VirtuosoHttpDatabaseConnector { +} + +class SMWSparqlStore extends SMW\SPARQLStore\SPARQLStore { +} + +class SMWSparqlDatabase extends SMW\SPARQLStore\GenericHttpDatabaseConnector { +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/QueryPrinterFactory.php b/www/wiki/extensions/SemanticMediaWiki/includes/QueryPrinterFactory.php new file mode 100644 index 00000000..3481405e --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/QueryPrinterFactory.php @@ -0,0 +1,196 @@ +<?php + +namespace SMW; + +use InvalidArgumentException; +use SMW\Query\Exception\ResultFormatNotFoundException; + +/** + * Factory for "result formats", ie classes implementing QueryResultPrinter. + * + * @license GNU GPL v2+ + * @since 2.5 (since 1.9, renamed in 2.5) + * + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +final class QueryPrinterFactory { + + /** + * Returns the global instance of the factory. + * + * @since 2.5 + * + * @return QueryPrinterFactory + */ + public static function singleton() { + static $instance = null; + + if ( $instance === null ) { + $instance = self::newFromGlobalState(); + } + + return $instance; + } + + private static function newFromGlobalState() { + $instance = new self(); + + foreach ( $GLOBALS['smwgResultFormats'] as $formatName => $printerClass ) { + $instance->registerFormat( $formatName, $printerClass ); + } + + foreach ( $GLOBALS['smwgResultAliases'] as $formatName => $aliases ) { + $instance->registerAliases( $formatName, $aliases ); + } + + return $instance; + } + + /** + * Format registry. Format names pointing to their associated QueryResultPrinter implementing classes. + * + * @var string[] + */ + private $formats = []; + + /** + * Form alias registry. Aliases pointing to their canonical format name. + * + * @var string[] + */ + private $aliases = []; + + /** + * Registers a format. + * If there is a format already with the provided name, + * it will be overridden with the newly provided data. + * + * @since 2.5 + * + * @param string $formatName + * @param string $class + * + * @throws InvalidArgumentException + */ + public function registerFormat( $formatName, $class ) { + + if ( !is_string( $formatName ) ) { + throw new InvalidArgumentException( 'Format names can only be of type string' ); + } + + if ( !is_string( $class ) ) { + throw new InvalidArgumentException( 'Format class names can only be of type string' ); + } + + $this->formats[$formatName] = $class; + } + + /** + * Registers the provided format aliases. + * If an aliases is already registered, it will + * be overridden with the newly provided data. + * + * @since 2.5 + * + * @param string $formatName + * @param array $aliases + * + * @throws InvalidArgumentException + */ + public function registerAliases( $formatName, array $aliases ) { + + if ( !is_string( $formatName ) ) { + throw new InvalidArgumentException( 'Format names can only be of type string' ); + } + + foreach ( $aliases as $alias ) { + if ( !is_string( $alias ) ) { + throw new InvalidArgumentException( 'Format aliases can only be of type string' ); + } + + $this->aliases[$alias] = $formatName; + } + } + + /** + * Returns the canonical format names. + * + * @since 2.5 + * + * @return string[] + */ + public function getFormats() { + return array_keys( $this->formats ); + } + + /** + * Returns if there is a format or format alias with the provided name. + * + * @since 2.5 + * + * @param string $formatName Format name or alias + * + * @return boolean + */ + public function hasFormat( $formatName ) { + $formatName = $this->getCanonicalName( $formatName ); + return array_key_exists( $formatName, $this->formats ); + } + + /** + * Returns a new instance of the handling result printer for the provided format. + * + * @since 2.5 + * + * @param string $formatName + * + * @return QueryResultPrinter + * @throws ResultFormatNotFoundException + */ + public function getPrinter( $formatName ) { + $class = $this->getPrinterClass( $formatName ); + return new $class( $formatName ); + } + + /** + * Returns the QueryResultPrinter implementing class that is the printer for the provided format. + * + * @param string $formatName Format name or alias + * + * @return string + * @throws ResultFormatNotFoundException + */ + private function getPrinterClass( $formatName ) { + $formatName = $this->getCanonicalName( $formatName ); + + if ( !array_key_exists( $formatName, $this->formats ) ) { + throw new ResultFormatNotFoundException( 'Unknown format name "' . $formatName . '" has no associated printer class' ); + } + + return $this->formats[$formatName]; + } + + /** + * Resolves format aliases into their associated canonical format name. + * + * @since 2.5 + * + * @param string $formatName Format name or alias + * + * @return string + * @throws InvalidArgumentException + */ + public function getCanonicalName( $formatName ) { + + if ( !is_string( $formatName ) ) { + throw new InvalidArgumentException( 'Format names can only be of type string' ); + } + + if ( array_key_exists( $formatName, $this->aliases ) ) { + $formatName = $this->aliases[$formatName]; + } + + return $formatName; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/RecurringEvents.php b/www/wiki/extensions/SemanticMediaWiki/includes/RecurringEvents.php new file mode 100644 index 00000000..1a171fec --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/RecurringEvents.php @@ -0,0 +1,374 @@ +<?php + +namespace SMW; + +use SMWDITime; + +/** + * TODO This class needs some real refactoring! + * + * @private This class should not be instantiated directly. + * + * This class determines recurring events based on invoked parameters + * + * @see https://www.semantic-mediawiki.org/wiki/Help:Recurring_events + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author Yaron Koren + * @author Jeroen De Dauw + * @author mwjames + */ +class RecurringEvents { + + /** + * Defines the property used + */ + private $property = null; + + /** + * Defines the dates + */ + private $dates = []; + + /** + * Defines remaining / unused parameters + */ + private $parameters = []; + + /** + * Defines errors + */ + private $errors = []; + + /** + * @var integer + */ + private $defaultNumRecurringEvents = 25; + + /** + * @var integer + */ + private $maxNumRecurringEvents = 25; + + /** + * @since 2.5 + * + * @param integer $defaultNumRecurringEvents + */ + public function setDefaultNumRecurringEvents( $defaultNumRecurringEvents ) { + $this->defaultNumRecurringEvents = $defaultNumRecurringEvents; + } + + /** + * @since 2.5 + * + * @param integer $maxNumRecurringEvents + */ + public function setMaxNumRecurringEvents( $maxNumRecurringEvents ) { + $this->maxNumRecurringEvents = $maxNumRecurringEvents; + } + + /** + * Returns property used + * + * @since 1.9 + * + * @return string + */ + public function getProperty() { + return $this->property; + } + + /** + * Returns dates + * + * @since 1.9 + * + * @return array + */ + public function getDates() { + return $this->dates; + } + + /** + * Returns unused parameters + * + * @since 1.9 + * + * @return array + */ + public function getParameters() { + return $this->parameters; + } + + /** + * Returns errors + * + * @since 1.9 + * + * @return array + */ + public function getErrors() { + return $this->errors; + } + + /** + * Set error + * + * @since 1.9 + * + * @return mixed + */ + private function setError( $error ) { + $this->errors = array_merge( $error, $this->errors ); + } + + /** + * Returns the "Julian day" value from an object of type + * SMWTimeValue. + */ + public function getJulianDay( $dateDataValue ) { + if ( is_null( $dateDataValue ) ) { + return null; + } + $dateDataItem = $dateDataValue->getDataItem(); + // This might have returned an 'SMWDIError' object. + if ( $dateDataItem instanceof SMWDITime ) { + return $dateDataItem->getJD(); + } + return null; + } + + /** + * Parse parameters and set internal properties + * + * @since 1.9 + * + * @param array $parameters + */ + public function parse( array $parameters ) { + // Initialize variables. + $all_date_strings = []; + $start_date = $end_date = $unit = $period = $week_num = null; + $included_dates = []; + $excluded_dates = []; + $excluded_dates_jd = []; + + // Parse parameters and assign values + foreach ( $parameters as $name => $values ) { + + foreach ( $values as $value ) { + switch( $name ) { + case 'property': + $this->property = $value; + break; + case 'start': + $start_date = DataValueFactory::getInstance()->newTypeIDValue( '_dat', $value ); + break; + case 'end': + $end_date = DataValueFactory::getInstance()->newTypeIDValue( '_dat', $value ); + break; + case 'limit': + // Override default limit with query specific limit + $this->defaultNumRecurringEvents = (int)$value; + break; + case 'unit': + $unit = $value; + break; + case 'period': + $period = (int)$value; + break; + case 'week number': + $week_num = (int)$value; + break; + case 'include': + // This is for compatibility only otherwise we break + // to much at once. Instead of writing include=...;... + // it should be include=...;...|+sep=; because the + // ParameterParser class is conditioned to split those + // parameter accordingly + if ( strpos( $value, ';' ) ){ + $included_dates = explode( ';', $value ); + } else { + $included_dates[] = $value; + } + break; + case 'exclude': + // Some as above + if ( strpos( $value, ';' ) ){ + $excluded_dates = explode( ';', $value ); + } else { + $excluded_dates[] = $value; + } + break; + default: + $this->parameters[$name][] = $value; + } + } + } + + if ( $start_date === null ) { + $this->errors[] = Message::get( 'smw-events-start-date-missing' ); + return; + } elseif ( !( $start_date->getDataItem() instanceof SMWDITime ) ) { + $this->setError( $start_date->getErrors() ); + return; + } + + // Check property + if ( is_null( $this->property ) ) { + $this->errors[] = Message::get( 'smw-events-property-missing' ); + return; + } + + // Exclude dates + foreach ( $excluded_dates as $date_str ) { + $excluded_dates_jd[] = $this->getJulianDay( + DataValueFactory::getInstance()->newTypeIDValue( '_dat', $date_str ) + ); + } + + // If the period is null, or outside of normal bounds, set it to 1. + if ( is_null( $period ) || $period < 1 || $period > 500 ) { + $period = 1; + } + + // Handle 'week number', but only if it's of unit 'month'. + if ( $unit == 'month' && ! is_null( $week_num ) ) { + $unit = 'dayofweekinmonth'; + + if ( $week_num < -4 || $week_num > 5 || $week_num == 0 ) { + $week_num = null; + } + } + + if ( $unit == 'dayofweekinmonth' && is_null( $week_num ) ) { + $week_num = ceil( $start_date->getDay() / 7 ); + } + + // Get the Julian day value for both the start and end date. + $end_date_jd = $this->getJulianDay( $end_date ); + + $cur_date = $start_date; + $cur_date_jd = $this->getJulianDay( $cur_date ); + $i = 0; + + do { + $i++; + $exclude_date = ( in_array( $cur_date_jd, $excluded_dates_jd ) ); + + if ( !$exclude_date ) { + $all_date_strings[] = $cur_date->getLongWikiText(); + } + + // Now get the next date. + // Handling is different depending on whether it's + // month/year or week/day since the latter is a set + // number of days while the former isn't. + if ( $unit === 'year' || $unit == 'month' ) { + $cur_year = $cur_date->getYear(); + $cur_month = $cur_date->getMonth(); + $cur_day = $start_date->getDay(); + $cur_time = $cur_date->getTimeString(); + + if ( $unit == 'year' ) { + $cur_year += $period; + $display_month = $cur_month; + } else { // $unit === 'month' + $cur_month += $period; + $cur_year += (int)( ( $cur_month - 1 ) / 12 ); + $cur_month %= 12; + $display_month = ( $cur_month == 0 ) ? 12 : $cur_month; + } + + // If the date is greater than 28 for February, and it is not + // a leap year, change it to be a fixed 28 otherwise set it to + // 29 (for a leap year date) + if ( $cur_month == 2 && $cur_day > 28 ) { + $cur_day = !date( 'L', strtotime( "$cur_year-1-1" ) ) ? 28 : 29; + } elseif ( $cur_day > 30 ) { + // Check whether 31 is a valid day of a month + $cur_day = ( $display_month - 1 ) % 7 % 2 ? 30 : 31; + } + + $date_str = "$cur_year-$display_month-$cur_day $cur_time"; + $cur_date = DataValueFactory::getInstance()->newTypeIDValue( '_dat', $date_str ); + $all_date_strings = array_merge( $all_date_strings, $included_dates); + if ( $cur_date->isValid() ) { + $cur_date_jd = $cur_date->getDataItem()->getJD(); + } + } elseif ( $unit == 'dayofweekinmonth' ) { + // e.g., "3rd Monday of every month" + $prev_month = $cur_date->getMonth(); + $prev_year = $cur_date->getYear(); + + $new_month = ( $prev_month + $period ) % 12; + if ( $new_month == 0 ) { + $new_month = 12; + } + + $new_year = $prev_year + floor( ( $prev_month + $period - 1 ) / 12 ); + $cur_date_jd += ( 28 * $period ) - 7; + + // We're sometime before the actual date now - + // keep incrementing by a week, until we get there. + do { + $cur_date_jd += 7; + $cur_date = $this->getJulianDayTimeValue( $cur_date_jd ); + $right_month = ( $cur_date->getMonth() == $new_month ); + + if ( $week_num < 0 ) { + $next_week_jd = $cur_date_jd; + + do { + $next_week_jd += 7; + $next_week_date = $this->getJulianDayTimeValue( $next_week_jd ); + $right_week = ( $next_week_date->getMonth() != $new_month ) || ( $next_week_date->getYear() != $new_year ); + } while ( !$right_week ); + + $cur_date_jd = $next_week_jd + ( 7 * $week_num ); + $cur_date = $this->getJulianDayTimeValue( $cur_date_jd ); + } else { + $cur_week_num = ceil( $cur_date->getDay() / 7 ); + $right_week = ( $cur_week_num == $week_num ); + + if ( $week_num == 5 && ( $cur_date->getMonth() % 12 == ( $new_month + 1 ) % 12 ) ) { + $cur_date_jd -= 7; + $cur_date = $this->getJulianDayTimeValue( $cur_date_jd ); + $right_month = $right_week = true; + } + } + } while ( !$right_month || !$right_week); + } else { // $unit == 'day' or 'week' + // Assume 'day' if it's none of the above. + $cur_date_jd += ( $unit === 'week' ) ? 7 * $period : $period; + $cur_date = $this->getJulianDayTimeValue( $cur_date_jd ); + } + + // should we stop? + if ( is_null( $end_date ) ) { + $reached_end_date = $i > $this->defaultNumRecurringEvents; + } else { + $reached_end_date = ( $cur_date_jd > $end_date_jd ) || ( $i > $this->maxNumRecurringEvents ); + } + } while ( !$reached_end_date ); + + // Handle the 'include' dates as well. + $all_date_strings = array_filter( array_merge( $all_date_strings, $included_dates ) ); + + // Set dates + $this->dates = str_replace( ' 00:00:00', '', $all_date_strings ); + } + + /** + * Helper function - creates an object of type SMWTimeValue based + * on a "Julian day" integer + */ + private function getJulianDayTimeValue( $jd ) { + $timeDataItem = SMWDITime::newFromJD( $jd, SMWDITime::CM_GREGORIAN, SMWDITime::PREC_YMDT ); + return DataValueFactory::getInstance()->newDataValueByItem( $timeDataItem ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/SMW_Infolink.php b/www/wiki/extensions/SemanticMediaWiki/includes/SMW_Infolink.php new file mode 100644 index 00000000..18a37b30 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/SMW_Infolink.php @@ -0,0 +1,666 @@ +<?php + +use SMW\Site; + +/** + * This class mainly is a container to store URLs for the factbox in a + * clean way. The class provides methods for creating source code for + * realising them in wiki or html contexts. + * + * @license GNU GPL v2+ + * @since 1.0 + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + * @author mwjames + */ +class SMWInfolink { + + const LINK_UPPER_LENGTH_RESTRICTION = 2000; + + /** + * The actual link target. + * + * @var string + */ + protected $mTarget; + + /** + * The label for the link. + * + * @var string + */ + protected $mCaption; + + /** + * CSS class of a span to embedd the link into, + * or false if no extra style is required. + * + * @var mixed + */ + protected $mStyle; + + /** + * @var array + */ + private $linkAttributes = []; + + /** + * Indicates whether $target is a page name (true) or URL (false). + * + * @var boolean + */ + protected $mInternal; + + /** + * Array of parameters, format $name => $value, if any. + * + * @var array + */ + protected $mParams; + + /** + * @var boolean + */ + private $isRestricted = false; + + /** + * @var boolean + */ + private $isCompactLink = false; + + /** + * Create a new link to some internal page or to some external URL. + * + * @param boolean $internal Indicates whether $target is a page name (true) or URL (false). + * @param string $caption The label for the link. + * @param string $target The actual link target. + * @param mixed $style CSS class of a span to embedd the link into, or false if no extra style is required. + * @param array $params Array of parameters, format $name => $value, if any. + */ + public function __construct( $internal, $caption, $target, $style = false, array $params = [] ) { + $this->mInternal = $internal; + $this->mCaption = $caption; + $this->mTarget = $target; + $this->mStyle = $style; + $this->mParams = $params; + $this->setCompactLink( $GLOBALS['smwgCompactLinkSupport'] ); + } + + /** + * @since 3.0 + * + * @param boolean $isRestricted + */ + public function isRestricted( $isRestricted ) { + $this->isRestricted = (bool)$isRestricted; + } + + /** + * @since 3.0 + * + * @param boolean $isCompactLink + */ + public function setCompactLink( $isCompactLink = true ) { + $this->isCompactLink = (bool)$isCompactLink; + } + + /** + * Create a new link to an internal page $target. + * All parameters are mere strings as used by wiki users. + * + * @param string $caption The label for the link. + * @param string $target The actual link target. + * @param mixed $style CSS class of a span to embedd the link into, or false if no extra style is required. + * @param array $params Array of parameters, format $name => $value, if any. + * + * @return SMWInfolink + */ + public static function newInternalLink( $caption, $target, $style = false, array $params = [] ) { + return new SMWInfolink( true, $caption, $target, $style, $params ); + } + + /** + * Create a new link to an external location $url. + * + * @param string $caption The label for the link. + * @param string $url The actual link target. + * @param mixed $style CSS class of a span to embedd the link into, or false if no extra style is required. + * @param array $params Array of parameters, format $name => $value, if any. + * + * @return SMWInfolink + */ + public static function newExternalLink( $caption, $url, $style = false, array $params = [] ) { + return new SMWInfolink( false, $caption, $url, $style, $params ); + } + + /** + * Static function to construct links to property searches. + * + * @param string $caption The label for the link. + * @param string $propertyName + * @param string $propertyValue + * @param mixed $style CSS class of a span to embedd the link into, or false if no extra style is required. + * + * @return SMWInfolink + */ + public static function newPropertySearchLink( $caption, $propertyName, $propertyValue, $style = 'smwsearch' ) { + global $wgContLang; + + $infolink = new SMWInfolink( + true, + $caption, + $wgContLang->getNsText( NS_SPECIAL ) . ':SearchByProperty', + $style, + [ ':' . $propertyName, $propertyValue ] // `:` is marking that the link was auto-generated + ); + + // Link that reaches a length restriction will most likely cause a + // "HTTP 414 "Request URI too long ..." therefore prevent a link creation + if ( mb_strlen( $propertyName . $propertyValue ) > self::LINK_UPPER_LENGTH_RESTRICTION ) { + $infolink->isRestricted( true ); + } + + return $infolink; + } + + /** + * Static function to construct links to inverse property searches. + * + * @param string $caption The label for the link. + * @param string $subject + * @param string $propertyName + * @param mixed $style CSS class of a span to embed the link into, or false if no extra style is required. + * + * @return SMWInfolink + */ + public static function newInversePropertySearchLink( $caption, $subject, $propertyname, $style = false ) { + global $wgContLang; + return new SMWInfolink( + true, + $caption, + $wgContLang->getNsText( NS_SPECIAL ) . ':PageProperty', + $style, + [ $subject . '::' . $propertyname ] + ); + } + + /** + * Static function to construct links to the browsing special. + * + * @param string $caption The label for the link. + * @param string $titleText + * @param mixed $style CSS class of a span to embedd the link into, or false if no extra style is required. + * + * @return SMWInfolink + */ + public static function newBrowsingLink( $caption, $titleText, $style = 'smwbrowse' ) { + global $wgContLang; + return new SMWInfolink( + true, + $caption, + $wgContLang->getNsText( NS_SPECIAL ) . ':Browse', + $style, + [ ':' . $titleText ] + ); + } + + /** + * Set (or add) parameter values for an existing link. + * + * @param mixed $value + * @param mixed $key + */ + public function setParameter( $value, $key = false ) { + if ( $key === false ) { + $this->mParams[] = $value; + } else { + $this->mParams[$key] = $value; + } + } + + /** + * Get the value of some named parameter, or null if no parameter of + * that name exists. + */ + public function getParameter( $key ) { + if ( array_key_exists( $key, $this->mParams ) ) { + return $this->mParams[$key]; + } else { + return null; + } + } + + /** + * Change the link text. + */ + public function setCaption( $caption ) { + $this->mCaption = $caption; + } + + /** + * Change the link's CSS class. + */ + public function setStyle( $style ) { + $this->mStyle = $style; + } + + /** + * Modify link attributes + * + * @since 3.0 + * + * @param array $linkAttributes + */ + public function setLinkAttributes( array $linkAttributes ) { + $this->linkAttributes = $linkAttributes; + } + + /** + * Returns a suitable text string for displaying this link in HTML or wiki, depending + * on whether $outputformat is SMW_OUTPUT_WIKI or SMW_OUTPUT_HTML. + * + * The parameter $linker controls linking of values such as titles and should + * be some Linker object (for HTML output). Some default linker will be created + * if needed and not provided. + */ + public function getText( $outputformat, $linker = null ) { + + if ( $this->isRestricted ) { + return ''; + } + + if ( $this->mStyle !== false ) { + SMWOutputs::requireResource( 'ext.smw.style' ); + $start = "<span class=\"$this->mStyle\">"; + $end = '</span>'; + } else { + $start = ''; + $end = ''; + } + + if ( $this->mInternal ) { + if ( count( $this->mParams ) > 0 ) { + + $query = self::encodeParameters( $this->mParams ); + + if ( $this->isCompactLink ) { + $query = self::encodeCompactLink( $query ); + } + + $titletext = $this->mTarget . '/' . $query; + } else { + $titletext = $this->mTarget; + } + + $title = Title::newFromText( $titletext ); + + if ( $title !== null ) { + if ( $outputformat == SMW_OUTPUT_WIKI ) { + $link = "[[$titletext|$this->mCaption]]"; + } elseif ( $outputformat == SMW_OUTPUT_RAW ) { + return $this->getURL(); + } else { // SMW_OUTPUT_HTML, SMW_OUTPUT_FILE + $link = $this->getLinker( $linker )->link( $title, $this->mCaption, $this->linkAttributes ); + } + } else { + // Title creation failed, maybe illegal symbols or too long; make + // a direct URL link (only possible if offending target parts belong + // to some parameter that can be separated from title text, e.g. + // as in Special:Bla/il<leg>al -> Special:Bla&p=il<leg>al) + $title = Title::newFromText( $this->mTarget ); + + // Just give up due to the title being bad, normally this would + // indicate a software bug + if ( $title === null ) { + return ''; + } + + $query = self::encodeParameters( $this->mParams, $this->isCompactLink ); + + if ( $outputformat == SMW_OUTPUT_WIKI ) { + + if ( $this->isCompactLink ) { + $query = self::encodeCompactLink( $query, false ); + } + + $link = '[' . $title->getFullURL( $query ) . " $this->mCaption]"; + } else { // SMW_OUTPUT_HTML, SMW_OUTPUT_FILE + + if ( $this->isCompactLink ) { + $query = self::encodeCompactLink( $query, true ); + } else { + // #511, requires an array + $query = wfCgiToArray( $query ); + } + + $link = $this->getLinker( $linker )->link( + $title, + $this->mCaption, + $this->linkAttributes, + $query + ); + } + } + } else { + $target = $this->getURL(); + + if ( $outputformat == SMW_OUTPUT_WIKI ) { + $link = "[$target $this->mCaption]"; + } else { // SMW_OUTPUT_HTML, SMW_OUTPUT_FILE + $link = '<a href="' . htmlspecialchars( $target ) . "\">$this->mCaption</a>"; + } + } + + return $start . $link . $end; + } + + /** + * Return hyperlink for this infolink in HTML format. + * + * @return string + */ + public function getHTML( $linker = null ) { + return $this->getText( SMW_OUTPUT_HTML, $linker ); + } + + /** + * Return hyperlink for this infolink in wiki format. + * + * @return string + */ + public function getWikiText( $linker = null ) { + return $this->getText( SMW_OUTPUT_WIKI, $linker ); + } + + /** + * Return a fully qualified URL that points to the link target (whether internal or not). + * This function might be used when the URL is needed outside normal links, e.g. in the HTML + * header or in some metadata file. For making normal links, getText() should be used. + * + * @return string + */ + public function getURL() { + + $query = self::encodeParameters( $this->mParams, $this->isCompactLink ); + + if ( $this->isCompactLink && $query !== '' ) { + $query = self::encodeCompactLink( $query, true ); + } + + if ( !$this->mInternal ) { + return $this->buildTarget( $query ); + } + + $title = Title::newFromText( $this->mTarget ); + + if ( $title !== null ) { + return $title->getFullURL( $query ); + } + + // the title was bad, normally this would indicate a software bug + return ''; + } + + /** + * @since 3.0 + * + * @return string + */ + public function getLocalURL() { + + $query = self::encodeParameters( $this->mParams, $this->isCompactLink ); + + if ( $this->isCompactLink && $query !== '' ) { + $query = self::encodeCompactLink( $query, true ); + } + + if ( !$this->mInternal ) { + return $this->buildTarget( $query ); + } + + $title = Title::newFromText( $this->mTarget ); + + if ( $title !== null ) { + return $title->getLocalURL( $query ); + } + + // the title was bad, normally this would indicate a software bug + return ''; + } + + /** + * Return a Linker object, using the parameter $linker if not null, and creatng a new one + * otherwise. $linker is usually a user skin object, while the fallback linker object is + * not customised to user settings. + * + * @return Linker + */ + protected function getLinker( &$linker = null ) { + if ( is_null( $linker ) ) { + $linker = new Linker; + } + return $linker; + } + + /** + * Encode an array of parameters, formatted as $name => $value, to a parameter + * string that can be used for linking. If $forTitle is true (default), then the + * parameters are encoded for use in a MediaWiki page title (useful for making + * internal links to parameterised special pages), otherwise the parameters are + * encoded HTTP GET style. The parameter name "x" is used to collect parameters + * that do not have any string keys in GET, and hence "x" should never be used + * as a parameter name. + * + * The function SMWInfolink::decodeParameters() can be used to undo this encoding. + * It is strongly recommended to not create any code that depends on the concrete + * way of how parameters are encoded within this function, and to always use the + * respective encoding/decoding methods instead. + * + * @param array $params + * @param boolean $forTitle + */ + static public function encodeParameters( array $params, $forTitle = true ) { + $result = ''; + + if ( $forTitle ) { + foreach ( $params as $name => $value ) { + if ( is_string( $name ) && ( $name !== '' ) ) { + $value = $name . '=' . $value; + } + // Escape certain problematic values. Use SMW-escape + // (like URLencode but - instead of % to prevent double encoding by later MW actions) + // + /// : SMW's parameter separator, must not occur within params + // - : used in SMW-encoding strings, needs escaping too + // [ ] < > < > '' |: problematic in MW titles + // & : sometimes problematic in MW titles ([[&]] is OK, [[&test]] is OK, [[&test;]] is not OK) + // (Note: '&' in strings obtained during parsing already has &entities; replaced by + // UTF8 anyway) + // ' ': are equivalent with '_' in MW titles, but are not equivalent in certain parameter values + // "\n": real breaks not possible in [[...]] + // "#": has special meaning in URLs, triggers additional MW escapes (using . for %) + // '%': must be escaped to prevent any impact of double decoding when replacing - + // by % before urldecode + // '?': if not escaped, strange effects were observed on some sites (printout and other + // parameters ignored without obvious cause); SMW-escaping is always save to do -- it just + // make URLs less readable + // + $value = str_replace( + [ '-', '#', "\n", ' ', '/', '[', ']', '<', '>', '<', '>', '&', '\'\'', '|', '&', '%', '?', '$', "\\", ";", "_" ], + [ '-2D', '-23', '-0A', '-20', '-2F', '-5B', '-5D', '-3C', '-3E', '-3C', '-3E', '-26', '-27-27', '-7C', '-26', '-25', '-3F', '-24', '-5C', "-3B", "-5F" ], + $value + ); + + if ( $result !== '' ) { + $result .= '/'; + } + + $result .= $value; + } + } else { // Note: this requires to have HTTP compatible parameter names (ASCII) + $q = []; // collect unlabelled query parameters here + + foreach ( $params as $name => $value ) { + if ( is_string( $name ) && ( $name !== '' ) ) { + $value = rawurlencode( $name ) . '=' . rawurlencode( $value ); + + if ( $result !== '' ) { + $result .= '&'; + } + + $result .= $value; + } else { + $q[] = $value; + } + } + if ( count( $q ) > 0 ) { // prepend encoding for unlabelled parameters + if ( $result !== '' ) { + $result = '&' . $result; + } + $result = 'x=' . rawurlencode( self::encodeParameters( $q, true ) ) . $result; + } + } + + return $result; + } + + /** + * Obtain an array of parameters from the parameters given to some HTTP service. + * In particular, this function performs all necessary decoding as may be needed, e.g., + * to recover the proper parameter strings after encoding for use in wiki title names + * as done by SMWInfolink::encodeParameters(). + * + * If $allparams is set to true, it is assumed that further data should be obtained + * from the global $wgRequest, and all given parameters are read. + * + * $titleparam is the string extracted by MediaWiki from special page calls of the + * form Special:Something/titleparam + * Note: it is assumed that the given $titleparam is already urldecoded, as is normal + * when getting such parameters from MediaWiki. SMW-escaped parameters largely prevent + * double decoding effects (i.e. there are no new "%" after one pass of urldecoding) + * + * The function SMWInfolink::encodeParameters() can be used to create a suitable + * encoding. It is strongly recommended to not create any code that depends on the + * concrete way of how parameters are encoded within this function, and to always use + * the respective encoding/decoding methods instead. + * + * @param string $titleParam + * @param boolean $allParams + */ + static public function decodeParameters( $titleParam = '', $allParams = false ) { + global $wgRequest; + + $result = []; + + if ( $allParams ) { + $result = $wgRequest->getValues(); + + if ( array_key_exists( 'x', $result ) ) { // Considered to be part of the title param. + if ( $titleParam !== '' ) { + $titleParam .= '/'; + } + $titleParam .= $result['x']; + unset( $result['x'] ); + } + } + + if ( is_array( $titleParam ) ) { + return $titleParam; + } elseif ( $titleParam !== '' ) { + // unescape $p; escaping scheme: all parameters rawurlencoded, "-" and "/" urlencoded, all "%" replaced by "-", parameters then joined with / + $ps = explode( '/', $titleParam ); // params separated by / here (compatible with wiki link syntax) + + foreach ( $ps as $p ) { + if ( $p !== '' ) { + $result[] = rawurldecode( str_replace( '-', '%', $p ) ); + } + } + } + + return $result; + } + + /** + * @since 3.0 + * + * @param string $value + * + * @return string|array + */ + public static function encodeCompactLink( $value, $compound = false ) { + + // Expect to gain on larger strings and set an identifier to + // distinguish between compressed and non compressed + if ( mb_strlen( $value ) > 150 ) { + $value = 'c:' . gzdeflate( $value, 9 ); + } + + // https://en.wikipedia.org/wiki/Base64#URL_applications + // The MW parser swallows `__` and transforms it into a simple `_` + // hence we need to encode it once more + $value = rtrim( str_replace( '__', '.', strtr( base64_encode( $value ), '+/', '-_' ) ), '=' ); + + if ( $compound ) { + return [ 'cl' => $value ]; + } + + return "cl:$value"; + } + + /** + * @since 3.0 + * + * @param string $value + * + * @return string + */ + public static function decodeCompactLink( $value ) { + + if ( !is_string( $value ) || mb_substr( $value, 0, 3 ) !== 'cl:' ) { + return $value; + } + + $value = mb_substr( $value, 3 ); + + $value = base64_decode( + str_pad( strtr( str_replace( '.', '__', $value ), '-_', '+/' ), strlen( $value ) % 4, '=', STR_PAD_RIGHT ) + ); + + // Compressed? + if ( mb_substr( $value, 0, 2 ) === 'c:' ) { + $val = @gzinflate( mb_substr( $value, 2 ) ); + + // Guessing that MediaWiki swallowed the last `_` + if ( $val === false ) { + $val = @gzinflate( mb_substr( $value , 2 ) . '?' ); + } + + $value = $val; + } + + // Normalize if nceessary for those that are "encoded for use in a + // MediaWiki page title" + if ( mb_substr( $value, 0, 2 ) === 'x=' ) { + $value = str_replace( [ 'x=', '=-&' , '&', '%2F' ], [ '' , '=-2D&', '/', '/' ], $value ); + } + + return $value; + } + + private function buildTarget( $query ) { + + $target = $this->mTarget; + + if ( count( $this->mParams ) > 0 ) { + if ( strpos( Site::wikiurl(), '?' ) === false ) { + $target = $this->mTarget . '?' . $query; + } else { + $target = $this->mTarget . '&' . $query; + } + } + + return $target; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/SMW_Outputs.php b/www/wiki/extensions/SemanticMediaWiki/includes/SMW_Outputs.php new file mode 100644 index 00000000..2d2c1e4f --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/SMW_Outputs.php @@ -0,0 +1,225 @@ +<?php + +/** + * This class attempts to provide safe yet simple means for managing data that is relevant + * for the final HTML output of MediaWiki. In particular, this concerns additions to the HTML + * header in the form of scripts of stylesheets. + * + * The problem is that many components in SMW create hypertext that should eventually be displayed. + * The normal way of accessing such text are functions of the form getText() which return a + * (hypertext) string. Such a string, however, might refer to styles or scripts that are also + * needed. It is not possible to directly add those scripts to the MediaWiki output, since the form + * of this output depends on the context in which the function is called. Many functions might be + * called both during parsing and directly in special pages that do not use the usual parsing and + * caching mechanisms. + * + * Ideally, all functions that generate hypertext with dependencies would also include parameters to + * record required scripts. Since this would require major API changes, the current solution is to have + * a "temporal" global storage for the required items, managed in this class. It is not safe to use + * such a global store across hooks -- you never know what happens in between! Hence, every function + * that creates SMW outputs that may require head items must afterwards clear the temporal store by + * writing its contents to the according output. + * + * @ingroup SMW + * + * @author Markus Krötzsch + */ +class SMWOutputs { + + /** + * Protected member for temporarily storing header items. + * Format $id => $headItem where $id is used only to avoid duplicate + * items in the time before they are forwarded to the output. + */ + protected static $headItems = []; + + /** + * Protected member for temporarily storing additional Javascript + * snippets. Format $id => $scriptText where $id is used only to + * avoid duplicate scripts in the time before they are forwarded + * to the output. + */ + protected static $scripts = []; + + /** + * Protected member for temporarily storing resource modules. + * + * @var array + */ + protected static $resourceModules = []; + + /** + * Protected member for temporarily storing resource modules. + * + * @var array + */ + protected static $resourceStyles = []; + + /** + * Adds a resource module to the parser output. + * + * @since 1.5.3 + * + * @param string $moduleName + */ + public static function requireResource( $moduleName ) { + self::$resourceModules[$moduleName] = $moduleName; + } + + /** + * @since 3.0 + * + * @param string $stylesName + */ + public static function requireStyle( $stylesName ) { + self::$resourceStyles[$stylesName] = $stylesName; + } + + /** + * Require the presence of header scripts, provided as strings with + * enclosing script tags. Note that the same could be achieved with + * requireHeadItems, but scripts use a special method "addScript" in + * MediaWiki OutputPage, hence we distinguish them. + * + * The id is used to avoid that the requirement for one script is + * recorded multiple times in SMWOutputs. + * + * @param string $id + * @param string $item + */ + public static function requireScript( $id, $script ) { + self::$scripts[$id] = $script; + } + + /** + * Adds head items that are not Resource Loader modules. Should only + * be used for custom head items such as RSS fedd links. + * + * The id is used to avoid that the requirement for one script is + * recorded multiple times in SMWOutputs. + * + * Support for calling this with the old constants SMW_HEADER_STYLE + * and SMW_HEADER_TOOLTIP will vanish in SMW 1.7 at the latest. + * + * @param mixed $id + * @param string $item + */ + public static function requireHeadItem( $id, $item = '' ) { + if ( is_numeric( $id ) ) { + switch ( $id ) { + case SMW_HEADER_TOOLTIP: + self::requireResource( 'ext.smw.tooltips' ); + break; + case SMW_HEADER_STYLE: + self::requireStyle( 'ext.smw.style' ); + break; + } + } else { + self::$headItems[$id] = $item; + } + } + + /** + * This function takes output requirements as can be found in a given ParserOutput + * object and puts them back in to the internal temporal requirement list from + * which they can be committed to some other output. It is needed when code that + * would normally call SMWOutputs::requireHeadItem() has need to use a full + * independent parser call (Parser::parse()) that produces its own parseroutput. + * If omitted, all output items potentially committed to this parseroutput during + * parsing will not be passed on to higher levels. + * + * Note that this is not required if the $parseroutput is further processed by + * MediaWiki, but there are cases where the output is discarded and only its text + * is used. + * + * @param ParserOutput $parserOutput + */ + static public function requireFromParserOutput( ParserOutput $parserOutput ) { + // Note: we do not attempt to recover which head items where scripts here. + + $parserOutputHeadItems = $parserOutput->getHeadItems(); + + self::$headItems = array_merge( (array)self::$headItems, $parserOutputHeadItems ); + + /// TODO Is the following needed? + if ( isset( $parserOutput->mModules ) ) { + foreach ( $parserOutput->mModules as $module ) { + self::$resourceModules[$module] = $module; + } + } + } + + /** + * Actually commit the collected requirements to a given parser that is about to parse + * what will later be the HTML output. This makes sure that HTML output based on the + * parser results contains all required output items. + * + * If the parser creates output for a normal wiki page, then the committed items will + * also become part of the page cache so that they will correctly be added to all page + * outputs built from this cache later on. + * + * @param Parser $parser + */ + static public function commitToParser( Parser $parser ) { + /// TODO find out and document when this b/c code can go away + if ( method_exists( $parser, 'getOutput' ) ) { + $po = $parser->getOutput(); + } else { + $po = $parser->mOutput; + } + + if ( isset( $po ) ) { + self::commitToParserOutput( $po ); + } + } + + /** + * Similar to SMWOutputs::commitToParser() but acting on a ParserOutput object. + * + * @param ParserOutput $parserOutput + */ + static public function commitToParserOutput( ParserOutput $parserOutput ) { + + foreach ( self::$scripts as $key => $script ) { + $parserOutput->addHeadItem( $script . "\n", $key ); + } + + foreach ( self::$headItems as $key => $item ) { + $parserOutput->addHeadItem( "\t\t" . $item . "\n", $key ); + } + + $parserOutput->addModuleStyles( array_values( self::$resourceStyles ) ); + $parserOutput->addModules( array_values( self::$resourceModules ) ); + + self::$resourceStyles = []; + self::$resourceModules = []; + self::$headItems = []; + } + + /** + * Actually commit the collected requirements to a given OutputPage object that + * will later generate the HTML output. This makes sure that HTML output contains + * all required output items. Note that there is no parser caching at this level of + * processing. In particular, data should not be committed to $wgOut in methods + * that run during page parsing, since these would not run next time when the page + * is produced from parser cache. + * + * @param OutputPage $output + */ + static public function commitToOutputPage( OutputPage $output ) { + foreach ( self::$scripts as $script ) { + $output->addScript( $script ); + } + foreach ( self::$headItems as $key => $item ) { + $output->addHeadItem( $key, "\t\t" . $item . "\n" ); + } + + $output->addModuleStyles( array_values( self::$resourceStyles ) ); + $output->addModules( array_values( self::$resourceModules ) ); + + self::$resourceStyles = []; + self::$resourceModules = []; + self::$headItems = []; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/SMW_PageLister.php b/www/wiki/extensions/SemanticMediaWiki/includes/SMW_PageLister.php new file mode 100644 index 00000000..9e40d1fb --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/SMW_PageLister.php @@ -0,0 +1,355 @@ +<?php + +use SMW\Query\PrintRequest; + +/** + * Helper class to generate HTML lists of wiki pages, with support for paged + * navigation using the from/until and limit settings as in MediaWiki's + * CategoryPage. + * + * The class attempts to allow as much code as possible to be shared among + * different places where similar lists are used. + * + * Some code adapted from CategoryPage.php + * + * @ingroup SMW + * + * @author Nikolas Iwan + * @author Markus Krötzsch + * @author Jeroen De Dauw + */ +class SMWPageLister { + + protected $mDiWikiPages; + protected $mDiProperty; + protected $mLimit; + protected $mFrom; + protected $mUntil; + + /** + * Constructor + * + * @param $diWikiPages array of SMWDIWikiPage + * @param $diProperty mixed SMWDIProperty that the wikipages are values of, or null + * @param $limit integer maximal amount of items to display + * @param $from string if the results were selected starting from this string + * @param $until string if the results were selected reaching until this string + */ + public function __construct( $diWikiPages, $diProperty, $limit, $from = '', $until = '' ) { + $this->mDiWikiPages = $diWikiPages; + $this->mDiProperty = $diProperty; + $this->mLimit = $limit; + $this->mFrom = $from; + $this->mUntil = $until; + } + + /** + * Generates the prev/next link part to the HTML code of the top and + * bottom section of the page. Whether and how these links appear + * depends on specified boundaries, limit, and results. The title is + * required to create a link to the right page. The query array gives + * optional further parameters to append to all navigation links. + * + * @param $title Title + * @param $query array that associates parameter names to parameter values + * @return string + */ + public function getNavigationLinks( Title $title, $query = [] ) { + global $wgLang; + + $limitText = $wgLang->formatNum( $this->mLimit ); + + $resultCount = count( $this->mDiWikiPages ); + $beyondLimit = ( $resultCount > $this->mLimit ); + + if ( !is_null( $this->mUntil ) && $this->mUntil !== '' ) { + if ( $beyondLimit ) { + $first = \SMW\StoreFactory::getStore()->getWikiPageSortKey( $this->mDiWikiPages[1] ); + } else { + $first = ''; + } + + $last = $this->mUntil; + } elseif ( $beyondLimit || ( !is_null( $this->mFrom ) && $this->mFrom !== '' ) ) { + $first = $this->mFrom; + + if ( $beyondLimit ) { + $last = \SMW\StoreFactory::getStore()->getWikiPageSortKey( $this->mDiWikiPages[$resultCount - 1] ); + } else { + $last = ''; + } + } else { + return ''; + } + + $prevLink = wfMessage( 'prevn', $limitText )->escaped(); + if ( $first !== '' ) { + $prevLink = $this->makeSelfLink( $title, $prevLink, $query + [ 'until' => $first ] ); + } + + $nextLink = wfMessage( 'nextn', $limitText )->escaped(); + if ( $last !== '' ) { + $nextLink = $this->makeSelfLink( $title, $nextLink, $query + [ 'from' => $last ] ); + } + + return "($prevLink) ($nextLink)"; + } + + /** + * Format an HTML link with the given text and parameters. + * + * @return string + */ + protected function makeSelfLink( Title $title, $linkText, array $parameters ) { + return smwfGetLinker()->link( $title, $linkText, [], $parameters ); + } + + /** + * Make SMWRequestOptions suitable for obtaining a list of results for + * the given limit, and from or until string. One more result than the + * limit will be created, and the results may have to be reversed in + * order if ascending is set to false in the resulting object. + * + * @param $limit integer + * @param $from string can be empty if no from condition is desired + * @param $until string can be empty if no until condition is desired + * @return SMWRequestOptions + */ + public static function getRequestOptions( $limit, $from, $until ) { + $options = new SMWRequestOptions(); + $options->limit = $limit + 1; + $options->sort = true; + + if ( $from !== '' ) { + $options->boundary = $from; + $options->ascending = true; + $options->include_boundary = true; + } elseif ( $until !== '' ) { + $options->boundary = $until; + $options->ascending = false; + $options->include_boundary = false; + } + + return $options; + } + + /** + * Make SMWQuery suitable for obtaining a list of results based on the + * given description, limit, and from or until string. One more result + * than the limit will be created, and the results may have to be + * reversed in order if $until is nonempty. + * + * @param $description SMWDescription main query description + * @param $limit integer + * @param $from string can be empty if no from condition is desired + * @param $until string can be empty if no until condition is desired + * @return SMWQuery + */ + public static function getQuery( SMWDescription $description, $limit, $from, $until ) { + if ( $from !== '' ) { + $diWikiPage = new SMWDIWikiPage( $from, NS_MAIN, '' ); // make a dummy wiki page as boundary + $fromDescription = new SMWValueDescription( $diWikiPage, null, SMW_CMP_GEQ ); + $queryDescription = new SMWConjunction( [ $description, $fromDescription ] ); + $order = 'ASC'; + } elseif ( $until !== '' ) { + $diWikiPage = new SMWDIWikiPage( $until, NS_MAIN, '' ); // make a dummy wiki page as boundary + $untilDescription = new SMWValueDescription( $diWikiPage, null, SMW_CMP_LESS ); // do not include boundary in this case + $queryDescription = new SMWConjunction( [ $description, $untilDescription ] ); + $order = 'DESC'; + } else { + $queryDescription = $description; + $order = 'ASC'; + } + + $queryDescription->addPrintRequest( new PrintRequest( PrintRequest::PRINT_THIS, '' ) ); + + $query = new SMWQuery( $queryDescription ); + $query->sortkeys[''] = $order; + $query->setLimit( $limit + 1 ); + + return $query; + } + + /** + * Format a list of data items chunked by letter, either as a + * bullet list or a columnar format, depending on the length. + * + * @param $cutoff integer, use columns for more results than that + * @return string + */ + public function formatList( $cutoff = 6 ) { + $end = count( $this->mDiWikiPages ); + $start = 0; + if ( $end > $this->mLimit ) { + if ( $this->mFrom !== '' ) { + $end -= 1; + } else { + $start += 1; + } + } + + if ( count ( $this->mDiWikiPages ) > $cutoff ) { + return self::getColumnList( $start, $end, $this->mDiWikiPages, $this->mDiProperty ); + } elseif ( count( $this->mDiWikiPages ) > 0 ) { + return self::getShortList( $start, $end, $this->mDiWikiPages, $this->mDiProperty ); + } else { + return ''; + } + } + + /** + * Format a list of SMWDIWikiPage objects chunked by letter in a three-column + * list, ordered vertically. + * + * @param $start integer + * @param $end integer + * @param $diWikiPages array of SMWDIWikiPage + * @param $diProperty SMWDIProperty that the wikipages are values of, or null + * + * @return string + */ + public static function getColumnList( $start, $end, $diWikiPages, $diProperty, $moreCallback = null ) { + global $wgContLang; + + if ( $diWikiPages instanceof \Iterator ) { + $diWikiPages = iterator_to_array( $diWikiPages ); + } + + // Divide list into three equal chunks. + $chunk = (int) ( ( $end - $start + 1 ) / 3 ); + + // Get and display header. + $r = '<table width="100%"><tr valign="top">'; + + $prevStartChar = 'none'; + + // Loop through the chunks. + for ( $startChunk = $start, $endChunk = $chunk, $chunkIndex = 0; + $chunkIndex < 3; + ++$chunkIndex, $startChunk = $endChunk, $endChunk += $chunk + 1 ) { + $r .= "<td width='33%'>\n"; + $atColumnTop = true; + + // output all diWikiPages + for ( $index = $startChunk; $index < $endChunk && $index < $end; ++$index ) { + + if ( !isset( $diWikiPages[$index] ) ) { + continue; + } + + $dataValue = \SMW\DataValueFactory::getInstance()->newDataValueByItem( $diWikiPages[$index], $diProperty ); + $searchlink = \SMWInfolink::newBrowsingLink( '+', $dataValue->getWikiValue() ); + + // check for change of starting letter or beginning of chunk + $sortkey = \SMW\StoreFactory::getStore()->getWikiPageSortKey( $diWikiPages[$index] ); + $startChar = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) ); + + if ( ( $index == $startChunk ) || + ( $startChar != $prevStartChar ) ) { + if ( $atColumnTop ) { + $atColumnTop = false; + } else { + $r .= "</ul>\n"; + } + + if ( $startChar == $prevStartChar ) { + $cont_msg = ' ' . wfMessage( 'listingcontinuesabbrev' )->escaped(); + } else { + $cont_msg = ''; + } + + $r .= "<h3>" . htmlspecialchars( $startChar ) . $cont_msg . "</h3>\n<ul>"; + + $prevStartChar = $startChar; + } + + $r .= "<li>" . $dataValue->getLongHTMLText( smwfGetLinker() ) . ' ' . $searchlink->getHTML( smwfGetLinker() ) . "</li>\n"; + } + + if ( $index == $end && $moreCallback !== null ) { + $r .= "<li>" . call_user_func( $moreCallback ) . "</li>\n"; + } + + if ( !$atColumnTop ) { + $r .= "</ul>\n"; + } + + $r .= "</td>\n"; + } + + $r .= '</tr></table>'; + + return $r; + } + + /** + * Format a list of diWikiPages chunked by letter in a bullet list. + * + * @param $start integer + * @param $end integer + * @param $diWikiPages array of SMWDataItem + * @param $diProperty SMWDIProperty that the wikipages are values of, or null + * + * @return string + */ + public static function getShortList( $start, $end, $diWikiPages, $diProperty, $moreCallback = null ) { + + if ( $diWikiPages instanceof \Iterator ) { + $diWikiPages = iterator_to_array( $diWikiPages ); + } + + $startDv = \SMW\DataValueFactory::getInstance()->newDataValueByItem( $diWikiPages[$start], $diProperty ); + $searchlink = \SMWInfolink::newBrowsingLink( '+', $startDv->getWikiValue() ); + + // For a redirect, disable the DisplayTitle to show the original (aka source) page + if ( $diProperty !== null && $diProperty->getKey() == '_REDI' ) { + $startDv->setOption( 'smwgDVFeatures', ( $startDv->getOption( 'smwgDVFeatures' ) & ~SMW_DV_WPV_DTITLE ) ); + } + + $startChar = self::getFirstChar( $diWikiPages[$start] ); + + $r = '<h3>' . htmlspecialchars( $startChar ) . "</h3>\n" . + '<ul><li>' . $startDv->getLongHTMLText( smwfGetLinker() ) . ' ' . $searchlink->getHTML( smwfGetLinker() ) . '</li>'; + + $prevStartChar = $startChar; + for ( $index = $start + 1; $index < $end; $index++ ) { + $dataValue = \SMW\DataValueFactory::getInstance()->newDataValueByItem( $diWikiPages[$index], $diProperty ); + $searchlink = \SMWInfolink::newBrowsingLink( '+', $dataValue->getWikiValue() ); + + // For a redirect, disable the DisplayTitle to show the original (aka source) page + if ( $diProperty !== null && $diProperty->getKey() == '_REDI' ) { + $dataValue->setOption( 'smwgDVFeatures', ( $dataValue->getOption( 'smwgDVFeatures' ) & ~SMW_DV_WPV_DTITLE ) ); + } + + $startChar = self::getFirstChar( $diWikiPages[$index] ); + + if ( $startChar != $prevStartChar ) { + $r .= "</ul><h3>" . htmlspecialchars( $startChar ) . "</h3>\n<ul>"; + $prevStartChar = $startChar; + } + + $r .= '<li>' . $dataValue->getLongHTMLText( smwfGetLinker() ) . ' ' . $searchlink->getHTML( smwfGetLinker() ) . '</li>'; + } + + if ( $moreCallback !== null ) { + $r .= '<li>' . call_user_func( $moreCallback ) . '</li>'; + } + + $r .= '</ul>'; + + return $r; + } + + private static function getFirstChar( $dataItem ) { + global $wgContLang; + + $sortkey = \SMW\StoreFactory::getStore()->getWikiPageSortKey( $dataItem ); + + if ( $sortkey === '' ) { + $sortkey = $dataItem->getDBKey(); + } + + return $wgContLang->convert( $wgContLang->firstChar( $sortkey ) ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/SMW_PageSchemas.php b/www/wiki/extensions/SemanticMediaWiki/includes/SMW_PageSchemas.php new file mode 100644 index 00000000..8143ace2 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/SMW_PageSchemas.php @@ -0,0 +1,379 @@ +<?php + +/** + * Functions for handling Semantic MediaWiki data within the Page Schemas + * extension. + * + * @author Ankit Garg + * @author Yaron Koren + * @ingroup SMW + */ + +class SMWPageSchemas extends PSExtensionHandler { + + public static function getDisplayColor() { + return '#DEF'; + } + + public static function getTemplateDisplayString() { + return 'Connecting property'; + } + + public static function getFieldDisplayString() { + return 'Semantic property'; + } + + /** + * Returns the display info for the "connecting property" (if any) + * of the #subobject call (if any) in this template. + */ + public static function getTemplateDisplayValues( $templateXML ) { + foreach ( $templateXML->children() as $tag => $child ) { + if ( $tag == "semanticmediawiki_ConnectingProperty" ) { + $propName = $child->attributes()->name; + $values = []; + return [ $propName, $values ]; + } + } + return null; + } + + /** + * Returns the display info for the property (if any is defined) + * for a single field in the Page Schemas XML. + */ + public static function getFieldDisplayValues( $fieldXML ) { + foreach ( $fieldXML->children() as $tag => $child ) { + if ( $tag == "semanticmediawiki_Property" ) { + $propName = $child->attributes()->name; + $values = []; + foreach ( $child->children() as $prop => $value ) { + $values[$prop] = (string)$value; + } + return [ $propName, $values ]; + } + } + return null; + } + + /** + * Returns the set of SMW property data from the entire page schema. + */ + static function getAllPropertyData( $pageSchemaObj ) { + $propertyDataArray = []; + $psTemplates = $pageSchemaObj->getTemplates(); + foreach ( $psTemplates as $psTemplate ) { + $psTemplateFields = $psTemplate->getFields(); + foreach ( $psTemplateFields as $psTemplateField ) { + $prop_array = $psTemplateField->getObject('semanticmediawiki_Property'); + if ( empty( $prop_array ) ) { + continue; + } + // If property name is blank, set it to the + // field name. + if ( !array_key_exists( 'name', $prop_array ) || empty( $prop_array['name'] ) ) { + $prop_array['name'] = $psTemplateField->getName(); + } + $propertyDataArray[] = $prop_array; + } + } + return $propertyDataArray; + } + + /** + * Constructs XML for the "connecting property", based on what was + * submitted in the 'edit schema' form. + */ + public static function createTemplateXMLFromForm() { + global $wgRequest; + + $xmlPerTemplate = []; + foreach ( $wgRequest->getValues() as $var => $val ) { + if ( substr( $var, 0, 24 ) == 'smw_connecting_property_' ) { + $templateNum = substr( $var, 24 ); + $xml = '<semanticmediawiki_ConnectingProperty name="' . $val . '" />'; + $xmlPerTemplate[$templateNum] = $xml; + } + } + return $xmlPerTemplate; + } + + static function getConnectingPropertyName( $psTemplate ) { + // TODO - there should be a more direct way to get + // this data. + $smwConnectingPropertyArray = $psTemplate->getObject( 'semanticmediawiki_ConnectingProperty' ); + return PageSchemas::getValueFromObject( $smwConnectingPropertyArray, 'name' ); + } + + /** + * Sets the list of property pages defined by the passed-in + * Page Schemas object. + */ + public static function getPagesToGenerate( $pageSchemaObj ) { + $pagesToGenerate = []; + + $psTemplates = $pageSchemaObj->getTemplates(); + foreach ( $psTemplates as $psTemplate ) { + $smwConnectingPropertyName = self::getConnectingPropertyName( $psTemplate ); + if ( is_null( $smwConnectingPropertyName ) ) { + continue; + } + $pagesToGenerate[] = Title::makeTitleSafe( SMW_NS_PROPERTY, $smwConnectingPropertyName ); + } + + $propertyDataArray = self::getAllPropertyData( $pageSchemaObj ); + foreach ( $propertyDataArray as $propertyData ) { + $title = Title::makeTitleSafe( SMW_NS_PROPERTY, $propertyData['name'] ); + $pagesToGenerate[] = $title; + } + return $pagesToGenerate; + } + + /** + * Constructs XML for the SMW property, based on what was submitted + * in the 'edit schema' form. + */ + public static function createFieldXMLFromForm() { + global $wgRequest; + + $fieldNum = -1; + $xmlPerField = []; + foreach ( $wgRequest->getValues() as $var => $val ) { + if ( substr( $var, 0, 18 ) == 'smw_property_name_' ) { + $fieldNum = substr( $var, 18 ); + $xml = '<semanticmediawiki_Property name="' . $val . '" >'; + } elseif ( substr( $var, 0, 18 ) == 'smw_property_type_'){ + $xml .= '<Type>' . $val . '</Type>'; + } elseif ( substr( $var, 0, 16 ) == 'smw_linked_form_') { + if ( $val !== '' ) { + $xml .= '<LinkedForm>' . $val . '</LinkedForm>'; + } + } elseif ( substr( $var, 0, 11 ) == 'smw_values_') { + if ( $val !== '' ) { + // replace the comma substitution character that has no chance of + // being included in the values list - namely, the ASCII beep + $listSeparator = ','; + $allowed_values_str = str_replace( "\\$listSeparator", "\a", $val ); + $allowed_values_array = explode( $listSeparator, $allowed_values_str ); + foreach ( $allowed_values_array as $value ) { + // replace beep back with comma, trim + $value = str_replace( "\a", $listSeparator, trim( $value ) ); + $xml .= '<AllowedValue>' . $value . '</AllowedValue>'; + } + } + $xml .= '</semanticmediawiki_Property>'; + $xmlPerField[$fieldNum] = $xml; + } + } + return $xmlPerField; + } + + /** + * Returns the HTML necessary for getting information about the + * "connecting property" within the Page Schemas 'editschema' page. + */ + public static function getTemplateEditingHTML( $psTemplate) { + // Only display this if the Semantic Internal Objects extension + // isn't displaying something similar. + if ( class_exists( 'SIOPageSchemas' ) ) { + return null; + } + + $prop_array = []; + $hasExistingValues = false; + if ( !is_null( $psTemplate ) ) { + $prop_array = $psTemplate->getObject( 'semanticmediawiki_ConnectingProperty' ); + if ( !is_null( $prop_array ) ) { + $hasExistingValues = true; + } + } + $text = '<p>' . 'Name of property to connect this template\'s fields to the rest of the page:' . ' ' . '(should only be used if this template can have multiple instances)' . ' '; + $propName = PageSchemas::getValueFromObject( $prop_array, 'name' ); + $text .= Html::input( 'smw_connecting_property_num', $propName, [ 'size' => 15 ] ) . "\n"; + + return [ $text, $hasExistingValues ]; + } + + /** + * Returns the HTML necessary for getting information about a regular + * semantic property within the Page Schemas 'editschema' page. + */ + public static function getFieldEditingHTML( $psTemplateField ) { + global $smwgContLang; + + $prop_array = []; + $hasExistingValues = false; + if ( !is_null( $psTemplateField ) ) { + $prop_array = $psTemplateField->getObject('semanticmediawiki_Property'); + if ( !is_null( $prop_array ) ) { + $hasExistingValues = true; + } + } + $html_text = '<p>' . wfMessage( 'ps-optional-name' )->text() . ' '; + $propName = PageSchemas::getValueFromObject( $prop_array, 'name' ); + $html_text .= Html::input( 'smw_property_name_num', $propName, [ 'size' => 15 ] ) . "\n"; + $propType = PageSchemas::getValueFromObject( $prop_array, 'Type' ); + $select_body = ""; + $datatype_labels = $smwgContLang->getDatatypeLabels(); + foreach ( $datatype_labels as $label ) { + $optionAttrs = []; + if ( $label == $propType) { + $optionAttrs['selected'] = 'selected'; + } + $select_body .= "\t" . Xml::element( 'option', $optionAttrs, $label ) . "\n"; + } + $propertyDropdownAttrs = [ + 'id' => 'property_dropdown', + 'name' => 'smw_property_type_num', + 'value' => $propType + ]; + $html_text .= "Type: " . Xml::tags( 'select', $propertyDropdownAttrs, $select_body ) . "</p>\n"; + + // This can't be last, because of the hacky way the XML is + // ocnstructed from this form's output. + if ( defined( 'SF_VERSION' ) ) { + $html_text .= '<p>' . wfMessage( 'sf_createproperty_linktoform' )->text() . ' '; + $linkedForm = PageSchemas::getValueFromObject( $prop_array, 'LinkedForm' ); + $html_text .= Html::input( 'smw_linked_form_num', $linkedForm, [ 'size' => 15 ] ) . "\n"; + $html_text .= "(for Page properties only)</p>\n"; + } + + $html_text .= '<p>If you want this property to only be allowed to have certain values, enter the list of allowed values, separated by commas (if a value contains a comma, replace it with "\,"):</p>'; + $allowedValsInputAttrs = [ + 'size' => 80 + ]; + $allowedValues = PageSchemas::getValueFromObject( $prop_array, 'allowed_values' ); + if ( is_null( $allowedValues ) ) { + $allowed_val_string = ''; + } else { + $allowed_val_string = implode( ', ', $allowedValues ); + } + $html_text .= '<p>' . Html::input( 'smw_values_num', $allowed_val_string, 'text', $allowedValsInputAttrs ) . "</p>\n"; + + return [ $html_text, $hasExistingValues ]; + } + + /** + * Creates the property page for each property specified in the + * passed-in Page Schemas XML object. + */ + public static function generatePages( $pageSchemaObj, $selectedPages ) { + global $smwgContLang, $wgUser; + + $datatypeLabels = $smwgContLang->getDatatypeLabels(); + $pageTypeLabel = $datatypeLabels['_wpg']; + + $jobs = []; + $jobParams = []; + $jobParams['user_id'] = $wgUser->getId(); + + // First, create jobs for all "connecting properties". + $psTemplates = $pageSchemaObj->getTemplates(); + foreach ( $psTemplates as $psTemplate ) { + $smwConnectingPropertyName = self::getConnectingPropertyName( $psTemplate ); + if ( is_null( $smwConnectingPropertyName ) ) { + continue; + } + $propTitle = Title::makeTitleSafe( SMW_NS_PROPERTY, $smwConnectingPropertyName ); + if ( !in_array( $propTitle, $selectedPages ) ) { + continue; + } + + $jobParams['page_text'] = self::createPropertyText( $pageTypeLabel, null, null ); + $jobs[] = new PSCreatePageJob( $propTitle, $jobParams ); + } + + // Second, create jobs for all regular properties. + $propertyDataArray = self::getAllPropertyData( $pageSchemaObj ); + foreach ( $propertyDataArray as $propertyData ) { + $propTitle = Title::makeTitleSafe( SMW_NS_PROPERTY, $propertyData['name'] ); + if ( !in_array( $propTitle, $selectedPages ) ) { + continue; + } + $propertyType = array_key_exists( 'Type', $propertyData ) ? $propertyData['Type'] : null; + $propertyAllowedValues = array_key_exists( 'allowed_values', $propertyData ) ? $propertyData['allowed_values'] : null; + $propertyLinkedForm = array_key_exists( 'LinkedForm', $propertyData ) ? $propertyData['LinkedForm'] : null; + $jobParams['page_text'] = self::createPropertyText( $propertyType, $propertyAllowedValues, $propertyLinkedForm ); + $jobs[] = new PSCreatePageJob( $propTitle, $jobParams ); + } + if ( class_exists( 'JobQueueGroup' ) ) { + JobQueueGroup::singleton()->push( $jobs ); + } else { + // MW <= 1.20 + Job::batchInsert( $jobs ); + } + } + + /** + * Creates the text for a property page. + */ + static public function createPropertyText( $propertyType, $allowedValues, $linkedForm = null ) { + /** + * @var SMWLanguage $smwgContLang + */ + global $smwgContLang, $wgContLang; + + $propLabels = $smwgContLang->getPropertyLabels(); + $hasTypeLabel = $propLabels['_TYPE']; + $typeTag = "[[$hasTypeLabel::$propertyType]]"; + $text = wfMessage( 'smw-createproperty-isproperty', $typeTag )->inContentLanguage()->text(); + + if ( $linkedForm !== '' && defined( 'SF_VERSION' ) ) { + global $sfgContLang; + $sfPropLabels = $sfgContLang->getPropertyLabels(); + $defaultFormTag = "[[{$sfPropLabels[SF_SP_HAS_DEFAULT_FORM]}::$linkedForm]]"; + $text .= ' ' . wfMessage( 'sf_property_linkstoform', $defaultFormTag )->inContentLanguage()->text(); + } + + if ( $allowedValues != null) { + $text .= "\n\n" . wfMessage( 'smw-createproperty-allowedvals', $wgContLang->formatNum( count( $allowedValues ) ) )->inContentLanguage()->text(); + + foreach ( $allowedValues as $value ) { + $prop_labels = $smwgContLang->getPropertyLabels(); + $text .= "\n* [[" . $prop_labels['_PVAL'] . "::$value]]"; + } + } + + return $text; + } + + /** + * Returns either the "connecting property", or a field property, based + * on the XML passed from the Page Schemas extension. + */ + public static function createPageSchemasObject( $tagName, $xml ) { + if ( $tagName == "semanticmediawiki_ConnectingProperty" ) { + foreach ( $xml->children() as $tag => $child ) { + if ( $tag == $tagName ) { + $smw_array = []; + $propName = $child->attributes()->name; + $smw_array['name'] = (string)$propName; + foreach ( $child->children() as $prop => $value ) { + $smw_array[$prop] = (string)$value; + } + return $smw_array; + } + } + } elseif ( $tagName == "semanticmediawiki_Property" ) { + foreach ( $xml->children() as $tag => $child ) { + if ( $tag == $tagName ) { + $smw_array = []; + $propName = $child->attributes()->name; + $smw_array['name'] = (string)$propName; + $allowed_values = []; + $count = 0; + foreach ( $child->children() as $prop => $value ) { + if ( $prop == "AllowedValue" ) { + $allowed_values[$count++] = $value; + } else { + $smw_array[$prop] = (string)$value; + } + } + $smw_array['allowed_values'] = $allowed_values; + return $smw_array; + } + } + } + return null; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/SemanticData.php b/www/wiki/extensions/SemanticMediaWiki/includes/SemanticData.php new file mode 100644 index 00000000..e54ec20e --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/SemanticData.php @@ -0,0 +1,782 @@ +<?php + +namespace SMW; + +use MWException; +use SMW\DataModel\SubSemanticData; +use SMW\Exception\SemanticDataImportException; +use SMWContainerSemanticData; +use SMWDataItem; +use SMWDataValue; +use SMWDIContainer; + +/** + * Class for representing chunks of semantic data for one given + * subject. This consists of property-value pairs, grouped by property, + * and possibly by SMWSemanticData objects about subobjects. + * + * Data about subobjects can be added in two ways: by directly adding it + * using addSubSemanticData() or by adding a property value of type + * SMWDIContainer. + * + * By its very design, the container is unable to hold inverse properties. + * For one thing, it would not be possible to identify them with mere keys. + * Since SMW cannot annotate pages with inverses, this is not a limitation. + * + * @ingroup SMW + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + */ +class SemanticData { + + /** + * Returns the last modified timestamp the data were stored to the Store or + * have been fetched from cache. + */ + const OPT_LAST_MODIFIED = 'opt.last.modified'; + + /** + * Identifies that a data block was created by a user. + */ + const PROC_USER = 'proc.user'; + + /** + * Identifies that a data block was initiated by a delete request. + */ + const PROC_DELETE = 'proc.delete'; + + /** + * Cache for the localized version of the namespace prefix "Property:". + * + * @var string + */ + static protected $mPropertyPrefix = ''; + + /** + * States whether this is a stub object. Stubbing might happen on + * serialisation to save DB space. + * + * @todo Check why this is public and document this here. Or fix it. + * + * @var boolean + */ + public $stubObject; + + /** + * Array mapping property keys (string) to arrays of SMWDataItem + * objects. + * + * @var SMWDataItem[] + */ + protected $mPropVals = []; + + /** + * Array mapping property keys (string) to DIProperty objects. + * + * @var DIProperty[] + */ + protected $mProperties = []; + + /** + * States whether the container holds any normal properties. + * + * @var boolean + */ + protected $mHasVisibleProps = false; + + /** + * States whether the container holds any displayable predefined + * $mProperties (as opposed to predefined properties without a display + * label). For some settings we need this to decide if a Factbox is + * displayed. + * + * @var boolean + */ + protected $mHasVisibleSpecs = false; + + /** + * States whether repeated values should be avoided. Not needing + * duplicate elimination (e.g. when loading from store) can save some + * time, especially in subclasses like SMWSqlStubSemanticData, where + * the first access to a data item is more costy. + * + * @note This setting is merely for optimization. The SMW data model + * never cares about the multiplicity of identical data assignments. + * + * @var boolean + */ + protected $mNoDuplicates; + + /** + * DIWikiPage object that is the subject of this container. + * Subjects can never be null (and this is ensured in all methods setting + * them in this class). + * + * @var DIWikiPage + */ + protected $mSubject; + + /** + * Semantic data associated to subobjects of the subject of this + * SMWSemanticData. + * These key-value pairs of subObjectName (string) =>SMWSemanticData. + * + * @since 1.8 + * @var SubSemanticData + */ + protected $subSemanticData; + + /** + * Internal flag that indicates if this semantic data will accept + * subdata. Semantic data objects that are subdata already do not allow + * (second level) subdata to be added. This ensures that all data is + * collected on the top level, and in particular that there is only one + * way to represent the same data with subdata. This is also useful for + * diff computation. + */ + protected $subDataAllowed = true; + + /** + * @var array + */ + protected $errors = []; + + /** + * Cache the hash to ensure a minimal impact in case of repeated usage. Any + * removal or insert action will reset the hash to null to ensure it is + * recreated in corresponds to changed nature of the data. + * + * @var string|null + */ + private $hash = null; + + /** + * @var Options + */ + protected $options; + + /** + * @var array + */ + protected $extensionData = []; + + /** + * This is kept public to keep track of the depth during a recursive processing + * when accessed through the SubSemanticData instance. + * + * @var integer + */ + public $subContainerDepthCounter = 0; + + /** + * Constructor. + * + * @param DIWikiPage $subject to which this data refers + * @param boolean $noDuplicates stating if duplicate data should be avoided + */ + public function __construct( DIWikiPage $subject, $noDuplicates = true ) { + $this->clear(); + $this->mSubject = $subject; + $this->mNoDuplicates = $noDuplicates; + $this->subSemanticData = new SubSemanticData( $subject, $noDuplicates ); + } + + /** + * This object is added to the parser output of MediaWiki, but it is + * not useful to have all its data as part of the parser cache since + * the data is already stored in more accessible format in SMW. Hence + * this implementation of __sleep() makes sure only the subject is + * serialised, yielding a minimal stub data container after + * unserialisation. This is a little safer than serialising nothing: + * if, for any reason, SMW should ever access an unserialised parser + * output, then the Semdata container will at least look as if properly + * initialised (though empty). + * + * @return array + */ + public function __sleep() { + return [ 'mSubject', 'mPropVals', 'mProperties', 'subSemanticData', 'mHasVisibleProps', 'mHasVisibleSpecs', 'options', 'extensionData' ]; + } + + /** + * Return subject to which the stored semantic annotations refer to. + * + * @return DIWikiPage subject + */ + public function getSubject() { + return $this->mSubject; + } + + /** + * Get the array of all properties that have stored values. + * + * @return array of DIProperty objects + */ + public function getProperties() { + ksort( $this->mProperties, SORT_STRING ); + return $this->mProperties; + } + + /** + * @since 2.4 + * + * @param DIProperty $property + * + * @return boolean + */ + public function hasProperty( DIProperty $property ) { + return isset( $this->mProperties[$property->getKey()] ) || array_key_exists( $property->getKey(), $this->mProperties ); + } + + /** + * Get the array of all stored values for some property. + * + * @param DIProperty $property + * @return SMWDataItem[] + */ + public function getPropertyValues( DIProperty $property ) { + if ( $property->isInverse() ) { // we never have any data for inverses + return []; + } + + if ( array_key_exists( $property->getKey(), $this->mPropVals ) ) { + return array_values( $this->mPropVals[$property->getKey()] ); + } + + return []; + } + + /** + * @since 3.0 + * + * @param string $key + * @param mixed $value + */ + public function setExtensionData( $key, $value ) { + $this->extensionData[$key] = $value; + } + + /** + * @since 3.0 + * + * @param string $key + * + * @return mixed|null + */ + public function getExtensionData( $key ) { + + if ( !isset( $this->extensionData[$key] ) ) { + return null; + } + + return $this->extensionData[$key]; + } + + /** + * @since 2.5 + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function getOption( $key, $default = null ) { + + if ( !$this->options instanceof Options ) { + $this->options = new Options(); + } + + if ( $this->options->has( $key ) ) { + return $this->options->get( $key ); + } + + return $default; + } + + /** + * @since 2.5 + * + * @param string $key + * @param string $value + */ + public function setOption( $key, $value ) { + + if ( !$this->options instanceof Options ) { + $this->options = new Options(); + } + + $this->options->set( $key, $value ); + } + + /** + * Returns collected errors occurred during processing + * + * @since 1.9 + * + * @return array + */ + public function getErrors() { + return $this->errors; + } + + /** + * Adds an error array + * + * @since 1.9 + * + * @return array|string + */ + public function addError( $error ) { + $this->errors = array_merge( $this->errors, (array)$error ); + } + + /** + * Generate a hash value to simplify the comparison of this data + * container with other containers. Subdata is taken into account. + * + * The hash uses PHP's md5 implementation, which is among the fastest + * hash algorithms that PHP offers. + * + * @note This function may be used to obtain keys for SemanticData + * objects or to do simple equality tests. Equal hashes with very + * high probability indicate equal data. + * + * @return string + */ + public function getHash() { + + if ( $this->hash !== null ) { + return $this->hash; + } + + return $this->hash = Hash::createFromSemanticData( $this ); + } + + /** + * @see SubSemanticData::getSubSemanticData + * + * @since 1.8 + * + * @return ContainerSemanticData[] + */ + public function getSubSemanticData() { + + // Remove the check in 3.0 + $subSemanticData = $this->subSemanticData; + + // Avoids an issue where the serialized array from a previous usage is + // returned from a __wakeup, where now a SubSemanticData (#2177) is expected. + if ( !$subSemanticData instanceof SubSemanticData ) { + $this->subSemanticData = new SubSemanticData( $this->mSubject, $this->mNoDuplicates ); + $this->subSemanticData->copyDataFrom( $subSemanticData ); + } + + return $this->subSemanticData->getSubSemanticData(); + } + + /** + * @since 2.5 + */ + public function clearSubSemanticData() { + + if ( $this->subContainerDepthCounter > 0 ) { + $this->subContainerDepthCounter--; + } + + if ( $this->subSemanticData !== null ) { + $this->subSemanticData->clear(); + } + } + + /** + * Return true if there are any visible properties. + * + * @note While called "visible" this check actually refers to the + * function DIProperty::isShown(). The name is kept for + * compatibility. + * + * @return boolean + */ + public function hasVisibleProperties() { + return $this->mHasVisibleProps; + } + + /** + * Return true if there are any special properties that can + * be displayed. + * + * @note While called "visible" this check actually refers to the + * function DIProperty::isShown(). The name is kept for + * compatibility. + * + * @return boolean + */ + public function hasVisibleSpecialProperties() { + return $this->mHasVisibleSpecs; + } + + /** + * Store a value for a property identified by its SMWDataItem object. + * + * @note There is no check whether the type of the given data item + * agrees with the type of the property. Since property types can + * change, all parts of SMW are prepared to handle mismatched data item + * types anyway. + * + * @param $property DIProperty + * @param $dataItem SMWDataItem + */ + public function addPropertyObjectValue( DIProperty $property, SMWDataItem $dataItem ) { + + $this->hash = null; + + if( $dataItem instanceof SMWDIContainer ) { + $this->addSubSemanticData( $dataItem->getSemanticData() ); + $dataItem = $dataItem->getSemanticData()->getSubject(); + } + + if( $property->getKey() === DIProperty::TYPE_MODIFICATION_DATE ) { + $this->setOption( self::OPT_LAST_MODIFIED, $dataItem->getMwTimestamp() ); + } + + if ( $property->isInverse() ) { // inverse properties cannot be used for annotation + return; + } + + if ( !array_key_exists( $property->getKey(), $this->mPropVals ) ) { + $this->mPropVals[$property->getKey()] = []; + $this->mProperties[$property->getKey()] = $property; + } + + if ( $this->mNoDuplicates ) { + $this->mPropVals[$property->getKey()][$dataItem->getHash()] = $dataItem; + } else { + $this->mPropVals[$property->getKey()][] = $dataItem; + } + + if ( !$property->isUserDefined() ) { + if ( $property->isShown() ) { + $this->mHasVisibleSpecs = true; + $this->mHasVisibleProps = true; + } + } else { + $this->mHasVisibleProps = true; + } + + // Account for things like DISPLAYTITLE or DEFAULTSORT which are only set + // after #subobject has been processed therefore keep them in-memory + // for a post process + if ( $this->mSubject->getSubobjectName() === '' && $property->getKey() === DIProperty::TYPE_SORTKEY ) { + foreach ( $this->getSubSemanticData() as $subSemanticData ) { + $subSemanticData->setExtensionData( 'sort.extension', $dataItem->getString() ); + } + } + } + + /** + * Store a value for a given property identified by its text label + * (without namespace prefix). + * + * @param $propertyName string + * @param $dataItem SMWDataItem + */ + public function addPropertyValue( $propertyName, SMWDataItem $dataItem ) { + $propertyKey = smwfNormalTitleDBKey( $propertyName ); + + if ( array_key_exists( $propertyKey, $this->mProperties ) ) { + $property = $this->mProperties[$propertyKey]; + } else { + if ( self::$mPropertyPrefix === '' ) { + global $wgContLang; + self::$mPropertyPrefix = $wgContLang->getNsText( SMW_NS_PROPERTY ) . ':'; + } // explicitly use prefix to cope with things like [[Property:User:Stupid::somevalue]] + + $propertyDV = DataValueFactory::getInstance()->newPropertyValueByLabel( self::$mPropertyPrefix . $propertyName ); + + if ( !$propertyDV->isValid() ) { // error, maybe illegal title text + return; + } + + $property = $propertyDV->getDataItem(); + } + + $this->addPropertyObjectValue( $property, $dataItem ); + } + + /** + * @since 1.9 + * + * @param SMWDataValue $dataValue + */ + public function addDataValue( SMWDataValue $dataValue ) { + + if ( !$dataValue->getProperty() instanceof DIProperty || !$dataValue->isValid() ) { + + $processingErrorMsgHandler = new ProcessingErrorMsgHandler( + $this->getSubject() + ); + + $processingErrorMsgHandler->addToSemanticData( + $this, + $processingErrorMsgHandler->newErrorContainerFromDataValue( $dataValue ) + ); + + return $this->addError( $dataValue->getErrors() ); + } + + $this->addPropertyObjectValue( + $dataValue->getProperty(), + $dataValue->getDataItem() + ); + } + + /** + * @since 2.1 + * + * @param Subobject $subobject + */ + public function addSubobject( Subobject $subobject ) { + $this->addPropertyObjectValue( + $subobject->getProperty(), + $subobject->getContainer() + ); + } + + /** + * Remove a value for a property identified by its SMWDataItem object. + * This method removes a property-value specified by the property and + * dataitem. If there are no more property-values for this property it + * also removes the property from the mProperties. + * + * @note There is no check whether the type of the given data item + * agrees with the type of the property. Since property types can + * change, all parts of SMW are prepared to handle mismatched data item + * types anyway. + * + * @param $property DIProperty + * @param $dataItem SMWDataItem + * + * @since 1.8 + */ + public function removePropertyObjectValue( DIProperty $property, SMWDataItem $dataItem ) { + + $this->hash = null; + + //delete associated subSemanticData + if( $dataItem instanceof SMWDIContainer ) { + $this->removeSubSemanticData( $dataItem->getSemanticData() ); + $dataItem = $dataItem->getSemanticData()->getSubject(); + } + + if ( $property->isInverse() ) { // inverse properties cannot be used for annotation + return; + } + + if ( !array_key_exists( $property->getKey(), $this->mPropVals ) || !array_key_exists( $property->getKey(), $this->mProperties ) ) { + return; + } + + if ( $this->mNoDuplicates ) { + //this didn't get checked for my tests, but should work + unset( $this->mPropVals[$property->getKey()][$dataItem->getHash()] ); + } else { + foreach( $this->mPropVals[$property->getKey()] as $index => $di ) { + if( $di->equals( $dataItem ) ) { + unset( $this->mPropVals[$property->getKey()][$index] ); + } + } + $this->mPropVals[$property->getKey()] = array_values( $this->mPropVals[$property->getKey()] ); + } + + if ( $this->mPropVals[$property->getKey()] === [] ) { + unset( $this->mProperties[$property->getKey()] ); + unset( $this->mPropVals[$property->getKey()] ); + } + } + + /** + * Removes a property and all the values associated with this property. + * + * @since 2.5 + * + * @param $property DIProperty + */ + public function removeProperty( DIProperty $property ) { + + $this->hash = null; + $key = $property->getKey(); + + // Inverse properties cannot be used for an annotation + if ( $property->isInverse() ) { + return; + } + + if ( !isset( $this->mProperties[$key] ) || !isset( $this->mPropVals[$key] ) ) { + return; + } + + // Find and remove associated assignments (e.g. _ASK as subobject + // contains _ASKSI ...) + foreach ( $this->mPropVals[$key] as $dataItem ) { + + if ( !$dataItem instanceof DIWikiPage || $dataItem->getSubobjectName() === '' ) { + continue; + } + + if ( ( $subSemanticData = $this->findSubSemanticData( $dataItem->getSubobjectName() ) ) !== null ) { + $this->removeSubSemanticData( $subSemanticData ); + } + } + + unset( $this->mPropVals[$key] ); + unset( $this->mProperties[$key] ); + } + + /** + * Delete all data other than the subject. + */ + public function clear() { + $this->mPropVals = []; + $this->mProperties = []; + $this->mHasVisibleProps = false; + $this->mHasVisibleSpecs = false; + $this->stubObject = false; + $this->clearSubSemanticData(); + $this->hash = null; + $this->options = null; + } + + /** + * Return true if this SemanticData is empty. + * This is the case when the subject has neither property values nor + * data for subobjects. + * + * @since 1.8 + * + * @return boolean + */ + public function isEmpty() { + return $this->getProperties() === [] && $this->getSubSemanticData() === []; + } + + /** + * Add all data from the given SMWSemanticData. + * Only works if the imported SMWSemanticData has the same subject as + * this SMWSemanticData; an exception is thrown otherwise. + * + * @since 1.7 + * + * @param SemanticData $semanticData object to copy from + * + * @throws SemanticDataImportException + */ + public function importDataFrom( SemanticData $semanticData ) { + + if( !$this->mSubject->equals( $semanticData->getSubject() ) ) { + throw new SemanticDataImportException( "SemanticData can only represent data about one subject. Importing data for another subject is not possible." ); + } + + $this->hash = null; + + // Shortcut when copying into empty objects that don't ask for + // more duplicate elimination: + if ( count( $this->mProperties ) == 0 && + ( $semanticData->mNoDuplicates >= $this->mNoDuplicates ) ) { + $this->mProperties = $semanticData->getProperties(); + $this->mPropVals = []; + + foreach ( $this->mProperties as $property ) { + $this->mPropVals[$property->getKey()] = $semanticData->getPropertyValues( $property ); + } + + $this->mHasVisibleProps = $semanticData->hasVisibleProperties(); + $this->mHasVisibleSpecs = $semanticData->hasVisibleSpecialProperties(); + } else { + foreach ( $semanticData->getProperties() as $property ) { + $values = $semanticData->getPropertyValues( $property ); + + foreach ( $values as $dataItem ) { + $this->addPropertyObjectValue( $property, $dataItem); + } + } + } + + foreach( $semanticData->getSubSemanticData() as $semData ) { + $this->addSubSemanticData( $semData ); + } + } + + /** + * Removes data from the given SMWSemanticData. + * If the subject of the data that is to be removed is not equal to the + * subject of this SMWSemanticData, it will just be ignored (nothing to + * remove). Likewise, removing data that is not present does not change + * anything. + * + * @since 1.8 + * + * @param SemanticData $semanticData + */ + public function removeDataFrom( SemanticData $semanticData ) { + if( !$this->mSubject->equals( $semanticData->getSubject() ) ) { + return; + } + + foreach ( $semanticData->getProperties() as $property ) { + $this->removeProperty( $property ); + } + + foreach( $semanticData->getSubSemanticData() as $semData ) { + $this->removeSubSemanticData( $semData ); + } + } + + /** + * @see SubSemanticData::hasSubSemanticData + * @since 1.9 + * + * @param string $subobjectName|null + * + * @return boolean + */ + public function hasSubSemanticData( $subobjectName = null ) { + return $this->subSemanticData->hasSubSemanticData( $subobjectName ); + } + + /** + * @see SubSemanticData::findSubSemanticData + * @since 1.9 + * + * @param string $subobjectName + * + * @return SMWContainerSemanticData|null + */ + public function findSubSemanticData( $subobjectName ) { + return $this->subSemanticData->findSubSemanticData( $subobjectName ); + } + + /** + * @see SubSemanticData::addSubSemanticData + * @since 1.8 + * + * @param SemanticData $semanticData + * @throws SubSemanticDataException + */ + public function addSubSemanticData( SemanticData $semanticData ) { + $this->hash = null; + $this->subSemanticData->addSubSemanticData( $semanticData ); + } + + /** + * @see SubSemanticData::removeSubSemanticData + * @since 1.8 + * + * @param SemanticData $semanticData + */ + public function removeSubSemanticData( SemanticData $semanticData ) { + $this->hash = null; + $this->subSemanticData->removeSubSemanticData( $semanticData ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/Settings.php b/www/wiki/extensions/SemanticMediaWiki/includes/Settings.php new file mode 100644 index 00000000..64d0b3d0 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/Settings.php @@ -0,0 +1,613 @@ +<?php + +namespace SMW; + +use SMW\Exception\SettingNotFoundException; + +/** + * Encapsulate Semantic MediaWiki settings to access values through a + * specified interface + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ +class Settings extends Options { + + /** + * @var Settings + */ + private static $instance = null; + + /** + * @var array + */ + private $iterate = []; + + /** + * Assemble individual SMW related settings into one accessible array for + * easy instantiation since we don't have unique way of accessing only + * SMW related settings ( e.g. $smwgSettings['...']) we need this method + * as short cut to invoke only smwg* related settings + * + * @par Example: + * @code + * $settings = Settings::newFromGlobals(); + * $settings->get( 'smwgDefaultStore' ); + * @endcode + * + * @since 1.9 + * + * @return Settings + */ + public static function newFromGlobals() { + + $configuration = [ + 'smwgIP' => $GLOBALS['smwgIP'], + 'smwgExtraneousLanguageFileDir' => $GLOBALS['smwgExtraneousLanguageFileDir'], + 'smwgServicesFileDir' => $GLOBALS['smwgServicesFileDir'], + 'smwgResourceLoaderDefFiles' => $GLOBALS['smwgResourceLoaderDefFiles'], + 'smwgMaintenanceDir' => $GLOBALS['smwgMaintenanceDir'], + 'smwgConfigFileDir' => $GLOBALS['smwgConfigFileDir'], + 'smwgImportFileDirs' => $GLOBALS['smwgImportFileDirs'], + 'smwgImportReqVersion' => $GLOBALS['smwgImportReqVersion'], + 'smwgSemanticsEnabled' => $GLOBALS['smwgSemanticsEnabled'], + 'smwgUpgradeKey' => $GLOBALS['smwgUpgradeKey'], + 'smwgJobQueueWatchlist' => $GLOBALS['smwgJobQueueWatchlist'], + 'smwgEnabledCompatibilityMode' => $GLOBALS['smwgEnabledCompatibilityMode'], + 'smwgDefaultStore' => $GLOBALS['smwgDefaultStore'], + 'smwgDefaultLoggerRole' => $GLOBALS['smwgDefaultLoggerRole'], + 'smwgLocalConnectionConf' => $GLOBALS['smwgLocalConnectionConf'], + 'smwgSparqlRepositoryConnector' => $GLOBALS['smwgSparqlRepositoryConnector'], + 'smwgSparqlCustomConnector' => $GLOBALS['smwgSparqlCustomConnector'], + 'smwgSparqlEndpoint' => $GLOBALS['smwgSparqlEndpoint'], + 'smwgSparqlDefaultGraph' => $GLOBALS['smwgSparqlDefaultGraph'], + 'smwgSparqlRepositoryConnectorForcedHttpVersion' => $GLOBALS['smwgSparqlRepositoryConnectorForcedHttpVersion'], + 'smwgSparqlReplicationPropertyExemptionList' => $GLOBALS['smwgSparqlReplicationPropertyExemptionList'], + 'smwgSparqlQFeatures' => $GLOBALS['smwgSparqlQFeatures'], + 'smwgNamespaceIndex' => $GLOBALS['smwgNamespaceIndex'], + 'smwgFactboxFeatures' => $GLOBALS['smwgFactboxFeatures'], + 'smwgShowFactbox' => $GLOBALS['smwgShowFactbox'], + 'smwgShowFactboxEdit' => $GLOBALS['smwgShowFactboxEdit'], + 'smwgCompactLinkSupport' => $GLOBALS['smwgCompactLinkSupport'], + 'smwgDefaultNumRecurringEvents' => $GLOBALS['smwgDefaultNumRecurringEvents'], + 'smwgMaxNumRecurringEvents' => $GLOBALS['smwgMaxNumRecurringEvents'], + 'smwgSearchByPropertyFuzzy' => $GLOBALS['smwgSearchByPropertyFuzzy'], + 'smwgPagingLimit' => $GLOBALS['smwgPagingLimit'], + 'smwgPropertyListLimit' => $GLOBALS['smwgPropertyListLimit'], + 'smwgQEnabled' => $GLOBALS['smwgQEnabled'], + 'smwgQMaxLimit' => $GLOBALS['smwgQMaxLimit'], + 'smwgIgnoreQueryErrors' => $GLOBALS['smwgIgnoreQueryErrors'], + 'smwgQSubcategoryDepth' => $GLOBALS['smwgQSubcategoryDepth'], + 'smwgQSubpropertyDepth' => $GLOBALS['smwgQSubpropertyDepth'], + 'smwgQEqualitySupport' => $GLOBALS['smwgQEqualitySupport'], + 'smwgQDefaultNamespaces' => $GLOBALS['smwgQDefaultNamespaces'], + 'smwgQComparators' => $GLOBALS['smwgQComparators'], + 'smwgQFilterDuplicates' => $GLOBALS['smwgQFilterDuplicates'], + 'smwStrictComparators' => $GLOBALS['smwStrictComparators'], + 'smwgQStrictComparators' => $GLOBALS['smwgQStrictComparators'], + 'smwgQMaxSize' => $GLOBALS['smwgQMaxSize'], + 'smwgQMaxDepth' => $GLOBALS['smwgQMaxDepth'], + 'smwgQFeatures' => $GLOBALS['smwgQFeatures'], + 'smwgQDefaultLimit' => $GLOBALS['smwgQDefaultLimit'], + 'smwgQUpperbound' => $GLOBALS['smwgQUpperbound'], + 'smwgQMaxInlineLimit' => $GLOBALS['smwgQMaxInlineLimit'], + 'smwgQPrintoutLimit' => $GLOBALS['smwgQPrintoutLimit'], + 'smwgQDefaultLinking' => $GLOBALS['smwgQDefaultLinking'], + 'smwgQConceptCaching' => $GLOBALS['smwgQConceptCaching'], + 'smwgQConceptMaxSize' => $GLOBALS['smwgQConceptMaxSize'], + 'smwgQConceptMaxDepth' => $GLOBALS['smwgQConceptMaxDepth'], + 'smwgQConceptFeatures' => $GLOBALS['smwgQConceptFeatures'], + 'smwgQConceptCacheLifetime' => $GLOBALS['smwgQConceptCacheLifetime'], + 'smwgQExpensiveThreshold' => $GLOBALS['smwgQExpensiveThreshold'], + 'smwgQExpensiveExecutionLimit' => $GLOBALS['smwgQExpensiveExecutionLimit'], + 'smwgRemoteReqFeatures' => $GLOBALS['smwgRemoteReqFeatures'], + 'smwgQuerySources' => $GLOBALS['smwgQuerySources'], + 'smwgQTemporaryTablesAutoCommitMode' => $GLOBALS['smwgQTemporaryTablesAutoCommitMode'], + 'smwgQSortFeatures' => $GLOBALS['smwgQSortFeatures'], + 'smwgResultFormats' => $GLOBALS['smwgResultFormats'], + 'smwgResultFormatsFeatures' => $GLOBALS['smwgResultFormatsFeatures'], + 'smwgResultAliases' => $GLOBALS['smwgResultAliases'], + 'smwgPDefaultType' => $GLOBALS['smwgPDefaultType'], + 'smwgAllowRecursiveExport' => $GLOBALS['smwgAllowRecursiveExport'], + 'smwgExportBacklinks' => $GLOBALS['smwgExportBacklinks'], + 'smwgExportResourcesAsIri' => $GLOBALS['smwgExportResourcesAsIri'], + 'smwgExportBCNonCanonicalFormUse' => $GLOBALS['smwgExportBCNonCanonicalFormUse'], + 'smwgExportBCAuxiliaryUse' => $GLOBALS['smwgExportBCAuxiliaryUse'], + 'smwgMaxNonExpNumber' => $GLOBALS['smwgMaxNonExpNumber'], + 'smwgEnableUpdateJobs' => $GLOBALS['smwgEnableUpdateJobs'], + 'smwgNamespacesWithSemanticLinks' => $GLOBALS['smwgNamespacesWithSemanticLinks'], + 'smwgPageSpecialProperties' => $GLOBALS['smwgPageSpecialProperties'], + 'smwgChangePropagationWatchlist' => $GLOBALS['smwgChangePropagationWatchlist'], + 'smwgDataTypePropertyExemptionList' => $GLOBALS['smwgDataTypePropertyExemptionList'], + 'smwgDefaultOutputFormatters' => $GLOBALS['smwgDefaultOutputFormatters'], + 'smwgTranslate' => $GLOBALS['smwgTranslate'], + 'smwgAutoRefreshSubject' => $GLOBALS['smwgAutoRefreshSubject'], + 'smwgAdminFeatures' => $GLOBALS['smwgAdminFeatures'], + 'smwgAutoRefreshOnPurge' => $GLOBALS['smwgAutoRefreshOnPurge'], + 'smwgAutoRefreshOnPageMove' => $GLOBALS['smwgAutoRefreshOnPageMove'], + 'smwgContLang' => isset( $GLOBALS['smwgContLang'] ) ? $GLOBALS['smwgContLang'] : '', + 'smwgMaxPropertyValues' => $GLOBALS['smwgMaxPropertyValues'], + 'smwgNamespace' => $GLOBALS['smwgNamespace'], + 'smwgMasterStore' => isset( $GLOBALS['smwgMasterStore'] ) ? $GLOBALS['smwgMasterStore'] : '', + 'smwgIQRunningNumber' => isset( $GLOBALS['smwgIQRunningNumber'] ) ? $GLOBALS['smwgIQRunningNumber'] : 0, + 'smwgCacheUsage' => $GLOBALS['smwgCacheUsage'], + 'smwgMainCacheType' => $GLOBALS['smwgMainCacheType'], + 'smwgEntityLookupCacheType' => $GLOBALS['smwgEntityLookupCacheType'], + 'smwgEntityLookupCacheLifetime' => $GLOBALS['smwgEntityLookupCacheLifetime'], + 'smwgEntityLookupFeatures' => $GLOBALS['smwgEntityLookupFeatures'], + 'smwgFixedProperties' => $GLOBALS['smwgFixedProperties'], + 'smwgPropertyLowUsageThreshold' => $GLOBALS['smwgPropertyLowUsageThreshold'], + 'smwgPropertyZeroCountDisplay' => $GLOBALS['smwgPropertyZeroCountDisplay'], + 'smwgQueryProfiler' => $GLOBALS['smwgQueryProfiler'], + 'smwgEnabledSpecialPage' => $GLOBALS['smwgEnabledSpecialPage'], + 'smwgFallbackSearchType' => $GLOBALS['smwgFallbackSearchType'], + 'smwgEnabledEditPageHelp' => $GLOBALS['smwgEnabledEditPageHelp'], + 'smwgEnabledDeferredUpdate' => $GLOBALS['smwgEnabledDeferredUpdate'], + 'smwgEnabledQueryDependencyLinksStore' => $GLOBALS['smwgEnabledQueryDependencyLinksStore'], + 'smwgQueryDependencyPropertyExemptionList' => $GLOBALS['smwgQueryDependencyPropertyExemptionList'], + 'smwgQueryDependencyAffiliatePropertyDetectionList' => $GLOBALS['smwgQueryDependencyAffiliatePropertyDetectionList'], + 'smwgParserFeatures' => $GLOBALS['smwgParserFeatures'], + 'smwgDVFeatures' => $GLOBALS['smwgDVFeatures'], + 'smwgEnabledFulltextSearch' => $GLOBALS['smwgEnabledFulltextSearch'], + 'smwgFulltextDeferredUpdate' => $GLOBALS['smwgFulltextDeferredUpdate'], + 'smwgFulltextSearchTableOptions' => $GLOBALS['smwgFulltextSearchTableOptions'], + 'smwgFulltextSearchPropertyExemptionList' => $GLOBALS['smwgFulltextSearchPropertyExemptionList'], + 'smwgFulltextSearchMinTokenSize' => $GLOBALS['smwgFulltextSearchMinTokenSize'], + 'smwgFulltextLanguageDetection' => $GLOBALS['smwgFulltextLanguageDetection'], + 'smwgFulltextSearchIndexableDataTypes' => $GLOBALS['smwgFulltextSearchIndexableDataTypes'], + 'smwgQueryResultCacheType' => $GLOBALS['smwgQueryResultCacheType'], + 'smwgQueryResultCacheLifetime' => $GLOBALS['smwgQueryResultCacheLifetime'], + 'smwgQueryResultNonEmbeddedCacheLifetime' => $GLOBALS['smwgQueryResultNonEmbeddedCacheLifetime'], + 'smwgQueryResultCacheRefreshOnPurge' => $GLOBALS['smwgQueryResultCacheRefreshOnPurge'], + 'smwgEditProtectionRight' => $GLOBALS['smwgEditProtectionRight'], + 'smwgCreateProtectionRight' => $GLOBALS['smwgCreateProtectionRight'], + 'smwgSimilarityLookupExemptionProperty' => $GLOBALS['smwgSimilarityLookupExemptionProperty'], + 'smwgPropertyInvalidCharacterList' => $GLOBALS['smwgPropertyInvalidCharacterList'], + 'smwgPropertyReservedNameList' => $GLOBALS['smwgPropertyReservedNameList'], + 'smwgEntityCollation' => $GLOBALS['smwgEntityCollation'], + 'smwgExperimentalFeatures' => $GLOBALS['smwgExperimentalFeatures'], + 'smwgFieldTypeFeatures' => $GLOBALS['smwgFieldTypeFeatures'], + 'smwgChangePropagationProtection' => $GLOBALS['smwgChangePropagationProtection'], + 'smwgUseComparableContentHash' => $GLOBALS['smwgUseComparableContentHash'], + 'smwgBrowseFeatures' => $GLOBALS['smwgBrowseFeatures'], + 'smwgCategoryFeatures' => $GLOBALS['smwgCategoryFeatures'], + 'smwgURITypeSchemeList' => $GLOBALS['smwgURITypeSchemeList'], + 'smwgSchemaTypes' => $GLOBALS['smwgSchemaTypes'], + 'smwgElasticsearchConfig' => $GLOBALS['smwgElasticsearchConfig'], + 'smwgElasticsearchProfile' => $GLOBALS['smwgElasticsearchProfile'], + 'smwgElasticsearchEndpoints' => $GLOBALS['smwgElasticsearchEndpoints'], + 'smwgPostEditUpdate' => $GLOBALS['smwgPostEditUpdate'], + 'smwgSpecialAskFormSubmitMethod' => $GLOBALS['smwgSpecialAskFormSubmitMethod'], + 'smwgSupportSectionTag' => $GLOBALS['smwgSupportSectionTag'], + ]; + + self::initLegacyMapping( $configuration ); + + \Hooks::run( 'SMW::Config::BeforeCompletion', [ &$configuration ] ); + + if ( self::$instance === null ) { + self::$instance = self::newFromArray( $configuration ); + } + + return self::$instance; + } + + /** + * Factory method for immediate instantiation of a settings object for a + * given array + * + * @par Example: + * @code + * $settings = Settings::newFromArray( array( 'Foo' => 'Bar' ) ); + * $settings->get( 'Foo' ); + * @endcode + * + * @since 1.9 + * + * @return Settings + */ + public static function newFromArray( array $settings ) { + return new self( $settings ); + } + + /** + * Returns settings for a given key (nested settings are supported) + * + * @par Example: + * @code + * $settings = Settings::newFromArray( array( + * 'Foo' => 'Bar' + * 'Parent' => array( + * 'Child' => array( 'Lisa', 'Lula', array( 'Lila' ) ) + * ) + * ); + * + * $settings->get( 'Child' ) will return array( 'Lisa', 'Lula', array( 'Lila' ) ) + * @endcode + * + * @since 1.9 + * + * @param string $key + * + * @return mixed + * @throws SettingNotFoundException + */ + public function get( $key ) { + + if ( $this->has( $key ) ) { + return parent::get( $key ); + } + + // If the key wasn't matched it could be because of a nested array + // hence iterate and verify otherwise throw an exception + return $this->doIterate( $key, $this->toArray() ); + } + + /** + * @since 3.0 + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function safeGet( $key, $default = false ) { + + try { + $r = $this->get( $key ); + } catch ( SettingNotFoundException $e ) { + return $default; + } + + return $r; + } + + /** + * @since 1.9 + */ + public static function clear() { + self::$instance = null; + } + + /** + * Iterates over a nested array to find an element + */ + private function doIterate( $key, $options ) { + + if ( isset( $this->iterate[$key] ) ) { + return $this->iterate[$key]; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveArrayIterator( $options ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach( $iterator as $it => $value ) { + if ( $key === $it ) { + return $this->iterate[$key] = $value; + } + } + + throw new SettingNotFoundException( "'{$key}' is not a valid settings key" ); + } + + private static function initLegacyMapping( &$configuration ) { + + if ( isset( $GLOBALS['smwgAdminRefreshStore'] ) && $GLOBALS['smwgAdminRefreshStore'] === false ) { + $configuration['smwgAdminFeatures'] = $configuration['smwgAdminFeatures'] & ~SMW_ADM_REFRESH; + } + + // smwgParserFeatures + if ( isset( $GLOBALS['smwgEnabledInTextAnnotationParserStrictMode'] ) && $GLOBALS['smwgEnabledInTextAnnotationParserStrictMode'] === false ) { + $configuration['smwgParserFeatures'] = $configuration['smwgParserFeatures'] & ~SMW_PARSER_STRICT; + } + + if ( isset( $GLOBALS['smwgInlineErrors'] ) && $GLOBALS['smwgInlineErrors'] === false ) { + $configuration['smwgParserFeatures'] = $configuration['smwgParserFeatures'] & ~SMW_PARSER_INL_ERROR; + } + + if ( isset( $GLOBALS['smwgShowHiddenCategories'] ) && $GLOBALS['smwgShowHiddenCategories'] === false ) { + $configuration['smwgParserFeatures'] = $configuration['smwgParserFeatures'] & ~SMW_PARSER_HID_CATS; + } + + // smwgFactboxFeatures + if ( isset( $GLOBALS['smwgFactboxUseCache'] ) && $GLOBALS['smwgFactboxUseCache'] === false ) { + $configuration['smwgFactboxFeatures'] = $configuration['smwgFactboxFeatures'] & ~SMW_FACTBOX_CACHE; + } + + if ( isset( $GLOBALS['smwgFactboxCacheRefreshOnPurge'] ) && $GLOBALS['smwgFactboxCacheRefreshOnPurge'] === false ) { + $configuration['smwgFactboxFeatures'] = $configuration['smwgFactboxFeatures'] & ~SMW_FACTBOX_PURGE_REFRESH; + } + + // smwgLinksInValues + if ( isset( $GLOBALS['smwgLinksInValues'] ) && $GLOBALS['smwgLinksInValues'] === SMW_LINV_PCRE ) { + $configuration['smwgParserFeatures'] = $configuration['smwgParserFeatures'] | SMW_PARSER_LINV; + } + + if ( isset( $GLOBALS['smwgLinksInValues'] ) && $GLOBALS['smwgLinksInValues'] === SMW_LINV_OBFU ) { + $configuration['smwgParserFeatures'] = $configuration['smwgParserFeatures'] | SMW_PARSER_LINV; + } + + if ( isset( $GLOBALS['smwgLinksInValues'] ) && $GLOBALS['smwgLinksInValues'] === true ) { + $configuration['smwgParserFeatures'] = $configuration['smwgParserFeatures'] | SMW_PARSER_LINV; + } + + // smwgCategoryFeatures + if ( isset( $GLOBALS['smwgUseCategoryRedirect'] ) && $GLOBALS['smwgUseCategoryRedirect'] === false ) { + $configuration['smwgCategoryFeatures'] = $configuration['smwgCategoryFeatures'] & ~SMW_CAT_REDIRECT; + } + + if ( isset( $GLOBALS['smwgCategoriesAsInstances'] ) && $GLOBALS['smwgCategoriesAsInstances'] === false ) { + $configuration['smwgCategoryFeatures'] = $configuration['smwgCategoryFeatures'] & ~SMW_CAT_INSTANCE; + } + + if ( isset( $GLOBALS['smwgUseCategoryHierarchy'] ) && $GLOBALS['smwgUseCategoryHierarchy'] === false ) { + $configuration['smwgCategoryFeatures'] = $configuration['smwgCategoryFeatures'] & ~SMW_CAT_HIERARCHY; + } + + if ( isset( $GLOBALS['smwgQueryDependencyPropertyExemptionlist'] ) ) { + $configuration['smwgQueryDependencyPropertyExemptionList'] = $GLOBALS['smwgQueryDependencyPropertyExemptionlist']; + } + + if ( isset( $GLOBALS['smwgQueryDependencyAffiliatePropertyDetectionlist'] ) ) { + $configuration['smwgQueryDependencyAffiliatePropertyDetectionList'] = $GLOBALS['smwgQueryDependencyAffiliatePropertyDetectionlist']; + } + + // smwgPropertyListLimit + if ( isset( $GLOBALS['smwgSubPropertyListLimit'] ) ) { + $configuration['smwgPropertyListLimit']['subproperty'] = $GLOBALS['smwgSubPropertyListLimit']; + } + + if ( isset( $GLOBALS['smwgRedirectPropertyListLimit'] ) ) { + $configuration['smwgPropertyListLimit']['redirect'] = $GLOBALS['smwgRedirectPropertyListLimit']; + } + + // smwgCacheUsage + if ( isset( $GLOBALS['smwgCacheUsage']['smwgStatisticsCacheExpiry'] ) ) { + $configuration['smwgCacheUsage']['special.statistics'] = $GLOBALS['smwgCacheUsage']['smwgStatisticsCacheExpiry']; + } + + if ( isset( $GLOBALS['smwgCacheUsage']['smwgStatisticsCache'] ) && $GLOBALS['smwgCacheUsage']['smwgStatisticsCache'] === false ) { + $configuration['smwgCacheUsage']['special.statistics'] = false; + } + + if ( isset( $GLOBALS['smwgCacheUsage']['smwgPropertiesCacheExpiry'] ) ) { + $configuration['smwgCacheUsage']['special.properties'] = $GLOBALS['smwgCacheUsage']['smwgPropertiesCacheExpiry']; + } + + if ( isset( $GLOBALS['smwgCacheUsage']['smwgPropertiesCache'] ) && $GLOBALS['smwgCacheUsage']['smwgPropertiesCache'] === false ) { + $configuration['smwgCacheUsage']['special.properties'] = false; + } + + if ( isset( $GLOBALS['smwgCacheUsage']['smwgUnusedPropertiesCacheExpiry'] ) ) { + $configuration['smwgCacheUsage']['special.unusedproperties'] = $GLOBALS['smwgCacheUsage']['smwgUnusedPropertiesCacheExpiry']; + } + + if ( isset( $GLOBALS['smwgCacheUsage']['smwgUnusedPropertiesCache'] ) && $GLOBALS['smwgCacheUsage']['smwgUnusedPropertiesCache'] === false ) { + $configuration['smwgCacheUsage']['special.unusedproperties'] = false; + } + + if ( isset( $GLOBALS['smwgCacheUsage']['smwgWantedPropertiesCacheExpiry'] ) ) { + $configuration['smwgCacheUsage']['special.wantedproperties'] = $GLOBALS['smwgCacheUsage']['smwgWantedPropertiesCacheExpiry']; + } + + if ( isset( $GLOBALS['smwgCacheUsage']['smwgWantedPropertiesCache'] ) && $GLOBALS['smwgCacheUsage']['smwgWantedPropertiesCache'] === false ) { + $configuration['smwgCacheUsage']['special.wantedproperties'] = false; + } + + // smwgQueryProfiler + if ( isset( $GLOBALS['smwgQueryProfiler']['smwgQueryDurationEnabled'] ) && $GLOBALS['smwgQueryProfiler']['smwgQueryDurationEnabled'] === true ) { + $configuration['smwgQueryProfiler'] = $configuration['smwgQueryProfiler'] | SMW_QPRFL_DUR; + } + + if ( isset( $GLOBALS['smwgQueryProfiler']['smwgQueryParametersEnabled'] ) && $GLOBALS['smwgQueryProfiler']['smwgQueryParametersEnabled'] === true ) { + $configuration['smwgQueryProfiler'] = $configuration['smwgQueryProfiler'] | SMW_QPRFL_PARAMS; + } + + if ( isset( $GLOBALS['smwgSparqlDatabaseConnector'] ) ) { + $configuration['smwgSparqlRepositoryConnector'] = $GLOBALS['smwgSparqlDatabaseConnector']; + } + + if ( isset( $GLOBALS['smwgSparqlDatabase'] ) ) { + $configuration['smwgSparqlCustomConnector'] = $GLOBALS['smwgSparqlDatabase']; + } + + if ( isset( $GLOBALS['smwgDeclarationProperties'] ) ) { + $configuration['smwgChangePropagationWatchlist'] = $GLOBALS['smwgDeclarationProperties']; + } + + // smwgBrowseFeatures + if ( isset( $GLOBALS['smwgToolboxBrowseLink'] ) && $GLOBALS['smwgToolboxBrowseLink'] === false ) { + $configuration['smwgBrowseFeatures'] = $configuration['smwgBrowseFeatures'] & ~SMW_BROWSE_TLINK; + } + + if ( isset( $GLOBALS['smwgBrowseShowInverse'] ) && $GLOBALS['smwgBrowseShowInverse'] === true ) { + $configuration['smwgBrowseFeatures'] = $configuration['smwgBrowseFeatures'] | SMW_BROWSE_SHOW_INVERSE; + } + + if ( isset( $GLOBALS['smwgBrowseShowAll'] ) && $GLOBALS['smwgBrowseShowAll'] === false ) { + $configuration['smwgBrowseFeatures'] = $configuration['smwgBrowseFeatures'] & ~SMW_BROWSE_SHOW_INCOMING; + } + + if ( isset( $GLOBALS['smwgBrowseByApi'] ) && $GLOBALS['smwgBrowseByApi'] === false ) { + $configuration['smwgBrowseFeatures'] = $configuration['smwgBrowseFeatures'] & ~SMW_BROWSE_USE_API; + } + + // smwgQSortFeatures + if ( isset( $GLOBALS['smwgQSortingSupport'] ) && $GLOBALS['smwgQSortingSupport'] === false ) { + $configuration['smwgQSortFeatures'] = $configuration['smwgQSortFeatures'] & ~SMW_QSORT; + } + + if ( isset( $GLOBALS['smwgQRandSortingSupport'] ) && $GLOBALS['smwgQRandSortingSupport'] === false ) { + $configuration['smwgQSortFeatures'] = $configuration['smwgQSortFeatures'] & ~SMW_QSORT_RANDOM; + } + + if ( isset( $GLOBALS['smwgImportFileDir'] ) ) { + $configuration['smwgImportFileDirs'] = (array)$GLOBALS['smwgImportFileDir']; + } + + // smwgValueLookupFeatures + if ( isset( $GLOBALS['smwgValueLookupCacheType'] ) ) { + $configuration['smwgEntityLookupCacheType'] = $GLOBALS['smwgValueLookupCacheType']; + } + + if ( isset( $GLOBALS['smwgValueLookupCacheLifetime'] ) ) { + $configuration['smwgEntityLookupCacheLifetime'] = $GLOBALS['smwgValueLookupCacheLifetime']; + } + + if ( isset( $GLOBALS['smwgValueLookupFeatures'] ) ) { + $configuration['smwgEntityLookupFeatures'] = $GLOBALS['smwgValueLookupFeatures']; + } + + // smwgPagingLimit + if ( isset( $GLOBALS['smwgTypePagingLimit'] ) ) { + $configuration['smwgPagingLimit']['type'] = $GLOBALS['smwgTypePagingLimit']; + } + + if ( isset( $GLOBALS['smwgConceptPagingLimit'] ) ) { + $configuration['smwgPagingLimit']['concept'] = $GLOBALS['smwgConceptPagingLimit']; + } + + if ( isset( $GLOBALS['smwgPropertyPagingLimit'] ) ) { + $configuration['smwgPagingLimit']['property'] = $GLOBALS['smwgPropertyPagingLimit']; + } + + // smwgSparqlEndpoint + if ( isset( $GLOBALS['smwgSparqlQueryEndpoint'] ) ) { + $configuration['smwgSparqlEndpoint']['query'] = $GLOBALS['smwgSparqlQueryEndpoint']; + } + + if ( isset( $GLOBALS['smwgSparqlUpdateEndpoint'] ) ) { + $configuration['smwgSparqlEndpoint']['update'] = $GLOBALS['smwgSparqlUpdateEndpoint']; + } + + if ( isset( $GLOBALS['smwgSparqlDataEndpoint'] ) ) { + $configuration['smwgSparqlEndpoint']['data'] = $GLOBALS['smwgSparqlDataEndpoint']; + } + + if ( isset( $GLOBALS['smwgCacheType'] ) ) { + $configuration['smwgMainCacheType'] = $GLOBALS['smwgCacheType']; + } + + $jobQueueWatchlist = []; + + // FIXME Remove with 3.1 + foreach ( $GLOBALS['smwgJobQueueWatchlist'] as $job ) { + if ( strpos( $job, 'SMW\\' ) !== false ) { + $jobQueueWatchlist[$job] = \SMW\MediaWiki\JobQueue::mapLegacyType( $job ); + } + } + + // Deprecated mapping used in DeprecationNoticeTaskHandler to detect and + // output notices + $GLOBALS['smwgDeprecationNotices']['smw'] = [ + 'notice' => [ + 'smwgAdminRefreshStore' => '3.1.0', + 'smwgQueryDependencyPropertyExemptionlist' => '3.1.0', + 'smwgQueryDependencyAffiliatePropertyDetectionlist' => '3.1.0', + 'smwgSubPropertyListLimit' => '3.1.0', + 'smwgRedirectPropertyListLimit' => '3.1.0', + 'smwgSparqlDatabaseConnector' => '3.1.0', + 'smwgSparqlDatabase' => '3.1.0', + 'smwgDeclarationProperties' => '3.1.0', + 'smwgToolboxBrowseLink' => '3.1.0', + 'smwgBrowseShowInverse' => '3.1.0', + 'smwgBrowseShowAll' => '3.1.0', + 'smwgBrowseByApi' => '3.1.0', + 'smwgEnabledInTextAnnotationParserStrictMode' => '3.1.0', + 'smwgInlineErrors' => '3.1.0', + 'smwgShowHiddenCategories' => '3.1.0', + 'smwgUseCategoryRedirect' => '3.1.0', + 'smwgCategoriesAsInstances' => '3.1.0', + 'smwgUseCategoryHierarchy' => '3.1.0', + 'smwgQSortingSupport' => '3.1.0', + 'smwgQRandSortingSupport' => '3.1.0', + 'smwgLinksInValues' => '3.1.0', + 'smwgTypePagingLimit' => '3.1.0', + 'smwgConceptPagingLimit' => '3.1.0', + 'smwgPropertyPagingLimit' => '3.1.0', + 'smwgSparqlQueryEndpoint' => '3.1.0', + 'smwgSparqlUpdateEndpoint' => '3.1.0', + 'smwgSparqlDataEndpoint' => '3.1.0', + 'smwgCacheType' => '3.1.0', + 'smwgFactboxUseCache' => '3.1.0', + 'smwgFactboxCacheRefreshOnPurge' => '3.1.0', + 'options' => [ + 'smwgCacheUsage' => [ + 'smwgStatisticsCache' => '3.1.0', + 'smwgStatisticsCacheExpiry' => '3.1.0', + 'smwgPropertiesCache' => '3.1.0', + 'smwgPropertiesCacheExpiry' => '3.1.0', + 'smwgUnusedPropertiesCache' => '3.1.0', + 'smwgUnusedPropertiesCacheExpiry' => '3.1.0', + 'smwgWantedPropertiesCache' => '3.1.0', + 'smwgWantedPropertiesCacheExpiry' => '3.1.0', + ], + 'smwgQueryProfiler' => [ + 'smwgQueryDurationEnabled' => '3.1.0', + 'smwgQueryParametersEnabled' => '3.1.0' + ] + ] + ], + 'replacement' => [ + 'smwgAdminRefreshStore' => 'smwgAdminFeatures', + 'smwgQueryDependencyPropertyExemptionlist' => 'smwgQueryDependencyPropertyExemptionList', + 'smwgQueryDependencyAffiliatePropertyDetectionlist' => 'smwgQueryDependencyAffiliatePropertyDetectionList', + 'smwgSubPropertyListLimit' => 'smwgPropertyListLimit', + 'smwgRedirectPropertyListLimit' => 'smwgPropertyListLimit', + 'smwgSparqlDatabaseConnector' => 'smwgSparqlRepositoryConnector', + 'smwgSparqlDatabase' => 'smwgSparqlCustomConnector', + 'smwgDeclarationProperties' => 'smwgChangePropagationWatchlist', + 'smwgToolboxBrowseLink' => 'smwgBrowseFeatures', + 'smwgBrowseShowInverse' => 'smwgBrowseFeatures', + 'smwgBrowseShowAll' => 'smwgBrowseFeatures', + 'smwgBrowseByApi' => 'smwgBrowseFeatures', + 'smwgEnabledInTextAnnotationParserStrictMode' => 'smwgParserFeatures', + 'smwgInlineErrors' => 'smwgParserFeatures', + 'smwgShowHiddenCategories' => 'smwgParserFeatures', + 'smwgLinksInValues' => 'smwgParserFeatures', + 'smwgUseCategoryRedirect' => 'smwgCategoryFeatures', + 'smwgCategoriesAsInstances' => 'smwgCategoryFeatures', + 'smwgUseCategoryHierarchy' => 'smwgCategoryFeatures', + 'smwgQSortingSupport' => 'smwgQSortFeatures', + 'smwgQRandSortingSupport' => 'smwgQSortFeatures', + 'smwgImportFileDir' => 'smwgImportFileDirs', + 'smwgValueLookupCacheType' => 'smwgEntityLookupCacheType', + 'smwgValueLookupCacheLifetime' => 'smwgEntityLookupCacheLifetime', + 'smwgValueLookupFeatures' => 'smwgEntityLookupFeatures', + 'smwgTypePagingLimit' => 'smwgPagingLimit', + 'smwgConceptPagingLimit' => 'smwgPagingLimit', + 'smwgPropertyPagingLimit' => 'smwgPagingLimit', + 'smwgSparqlQueryEndpoint' => 'smwgSparqlEndpoint', + 'smwgSparqlUpdateEndpoint' => 'smwgSparqlEndpoint', + 'smwgSparqlDataEndpoint' => 'smwgSparqlEndpoint', + 'smwgCacheType' => 'smwgMainCacheType', + 'smwgFactboxUseCache' => 'smwgFactboxFeatures', + 'smwgFactboxCacheRefreshOnPurge' => 'smwgFactboxFeatures', + 'options' => [ + 'smwgCacheUsage' => [ + 'smwgStatisticsCacheExpiry' => 'special.statistics', + 'smwgPropertiesCacheExpiry' => 'special.properties', + 'smwgUnusedPropertiesCacheExpiry' => 'special.unusedproperties', + 'smwgWantedPropertiesCacheExpiry' => 'special.wantedproperties', + ], + 'smwgQueryProfiler' => [ + 'smwgQueryDurationEnabled' => 'SMW_QPRFL_DUR', + 'smwgQueryParametersEnabled' => 'SMW_QPRFL_PARAMS' + ] + ] + ( $jobQueueWatchlist !== [] ? [ 'smwgJobQueueWatchlist' => $jobQueueWatchlist ] : [] ) + ], + 'removal' => [ + 'smwgOnDeleteAction' => '2.4.0', + 'smwgAutocompleteInSpecialAsk' => '3.0.0', + 'smwgSparqlDatabaseMaster' => '3.0.0', + 'smwgHistoricTypeNamespace' => '3.0.0', + 'smwgEnabledHttpDeferredJobRequest' => '3.0.0' + ] + ]; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/Setup.php b/www/wiki/extensions/SemanticMediaWiki/includes/Setup.php new file mode 100644 index 00000000..33d215a4 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/Setup.php @@ -0,0 +1,441 @@ +<?php + +namespace SMW; + +use Hooks; +use SMW\Connection\ConnectionManager; +use SMW\MediaWiki\Hooks\HookRegistry; +use SMW\SQLStore\Installer; + +/** + * Extension setup and registration + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ +final class Setup { + + /** + * @var ApplicationFactory + */ + private $applicationFactory; + + /** + * @since 1.9 + * + * @param ApplicationFactory $applicationFactory + */ + public function __construct( ApplicationFactory $applicationFactory ) { + $this->applicationFactory = $applicationFactory; + } + + /** + * Runs at the earliest possible event to initialize functions or hooks that + * are otherwise too late for the hook system to be recognized. + * + * @since 3.0 + */ + public static function initExtension( &$vars ) { + + /** + * @see https://www.mediawiki.org/wiki/Localisation#Localising_namespaces_and_special_page_aliases + */ + $vars['wgMessagesDirs']['SemanticMediaWiki'] = $vars['smwgIP'] . 'i18n'; + $vars['wgExtensionMessagesFiles']['SemanticMediaWikiAlias'] = $vars['smwgIP'] . 'i18n/extra/SemanticMediaWiki.alias.php'; + $vars['wgExtensionMessagesFiles']['SemanticMediaWikiMagic'] = $vars['smwgIP'] . 'i18n/extra/SemanticMediaWiki.magic.php'; + + HookRegistry::initExtension( $vars ); + } + + /** + * @see HookRegistry::initExtension + */ + public static function getAPIModules() { + + if ( !ApplicationFactory::getInstance()->getSettings()->get( 'smwgSemanticsEnabled' ) ) { + return []; + } + + return [ + 'smwinfo' => '\SMW\MediaWiki\Api\Info', + 'smwtask' => '\SMW\MediaWiki\Api\Task', + 'smwbrowse' => '\SMW\MediaWiki\Api\Browse', + 'ask' => '\SMW\MediaWiki\Api\Ask', + 'askargs' => '\SMW\MediaWiki\Api\AskArgs', + 'browsebysubject' => '\SMW\MediaWiki\Api\BrowseBySubject', + 'browsebyproperty' => '\SMW\MediaWiki\Api\BrowseByProperty' + ]; + } + + /** + * @see HookRegistry::initExtension + */ + public static function initSpecialPageList( array &$specialPages ) { + + if ( !ApplicationFactory::getInstance()->getSettings()->get( 'smwgSemanticsEnabled' ) ) { + return; + } + + $specials = [ + 'Ask' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialAsk' + ], + 'Browse' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialBrowse' + ], + 'PageProperty' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialPageProperty' + ], + 'SearchByProperty' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialSearchByProperty' + ], + 'ProcessingErrorList' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialProcessingErrorList' + ], + 'PropertyLabelSimilarity' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialPropertyLabelSimilarity' + ], + 'SMWAdmin' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialAdmin' + ], + 'Concepts' => [ + 'page' => 'SMW\SpecialConcepts' + ], + 'ExportRDF' => [ + 'page' => 'SMWSpecialOWLExport' + ], + 'Types' => [ + 'page' => 'SMWSpecialTypes' + ], + 'URIResolver' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialURIResolver' + ], + 'Properties' => [ + 'page' => 'SMW\SpecialProperties' + ], + 'UnusedProperties' => [ + 'page' => 'SMW\SpecialUnusedProperties' + ], + 'WantedProperties' => [ + 'page' => 'SMW\SpecialWantedProperties' + ], + 'DeferredRequestDispatcher' => [ + 'page' => 'SMW\MediaWiki\Specials\SpecialDeferredRequestDispatcher' + ], + ]; + + // Register data + foreach ( $specials as $special => $page ) { + $specialPages[$special] = $page['page']; + } + } + + /** + * @since 3.0 + */ + public static function isEnabled() { + return defined( 'SMW_VERSION' ) && $GLOBALS['smwgSemanticsEnabled']; + } + + /** + * @since 3.0 + */ + public static function isValid( $isCli = false ) { + return Installer::isGoodSchema( $isCli ); + } + + /** + * @since 3.0 + * + * @param array &$vars + */ + public function loadSchema( &$vars ) { + Installer::loadSchema( $vars ); + } + + /** + * @since 1.9 + * + * @param array &$vars + * @param string $directory + */ + public function init( &$vars, $directory ) { + + $this->initMessageCallbackHandler(); + + if ( $this->isValid() === false ) { + smwfAbort( + Message::get( [ 'smw-upgrade-error', $vars['smwgUpgradeKey'] ], Message::PARSE ) . + '<h3>' . Message::get( 'smw-upgrade-error-why-title' ) . '</h3>' . + Message::get( 'smw-upgrade-error-why-explain', Message::PARSE ) . + '<h3>' . Message::get( 'smw-upgrade-error-how-title' ) . '</h3>' . + Message::get( 'smw-upgrade-error-how-explain', Message::PARSE ) + ); + } + + $this->addDefaultConfigurations( $vars ); + + if ( CompatibilityMode::extensionNotEnabled() ) { + CompatibilityMode::disableSemantics(); + } + + $this->initConnectionProviders( ); + + $this->registerJobClasses( $vars ); + $this->registerPermissions( $vars ); + + $this->registerParamDefinitions( $vars ); + $this->registerFooterIcon( $vars, $directory ); + $this->registerHooks( $vars, $directory ); + + Hooks::run( 'SMW::Setup::AfterInitializationComplete', [ &$vars ] ); + } + + private function addDefaultConfigurations( &$vars ) { + + $vars['wgLogTypes'][] = 'smw'; + $vars['wgFilterLogTypes']['smw'] = true; + + $vars['smwgMasterStore'] = null; + $vars['smwgIQRunningNumber'] = 0; + + if ( !isset( $vars['smwgNamespace'] ) ) { + $vars['smwgNamespace'] = parse_url( $vars['wgServer'], PHP_URL_HOST ); + } + + foreach ( $vars['smwgResourceLoaderDefFiles'] as $key => $file ) { + if ( is_readable( $file ) ) { + $vars['wgResourceModules'] = array_merge( $vars['wgResourceModules'], include ( $file ) ); + } + } + } + + private function initConnectionProviders() { + + $mwCollaboratorFactory = $this->applicationFactory->newMwCollaboratorFactory(); + $connectionManager = $this->applicationFactory->getConnectionManager(); + + $connectionManager->registerConnectionProvider( + DB_MASTER, + $mwCollaboratorFactory->newLoadBalancerConnectionProvider( DB_MASTER ) + ); + + $connectionManager->registerConnectionProvider( + DB_SLAVE, + $mwCollaboratorFactory->newLoadBalancerConnectionProvider( DB_SLAVE ) + ); + + $connectionManager->registerConnectionProvider( + 'mw.db', + $mwCollaboratorFactory->newConnectionProvider( 'mw.db' ) + ); + + // Connection can be used to redirect queries to another DB cluster + $connectionManager->registerConnectionProvider( + 'mw.db.queryengine', + $mwCollaboratorFactory->newConnectionProvider( 'mw.db.queryengine' ) + ); + + $connectionManager->registerConnectionProvider( + 'elastic', + $this->applicationFactory->singleton( 'ElasticFactory' )->newConnectionProvider() + ); + } + + private function initMessageCallbackHandler() { + + Message::registerCallbackHandler( Message::TEXT, function( $arguments, $language ) { + + if ( $language === Message::CONTENT_LANGUAGE ) { + $language = Localizer::getInstance()->getContentLanguage(); + } + + if ( $language === Message::USER_LANGUAGE ) { + $language = Localizer::getInstance()->getUserLanguage(); + } + + return call_user_func_array( 'wfMessage', $arguments )->inLanguage( $language )->text(); + } ); + + Message::registerCallbackHandler( Message::ESCAPED, function( $arguments, $language ) { + + if ( $language === Message::CONTENT_LANGUAGE ) { + $language = Localizer::getInstance()->getContentLanguage(); + } + + if ( $language === Message::USER_LANGUAGE ) { + $language = Localizer::getInstance()->getUserLanguage(); + } + + return call_user_func_array( 'wfMessage', $arguments )->inLanguage( $language )->escaped(); + } ); + + Message::registerCallbackHandler( Message::PARSE, function( $arguments, $language ) { + + if ( $language === Message::CONTENT_LANGUAGE ) { + $language = Localizer::getInstance()->getContentLanguage(); + } + + if ( $language === Message::USER_LANGUAGE ) { + $language = Localizer::getInstance()->getUserLanguage(); + } + + $message = call_user_func_array( 'wfMessage', $arguments )->inLanguage( $language ); + + // 1.27+ + // [GlobalTitleFail] MessageCache::parse called by ... + // Message::parseText/MessageCache::parse with no title set. + // + // Message::setInterfaceMessageFlag "... used to restore the flag + // after setting a language" + return $message->setInterfaceMessageFlag( true )->title( $GLOBALS['wgTitle'] )->parse(); + } ); + } + + /** + * @see https://www.mediawiki.org/wiki/Manual:$wgJobClasses + */ + private function registerJobClasses( &$vars ) { + + $jobClasses = [ + + 'smw.update' => 'SMW\MediaWiki\Jobs\UpdateJob', + 'smw.refresh' => 'SMW\MediaWiki\Jobs\RefreshJob', + 'smw.updateDispatcher' => 'SMW\MediaWiki\Jobs\UpdateDispatcherJob', + 'smw.parserCachePurge' => 'SMW\MediaWiki\Jobs\ParserCachePurgeJob', + 'smw.fulltextSearchTableUpdate' => 'SMW\MediaWiki\Jobs\FulltextSearchTableUpdateJob', + 'smw.entityIdDisposer' => 'SMW\MediaWiki\Jobs\EntityIdDisposerJob', + 'smw.propertyStatisticsRebuild' => 'SMW\MediaWiki\Jobs\PropertyStatisticsRebuildJob', + 'smw.fulltextSearchTableRebuild' => 'SMW\MediaWiki\Jobs\FulltextSearchTableRebuildJob', + 'smw.changePropagationDispatch' => 'SMW\MediaWiki\Jobs\ChangePropagationDispatchJob', + 'smw.changePropagationUpdate' => 'SMW\MediaWiki\Jobs\ChangePropagationUpdateJob', + 'smw.changePropagationClassUpdate' => 'SMW\MediaWiki\Jobs\ChangePropagationClassUpdateJob', + 'smw.elasticIndexerRecovery' => 'SMW\Elastic\Indexer\IndexerRecoveryJob', + 'smw.elasticFileIngest' => 'SMW\Elastic\Indexer\FileIngestJob', + + // Legacy 3.0- + 'SMW\UpdateJob' => 'SMW\MediaWiki\Jobs\UpdateJob', + 'SMW\RefreshJob' => 'SMW\MediaWiki\Jobs\RefreshJob', + 'SMW\UpdateDispatcherJob' => 'SMW\MediaWiki\Jobs\UpdateDispatcherJob', + 'SMW\ParserCachePurgeJob' => 'SMW\MediaWiki\Jobs\ParserCachePurgeJob', + 'SMW\FulltextSearchTableUpdateJob' => 'SMW\MediaWiki\Jobs\FulltextSearchTableUpdateJob', + 'SMW\EntityIdDisposerJob' => 'SMW\MediaWiki\Jobs\EntityIdDisposerJob', + 'SMW\PropertyStatisticsRebuildJob' => 'SMW\MediaWiki\Jobs\PropertyStatisticsRebuildJob', + 'SMW\FulltextSearchTableRebuildJob' => 'SMW\MediaWiki\Jobs\FulltextSearchTableRebuildJob', + 'SMW\ChangePropagationDispatchJob' => 'SMW\MediaWiki\Jobs\ChangePropagationDispatchJob', + 'SMW\ChangePropagationUpdateJob' => 'SMW\MediaWiki\Jobs\ChangePropagationUpdateJob', + 'SMW\ChangePropagationClassUpdateJob' => 'SMW\MediaWiki\Jobs\ChangePropagationClassUpdateJob', + + // Legacy 2.0- + 'SMWUpdateJob' => 'SMW\MediaWiki\Jobs\UpdateJob', + 'SMWRefreshJob' => 'SMW\MediaWiki\Jobs\RefreshJob' + ]; + + foreach ( $jobClasses as $job => $class ) { + $vars['wgJobClasses'][$job] = $class; + } + } + + /** + * @see https://www.mediawiki.org/wiki/Manual:$wgAvailableRights + * @see https://www.mediawiki.org/wiki/Manual:$wgGroupPermissions + */ + private function registerPermissions( &$vars ) { + + if ( !$this->applicationFactory->getSettings()->get( 'smwgSemanticsEnabled' ) ) { + return; + } + + $rights = [ + 'smw-admin' => [ + 'sysop', + 'smwadministrator' + ], + 'smw-patternedit' => [ + 'smwcurator' + ], + 'smw-schemaedit' => [ + 'smwcurator' + ], + 'smw-pageedit' => [ + 'smwcurator' + ], + // 'smw-watchlist' => [ + // 'smwcurator' + // ], + ]; + + foreach ( $rights as $right => $roles ) { + + // Rights + $vars['wgAvailableRights'][] = $right; + + // User group rights + foreach ( $roles as $role ) { + if ( !isset( $vars['wgGroupPermissions'][$role][$right] ) ) { + $vars['wgGroupPermissions'][$role][$right] = true; + } + } + } + + // Add an additional protection level restricting edit/move/etc + if ( ( $editProtectionRight = $this->applicationFactory->getSettings()->get( 'smwgEditProtectionRight' ) ) !== false ) { + $vars['wgRestrictionLevels'][] = $editProtectionRight; + } + } + + private function registerParamDefinitions( &$vars ) { + $vars['wgParamDefinitions']['smwformat'] = [ + 'definition'=> 'SMWParamFormat', + ]; + } + + /** + * @see https://www.mediawiki.org/wiki/Manual:$wgFooterIcons + */ + private function registerFooterIcon( &$vars, $path ) { + + if ( !$this->applicationFactory->getSettings()->get( 'smwgSemanticsEnabled' ) ) { + return; + } + + if( isset( $vars['wgFooterIcons']['poweredby']['semanticmediawiki'] ) ) { + return; + } + + $src = ''; + + if ( is_file( $path . '/res/DataURI.php' ) && ( $dataURI = include $path . '/res/DataURI.php' ) !== [] ) { + $src = $dataURI['footer']; + } + + $vars['wgFooterIcons']['poweredby']['semanticmediawiki'] = [ + 'src' => $src, + 'url' => 'https://www.semantic-mediawiki.org/wiki/Semantic_MediaWiki', + 'alt' => 'Powered by Semantic MediaWiki' + ]; + } + + /** + * @see https://www.mediawiki.org/wiki/Manual:$wgHooks + * + * @note $wgHooks contains a list of hooks which specifies for every event an + * array of functions to be called. + */ + private function registerHooks( &$vars, $directory ) { + + $hookRegistry = new HookRegistry( $vars, $directory ); + $hookRegistry->register(); + + if ( !$this->applicationFactory->getSettings()->get( 'smwgSemanticsEnabled' ) ) { + return; + } + + // Old-style registration + $vars['wgHooks']['AdminLinks'][] = 'SMWExternalHooks::addToAdminLinks'; + $vars['wgHooks']['PageSchemasRegisterHandlers'][] = 'SMWExternalHooks::onPageSchemasRegistration'; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/Subobject.php b/www/wiki/extensions/SemanticMediaWiki/includes/Subobject.php new file mode 100644 index 00000000..f1d129a6 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/Subobject.php @@ -0,0 +1,188 @@ +<?php + +namespace SMW; + +use InvalidArgumentException; +use SMW\Exception\SubSemanticDataException; +use SMWContainerSemanticData; +use SMWDataValue; +use SMWDIContainer; +use Title; + +/** + * @see http://www.semantic-mediawiki.org/wiki/Help:Subobject + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ +class Subobject { + + /** + * @var Title + */ + protected $title; + + /** + * @var SMWContainerSemanticData + */ + protected $semanticData; + + /** + * @var array + */ + protected $errors = []; + + /** + * @since 1.9 + * + * @param Title $title + */ + public function __construct( Title $title ) { + $this->title = $title; + } + + /** + * Returns the Title object + * + * @since 1.9 + * + * @return Title + */ + public function getTitle() { + return $this->title; + } + + /** + * @since 2.1 + * + * @return DIWikiPage + */ + public function getSubject() { + return $this->getSemanticData()->getSubject(); + } + + /** + * @since 2.1 + * + * @return string + */ + public function getSubobjectId() { + return $this->getSemanticData()->getSubject()->getSubobjectName(); + } + + /** + * @since 1.9 + * + * @return array + */ + public function getErrors() { + return $this->errors; + } + + /** + * @since 1.9 + * + * @param array|string $error + */ + public function addError( $error ) { + + if ( is_string( $error ) ) { + $error = [ md5( $error ) => $error ]; + } + + // Preserve the keys, avoid using array_merge to avert a possible + // Fatal error: Allowed memory size of ... bytes exhausted ... Subobject.php on line 89 + $this->errors += $error; + } + + /** + * @since 2.0 + * + * @param string $identifier + * + * @return self + * @throws InvalidArgumentException + */ + public function setEmptyContainerForId( $identifier ) { + + if ( $identifier === '' ) { + throw new InvalidArgumentException( 'Expected a valid (non-empty) indentifier' ); + } + + $subWikiPage = new DIWikiPage( + $this->title->getDBkey(), + $this->title->getNamespace(), + $this->title->getInterwiki(), + $identifier + ); + + $this->semanticData = new SMWContainerSemanticData( $subWikiPage ); + + return $this; + } + + /** + * @deprecated since 2.0 + */ + public function setSemanticData( $identifier ) { + $this->setEmptyContainerForId( $identifier ); + } + + /** + * Returns semantic data container for a subobject + * + * @since 1.9 + * + * @return SMWContainerSemanticData + */ + public function getSemanticData() { + + if ( !( $this->semanticData instanceof SMWContainerSemanticData ) ) { + throw new SubSemanticDataException( 'The semantic data container is not initialized' ); + } + + return $this->semanticData; + } + + /** + * Returns the property data item for the subobject + * + * @since 1.9 + * + * @return DIProperty + */ + public function getProperty() { + return new DIProperty( DIProperty::TYPE_SUBOBJECT ); + } + + /** + * Returns the container data item for the subobject + * + * @since 1.9 + * + * @return SMWDIContainer + */ + public function getContainer() { + return new SMWDIContainer( $this->getSemanticData() ); + } + + /** + * @since 1.9 + * + * @param DataValue $dataValue + * + * @throws SubSemanticDataException + */ + public function addDataValue( SMWDataValue $dataValue ) { + + if ( !( $this->semanticData instanceof SMWContainerSemanticData ) ) { + throw new SubSemanticDataException( 'The semantic data container is not initialized' ); + } + + $this->semanticData->addDataValue( $dataValue ); + $this->addError( $this->semanticData->getErrors() ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/DIConcept.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/DIConcept.php new file mode 100644 index 00000000..ea6cb70c --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/DIConcept.php @@ -0,0 +1,200 @@ +<?php + +namespace SMW; + +use SMWDataItem; + +/** + * This class implements Concept data items. + * + * @note These special data items for storing concept declaration data in SMW + * should vanish at some point since Container values could encode this data + * just as well. + * + * @since 1.6 + * + * @ingroup SMWDataItems + * + * @author Markus Krötzsch + * @author mwjames + */ +class DIConcept extends \SMWDataItem { + + /** + * Query string for this concept. Possibly long. + * @var string + */ + protected $m_concept; + /** + * Documentation for this concept. Possibly long. + * @var string + */ + protected $m_docu; + /** + * Flags of query features. + * @var integer + */ + protected $m_features; + /** + * Size of the query. + * @var integer + */ + protected $m_size; + /** + * Depth of the query. + * @var integer + */ + protected $m_depth; + + /** + * Status + * @var integer + */ + protected $cacheStatus; + + /** + * Date + * @var integer + */ + protected $cacheDate; + + /** + * Count + * @var integer + */ + protected $cacheCount; + + /** + * @param string $concept the concept query string + * @param string $docu user documentation + * @param integer $queryefeatures flags about query features + * @param integer $size concept query size + * @param integer $depth concept query depth + */ + public function __construct( $concept, $docu, $queryfeatures, $size, $depth ) { + $this->m_concept = $concept; + $this->m_docu = $docu; + $this->m_features = $queryfeatures; + $this->m_size = $size; + $this->m_depth = $depth; + } + + public function getDIType() { + return SMWDataItem::TYPE_CONCEPT; + } + + public function getConceptQuery() { + return $this->m_concept; + } + + public function getDocumentation() { + return $this->m_docu; + } + + public function getQueryFeatures() { + return $this->m_features; + } + + public function getSize() { + return $this->m_size; + } + + public function getDepth() { + return $this->m_depth; + } + + public function getSortKey() { + return $this->m_docu; + } + + public function getSerialization() { + return serialize( $this ); + } + + /** + * Sets cache status + * + * @since 1.9 + * + * @param string + */ + public function setCacheStatus( $status ) { + $this->cacheStatus = $status; + } + + /** + * Sets cache date + * + * @since 1.9 + * + * @param string + */ + public function setCacheDate( $date ) { + $this->cacheDate = $date; + } + + /** + * Sets cache count + * + * @since 1.9 + * + * @param int + */ + public function setCacheCount( $count ) { + $this->cacheCount = $count; + } + + /** + * Returns cache status + * + * @since 1.9 + * + * @return string + */ + public function getCacheStatus() { + return $this->cacheStatus; + } + + /** + * Returns cache date + * + * @since 1.9 + * + * @return string + */ + public function getCacheDate() { + return $this->cacheDate; + } + + /** + * Returns cache count + * + * @since 1.9 + * + * @return int + */ + public function getCacheCount() { + return $this->cacheCount; + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * @return DIConcept + */ + public static function doUnserialize( $serialization ) { + $result = unserialize( $serialization ); + if ( $result === false ) { + throw new DataItemException( "Unserialization failed." ); + } + return $result; + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_CONCEPT ) { + return false; + } + return $di->getSerialization() === $this->getSerialization(); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Blob.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Blob.php new file mode 100644 index 00000000..fee4d0ca --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Blob.php @@ -0,0 +1,77 @@ +<?php + +use Onoi\Tesa\Normalizer; + +/** + * @ingroup SMWDataItems + */ + +/** + * This class implements blob (long string) data items. + * + * @since 1.6 + * + * @author Markus Krötzsch + * @ingroup SMWDataItems + */ +class SMWDIBlob extends SMWDataItem { + + /** + * Internal value. + * @var string + */ + protected $m_string; + + public function __construct( $string ) { + $this->m_string = trim( $string ); + } + + public function getDIType() { + return SMWDataItem::TYPE_BLOB; + } + + public function getString() { + return $this->m_string; + } + + public static function normalize( $text ) { + return Normalizer::convertDoubleWidth( + Normalizer::applyTransliteration( + Normalizer::toLowercase( $text ) + ) + ); + } + + public function getSortKey() { + return $this->m_string; + } + + /** + * @see SMWDataItem::getSortKeyDataItem() + * @return SMWDataItem + */ + public function getSortKeyDataItem() { + return $this; + } + + public function getSerialization() { + return $this->m_string; + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * @return SMWDIBlob + */ + public static function doUnserialize( $serialization ) { + return new SMWDIBlob( $serialization ); + } + + public function equals( SMWDataItem $di ) { + if ( !( $di instanceof SMWDIBlob ) ) { + return false; + } + + return $di->getString() === $this->m_string; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Bool.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Bool.php new file mode 100644 index 00000000..8bddf9fb --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Bool.php @@ -0,0 +1,69 @@ +<?php +/** + * @ingroup SMWDataItems + */ + +use SMW\Exception\DataItemException; + +/** + * This class implements Boolean data items. + * + * @since 1.6 + * + * @author Markus Krötzsch + * @ingroup SMWDataItems + */ +class SMWDIBoolean extends SMWDataItem { + + /** + * Internal value. + * @var bool + */ + protected $m_boolean; + + public function __construct( $boolean ) { + if ( !is_bool( $boolean ) ) { + throw new DataItemException( "Initialization value '$boolean' is not a boolean." ); + } + + $this->m_boolean = ( $boolean == true ); + } + + public function getDIType() { + return SMWDataItem::TYPE_BOOLEAN; + } + + public function getBoolean() { + return $this->m_boolean; + } + + public function getSerialization() { + return $this->m_boolean ? 't' : 'f'; + } + + public function getSortKey() { + return $this->m_boolean ? 1 : 0; + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * @return SMWDIBoolean + */ + public static function doUnserialize( $serialization ) { + if ( $serialization == 't' ) { + return new SMWDIBoolean( true ); + } elseif ( $serialization == 'f' ) { + return new SMWDIBoolean( false ); + } else { + throw new DataItemException( "Boolean data item unserialised from illegal value '$serialization'" ); + } + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_BOOLEAN ) { + return false; + } + return $di->getBoolean() === $this->m_boolean; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Container.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Container.php new file mode 100644 index 00000000..f7f230e5 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Container.php @@ -0,0 +1,141 @@ +<?php +/** + * @ingroup SMWDataItems + */ + +use SMW\DIProperty; +use SMW\Exception\DataItemException; +use SMWDIBlob as DIBlob; + +/** + * This class implements container data items that can store SMWSemanticData + * objects. Containers are not dataitems in the proper sense: they do not + * represent a single, opaque value that can be assigned to a property. Rather, + * a container represents a "subobject" with a number of property-value + * assignments. When a container is stored, these individual data assignments + * are stored -- the data managed by SMW never contains any "container", just + * individual property assignments for the subobject. Likewise, when a container + * is used in search, it is interpreted as a patterns of possible property + * assignments, and this pattern is searched for. + * + * The data encapsulated in a container data item is essentially an + * SMWSemanticData object of class SMWContainerSemanticData. This class allows + * the subject to be kept anonymous if not known (if no context page is + * available for finding a suitable subobject name). See the repsective + * documentation for details. + * + * Being a mere placeholder/template for other data, an SMWDIContainer is not + * immutable as the other basic data items. New property-value pairs can always + * be added to the internal SMWContainerSemanticData. + * + * @since 1.6 + * + * @author Markus Krötzsch + * @ingroup SMWDataItems + */ +class SMWDIContainer extends SMWDataItem { + + /** + * Internal value. + * + * @var SMWSemanticData + */ + protected $m_semanticData; + + /** + * Constructor. The given SMWContainerSemanticData object will be owned + * by the constructed object afterwards, and in particular will not + * allow further changes. + * + * @param $semanticData SMWContainerSemanticData + */ + public function __construct( SMWContainerSemanticData $semanticData ) { + $this->m_semanticData = $semanticData; + } + + public function getDIType() { + return SMWDataItem::TYPE_CONTAINER; + } + + public function getSemanticData() { + return $this->m_semanticData; + } + + public function getSortKey() { + return ''; + } + + /** + * @since 2.5 + * + * @param string $sortKey + */ + public function setSortKey( $sortKey ) { + $this->m_semanticData->addPropertyObjectValue( + new DIProperty( '_SKEY' ), + new DIBlob( $this->m_semanticData->getSubject()->getSortKey() . '#' . $sortKey ) + ); + } + + public function getSerialization() { + return serialize( $this->m_semanticData ); + } + + /** + * Get a hash string for this data item. + * + * @return string + */ + public function getHash() { + + $hash = $this->getValueHash( $this->m_semanticData ); + sort( $hash ); + + return md5( implode( '#', $hash ) ); + + // We want a value hash, not an entity hash!! + // return $this->m_semanticData->getHash(); + } + + private function getValueHash( $semanticData ) { + + $hash = []; + + foreach ( $semanticData->getProperties() as $property ) { + $hash[] = $property->getKey(); + + foreach ( $semanticData->getPropertyValues( $property ) as $di ) { + $hash[] = $di->getHash(); + } + } + + foreach ( $semanticData->getSubSemanticData() as $data ) { + $hash[] = $this->getValueHash( $data ); + } + + return $hash; + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * + * @return SMWDIContainer + */ + public static function doUnserialize( $serialization ) { + /// TODO May issue an E_NOTICE when problems occur; catch this + $data = unserialize( $serialization ); + if ( !( $data instanceof SMWContainerSemanticData ) ) { + throw new DataItemException( "Could not unserialize SMWDIContainer from the given string." ); + } + return new SMWDIContainer( $data ); + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_CONTAINER ) { + return false; + } + + return $di->getSerialization() === $this->getSerialization(); + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Error.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Error.php new file mode 100644 index 00000000..099b306a --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Error.php @@ -0,0 +1,80 @@ +<?php +/** + * @ingroup SMWDataItems + */ + +/** + * This class implements error list data items. These data items are used to + * pass around lists of error messages within the application. They are not + * meant to be stored or exported, but they can be useful to a user. + * + * @since 1.6 + * + * @author Markus Krötzsch + * @ingroup SMWDataItems + */ +class SMWDIError extends SMWDataItem { + + /** + * List of error messages. Should always be safe for HTML. + * @var array of strings + */ + protected $m_errors; + + /** + * @var string + */ + private $userValue; + + public function __construct( $errors, $userValue = '' ) { + $this->m_errors = $errors; + $this->userValue = $userValue; + } + + public function getDIType() { + return SMWDataItem::TYPE_ERROR; + } + + public function getErrors() { + return $this->m_errors; + } + + /** + * @since 3.0 + * + * @return string + */ + public function getUserValue() { + return $this->userValue; + } + + public function getSortKey() { + return 'error'; + } + + public function getString() { + return $this->getSerialization(); + } + + public function getSerialization() { + return serialize( $this->m_errors ); + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * @todo Be more careful with unserialization. It can create E_NOTICEs. + * @return SMWDIError + */ + public static function doUnserialize( $serialization ) { + return new SMWDIError( unserialize( $serialization ) ); + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_ERROR ) { + return false; + } + + return $di->getSerialization() === $this->getSerialization(); + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_GeoCoord.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_GeoCoord.php new file mode 100644 index 00000000..208366e6 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_GeoCoord.php @@ -0,0 +1,185 @@ +<?php + +use SMW\Exception\DataItemException; + +/** + * @license GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class SMWDIGeoCoord extends SMWDataItem { + + /** + * @var float + */ + private $latitude; + + /** + * @var float + */ + private $longitude; + + /** + * @var float|null + */ + private $altitude = null; + + /** + * Takes a latitude and longitude, and optionally an altitude. These can be provided in 2 forms: + * * An associative array with lat, lon and alt keys + * * Lat, lon and alt arguments + * + * The second way to provide the arguments, as well as the altitude argument, where introduced in SMW 1.7. + */ + public function __construct() { + $args = func_get_args(); + + $count = count( $args ); + + if ( $count === 1 && is_array( $args[0] ) ) { + if ( array_key_exists( 'lat', $args[0] ) && array_key_exists( 'lon', $args[0] ) ) { + $this->setLatitude( $args[0]['lat'] ); + $this->setLongitude( $args[0]['lon'] ); + + if ( array_key_exists( 'alt', $args[0] ) ) { + $this->altitude = (float)$args[0]['alt']; + } + } + else { + throw new DataItemException( 'Invalid coordinate data passed to the SMWDIGeoCoord constructor' ); + } + } + elseif ( $count === 2 || $count === 3 ) { + $this->setLatitude( $args[0] ); + $this->setLongitude( $args[1] ); + + if ( $count === 3 ) { + $this->altitude = (float)$args[2]; + } + } + else { + throw new DataItemException( 'Invalid coordinate data passed to the SMWDIGeoCoord constructor' ); + } + } + + private function setLatitude( $latitude ) { + if ( is_int( $latitude ) ) { + $latitude = (float)$latitude; + } + + if ( !is_float( $latitude ) ) { + throw new DataItemException( '$latitude should be a float' ); + } + + $this->latitude = $latitude; + } + + private function setLongitude( $longitude ) { + if ( is_int( $longitude ) ) { + $longitude = (float)$longitude; + } + + if ( !is_float( $longitude ) ) { + throw new DataItemException( '$longitude should be a float' ); + } + + $this->longitude = $longitude; + } + + /** + * (non-PHPdoc) + * @see SMWDataItem::getDIType() + */ + public function getDIType() { + return SMWDataItem::TYPE_GEO; + } + + /** + * Returns the coordinate set as an array with lat and long (and alt) keys + * pointing to float values. + * + * @return array + */ + public function getCoordinateSet() { + $coords = [ 'lat' => $this->latitude, 'lon' => $this->longitude ]; + + if ( !is_null( $this->altitude ) ) { + $coords['alt'] = $this->altitude; + } + + return $coords; + } + + /** + * (non-PHPdoc) + * @see SMWDataItem::getSortKey() + */ + public function getSortKey() { + return $this->latitude . ',' . $this->longitude . ( $this->altitude !== null ? ','. $this->altitude : '' ); + } + + /** + * (non-PHPdoc) + * @see SMWDataItem::getSerialization() + */ + public function getSerialization() { + return implode( ',', $this->getCoordinateSet() ); + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * @note PHP can convert any string to some number, so we do not do + * validation here (because this would require less efficient parsing). + * + * @param string $serialization + * + * @return self + */ + public static function doUnserialize( $serialization ) { + $parts = explode( ',', $serialization ); + $count = count( $parts ); + + if ( $count !== 2 && $count !== 3 ) { + throw new DataItemException( 'Unserialization of coordinates failed' ); + } + + $coords = [ 'lat' => (float)$parts[0], 'lon' => (float)$parts[1] ]; + + if ( $count === 3 ) { + $coords['alt'] = (float)$parts[2]; + } + + return new self( $coords ); + } + + /** + * @return float + */ + public function getLatitude() { + return $this->latitude; + } + + /** + * @return float + */ + public function getLongitude() { + return $this->longitude; + } + + /** + * Returns the altitude if set, null otherwise. + * + * @return float|null + */ + public function getAltitude() { + return $this->altitude; + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_GEO ) { + return false; + } + + return $di->getSerialization() === $this->getSerialization(); + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Number.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Number.php new file mode 100644 index 00000000..7105f31c --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Number.php @@ -0,0 +1,74 @@ +<?php +/** + * @ingroup SMWDataItems + */ + +use SMW\Exception\DataItemException; + +/** + * This class implements number data items. + * + * @since 1.6 + * + * @author Markus Krötzsch + * @ingroup SMWDataItems + */ +class SMWDINumber extends SMWDataItem { + + /** + * Internal value. + * @var float|int + */ + protected $m_number; + + public function __construct( $number ) { + if ( !is_numeric( $number ) ) { + throw new DataItemException( "Initialization value '$number' is not a number." ); + } + $this->m_number = $number; + } + + public function getDIType() { + return SMWDataItem::TYPE_NUMBER; + } + + public function getNumber() { + return $this->m_number; + } + + public function getSortKey() { + return $this->m_number; + } + + /** + * @see SMWDataItem::getSortKeyDataItem() + * @return SMWDataItem + */ + public function getSortKeyDataItem() { + return $this; + } + + public function getSerialization() { + return strval( $this->m_number ); + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * @note PHP can convert any string to some number, so we do not do + * validation here (because this would require less efficient parsing). + * @return SMWDINumber + */ + public static function doUnserialize( $serialization ) { + return new SMWDINumber( floatval( $serialization ) ); + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_NUMBER ) { + return false; + } + + return $di->getNumber() === $this->m_number; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Property.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Property.php new file mode 100644 index 00000000..06e076df --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Property.php @@ -0,0 +1,509 @@ +<?php + +namespace SMW; + +use RuntimeException; +use SMW\Exception\DataTypeLookupException; +use SMW\Exception\PredefinedPropertyLabelMismatchException; +use SMW\Exception\PropertyLabelNotResolvedException; +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 + */ + private $m_key; + + /** + * Whether to take the inverse of this property or not. + * @var boolean + */ + private $m_inverse; + + /** + * @var string + */ + private $propertyValueType; + + /** + * Interwiki prefix for when a property represents a non-local entity + * + * @var string + */ + private $interwiki = ''; + + /** + * 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 $key string key for the property (internal SMW key or wikipage DB key) + * @param $inverse boolean states if the inverse of the property is constructed + */ + public function __construct( $key, $inverse = false ) { + + if ( ( $key === '' ) || ( $key{0} == '-' ) ) { + throw new PropertyLabelNotResolvedException( "Illegal property key \"$key\"." ); + } + + if ( $key{0} == '_' ) { + if ( !PropertyRegistry::getInstance()->isRegistered( $key ) ) { + throw new PredefinedPropertyLabelMismatchException( "There is no predefined property with \"$key\"." ); + } + } + + $this->m_key = $key; + $this->m_inverse = $inverse; + } + + /** + * @return integer + */ + public function getDIType() { + return SMWDataItem::TYPE_PROPERTY; + } + + /** + * @return string + */ + public function getKey() { + return $this->m_key; + } + + /** + * @return boolean + */ + public function isInverse() { + return $this->m_inverse; + } + + /** + * @return string + */ + public function getSortKey() { + 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). + * + * @return boolean + */ + public function isShown() { + + 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. + * + * @return boolean + */ + public function isUserDefined() { + return $this->m_key{0} != '_'; + } + + /** + * Whether a user can freely use this property for value annotation or + * not. + * + * @since 3.0 + * + * @return boolean + */ + public function isUserAnnotable() { + + // 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 "-". + * + * @return string + */ + public function getLabel() { + $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 + */ + public function getCanonicalLabel() { + $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( $languageCode = '' ) { + + $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( $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. + * + * @param string $subobjectName + * @return DIWikiPage|null + */ + public function getDiWikiPage( $subobjectName = '' ) { + + $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( $subobjectName = '' ) { + + 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 ); + } + + return $this->newDIWikiPage( $dbkey, $subobjectName ); + } + + /** + * @since 2.4 + * + * @return DIProperty + */ + public function getRedirectTarget() { + + 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( $valueType ) { + + 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() { + + 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 + * + * @return string + */ + public function getSerialization() { + return ( $this->m_inverse ? '-' : '' ) . $this->m_key; + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * + * @param string $serialization + * + * @return DIProperty + */ + public static function doUnserialize( $serialization ) { + $inverse = false; + + if ( $serialization{0} == '-' ) { + $serialization = substr( $serialization, 1 ); + $inverse = true; + } + + return new self( $serialization, $inverse ); + } + + /** + * @param SMWDataItem $di + * + * @return boolean + */ + public function equals( SMWDataItem $di ) { + 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, SMWPropertyValue should be used. + * + * @param $label string label for the property + * @param $inverse boolean states if the inverse of the property is constructed + * + * @return DIProperty object + */ + public static function newFromUserLabel( $label, $inverse = false, $languageCode = false ) { + + 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( $dbkey, $subobjectName ) { + + // 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; + } + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Time.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Time.php new file mode 100644 index 00000000..00a473d2 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Time.php @@ -0,0 +1,609 @@ +<?php + +use SMW\DataValues\Time\CalendarModel; +use SMW\DataValues\Time\JulianDay; +use SMW\Exception\DataItemException; + +/** + * This class implements time data items. + * Such data items represent a unique point in time, given in either Julian or + * Gregorian notation (possibly proleptic), and a precision setting that states + * which of the components year, month, day, time were specified expicitly. + * Even when not specified, the data item always assumes default values for the + * missing parts, so the item really captures one point in time, no intervals. + * Times are always assumed to be in UTC. + * + * "Y0K issue": Neither the Gregorian nor the Julian calendar assume a year 0, + * i.e. the year 1 BC(E) was followed by 1 AD/CE. See + * http://en.wikipedia.org/wiki/Year_zero + * This implementation adheres to this convention and disallows year 0. The + * stored year numbers use positive numbers for CE and negative numbers for + * BCE. This is not just relevant for the question of how many years have + * (exactly) passed since a given date, but also for the location of leap + * years. + * + * @since 1.6 + * + * @author Markus Krötzsch + * @ingroup SMWDataItems + */ +class SMWDITime extends SMWDataItem implements CalendarModel { + + const PREC_Y = SMW_PREC_Y; + const PREC_YM = SMW_PREC_YM; + const PREC_YMD = SMW_PREC_YMD; + const PREC_YMDT = SMW_PREC_YMDT; + + /** + * The year before which we do not accept anything but year numbers and + * largely discourage calendar models. + */ + const PREHISTORY = -10000; + + /** + * Maximal number of days in a given month. + * @var array + */ + protected static $m_daysofmonths = [ 1 => 31, 2 => 29, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31 ]; + + /** + * Precision SMWDITime::PREC_Y, SMWDITime::PREC_YM, + * SMWDITime::PREC_YMD, or SMWDITime::PREC_YMDT. + * @var integer + */ + protected $m_precision; + /** + * Calendar model: SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN. + * @var integer + */ + protected $m_model; + /** + * Number of year, possibly negative. + * @var integer + */ + protected $m_year; + /** + * Number of month. + * @var integer + */ + protected $m_month; + /** + * Number of day. + * @var integer + */ + protected $m_day; + /** + * Hours of the day. + * @var integer + */ + protected $m_hours; + /** + * Minutes of the hour. + * @var integer + */ + protected $m_minutes; + /** + * Seconds of the minute. + * @var integer + */ + protected $m_seconds; + + /** + * @var integer + */ + protected $timezone; + + /** + * @var integer|null + */ + protected $era = null; + + /** + * @var integer + */ + protected $julianDay = null; + + /** + * Create a time data item. All time components other than the year can + * be false to indicate that they are not specified. This will affect + * the internal precision setting. The missing values are initialised + * to minimal values (0 or 1) for internal calculations. + * + * @param $calendarmodel integer one of SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN + * @param $year integer number of the year (possibly negative) + * @param $month mixed integer number or false + * @param $day mixed integer number or false + * @param $hour mixed integer number or false + * @param $minute mixed integer number or false + * @param $second mixed integer number or false + * @param integer|false $timezone + * + * @todo Implement more validation here. + */ + public function __construct( $calendarmodel, $year, $month = false, $day = false, + $hour = false, $minute = false, $second = false, $timezone = false ) { + + if ( ( $calendarmodel != self::CM_GREGORIAN ) && ( $calendarmodel != self::CM_JULIAN ) ) { + throw new DataItemException( "Unsupported calendar model constant \"$calendarmodel\"." ); + } + + if ( $year == 0 ) { + throw new DataItemException( "There is no year 0 in Gregorian and Julian calendars." ); + } + + $this->m_model = $calendarmodel; + $this->m_year = intval( $year ); + $this->m_month = $month != false ? intval( $month ) : 1; + $this->m_day = $day != false ? intval( $day ) : 1; + $this->m_hours = $hour !== false ? intval( $hour ) : 0; + $this->m_minutes = $minute !== false ? intval( $minute ) : 0; + $this->m_seconds = $second !== false ? floatval( $second ) : 0; + + $this->timezone = $timezone !== false ? $timezone : 0; + $year = strval( $year ); + $this->era = $year{0} === '+' ? 1 : ( $year{0} === '-' ? -1 : 0 ); + + if ( $this->isOutOfBoundsBySome() ) { + throw new DataItemException( "Part of the date is out of bounds." ); + } + + if ( $this->isOutOfBoundsByDayNumberOfMonth() ) { + throw new DataItemException( "Month {$this->m_month} in year {$this->m_year} did not have {$this->m_day} days in this calendar model." ); + } + + $this->setPrecisionLevelBy( $month, $day, $hour ); + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getDIType() { + return SMWDataItem::TYPE_TIME; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getCalendarModel() { + return $this->m_model; + } + + /** + * @since 2.5 + * + * @return integer + */ + public function getTimezone() { + return $this->timezone; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getPrecision() { + return $this->m_precision; + } + + /** + * Indicates whether a user explicitly used an era marker even for a positive + * year. + * + * - [-1] indicates BC(E) + * - [0]/null indicates no era marker + * - [1] indicates AD/CE was used + * + * @since 2.4 + * + * @return integer + */ + public function getEra() { + return $this->era; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getYear() { + return $this->m_year; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getMonth() { + return $this->m_month; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getDay() { + return $this->m_day; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getHour() { + return $this->m_hours; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getMinute() { + return $this->m_minutes; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getSecond() { + return $this->m_seconds; + } + + /** + * @since 2.4 + * + * @return string + */ + public function getCalendarModelLiteral() { + + $literal = [ + self::CM_GREGORIAN => '', + self::CM_JULIAN => 'JL' + ]; + + return $literal[$this->m_model]; + } + + /** + * @since 2.4 + * + * @param DateTime $dateTime + * + * @return SMWDITime|false + */ + public static function newFromDateTime( DateTime $dateTime ) { + + $calendarModel = self::CM_JULIAN; + + $year = $dateTime->format( 'Y' ); + $month = $dateTime->format( 'm' ); + $day = $dateTime->format( 'd' ); + + if ( ( $year > 1582 ) || + ( ( $year == 1582 ) && ( $month > 10 ) ) || + ( ( $year == 1582 ) && ( $month == 10 ) && ( $day > 4 ) ) ) { + $calendarModel = self::CM_GREGORIAN; + } + + return self::doUnserialize( $calendarModel . '/' . $dateTime->format( 'Y/m/d/H/i/s.u' ) ); + } + + /** + * @since 2.4 + * + * @return DateTime + */ + public function asDateTime() { + + $year = str_pad( $this->m_year, 4, '0', STR_PAD_LEFT ); + + // Avoid "Failed to parse time string (-900-02-02 00:00:00) at + // position 7 (-): Double timezone specification" + if ( $this->m_year < 0 ) { + $year = '-' . str_pad( $this->m_year * -1, 4, '0', STR_PAD_LEFT ); + } + + // Avoid "Failed to parse time string (1300-11-02 12:03:25.888499949) at + // at position 11 (1): The timezone could not ..." + $seconds = number_format( str_pad( $this->m_seconds, 2, '0', STR_PAD_LEFT ), 7, '.', '' ); + + $time = $year . '-' . + str_pad( $this->m_month, 2, '0', STR_PAD_LEFT ) . '-' . + str_pad( $this->m_day, 2, '0', STR_PAD_LEFT ) . ' ' . + str_pad( $this->m_hours, 2, '0', STR_PAD_LEFT ) . ':' . + str_pad( $this->m_minutes, 2, '0', STR_PAD_LEFT ) . ':' . + $seconds; + + return new DateTime( $time ); + } + + /** + * Creates and returns a new instance of SMWDITime from a MW timestamp. + * + * @since 1.8 + * + * @param string $timestamp must be in format + * + * @return SMWDITime|false + */ + public static function newFromTimestamp( $timestamp ) { + $timestamp = wfTimestamp( TS_MW, (string)$timestamp ); + + if ( $timestamp === false ) { + return false; + } + + return new self( + self::CM_GREGORIAN, + substr( $timestamp, 0, 4 ), + substr( $timestamp, 4, 2 ), + substr( $timestamp, 6, 2 ), + substr( $timestamp, 8, 2 ), + substr( $timestamp, 10, 2 ), + substr( $timestamp, 12, 2 ) + ); + } + + /** + * Returns a MW timestamp representation of the value. + * + * @since 1.6.2 + * + * @param $outputtype + * + * @return mixed + */ + public function getMwTimestamp( $outputtype = TS_UNIX ) { + return wfTimestamp( + $outputtype, + implode( '', [ + str_pad( $this->m_year, 4, '0', STR_PAD_LEFT ), + str_pad( $this->m_month, 2, '0', STR_PAD_LEFT ), + str_pad( $this->m_day, 2, '0', STR_PAD_LEFT ), + str_pad( $this->m_hours, 2, '0', STR_PAD_LEFT ), + str_pad( $this->m_minutes, 2, '0', STR_PAD_LEFT ), + str_pad( $this->m_seconds, 2, '0', STR_PAD_LEFT ), + ] ) + ); + } + + /** + * Get the data in the specified calendar model. This might require + * conversion. + * @note Conversion can be unreliable for very large absolute year + * numbers when the internal calculations hit floating point accuracy. + * Callers might want to avoid this (calendar models make little sense + * in such cases anyway). + * @param $calendarmodel integer one of SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN + * @return SMWDITime + */ + public function getForCalendarModel( $calendarmodel ) { + if ( $calendarmodel == $this->m_model ) { + return $this; + } else { + return self::newFromJD( $this->getJD(), $calendarmodel, $this->m_precision ); + } + } + + /** + * Return a number that helps comparing time data items. For + * dates in the Julian Day era (roughly from 4713 BCE onwards), we use + * the Julian Day number. For earlier dates, the (negative) year number + * with a fraction for the date is used (times are ignored). This + * avoids calculation errors that would occur for very ancient dates + * if the JD number was used there. + * @return double sortkey + */ + public function getSortKey() { + $jd = ( $this->m_year >= -4713 ) ? $jd = $this->getJD() : -1; + if ( $jd > 0 ) { + return $jd; + } else { + return $this->m_year - 1 + ( $this->m_month - 1 ) / 12 + ( $this->m_day - 1 ) / 12 / 31; + } + } + + /** + * @since 1.6 + * + * @return double + */ + public function getJD() { + + if ( $this->julianDay !== null ) { + return $this->julianDay; + } + + $this->julianDay = JulianDay::getJD( + $this->getCalendarModel(), + $this->getYear(), + $this->getMonth(), + $this->getDay(), + $this->getHour(), + $this->getMinute(), + $this->getSecond() + ); + + return $this->julianDay; + } + + /** + * @since 1.6 + * + * @return string + */ + public function getSerialization() { + $result = strval( $this->m_model ) . '/' . ( $this->era > 0 ? '+' : '' ) . strval( $this->m_year ); + + if ( $this->m_precision >= self::PREC_YM ) { + $result .= '/' . strval( $this->m_month ); + } + + if ( $this->m_precision >= self::PREC_YMD ) { + $result .= '/' . strval( $this->m_day ); + } + + if ( $this->m_precision >= self::PREC_YMDT ) { + $result .= '/' . strval( $this->m_hours ) . '/' . strval( $this->m_minutes ) . '/' . strval( $this->m_seconds ) . '/' . strval( $this->timezone ); + } + + return $result; + } + + /** + * Create a data item from the provided serialization string. + * + * @return SMWDITime + */ + public static function doUnserialize( $serialization ) { + $parts = explode( '/', $serialization, 8 ); + $values = []; + + if ( count( $parts ) <= 1 ) { + throw new DataItemException( "Unserialization failed: the string \"$serialization\" is no valid URI." ); + } + + for ( $i = 0; $i < 8; $i += 1 ) { + + $values[$i] = false; + + // Can contain something like '1/1970/1/12/11/43/0/Asia/Tokyo' + if ( $i == 7 && isset( $parts[$i] ) ) { + $values[$i] = strval( $parts[$i] ); + continue; + } + + if ( $i < count( $parts ) ) { + + if ( $parts[$i] !== '' && !is_numeric( $parts[$i] ) ) { + throw new DataItemException( "Unserialization failed: the string \"$serialization\" is no valid datetime specification." ); + } + + // 6 == seconds, we want to keep microseconds + $values[$i] = $i == 6 ? floatval( $parts[$i] ) : intval( $parts[$i] ); + + // Find out whether the input contained an explicit AD/CE era marker + if ( $i == 1 ) { + $values[$i] = ( $parts[1]{0} === '+' ? '+' : '' ) . $values[$i]; + } + } + } + + return new self( $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6], $values[7] ); + } + + /** + * Create a new time dataItem from a specified Julian Day number, + * calendar model, presicion. + * + * @param double $jdValue + * @param integer|null $calendarmodel + * @param integer|null $precision + * + * @return DITime object + */ + public static function newFromJD( $jdValue, $calendarModel = null, $precision = null, $timezone = false ) { + + $hour = $minute = $second = false; + $year = $month = $day = false; + $jdValue = JulianDay::format( $jdValue ); + + if ( $precision === null ) { + $precision = strpos( strval( $jdValue ), '.5' ) !== false ? self::PREC_YMD : self::PREC_YMDT; + } + + list( $calendarModel, $year, $month, $day ) = JulianDay::JD2Date( $jdValue, $calendarModel ); + + if ( $precision <= self::PREC_YM ) { + $day = false; + if ( $precision === self::PREC_Y ) { + $month = false; + } + } + + if ( $precision === self::PREC_YMDT ) { + list( $hour, $minute, $second ) = JulianDay::JD2Time( $jdValue ); + } + + return new self( $calendarModel, $year, $month, $day, $hour, $minute, $second, $timezone ); + } + + /** + * Find out whether the given year number is a leap year. + * This calculation assumes that neither calendar has a year 0. + * @param $year integer year number + * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN + * @return boolean + */ + static public function isLeapYear( $year, $calendarmodel ) { + $astroyear = ( $year < 1 ) ? ( $year + 1 ) : $year; + if ( $calendarmodel == self::CM_JULIAN ) { + return ( $astroyear % 4 ) == 0; + } else { + return ( ( $astroyear % 400 ) == 0 ) || + ( ( ( $astroyear % 4 ) == 0 ) && ( ( $astroyear % 100 ) != 0 ) ); + } + } + + /** + * Find out how many days the given month had in the given year + * based on the specified calendar model. + * This calculation assumes that neither calendar has a year 0. + * @param $month integer month number + * @param $year integer year number + * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN + * @return boolean + */ + static public function getDayNumberForMonth( $month, $year, $calendarmodel ) { + if ( $month !== 2 ) { + return self::$m_daysofmonths[$month]; + } elseif ( self::isLeapYear( $year, $calendarmodel ) ) { + return 29; + } else { + return 28; + } + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_TIME ) { + return false; + } + + return $di->getSortKey() === $this->getSortKey(); + } + + private function isOutOfBoundsBySome() { + return ( $this->m_hours < 0 ) || ( $this->m_hours > 23 ) || + ( $this->m_minutes < 0 ) || ( $this->m_minutes > 59 ) || + ( $this->m_seconds < 0 ) || ( $this->m_seconds > 59 ) || + ( $this->m_month < 1 ) || ( $this->m_month > 12 ); + } + + private function isOutOfBoundsByDayNumberOfMonth() { + return $this->m_day > self::getDayNumberForMonth( $this->m_month, $this->m_year, $this->m_model ); + } + + private function setPrecisionLevelBy( $month, $day, $hour ) { + if ( $month === false ) { + $this->m_precision = self::PREC_Y; + } elseif ( $day === false ) { + $this->m_precision = self::PREC_YM; + } elseif ( $hour === false ) { + $this->m_precision = self::PREC_YMD; + } else { + $this->m_precision = self::PREC_YMDT; + } + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_URI.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_URI.php new file mode 100644 index 00000000..d1aaa67d --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_URI.php @@ -0,0 +1,165 @@ +<?php + +use SMW\Exception\DataItemException; + +/** + * This class implements URI data items. + * + * @since 1.6 + * + * @author Markus Krötzsch + * @ingroup SMWDataItems + */ +class SMWDIUri extends SMWDataItem { + + /** + * URI scheme such as "html" or "mailto". + * @var string + */ + protected $m_scheme; + /** + * "Hierpart" of the URI (usually some authority and path). + * @var string + */ + protected $m_hierpart; + /** + * Query part of the URI. + * @var string + */ + protected $m_query; + /** + * Fragment part of the URI. + * @var string + */ + protected $m_fragment; + + /** + * Initialise a URI by providing its scheme (e.g. "html"), 'hierpart' + * following "scheme:" (e.g. "//username@example.org/path), query (e.g. + * "q=Search+term", and fragment (e.g. "section-one"). The complete URI + * with these examples would be + * http://username@example.org/path?q=Search+term#section-one + * @param $scheme string for the scheme + * @param $hierpart string for the "hierpart" + * @param $query string for the query + * @param $fragment string for the fragment + * + * @todo Implement more validation here. + */ + public function __construct( $scheme, $hierpart, $query, $fragment, $strict = true ) { + if ( $strict && ( ( $scheme === '' ) || ( preg_match( '/[^a-zA-Z]/u', $scheme ) ) ) ) { + throw new DataItemException( "Illegal URI scheme \"$scheme\"." ); + } + if ( $strict && $hierpart === '' ) { + throw new DataItemException( "Illegal URI hierpart \"$hierpart\"." ); + } + $this->m_scheme = $scheme; + $this->m_hierpart = $hierpart; + $this->m_query = $query; + $this->m_fragment = $fragment; + } + + public function getDIType() { + return SMWDataItem::TYPE_URI; + } + + /// @todo This should be changed to the spelling getUri(). + public function getURI() { + $schemesWithDoubleslesh = [ + 'http', 'https', 'ftp' + ]; + + $uri = $this->m_scheme . ':' + . ( in_array( $this->m_scheme, $schemesWithDoubleslesh ) ? '//' : '' ) + . $this->m_hierpart + . ( $this->m_query ? '?' . $this->m_query : '' ) + . ( $this->m_fragment ? '#' . $this->m_fragment : '' ); + + // #1878 + // https://tools.ietf.org/html/rfc3986 + // Normalize spaces to use `_` instead of %20 and so ensure + // that http://example.org/Foo bar === http://example.org/Foo_bar === http://example.org/Foo%20bar + return str_replace( [ ' ', '%20'], '_', $uri ); + } + + public function getScheme() { + return $this->m_scheme; + } + + public function getHierpart() { + return $this->m_hierpart; + } + + public function getQuery() { + return $this->m_query; + } + + public function getFragment() { + return $this->m_fragment; + } + + /** + * @since 1.6 + * + * @return string + */ + public function getSortKey() { + return urldecode( $this->getURI() ); + } + + public function getSerialization() { + return $this->getURI(); + } + + /** + * Create a data item from the provided serialization string and type + * ID. + * @return SMWDIUri + */ + public static function doUnserialize( $serialization ) { + + // try to split "schema:rest" + $parts = explode( ':', $serialization, 2 ); + $strict = true; + + if ( $serialization !== null && count( $parts ) <= 1 ) { + throw new DataItemException( "Unserialization failed: the string \"$serialization\" is no valid URI." ); + } + + if ( $serialization === null ) { + $parts = [ '', 'NO_VALUE' ]; + $strict = false; + } + + $scheme = $parts[0]; + + // try to split "hier-part?queryfrag" + $parts = explode( '?', $parts[1], 2 ); + + if ( count( $parts ) == 2 ) { + $hierpart = $parts[0]; + // try to split "query#frag" + $parts = explode( '#', $parts[1], 2 ); + $query = $parts[0]; + $fragment = ( count( $parts ) == 2 ) ? $parts[1] : ''; + } else { + $query = ''; + // try to split "hier-part#frag" + $parts = explode( '#', $parts[0], 2 ); + $hierpart = $parts[0]; + $fragment = ( count( $parts ) == 2 ) ? $parts[1] : ''; + } + + $hierpart = ltrim( $hierpart, '/' ); + + return new SMWDIUri( $scheme, $hierpart, $query, $fragment, $strict ); + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_URI ) { + return false; + } + + return $di->getURI() === $this->getURI(); + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_WikiPage.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_WikiPage.php new file mode 100644 index 00000000..863cbef1 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_WikiPage.php @@ -0,0 +1,299 @@ +<?php + +namespace SMW; + +use SMW\Exception\DataItemDeserializationException; +use SMW\Exception\DataItemException; +use SMWDataItem; +use Title; + +/** + * This class implements wiki page data items. + * + * @since 1.6 + * @ingroup SMWDataItems + * + * @author Markus Krötzsch + */ +class DIWikiPage extends SMWDataItem { + + /** + * MediaWiki DB key string + * @var string + */ + protected $m_dbkey; + + /** + * MediaWiki namespace integer. + * @var integer + */ + protected $m_namespace; + + /** + * MediaWiki interwiki prefix. + * @var string + */ + protected $m_interwiki; + + /** + * Name for subobjects of pages, or empty string if the given object is + * the page itself (not a subobject). + * @var string + */ + protected $m_subobjectname; + + /** + * @var string + */ + private $sortkey = null; + + /** + * @var string + */ + private $contextReference = null; + + /** + * @var string + */ + private $pageLanguage = null; + + /** + * @var integer + */ + private $id = 0; + + /** + * Constructor. We do not bother with too much detailed validation here, + * regarding the known namespaces, canonicity of the dbkey (namespace + * exrtacted?), validity of interwiki prefix (known?), and general use + * of allowed characters (may depend on MW configuration). All of this + * would be more work than it is worth, since callers will usually be + * careful and since errors here do not have major consequences. + * + * @param string $dbkey + * @param integer $namespace + * @param string $interwiki + * @param string $subobjectname + */ + public function __construct( $dbkey, $namespace, $interwiki = '', $subobjectname = '' ) { + // Check if the provided value holds an integer + // (it can be of type string or float as well, as long as the value is an int) + if ( !ctype_digit( ltrim( (string)$namespace, '-' ) ) ) { + throw new DataItemException( "Given namespace '$namespace' is not an integer." ); + } + + // Check for a potential fragment such as Foo#Bar, Bar#_49c8ab + if ( strpos( $dbkey, '#' ) !== false ) { + list( $dbkey, $subobjectname ) = explode( '#', $dbkey ); + } + + $this->m_dbkey = str_replace( ' ', '_', $dbkey ); + $this->m_namespace = (int)$namespace; // really make this an integer + $this->m_interwiki = $interwiki; + $this->m_subobjectname = $subobjectname; + } + + public function getDIType() { + return SMWDataItem::TYPE_WIKIPAGE; + } + + public function getDBkey() { + return $this->m_dbkey; + } + + public function getNamespace() { + return $this->m_namespace; + } + + public function getInterwiki() { + return $this->m_interwiki; + } + + public function getSubobjectName() { + return $this->m_subobjectname; + } + + /** + * @since 2.1 + * + * @param string $sortkey + */ + public function setSortKey( $sortkey ) { + $this->sortkey = str_replace( '_', ' ', $sortkey ); + } + + /** + * Get the sortkey of the wiki page data item. Note that this is not + * the sortkey that might have been set for the corresponding wiki + * page. To obtain the latter, query for the values of the property + * "new SMW\DIProperty( '_SKEY' )". + */ + public function getSortKey() { + + if ( $this->sortkey === null || $this->sortkey === '' ) { + $this->sortkey = str_replace( '_', ' ', $this->m_dbkey ); + } + + return $this->sortkey; + } + + /** + * @since 2.3 + * + * @param string $contextReference + */ + public function setContextReference( $contextReference ) { + $this->contextReference = $contextReference; + } + + /** + * Returns a reference for the processing context (parser etc.). + * + * @since 2.3 + * + * @return string + */ + public function getContextReference() { + return $this->contextReference; + } + + /** + * Returns the page content language + * + * @since 2.5 + * + * @return string + */ + public function getPageLanguage() { + + if ( $this->pageLanguage === null ) { + $this->pageLanguage = false; + + if ( ( $title = $this->getTitle() ) !== null ) { + $this->pageLanguage = $title->getPageLanguage()->getCode(); + } + } + + return $this->pageLanguage; + } + + /** + * @since 2.5 + * + * @param integer $id + */ + public function setId( $id ) { + $this->id = (int)$id; + } + + /** + * @since 2.5 + * + * @return string + */ + public function getId() { + return $this->id; + } + + /** + * Create a MediaWiki Title object for this DIWikiPage. The result + * can be null if an error occurred. + * + * @return Title|null + */ + public function getTitle() { + return Title::makeTitleSafe( + $this->m_namespace, + $this->m_dbkey, + $this->m_subobjectname, + $this->m_interwiki + ); + } + + /** + * Returns the base part (without a fragment) of a wikipage representation. + * + * @since 2.4 + * + * @return DIWikiPage + */ + public function asBase() { + return new self ( + $this->m_dbkey, + $this->m_namespace, + $this->m_interwiki + ); + } + + /** + * @since 1.6 + * + * @return string + */ + public function getSerialization() { + $segments = [ + $this->m_dbkey, + $this->m_namespace, + $this->m_interwiki + ]; + + $segments[] = $this->m_subobjectname; + + return implode( '#', $segments ); + } + + /** + * Create a data item from the provided serialization string and type ID. + * + * @param string $serialization + * + * @return DIWikiPage + * @throws DataItemDeserializationException + */ + public static function doUnserialize( $serialization ) { + $parts = explode( '#', $serialization, 4 ); + + if ( count( $parts ) == 3 ) { + return new self( $parts[0], intval( $parts[1] ), $parts[2] ); + } elseif ( count( $parts ) == 4 ) { + return new self( $parts[0], intval( $parts[1] ), $parts[2], $parts[3] ); + } else { + throw new DataItemDeserializationException( "Unserialization failed: the string \"$serialization\" was not understood." ); + } + } + + /** + * Create a data item from a MediaWiki Title. + * + * @param Title $title + * @return DIWikiPage + */ + public static function newFromTitle( Title $title ) { + return new self( + $title->getDBkey(), + $title->getNamespace(), + $title->getInterwiki(), + str_replace( ' ', '_', $title->getFragment() ) + ); + } + + /** + * @since 2.1 + * + * @param string $text + * @param integer namespace + * + * @return DIWikiPage + */ + public static function newFromText( $text, $namespace = NS_MAIN ) { + return new self( $text, $namespace ); + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_WIKIPAGE ) { + return false; + } + + return $di->getSerialization() === $this->getSerialization(); + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DataItem.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DataItem.php new file mode 100644 index 00000000..96137956 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DataItem.php @@ -0,0 +1,236 @@ +<?php + +use SMW\Options; + +/** + * This group contains all parts of SMW that relate to the processing of dataitems + * of various types. + * + * @defgroup SMWDataItems SMWDataItems + * @ingroup SMW + */ + +/** + * Objects of this type represent all that is known about a certain piece of + * data that could act as the value of some property. Data items only represent + * the stored data, and are thus at the core of SMW's data model. Data items + * are always immutable, i.e. they must not be changed after creation (and this + * is mostly enforced by the API with some minor exceptions). + * + * The set of available data items is fixed and cannot be extended. These are + * the kinds of information that SMW can process. Their concrete use and + * handling might depend on the context in which they are used. In particular, + * property values may be influences by settings made for their property. This + * aspect, however, is not part of the data item API. + * + * @since 1.6 + * + * @ingroup SMWDataItems + */ +abstract class SMWDataItem { + + /// Data item ID that can be used to indicate that no data item class is appropriate + const TYPE_NOTYPE = 0; + /// Data item ID for SMWDINumber + const TYPE_NUMBER = 1; + /// Data item ID for SMWDIBlob + const TYPE_BLOB = 2; + /// Data item ID for SMWDIBoolean + const TYPE_BOOLEAN = 4; + /// Data item ID for SMWDIUri + const TYPE_URI = 5; + /// Data item ID for SMWDITimePoint + const TYPE_TIME = 6; + /// Data item ID for SMWDIGeoCoord + const TYPE_GEO = 7; + /// Data item ID for SMWDIContainer + const TYPE_CONTAINER = 8; + /// Data item ID for SMWDIWikiPage + const TYPE_WIKIPAGE = 9; + /// Data item ID for SMWDIConcept + const TYPE_CONCEPT = 10; + /// Data item ID for SMWDIProperty + const TYPE_PROPERTY = 11; + /// Data item ID for SMWDIError + const TYPE_ERROR = 12; + + /** + * @var Options + */ + private $options = null; + + /** + * Convenience method that returns a constant that defines the concrete + * class that implements this data item. Used to switch when processing + * data items. + * @return integer that specifies the basic type of data item + */ + abstract public function getDIType(); + + /** + * Return a value that can be used for sorting data of this type. + * If the data is of a numerical type, the sorting must be done in + * numerical order. If the data is a string, the data must be sorted + * alphabetically. + * + * @note Every data item returns a sort key, even if there is no + * natural linear order for the type. SMW must order listed data + * in some way in any case. If there is a natural order (e.g. for + * Booleans where false < true), then the sortkey must agree with + * this order (e.g. for Booleans where false maps to 0, and true + * maps to 1). + * + * @note Wiki pages are a special case in SMW. They are ordered by a + * sortkey that is assigned to them as a property value. When pages are + * sorted, this data should be used if possible. + * + * @return float|string + */ + abstract public function getSortKey(); + + /** + * Method to compare two SMWDataItems + * This should result true only if they are of the same DI type + * and have the same internal value + * + * @since 1.8 + * + * @param SMWDataItem $di + * @return boolean + */ + abstract public function equals( SMWDataItem $di ); + + /** + * Create a data item that represents the sortkey, i.e. either an + * SMWDIBlob or an SMWDINumber. For efficiency, these subclasses + * overwrite this method to return themselves. + * + * @return SMWDataItem + */ + public function getSortKeyDataItem() { + $sortKey = $this->getSortKey(); + + if ( is_numeric( $sortKey ) ) { + return new SMWDINumber( $sortKey ); + } + + return new SMWDIBlob( $sortKey ); + } + + /** + * Get a UTF-8 encoded string serialization of this data item. + * The serialisation should be concise and need not be pretty, but it + * must allow unserialization. Each subclass of SMWDataItem implements + * a static method doUnserialize() for this purpose. + * @return string + */ + abstract public function getSerialization(); + + /** + * Get a hash string for this data item. Might be overwritten in + * subclasses to obtain shorter or more efficient hashes. + * + * @return string + */ + public function getHash() { + return $this->getSerialization(); + } + + /** + * @since 2.1 + * + * @return string + */ + public function __toString() { + return $this->getSerialization(); + } + + /** + * Create a data item of the given dataitem ID based on the the + * provided serialization string and (optional) typeid. + * + * @param integer $diType dataitem ID + * @param string $serialization + * + * @return SMWDataItem + */ + public static function newFromSerialization( $diType, $serialization ) { + $diClass = self::getDataItemClassNameForId( $diType ); + return call_user_func( [ $diClass, 'doUnserialize' ], $serialization ); + } + + /** + * Gets the class name of the data item that has the provided type id. + * + * @param integer $diType Element of the SMWDataItem::TYPE_ enum + * + * @throws InvalidArgumentException + * + * @return string + */ + public static function getDataItemClassNameForId( $diType ) { + switch ( $diType ) { + case self::TYPE_NUMBER: + return SMWDINumber::class; + case self::TYPE_BLOB: + return SMWDIBlob::class; + case self::TYPE_BOOLEAN: + return SMWDIBoolean::class; + case self::TYPE_URI: + return SMWDIUri::class; + case self::TYPE_TIME: + return SMWDITime::class; + case self::TYPE_GEO: + return SMWDIGeoCoord::class; + case self::TYPE_CONTAINER: + return SMWDIContainer::class; + case self::TYPE_WIKIPAGE: + return SMWDIWikiPage::class; + case self::TYPE_CONCEPT: + return SMWDIConcept::class; + case self::TYPE_PROPERTY: + return SMWDIProperty::class; + case self::TYPE_ERROR: + return SMWDIError::class; + case self::TYPE_NOTYPE: default: + throw new InvalidArgumentException( "The value \"$diType\" is not a valid dataitem ID." ); + } + } + + /** + * @since 2.5 + * + * @param string $key + * @param string $value + */ + public function setOption( $key, $value ) { + + if ( !$this->options instanceof Options ) { + $this->options = new Options(); + } + + $this->options->set( $key, $value ); + } + + /** + * @since 2.5 + * + * @param string $key + * @param string|null $default + * + * @return mixed + */ + public function getOption( $key, $default = null ) { + + if ( !$this->options instanceof Options ) { + $this->options = new Options(); + } + + if ( $this->options->has( $key ) ) { + return $this->options->get( $key ); + } + + return $default; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Concept.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Concept.php new file mode 100644 index 00000000..01128751 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Concept.php @@ -0,0 +1,96 @@ +<?php +/** + * @ingroup SMWDataValues + */ + +/** + * This datavalue is used as a container for concept descriptions as used + * on Concept pages with the #concept parserfunction. It has a somewhat + * non-standard interface as compared to other datavalues, but this is not + * an issue. + * + * @author Markus Krötzsch + * @ingroup SMWDataValues + */ +class SMWConceptValue extends SMWDataValue { + + protected function parseUserValue( $value ) { + throw new Exception( 'Concepts cannot be initialized from user-provided strings. This should not happen.' ); + } + + /** + * @see SMWDataValue::loadDataItem() + * @param $dataItem SMWDataItem + * @return boolean + */ + protected function loadDataItem( SMWDataItem $dataItem ) { + + if ( $dataItem->getDIType() !== SMWDataItem::TYPE_CONCEPT ) { + return false; + } + + $this->m_dataitem = $dataItem; + $this->m_caption = $dataItem->getConceptQuery(); // probably useless + + return true; + } + + protected function clear() { + $this->m_dataitem = new \SMW\DIConcept( '', '', 0, -1, -1, $this->m_typeid ); + } + + public function getShortWikiText( $linked = null ) { + return $this->m_caption; + } + + public function getShortHTMLText( $linker = null ) { + return $this->getShortWikiText( $linker ); // should be save (based on xsdvalue) + } + + public function getLongWikiText( $linked = null ) { + if ( !$this->isValid() ) { + return $this->getErrorText(); + } else { + return $this->m_caption; + } + } + + public function getLongHTMLText( $linker = null ) { + if ( !$this->isValid() ) { + return $this->getErrorText(); + } else { + return $this->m_caption; // should be save (based on xsdvalue) + } + } + + public function getWikiValue() { + /// This should not be used for anything. This class does not support wiki values. + return str_replace( [ '<', '>', '&' ], [ '<', '>', '&' ], $this->m_dataitem->getConceptQuery() ); + } + + /// Return the concept's defining text (in SMW query syntax) + public function getConceptText() { + return $this->m_dataitem->getConceptQuery(); + } + + /// Return the optional concept documentation. + public function getDocu() { + return $this->m_dataitem->getDocumentation(); + } + + /// Return the concept's size (a metric used to estimate computation complexity). + public function getSize() { + return $this->m_dataitem->getSize(); + } + + /// Return the concept's depth (a metric used to estimate computation complexity). + public function getDepth() { + return $this->m_dataitem->getDepth(); + } + + /// Return the concept's query feature bit field (a metric used to estimate computation complexity). + public function getQueryFeatures() { + return $this->m_dataitem->getQueryFeatures(); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Error.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Error.php new file mode 100644 index 00000000..1ad6a3ab --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Error.php @@ -0,0 +1,81 @@ +<?php +/** + * @ingroup SMWDataValues + */ + +/** + * This datavalue implements error datavalues, a kind of pseudo data value that + * is used in places where a data value is expected but no more meaningful + * value could be created. It is always invalid and never gets stored or + * exported, but it can help to transport an error message. + * + * @note SMWDataValue will return a data item of type SMWDIError for invalid + * data values. Hence this is the DI type of this DV, even if not mentioned in + * this file. + * + * @author Markus Krötzsch + * @ingroup SMWDataValues + */ +class SMWErrorValue extends SMWDataValue { + + public function __construct( $typeid, $errormsg = '', $uservalue = '', $caption = false ) { + parent::__construct( $typeid ); + $this->m_caption = ( $caption !== false ) ? $caption : $uservalue; + if ( $errormsg !== '' ) { + $this->addErrorMsg( $errormsg ); + } + } + + protected function parseUserValue( $value ) { + if ( $this->m_caption === false ) { + $this->m_caption = $value; + } + $this->addErrorMsg( [ 'smw-datavalue-parse-error', $value ] ); + } + + /** + * @see SMWDataValue::loadDataItem() + * @param $dataitem SMWDataItem + * @return boolean + */ + protected function loadDataItem( SMWDataItem $dataItem ) { + + if ( $dataItem->getDIType() == SMWDataItem::TYPE_ERROR ) { + $this->addError( $dataItem->getErrors() ); + $this->m_caption = $this->getErrorText(); + return true; + } else { + return false; + } + } + + public function getShortWikiText( $linked = null ) { + return $this->m_caption; + } + + public function getShortHTMLText( $linker = null ) { + return htmlspecialchars( $this->getShortWikiText( $linker ) ); + } + + public function getLongWikiText( $linked = null ) { + return $this->getErrorText(); + } + + public function getLongHTMLText( $linker = null ) { + return $this->getErrorText(); + } + + public function getWikiValue() { + + if ( $this->m_dataitem !== null ) { + return $this->m_dataitem->getString(); + } + + return $this->getErrorText(); + } + + public function isValid() { + return false; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Number.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Number.php new file mode 100644 index 00000000..cfa75cfb --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Number.php @@ -0,0 +1,602 @@ +<?php + +use SMW\DataValues\Number\IntlNumberFormatter; +use SMW\DataValues\ValueFormatters\DataValueFormatter; +use SMW\Localizer; +use SMW\Message; + +/** + * @ingroup SMWDataValues + */ + +/** + * This datavalue implements numerical datavalues, and supports optional + * unit conversions. It parses and manages unit strings, since even plain + * numbers may have (not further specified) units that are stored. However, + * only subclasses implement full unit conversion by extending the methods + * convertToMainUnit() and makeConversionValues(). + * + * Units work as follows: a unit is a string, but many such strings might + * refer to the same unit of measurement. There is always one string, that + * canonically represents the unit, and we will call this version of writing + * the unit the /unit id/. IDs for units are needed for tasks like duplicate + * avoidance. If no conversion information is given, any unit is its own ID. + * In any case, units are /normalised/, i.e. given a more standardised meaning + * before being processed. All units, IDs or otherwise, should be suitable for + * printout in wikitext, and main IDs should moreover be suitable for printout + * in HTML. + * + * Subclasses that support unit conversion may interpret the output format set + * via setOutputFormat() to allow a unit to be selected for display. Note that + * this setting does not affect the internal representation of the value + * though. So choosing a specific output format will change the behavior of + * output functions like getLongWikiText(), but not of functions that access + * the value itself, such as getUnit() or getDBKeys(). + * + * @author Markus Krötzsch + * @ingroup SMWDataValues + * + * @todo Wiki-HTML-conversion for unit strings must be revisited, as the current + * solution might be unsafe. + */ +class SMWNumberValue extends SMWDataValue { + + /** + * DV identifier + */ + const TYPE_ID = '_num'; + + /** + * Internal state to ensure no precision limitation is applied to an output + */ + const NO_DISP_PRECISION_LIMIT = 'num.no.displayprecision.limit'; + + /** + * Separator related constants + */ + const DECIMAL_SEPARATOR = 'decimal.separator'; + const THOUSANDS_SEPARATOR = 'thousands.separator'; + + /** + * Array with entries unit=>value, mapping a normalized unit to the + * converted value. Used for conversion tooltips. + * @var array + */ + protected $m_unitvalues; + + /** + * Whether the unit is preferred as prefix or not + * + * @var array + */ + protected $prefixalUnitPreference = []; + + /** + * Canonical identifier for the unit that the user gave as input. Used + * to avoid printing this in conversion tooltips again. If the + * outputformat was set to show another unit, then the values of + * $m_caption and $m_unitin will be updated as if the formatted string + * had been the original user input, i.e. the two values reflect what + * is currently printed. + * @var string + */ + protected $m_unitin; + + /** + * @var integer|null + */ + protected $precision = null; + + /** + * @var IntlNumberFormatter + */ + private $intlNumberFormatter; + + /** + * @var ValueFormatter + */ + private $valueFormatter; + + /** + * @since 2.4 + * + * @param string $typeid + */ + public function __construct( $typeid = '' ) { + parent::__construct( $typeid ); + $this->intlNumberFormatter = IntlNumberFormatter::getInstance(); + $this->intlNumberFormatter->reset(); + } + + /** + * Parse a string of the form "number unit" where unit is optional. The + * results are stored in the $number and $unit parameters. Returns an + * error code. + * @param $value string to parse + * @param $number call-by-ref parameter that will be set to the numerical value + * @param $unit call-by-ref parameter that will be set to the "unit" string (after the number) + * @return integer 0 (no errors), 1 (no number found at all), 2 (number + * too large for this platform) + */ + public function parseNumberValue( $value, &$number, &$unit, &$asPrefix = false ) { + + $intlNumberFormatter = $this->getNumberFormatter(); + + // Parse to find $number and (possibly) $unit + $kiloseparator = $intlNumberFormatter->getSeparatorByLanguage( + self::THOUSANDS_SEPARATOR, + IntlNumberFormatter::CONTENT_LANGUAGE + ); + + $decseparator = $intlNumberFormatter->getSeparatorByLanguage( + self::DECIMAL_SEPARATOR, + IntlNumberFormatter::CONTENT_LANGUAGE + ); + + // #753 + $regex = '/([-+]?\s*(?:' . + // Either numbers like 10,000.99 that start with a digit + '\d+(?:\\' . $kiloseparator . '\d\d\d)*(?:\\' . $decseparator . '\d+)?' . + // or numbers like .001 that start with the decimal separator + '|\\' . $decseparator . '\d+' . + ')\s*(?:[eE][-+]?\d+)?)/u'; + + // #1718 Whether to preserve spaces in unit labels or not (e.g. sq mi, sqmi) + $space = $this->isEnabledFeature( SMW_DV_NUMV_USPACE ) ? ' ' : ''; + + $parts = preg_split( + $regex, + trim( str_replace( [ ' ', ' ', ' ', ' ' ], $space, $value ) ), + 2, + PREG_SPLIT_DELIM_CAPTURE + ); + + if ( count( $parts ) >= 2 ) { + $numstring = str_replace( $kiloseparator, '', preg_replace( '/\s*/u', '', $parts[1] ) ); // simplify + if ( $decseparator != '.' ) { + $numstring = str_replace( $decseparator, '.', $numstring ); + } + list( $number ) = sscanf( $numstring, "%f" ); + if ( count( $parts ) >= 3 ) { + $asPrefix = $parts[0] !== ''; + $unit = $this->normalizeUnit( $parts[0] !== '' ? $parts[0] : $parts[2] ); + } + } + + if ( ( count( $parts ) == 1 ) || ( $numstring === '' ) ) { // no number found + return 1; + } elseif ( is_infinite( $number ) ) { // number is too large for this platform + return 2; + } else { + return 0; + } + } + + /** + * @see DataValue::parseUserValue + */ + protected function parseUserValue( $value ) { + // Set caption + if ( $this->m_caption === false ) { + $this->m_caption = $value; + } + + if ( $value !== '' && $value{0} === ':' ) { + $this->addErrorMsg( [ 'smw-datavalue-invalid-number', $value ] ); + return; + } + + $this->m_unitin = false; + $this->m_unitvalues = false; + $number = $unit = ''; + $error = $this->parseNumberValue( $value, $number, $unit ); + + if ( $error == 1 ) { // no number found + $this->addErrorMsg( [ 'smw_nofloat', $value ] ); + } elseif ( $error == 2 ) { // number is too large for this platform + $this->addErrorMsg( [ 'smw_infinite', $value ] ); + } elseif ( $this->getTypeID() === '_num' && $unit !== '' ) { + $this->addErrorMsg( [ 'smw-datavalue-number-textnotallowed', $unit, $number ] ); + } elseif ( $number === null ) { + $this->addErrorMsg( [ 'smw-datavalue-number-nullnotallowed', $value ] ); // #1628 + } elseif ( $this->convertToMainUnit( $number, $unit ) === false ) { // so far so good: now convert unit and check if it is allowed + $this->addErrorMsg( [ 'smw_unitnotallowed', $unit ] ); + } // note that convertToMainUnit() also sets m_dataitem if valid + } + + /** + * @see SMWDataValue::loadDataItem() + * @param $dataitem SMWDataItem + * @return boolean + */ + protected function loadDataItem( SMWDataItem $dataItem ) { + + if ( $dataItem->getDIType() !== SMWDataItem::TYPE_NUMBER ) { + return false; + } + + $this->m_dataitem = $dataItem; + $this->m_caption = false; + $this->m_unitin = false; + $this->makeUserValue(); + $this->m_unitvalues = false; + + return true; + } + + /** + * @see DataValue::setOutputFormat + * + * @param $string $formatstring + */ + public function setOutputFormat( $formatstring ) { + + if ( $formatstring == $this->m_outformat ) { + return null; + } + + // #1591 + $this->findPreferredLanguageFrom( $formatstring ); + + // #1335 + $this->m_outformat = $this->findPrecisionFrom( $formatstring ); + + if ( $this->isValid() ) { // update caption/unitin for this format + $this->m_caption = false; + $this->m_unitin = false; + $this->makeUserValue(); + } + } + + /** + * @since 1.6 + * + * @return float + */ + public function getNumber() { + + if ( !$this->isValid() ) { + return 999999999999999; + } + + return $this->m_dataitem->getNumber(); + } + + /** + * @since 2.4 + * + * @return float + */ + public function getLocalizedFormattedNumber( $value ) { + return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision() ); + } + + /** + * @since 2.4 + * + * @return float + */ + public function getNormalizedFormattedNumber( $value ) { + return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision(), IntlNumberFormatter::VALUE_FORMAT ); + } + + /** + * @see DataValue::getShortWikiText + * + * @return string + */ + public function getShortWikiText( $linker = null ) { + + if ( $this->valueFormatter === null ) { + $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); + } + + $this->valueFormatter->setDataValue( $this ); + + return $this->valueFormatter->format( DataValueFormatter::WIKI_SHORT, $linker ); + } + + /** + * @see DataValue::getShortHTMLText + * + * @return string + */ + public function getShortHTMLText( $linker = null ) { + + if ( $this->valueFormatter === null ) { + $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); + } + + $this->valueFormatter->setDataValue( $this ); + + return $this->valueFormatter->format( DataValueFormatter::HTML_SHORT, $linker ); + } + + /** + * @see DataValue::getLongWikiText + * + * @return string + */ + public function getLongWikiText( $linker = null ) { + + if ( $this->valueFormatter === null ) { + $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); + } + + $this->valueFormatter->setDataValue( $this ); + + return $this->valueFormatter->format( DataValueFormatter::WIKI_LONG, $linker ); + } + + /** + * @see DataValue::getLongHTMLText + * + * @return string + */ + public function getLongHTMLText( $linker = null ) { + + if ( $this->valueFormatter === null ) { + $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); + } + + $this->valueFormatter->setDataValue( $this ); + + return $this->valueFormatter->format( DataValueFormatter::HTML_LONG, $linker ); + } + + /** + * @see DataValue::getWikiValue + * + * @return string + */ + public function getWikiValue() { + + if ( $this->valueFormatter === null ) { + $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); + } + + $this->valueFormatter->setDataValue( $this ); + + return $this->valueFormatter->format( DataValueFormatter::VALUE ); + } + + /** + * @see DataVelue::getInfolinks + * + * @return array + */ + public function getInfolinks() { + + // When generating an infoLink, use the normalized value without any + // precision limitation + $this->setOption( self::NO_DISP_PRECISION_LIMIT, true ); + $this->setOption( self::OPT_CONTENT_LANGUAGE, Message::CONTENT_LANGUAGE ); + + $infoLinks = parent::getInfolinks(); + + $this->setOption( self::NO_DISP_PRECISION_LIMIT, false ); + + return $infoLinks; + } + + /** + * @since 2.4 + * + * @return string + */ + public function getCanonicalMainUnit() { + return $this->m_unitin; + } + + /** + * Returns array of converted unit-value-pairs that can be + * printed. + * + * @since 2.4 + * + * @return array + */ + public function getConvertedUnitValues() { + $this->makeConversionValues(); + return $this->m_unitvalues; + } + + /** + * Return the unit in which the returned value is to be interpreted. + * This string is a plain UTF-8 string without wiki or html markup. + * The returned value is a canonical ID for the main unit. + * Returns the empty string if no unit is given for the value. + * Overwritten by subclasses that support units. + */ + public function getUnit() { + return ''; + } + + /** + * @since 2.4 + * + * @param string $unit + * + * @return boolean + */ + public function hasPrefixalUnitPreference( $unit ) { + return isset( $this->prefixalUnitPreference[$unit] ) && $this->prefixalUnitPreference[$unit]; + } + + /** + * Create links to mapping services based on a wiki-editable message. + * The parameters available to the message are: + * $1: string of numerical value in English punctuation + * $2: string of integer version of value, in English punctuation + * + * @return array + */ + protected function getServiceLinkParams() { + if ( $this->isValid() ) { + return [ strval( $this->m_dataitem->getNumber() ), strval( round( $this->m_dataitem->getNumber() ) ) ]; + } else { + return []; + } + } + + /** + * Transform a (typically unit-) string into a normalised form, + * so that, e.g., "km²" and "km<sup>2</sup>" do not need to be + * distinguished. + */ + public function normalizeUnit( $unit ) { + $unit = str_replace( [ '[[', ']]' ], '', trim( $unit ) ); // allow simple links to be used inside annotations + $unit = str_replace( [ '²', '<sup>2</sup>' ], '²', $unit ); + $unit = str_replace( [ '³', '<sup>3</sup>' ], '³', $unit ); + return smwfXMLContentEncode( $unit ); + } + + /** + * Compute the value based on the given input number and unit string. + * If the unit is not supported, return false, otherwise return true. + * This is called when parsing user input, where the given unit value + * has already been normalized. + * + * This class does not support any (non-empty) units, but subclasses + * may overwrite this behavior. + * @param $number float value obtained by parsing user input + * @param $unit string after the numericla user input + * @return boolean specifying if the unit string is allowed + */ + protected function convertToMainUnit( $number, $unit ) { + $this->m_dataitem = new SMWDINumber( $number ); + $this->m_unitin = ''; + return ( $unit === '' ); + } + + /** + * This method creates an array of unit-value-pairs that should be + * printed. Units are the keys and should be canonical unit IDs. + * The result is stored in $this->m_unitvalues. Again, any class that + * requires effort for doing this should first check whether the array + * is already set (i.e. not false) before doing any work. + * Note that the values should be plain numbers. Output formatting is done + * later when needed. Also, it should be checked if the value is valid + * before trying to calculate with its contents. + * This method also must call or implement convertToMainUnit(). + * + * Overwritten by subclasses that support units. + */ + protected function makeConversionValues() { + $this->m_unitvalues = [ '' => $this->m_dataitem->getNumber() ]; + } + + /** + * This method is used when no user input was given to find the best + * values for m_unitin and m_caption. After conversion, + * these fields will look as if they were generated from user input, + * and convertToMainUnit() will have been called (if not, it would be + * blocked by the presence of m_unitin). + * + * Overwritten by subclasses that support units. + */ + protected function makeUserValue() { + $this->m_caption = ''; + + $number = $this->m_dataitem->getNumber(); + + // -u is the format for displaying the unit only + if ( $this->m_outformat == '-u' ) { + $this->m_caption = ''; + } elseif ( ( $this->m_outformat != '-' ) && ( $this->m_outformat != '-n' ) ) { + $this->m_caption = $this->getLocalizedFormattedNumber( $number ); + } else { + $this->m_caption = $this->getNormalizedFormattedNumber( $number ); + } + + // no unit ever, so nothing to do about this + $this->m_unitin = ''; + } + + /** + * Return an array of major unit strings (ids only recommended) supported by + * this datavalue. + * + * Overwritten by subclasses that support units. + */ + public function getUnitList() { + return [ '' ]; + } + + protected function getPreferredDisplayPrecision() { + + // Don't restrict the value with a display precision + if ( $this->getProperty() === null || $this->getOption( self::NO_DISP_PRECISION_LIMIT ) ) { + return false; + } + + if ( $this->precision === null ) { + $this->precision = $this->dataValueServiceFactory->getPropertySpecificationLookup()->getDisplayPrecision( + $this->getProperty() + ); + } + + return $this->precision; + } + + private function findPrecisionFrom( $formatstring ) { + + if ( strpos( $formatstring, '-' ) === false ) { + return $formatstring; + } + + $parts = explode( '-', $formatstring ); + + // Find precision from annotated -p<number of digits> formatstring which + // has priority over a possible _PREC value + foreach ( $parts as $key => $value ) { + if ( strpos( $value, 'p' ) !== false && is_numeric( substr( $value, 1 ) ) ) { + $this->precision = strval( substr( $value, 1 ) ); + unset( $parts[$key] ); + } + } + + // Rebuild formatstring without a possible p element to ensure other + // options can be used in combination such as -n-p2 etc. + return implode( '-', $parts ); + } + + private function getNumberFormatter() { + + $this->intlNumberFormatter->setOption( + IntlNumberFormatter::USER_LANGUAGE, + $this->getOption( self::OPT_USER_LANGUAGE ) + ); + + $this->intlNumberFormatter->setOption( + IntlNumberFormatter::CONTENT_LANGUAGE, + $this->getOption( self::OPT_CONTENT_LANGUAGE ) + ); + + $this->intlNumberFormatter->setOption( + self::THOUSANDS_SEPARATOR, + $this->getOption( self::THOUSANDS_SEPARATOR ) + ); + + $this->intlNumberFormatter->setOption( + self::DECIMAL_SEPARATOR, + $this->getOption( self::DECIMAL_SEPARATOR ) + ); + + return $this->intlNumberFormatter; + } + + private function findPreferredLanguageFrom( &$formatstring ) { + // Localized preferred user language + if ( strpos( $formatstring, 'LOCL' ) !== false && ( $languageCode = Localizer::getLanguageCodeFrom( $formatstring ) ) !== false ) { + $this->intlNumberFormatter->setOption( + IntlNumberFormatter::PREFERRED_LANGUAGE, + $languageCode + ); + } + + // Remove any remaining + $formatstring = str_replace( [ '#LOCL', 'LOCL' ], '', $formatstring ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_PropertyList.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_PropertyList.php new file mode 100644 index 00000000..7c255aaa --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_PropertyList.php @@ -0,0 +1,149 @@ +<?php + +/** + * @ingroup SMWDataValues + */ + +/** + * This datavalue implements special processing suitable for defining the list + * of properties that is required for SMWRecordValue objects. The input is a + * plain semicolon-separated list of property names, optionally with the + * namespace prefix. + * + * @author Markus Krötzsch + * @ingroup SMWDataValues + */ +class SMWPropertyListValue extends SMWDataValue { + /** + * List of properte data items that are stored. + * @var array of SMWDIProperty + */ + protected $m_diProperties; + + protected function parseUserValue( $value ) { + global $wgContLang; + + $this->m_diProperties = []; + $stringValue = ''; + $valueList = preg_split( '/[\s]*;[\s]*/u', trim( $value ) ); + foreach ( $valueList as $propertyName ) { + $propertyNameParts = explode( ':', $propertyName, 2 ); + if ( count( $propertyNameParts ) > 1 ) { + $namespace = smwfNormalTitleText( $propertyNameParts[0] ); + $propertyName = $propertyNameParts[1]; + $propertyNamespace = $wgContLang->getNsText( SMW_NS_PROPERTY ); + if ( $namespace != $propertyNamespace ) { + $this->addErrorMsg( [ 'smw_wrong_namespace', $propertyNamespace ] ); + } + } + + $propertyName = smwfNormalTitleText( $propertyName ); + + try { + $diProperty = SMW\DIProperty::newFromUserLabel( $propertyName ); + } catch ( SMWDataItemException $e ) { + $diProperty = new SMW\DIProperty( 'Error' ); + $this->addErrorMsg( [ 'smw_noproperty', $propertyName ] ); + } + + $this->m_diProperties[] = $diProperty; + $stringValue .= ( $stringValue ? ';' : '' ) . $diProperty->getKey(); + } + + $this->m_dataitem = new SMWDIBlob( $stringValue ); + } + + /** + * @see SMWDataValue::loadDataItem() + * + * @param $dataitem SMWDataItem + * + * @return boolean + */ + protected function loadDataItem( SMWDataItem $dataItem ) { + + if ( !$dataItem instanceof SMWDIBlob ) { + return false; + } + + $this->m_dataitem = $dataItem; + $this->m_diProperties = []; + + foreach ( explode( ';', $dataItem->getString() ) as $propertyKey ) { + $property = null; + + try { + $property = new SMW\DIProperty( $propertyKey ); + } catch ( SMWDataItemException $e ) { + $property = new SMW\DIProperty( 'Error' ); + $this->addErrorMsg( [ 'smw-datavalue-propertylist-invalid-property-key', $dataItem->getString(), $propertyKey ] ); + } + + if ( $property instanceof SMWDIProperty ) { + // Find a possible redirect + $this->m_diProperties[] = $property->getRedirectTarget(); + } + } + + $this->m_caption = false; + + return true; + } + + public function getShortWikiText( $linked = null ) { + return ( $this->m_caption !== false ) ? $this->m_caption : $this->makeOutputText( 2, $linked ); + } + + public function getShortHTMLText( $linker = null ) { + return ( $this->m_caption !== false ) ? $this->m_caption : $this->makeOutputText( 3, $linker ); + } + + public function getLongWikiText( $linked = null ) { + return $this->makeOutputText( 2, $linked ); + } + + public function getLongHTMLText( $linker = null ) { + return $this->makeOutputText( 3, $linker ); + } + + public function getWikiValue() { + return $this->makeOutputText( 4 ); + } + + public function getPropertyDataItems() { + return $this->m_diProperties; + } + +////// Internal helper functions + + protected function makeOutputText( $type, $linker = null ) { + if ( !$this->isValid() ) { + return ( ( $type == 0 ) || ( $type == 1 ) ) ? '' : $this->getErrorText(); + } + $result = ''; + $sep = ( $type == 4 ) ? '; ' : ', '; + foreach ( $this->m_diProperties as $diProperty ) { + if ( $result !== '' ) { + $result .= $sep; + } + $propertyValue = \SMW\DataValueFactory::getInstance()->newDataValueByItem( $diProperty, null ); + $result .= $this->makeValueOutputText( $type, $propertyValue, $linker ); + } + return $result; + } + + protected function makeValueOutputText( $type, $propertyValue, $linker ) { + switch ( $type ) { + case 0: + return $propertyValue->getShortWikiText( $linker ); + case 1: + return $propertyValue->getShortHTMLText( $linker ); + case 2: + return $propertyValue->getLongWikiText( $linker ); + case 3: + return $propertyValue->getLongHTMLText( $linker ); + case 4: + return $propertyValue->getWikiValue(); + } + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Quantity.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Quantity.php new file mode 100644 index 00000000..75e1dc57 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Quantity.php @@ -0,0 +1,218 @@ +<?php + +use SMW\DataValues\Number\UnitConverter; +use SMW\Message; + +/** + * @ingroup SMWDataValues + */ + +/** + * This datavalue implements unit support custom units, for which users have + * provided linear conversion factors within the wiki. Those user settings + * are retrieved from a property page associated with this object. + * + * @author Markus Krötzsch + * @ingroup SMWDataValues + */ +class SMWQuantityValue extends SMWNumberValue { + + /** + * DV identifier + */ + const TYPE_ID = '_qty'; + + /** + * Array with format (canonical unit ID string) => (conversion factor) + * @var float[]|bool + */ + protected $m_unitfactors = false; + + /** + * Array with format (normalised unit string) => (canonical unit ID string) + * @var string[]|bool + */ + protected $m_unitids = false; + + /** + * Ordered array of (normalized) units that should be displayed in tooltips, etc. + * @var string[]|bool + */ + protected $m_displayunits = false; + + /** + * Main unit in canonical form (recognised by the conversion factor 1) + * @var string|bool + */ + protected $m_mainunit = false; + + protected function convertToMainUnit( $number, $unit ) { + $this->initConversionData(); + + if ( array_key_exists( $unit, $this->m_unitids ) ) { + $this->m_unitin = $this->m_unitids[$unit]; + assert( $this->m_unitfactors[$this->m_unitin] != 0 /* Should be filtered by initConversionData() */ ); + $this->m_dataitem = new SMWDINumber( $number / $this->m_unitfactors[$this->m_unitin], $this->m_typeid ); + return true; + } else { // unsupported unit + return false; + } + } + + protected function makeConversionValues() { + if ( $this->m_unitvalues !== false ) { + return; // do this only once + } + + $this->m_unitvalues = []; + + if ( !$this->isValid() ) { + return; + } + + $this->initDisplayData(); + + if ( count( $this->m_displayunits ) == 0 ) { // no display units, just show all + foreach ( $this->m_unitfactors as $unit => $factor ) { + if ( $unit !== '' ) { // filter out the empty fallback unit that is always there + $this->m_unitvalues[$unit] = $this->m_dataitem->getNumber() * $factor; + } + } + } else { + foreach ( $this->m_displayunits as $unit ) { + /// NOTE We keep non-ID units unless the input unit is used, so display units can be used to pick + /// the preferred form of a unit. Doing this requires us to recompute the conversion values whenever + /// the m_unitin changes. + $unitkey = ( $this->m_unitids[$unit] == $this->m_unitin ) ? $this->m_unitids[$unit] : $unit; + $this->m_unitvalues[$unitkey] = $this->m_dataitem->getNumber() * $this->m_unitfactors[$this->m_unitids[$unit]]; + } + } + } + + protected function makeUserValue() { + + // The normalised string of a known unit to use for printouts + $printunit = false; + $unitfactor = 1; + + // Check if a known unit is given as outputformat: + if ( ( $this->m_outformat ) && ( $this->m_outformat != '-' ) && + ( $this->m_outformat != '-n' ) && ( $this->m_outformat != '-u' ) ) { // first try given output unit + $wantedunit = $this->normalizeUnit( $this->m_outformat ); + if ( array_key_exists( $wantedunit, $this->m_unitids ) ) { + $printunit = $wantedunit; + } + } + + // Alternatively, try to use the main display unit as a default: + if ( $printunit === false ) { + $this->initDisplayData(); + if ( count( $this->m_displayunits ) > 0 ) { + $printunit = reset( $this->m_displayunits ); + } + } + // Finally, fall back to main unit: + if ( $printunit === false ) { + $printunit = $this->getUnit(); + } + + $asPrefix = isset( $this->prefixalUnitPreference[$printunit] ) && $this->prefixalUnitPreference[$printunit]; + $this->m_unitin = isset( $this->m_unitids[$printunit] ) ? $this->m_unitids[$printunit] : 0; + + // This array depends on m_unitin if displayunits were used, better + // invalidate it here + $this->m_unitvalues = false; + $this->m_caption = ''; + + if ( isset( $this->m_unitfactors[$this->m_unitin] ) ) { + $unitfactor = $this->m_unitfactors[$this->m_unitin]; + } + + $value = $this->m_dataitem->getNumber() * $unitfactor; + + // -u is the format for displaying the unit only + if ( $this->m_outformat != '-u' ) { + $this->m_caption .= ( ( $this->m_outformat != '-' ) && ( $this->m_outformat != '-n' ) ? $this->getLocalizedFormattedNumber( $value ) : $this->getNormalizedFormattedNumber( $value ) ); + } + + // -n is the format for displaying the number only + if ( ( $printunit !== '' ) && ( $this->m_outformat != '-n' ) ) { + + $sep = ''; + + if ( $this->m_outformat != '-u' ) { + $sep = ( $this->m_outformat != '-' ? ' ' : ' ' ); + } + + $this->m_caption = $asPrefix ? $printunit . $sep . $this->m_caption : $this->m_caption . $sep . $printunit; + } + } + + public function getUnitList() { + $this->initConversionData(); + return array_keys( $this->m_unitfactors ); + } + + public function getUnit() { + $this->initConversionData(); + return $this->m_mainunit; + } + +/// The remaining functions are relatively "private" but are kept protected since +/// subclasses might exploit this to, e.g., "fake" conversion factors instead of +/// getting them from the database. A cheap way of making built-in types. + + /** + * This method initializes $m_unitfactors, $m_unitids, and $m_mainunit. + */ + protected function initConversionData() { + + if ( $this->m_unitids !== false ) { + return; + } + + $unitConverter = new UnitConverter( $this ); + $unitConverter->initConversionData( $this->m_property ); + + if ( $unitConverter->getErrors() !== [] ) { + foreach ( $unitConverter->getErrors() as $error ) { + $this->addErrorMsg( + $error, + Message::TEXT, + Message::USER_LANGUAGE + ); + } + } + + $this->m_unitids = $unitConverter->getUnitIds(); + $this->m_unitfactors = $unitConverter->getUnitFactors(); + $this->m_mainunit = $unitConverter->getMainUnit(); + $this->prefixalUnitPreference = $unitConverter->getPrefixalUnitPreference(); + } + + /** + * This method initializes $m_displayunits. + */ + protected function initDisplayData() { + if ( $this->m_displayunits !== false ) { + return; // do the below only once + } + $this->initConversionData(); // needed to normalise unit strings + $this->m_displayunits = []; + + if ( is_null( $this->m_property ) || is_null( $this->m_property->getDIWikiPage() ) ) { + return; + } + + $units = $this->dataValueServiceFactory->getPropertySpecificationLookup()->getDisplayUnits( + $this->getProperty() + ); + + foreach ( $units as $unit ) { + $unit = $this->normalizeUnit( $unit ); + if ( array_key_exists( $unit, $this->m_unitids ) ) { + $this->m_displayunits[] = $unit; // do not avoid duplicates, users can handle this + } // note: we ignore unsuppported units -- no way to display them + } + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Record.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Record.php new file mode 100644 index 00000000..f4bab7e6 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Record.php @@ -0,0 +1,300 @@ +<?php + +use SMW\ApplicationFactory; +use SMW\DataValueFactory; +use SMW\DataValues\AbstractMultiValue; +use SMW\DIProperty; +use SMW\DIWikiPage; +use SMWContainerSemanticData as ContainerSemanticData; +use SMWDataItem as DataItem; +use SMWDIContainer as DIContainer; + +/** + * SMWDataValue implements the handling of small sets of property-value pairs. + * The declaration of Records in SMW uses the order of values to encode the + * property that should be used, so the user only needs to enter a list of + * values. Internally, however, the property-value assignments are not stored + * with a particular order; they will only be ordered for display, following + * the declaration. This is why it is not supported to have Records using the + * same property for more than one value. + * + * The class uses DIContainer objects to return its inner state. See the + * documentation for DIContainer for details on how this "pseudo" data + * encapsulated many property assignments. Such data is stored internally + * like a page with various property-value assignments. Indeed, record values + * can be created from DIWikiPage objects (the missing information will + * be fetched from the store). + * + * @todo Enforce limitation of maximal number of values. + * @todo Enforce uniqueness of properties in declaration. + * @todo Complete internationalisation. + * + * @author Markus Krötzsch + * @ingroup SMWDataValues + */ +class SMWRecordValue extends AbstractMultiValue { + + /// cache for properties for the fields of this data value + protected $m_diProperties = null; + + /** + * @param string $typeid + */ + public function __construct( $typeid = '' ) { + parent::__construct( '_rec' ); + } + + /** + * @since 2.3 + * + * @return DIProperty[]|null + */ + public function getProperties() { + return $this->m_diProperties; + } + + /** + * @since 2.3 + * + * @param string $value + * + * @return array + */ + public function getValuesFromString( $value ) { + // #664 / T17732 + $value = str_replace( "\;", "-3B", $value ); + + // Bug 21926 / T23926 + // Values that use html entities are encoded with a semicolon + $value = htmlspecialchars_decode( $value, ENT_QUOTES ); + $values = preg_split( '/[\s]*;[\s]*/u', trim( $value ) ); + + return str_replace( "-3B", ";", $values ); + } + + protected function parseUserValue( $value ) { + + if ( $value === '' ) { + $this->addErrorMsg( [ 'smw_novalues' ] ); + return; + } + + $containerSemanticData = $this->newContainerSemanticData( $value ); + $sortKeys = []; + + $values = $this->getValuesFromString( $value ); + $valueIndex = 0; // index in value array + $propertyIndex = 0; // index in property list + $empty = true; + + foreach ( $this->getPropertyDataItems() as $diProperty ) { + + if ( !array_key_exists( $valueIndex, $values ) || $this->getErrors() !== [] ) { + break; // stop if there are no values left + } + + // generating the DVs: + if ( ( $values[$valueIndex] === '' ) || ( $values[$valueIndex] == '?' ) ) { // explicit omission + $valueIndex++; + } else { + $dataValue = DataValueFactory::getInstance()->newDataValueByProperty( + $diProperty, + $values[$valueIndex], + false, + $containerSemanticData->getSubject() + ); + + if ( $dataValue->isValid() ) { // valid DV: keep + $containerSemanticData->addPropertyObjectValue( $diProperty, $dataValue->getDataItem() ); + $sortKeys[] = $dataValue->getDataItem()->getSortKey(); + + $valueIndex++; + $empty = false; + } elseif ( ( count( $values ) - $valueIndex ) == ( count( $this->m_diProperties ) - $propertyIndex ) ) { + $containerSemanticData->addPropertyObjectValue( $diProperty, $dataValue->getDataItem() ); + $this->addError( $dataValue->getErrors() ); + ++$valueIndex; + } + } + ++$propertyIndex; + } + + if ( $empty && $this->getErrors() === [] ) { + $this->addErrorMsg( [ 'smw_novalues' ] ); + } + + // Remember the data to extend the sortkey + $containerSemanticData->setExtensionData( 'sort.data', implode( ';', $sortKeys ) ); + + $this->m_dataitem = new DIContainer( $containerSemanticData ); + } + + /** + * @see SMWDataValue::loadDataItem() + * @param $dataitem DataItem + * @return boolean + */ + protected function loadDataItem( DataItem $dataItem ) { + if ( $dataItem->getDIType() == DataItem::TYPE_CONTAINER ) { + $this->m_dataitem = $dataItem; + return true; + } elseif ( $dataItem->getDIType() == DataItem::TYPE_WIKIPAGE ) { + $semanticData = new ContainerSemanticData( $dataItem ); + $semanticData->copyDataFrom( ApplicationFactory::getInstance()->getStore()->getSemanticData( $dataItem ) ); + $this->m_dataitem = new DIContainer( $semanticData ); + return true; + } else { + return false; + } + } + + public function getShortWikiText( $linked = null ) { + if ( $this->m_caption !== false ) { + return $this->m_caption; + } + return $this->makeOutputText( 0, $linked ); + } + + public function getShortHTMLText( $linker = null ) { + if ( $this->m_caption !== false ) { + return $this->m_caption; + } + return $this->makeOutputText( 1, $linker ); + } + + public function getLongWikiText( $linked = null ) { + return $this->makeOutputText( 2, $linked ); + } + + public function getLongHTMLText( $linker = null ) { + return $this->makeOutputText( 3, $linker ); + } + + public function getWikiValue() { + return $this->makeOutputText( 4 ); + } + + /** + * Make sure that the content is reset in this case. + * @todo This is not a full reset yet (the case that property is changed after a value + * was set does not occur in the normal flow of things, hence this has low priority). + */ + public function setProperty( DIProperty $property ) { + parent::setProperty( $property ); + $this->m_diProperties = null; + } + + /** + * @since 2.1 + * + * @param DIProperty[] $properties + */ + public function setFieldProperties( array $properties ) { + foreach ( $properties as $property ) { + if ( $property instanceof DIProperty ) { + $this->m_diProperties[] = $property; + } + } + } + + /** + * @since 1.6 + * + * {@inheritDoc} + */ + public function getDataItems() { + return parent::getDataItems(); + } + + /** + * Return the array (list) of properties that the individual entries of + * this datatype consist of. + * + * @since 1.6 + * + * @todo I18N for error message. + * + * @return array of DIProperty + */ + public function getPropertyDataItems() { + + if ( $this->m_diProperties !== null ) { + return $this->m_diProperties; + } + + $this->m_diProperties = $this->getFieldProperties( $this->m_property ); + + if ( $this->m_diProperties === [] ) { // TODO internalionalize + $this->addError( 'The list of properties to be used for the data fields has not been specified properly.' ); + } + + return $this->m_diProperties; + } + + protected function makeOutputText( $type = 0, $linker = null ) { + if ( !$this->isValid() ) { + return ( ( $type == 0 ) || ( $type == 1 ) ) ? '' : $this->getErrorText(); + } + + $result = ''; + $i = 0; + foreach ( $this->getPropertyDataItems() as $propertyDataItem ) { + if ( $i == 1 ) { + $result .= ( $type == 4 ) ? '; ' : ' ('; + } elseif ( $i > 1 ) { + $result .= ( $type == 4 ) ? '; ' : ', '; + } + ++$i; + $propertyValues = $this->m_dataitem->getSemanticData()->getPropertyValues( $propertyDataItem ); // combining this with next line violates PHP strict standards + $dataItem = reset( $propertyValues ); + if ( $dataItem !== false ) { + $dataValue = DataValueFactory::getInstance()->newDataValueByItem( $dataItem, $propertyDataItem ); + $result .= $this->makeValueOutputText( $type, $dataValue, $linker ); + } else { + $result .= '?'; + } + } + if ( ( $i > 1 ) && ( $type != 4 ) ) { + $result .= ')'; + } + + return $result; + } + + protected function makeValueOutputText( $type, SMWDataValue $dataValue, $linker ) { + switch ( $type ) { + case 0: + return $dataValue->getShortWikiText( $linker ); + case 1: + return $dataValue->getShortHTMLText( $linker ); + case 2: + return $dataValue->getShortWikiText( $linker ); + case 3: + return $dataValue->getShortHTMLText( $linker ); + case 4: + return str_replace( ";", "\;", $dataValue->getWikiValue() ); + } + } + + private function newContainerSemanticData( $value ) { + + if ( $this->m_contextPage === null ) { + $containerSemanticData = ContainerSemanticData::makeAnonymousContainer(); + $containerSemanticData->skipAnonymousCheck(); + } else { + $subobjectName = '_' . hash( 'md4', $value, false ); // md4 is probably fastest of PHP's hashes + + $subject = new DIWikiPage( + $this->m_contextPage->getDBkey(), + $this->m_contextPage->getNamespace(), + $this->m_contextPage->getInterwiki(), + $subobjectName + ); + + $containerSemanticData = new ContainerSemanticData( $subject ); + } + + return $containerSemanticData; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Time.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Time.php new file mode 100644 index 00000000..9a6962cd --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_Time.php @@ -0,0 +1,699 @@ +<?php + +use SMW\DataValues\Time\Components; +use SMW\DataValues\ValueFormatters\DataValueFormatter; +use SMW\Localizer; +use SMWDITime as DITime; + +/** + * @ingroup SMWDataValues + */ + +/** + * This datavalue captures values of dates and times, in many formats, + * throughout history and pre-history. The implementation can handle dates + * across history with full precision for storing, and substantial precision + * for sorting and querying. The range of supported past dates should encompass + * the Beginning of Time according to most of today's theories. The range of + * supported future dates is limited more strictly, but it does also allow + * year numbers in the order of 10^9. + * + * The implementation notices and stores whether parts of a date/time have been + * omitted (as in "2008" or "May 2007"). For all exporting and sorting + * purposes, incomplete dates are completed with defaults (usually using the + * earliest possible time, i.e. interpreting "2008" as "Jan 1 2008 00:00:00"). + * The information on what was unspecified is kept internally for improving + * behavior e.g. for outputs (defaults are not printed when querying for a + * value). This largely uses the precision handling of DITime. + * + * + * Date formats + * + * Dates can be given in many formats, using numbers, month names, and + * abbreviated month names. The preferred interpretation of ambiguous dates + * ("1 2 2008" or even "1 2 3 BC") is controlled by the language file, as is + * the local naming of months. English month names are always supported. + * + * Dates can be given in Gregorian or Julian calendar, set by the token "Jl" + * or "Gr" in the input. If neither is set, a default is chosen: inputs after + * October 15, 1582 (the time when the Gregorian calendar was first inaugurated + * in some parts of the world) are considered Gr, earlier inputs are considered + * Jl. In addition to Jl and Gr, we support "OS" (Old Style English dates that + * refer to the use of Julian calendar with a displaced change of year on March + * 24), JD (direct numerical input in Julian Day notation), and MJD (direct + * numerical input in Modified Julian Day notation as used in aviation and + * space flight). + * + * The class does not support the input of negative year numbers but uses the + * markers "BC"/"BCE" and "AD"/"CE" instead. There is no year 0 in Gregorian or + * Julian calendars, but the class graciously considers this input to mean year + * 1 BC(E). + * + * For prehisoric dates before 9999 BC(E) only year numbers are allowed + * (nothing else makes much sense). At this time, the years of Julian and + * Gregorian calendar still overlap significantly, so the transition to a + * purely solar annotation of prehistoric years is smooth. Technically, the + * class will consider prehistoric dates as Gregorian but very ancient times + * may be interpreted as desired (probably with reference to a physical notion + * of time that is not dependent on revolutions of earth around the sun). + * + * + * Time formats + * + * Times can be in formats like "23:12:45" and "12:30" possibly with additional + * modifiers "am" or "pm". Timezones are supported: the class knows many + * international timezone monikers (e.g. CET or GMT) and also allows time + * offsets directly after a time (e.g. "10:30-3:30" or "14:45:23+2"). Such + * offsets always refer to UTC. Timezones are only used on input and are not + * stored as part of the value. + * + * Time offsets take leap years into account, e.g. the date + * "Feb 28 2004 23:00+2:00" is equivalent to "29 February 2004 01:00:00", while + * "Feb 28 1900 23:00+2:00" is equivalent to "1 March 1900 01:00:00". + * + * Military time format is supported. This consists of 4 or 6 numeric digits + * followed by a one-letter timezone code (e.g. 1240Z is equivalent to 12:40 + * UTC). + * + * + * I18N + * + * Currently, neither keywords like "BCE", "Jl", or "pm", nor timezone monikers + * are internationalized. Timezone monikers may not require this, other than + * possibly for Cyrillic (added when needed). Month names are fully + * internationalized, but English names and abbreviations will also work in all + * languages. The class also supports ordinal day-of-month annotations like + * "st" and "rd", again only for English. + * + * I18N includes the preferred order of dates, e.g. to interpret "5 6 2010". + * + * @todo Theparsing process can encounter many kinds of well-defined problems + * but uses only one error message. More detailed reporting should be done. + * @todo Try to reuse more of MediaWiki's records, e.g. to obtain month names + * or to format dates. The problem is that MW is based on SIO timestamps that + * don't extend to very ancient or future dates, and that MW uses PHP functions + * that are bound to UNIX time. + * + * @author Markus Krötzsch + * @author Fabian Howahl + * @author Terry A. Hurlbut + * @ingroup SMWDataValues + */ +class SMWTimeValue extends SMWDataValue { + + /** + * DV identifier + */ + const TYPE_ID = '_dat'; + + protected $m_dataitem_greg = null; + protected $m_dataitem_jul = null; + + protected $m_wikivalue; // a suitable wiki input value + + /** + * The following are constant (array-valued constants are not supported, hence + * the declaration as private static variable): + * + * @var array + */ + protected static $m_formats = [ + SMW_Y => [ 'y' ], + SMW_YM => [ 'y', 'm' ], + SMW_MY => [ 'm', 'y' ], + SMW_YDM => [ 'y', 'd', 'm' ], + SMW_YMD => [ 'y', 'm', 'd' ], + SMW_DMY => [ 'd', 'm', 'y' ], + SMW_MDY => [ 'm', 'd', 'y' ] + ]; + + /** + * Moment of switchover to Gregorian calendar. + */ + const J1582 = 2299160.5; + + /** + * Offset of Julian Days for Modified JD inputs. + */ + const MJD_EPOCH = 2400000.5; + + /** + * The year before which we do not accept anything but year numbers and + * largely discourage calendar models. + */ + const PREHISTORY = -10000; + + /** + * @see DataValue::parseUserValue + */ + protected function parseUserValue( $value ) { + + $value = Localizer::convertDoubleWidth( $value ); + + $this->m_wikivalue = $value; + $this->m_dataitem = null; + + // Store the caption now + if ( $this->m_caption === false ) { + $this->m_caption = $value; + } + + $timeValueParser = $this->dataValueServiceFactory->getValueParser( + $this + ); + + $timeValueParser->clearErrors(); + + // Parsing is bound to the content language otherwise any change of a user + // preferred user language would negate the parsing results + $timeValueParser->setLanguageCode( + $this->getOption( self::OPT_CONTENT_LANGUAGE ) + ); + + if ( $this->isYear( $value ) ) { + try { + $this->m_dataitem = new DITime( $this->getCalendarModel( null, $value, null, null ), $value ); + } catch ( SMWDataItemException $e ) { + $this->addErrorMsg( [ 'smw-datavalue-time-invalid', $value, $e->getMessage() ] ); + } + } elseif ( $this->isTimestamp( $value ) ) { + $this->m_dataitem = DITime::newFromTimestamp( $value ); + } elseif ( ( $components = $timeValueParser->parse( $value ) ) ) { + + $calendarmodel = $components->get( 'calendarmodel' ); + + if ( ( $calendarmodel == 'JD' ) || ( $calendarmodel == 'MJD' ) ) { + $this->setDateFromJD( $components ); + } else { + $this->setDateFromParsedValues( $components ); + } + } + + foreach ( $timeValueParser->getErrors() as $err ) { + $this->addErrorMsg( $err ); + } + + // Make sure that m_dataitem is set in any case + if ( $this->m_dataitem === null ) { + $this->m_dataitem = new DITime( DITime::CM_GREGORIAN, 32202 ); + } + } + + /** + * Validate and interpret the date components as retrieved when parsing + * a user input. The method takes care of guessing how a list of values + * such as "10 12 13" is to be interpreted using the current language + * settings. The result is stored in the call-by-ref parameter + * $date that uses keys 'y', 'm', 'd' and contains the respective + * numbers as values, or false if not specified. If errors occur, error + * messages are added to the objects list of errors, and false is + * returned. Otherwise, true is returned. + * + * @param $datecomponents array of strings that might belong to the specification of a date + * @param $date array set to result + * + * @return boolean stating if successful + */ + protected function interpretDateComponents( $datecomponents, &$date ) { + + // The following code segment creates a bit vector to encode + // which role each digit of the entered date can take (day, + // year, month). The vector starts with 1 and contains three + // bits per date component, set ot true whenever this component + // could be a month, a day, or a year (this is the order). + // Examples: + // 100 component could only be a month + // 010 component could only be a day + // 001 component could only be a year + // 011 component could be a day or a year but no month etc. + // For three components, we thus get a 10 digit bit vector. + $datevector = 1; + $propercomponents = []; + $justfounddash = true; // avoid two dashes in a row, or dashes at the end + $error = false; + $numvalue = 0; + foreach ( $datecomponents as $component ) { + if ( $component == "-" ) { + if ( $justfounddash ) { + $error = true; + break; + } + $justfounddash = true; + } else { + $justfounddash = false; + $datevector = ( $datevector << 3 ) | $this->checkDateComponent( $component, $numvalue ); + $propercomponents[] = $numvalue; + } + } + + if ( ( $error ) || ( $justfounddash ) || ( count( $propercomponents ) == 0 ) || ( count( $propercomponents ) > 3 ) ) { + + $msgKey = 'smw-datavalue-time-invalid-date-components'; + + if ( $justfounddash ) { + $msgKey .= '-dash'; + } elseif ( count( $propercomponents ) == 0 ) { + $msgKey .= '-empty'; + } elseif ( count( $propercomponents ) > 3 ) { + $msgKey .= '-three'; + } else{ + $msgKey .= '-common'; + } + + $this->addErrorMsg( [ $msgKey, $this->m_wikivalue ] ); + return false; + } + + // Now use the bitvector to find the preferred interpretation of the date components: + $dateformats = Localizer::getInstance()->getLang( $this->getOption( self::OPT_CONTENT_LANGUAGE ) )->getDateFormats(); + $date = [ 'y' => false, 'm' => false, 'd' => false ]; + + foreach ( $dateformats[count( $propercomponents ) - 1] as $formatvector ) { + if ( !( ~$datevector & $formatvector ) ) { // check if $formatvector => $datevector ("the input supports the format") + $i = 0; + foreach ( self::$m_formats[$formatvector] as $fieldname ) { + $date[$fieldname] = $propercomponents[$i]; + $i += 1; + } + break; + } + } + + if ( $date['y'] === false ) { // no band matches the entered date + $this->addErrorMsg( [ 'smw-datavalue-time-invalid-date-components-sequence', $this->m_wikivalue ] ); + return false; + } + + return true; + } + + /** + * Initialise data from an anticipated JD value. + */ + private function setDateFromJD( $components ) { + + $datecomponents = $components->get( 'datecomponents' ); + $calendarmodel = $components->get( 'calendarmodel' ); + $era = $components->get( 'era' ); + $hours = $components->get( 'hours' ); + + if ( ( $era === false ) && ( $hours === false ) && ( $components->get( 'timeoffset' ) == 0 ) ) { + try { + $jd = floatval( isset( $datecomponents[1] ) ? $datecomponents[0] . '.' . $datecomponents[1] : $datecomponents[0] ); + if ( $calendarmodel == 'MJD' ) { + $jd += self::MJD_EPOCH; + } + $this->m_dataitem = DITime::newFromJD( $jd, DITime::CM_GREGORIAN, DITime::PREC_YMDT, $components->get( 'timezone' ) ); + } catch ( SMWDataItemException $e ) { + $this->addErrorMsg( [ 'smw-datavalue-time-invalid-jd', $this->m_wikivalue, $e->getMessage() ] ); + } + } else { + $this->addErrorMsg( [ 'smw-datavalue-time-invalid-jd', $this->m_wikivalue, "NO_EXCEPTION" ] ); + } + } + + /** + * Initialise data from the provided intermediate results after + * parsing, assuming that a conventional date notation is used. + * If errors occur, error messages are added to the objects list of + * errors, and false is returned. Otherwise, true is returned. + * + * @param $datecomponents array of strings that might belong to the specification of a date + * @param $calendarmodesl string if model was set in input, otherwise false + * @param $era string '+' or '-' if provided, otherwise false + * @param $hours integer value between 0 and 24 + * @param $minutes integer value between 0 and 59 + * @param $seconds integer value between 0 and 59, or false if not given + * @param $timeoffset double value for time offset (e.g. 3.5), or false if not given + * + * @return boolean stating if successful + */ + protected function setDateFromParsedValues( $components ) { + + $datecomponents = $components->get( 'datecomponents' ); + $calendarmodel = $components->get( 'calendarmodel' ); + $era = $components->get( 'era' ); + $hours = $components->get( 'hours' ); + $minutes = $components->get( 'minutes' ); + $seconds = $components->get( 'seconds' ); + $microseconds = $components->get( 'microseconds' ); + $timeoffset = $components->get( 'timeoffset' ); + $timezone = $components->get( 'timezone' ); + + $date = false; + + if ( !$this->interpretDateComponents( $datecomponents, $date ) ) { + return false; + } + + // Handle BC: the year is negative. + if ( ( $era == '-' ) && ( $date['y'] > 0 ) ) { // see class documentation on BC, "year 0", and ISO conformance ... + $date['y'] = -( $date['y'] ); + } + + // Keep information about the era + if ( ( $era == '+' ) && ( $date['y'] > 0 ) ) { + $date['y'] = $era . $date['y']; + } + + // Old Style is a special case of Julian calendar model where the change of the year was 25 March: + if ( ( $calendarmodel == 'OS' ) && + ( ( $date['m'] < 3 ) || ( ( $date['m'] == 3 ) && ( $date['d'] < 25 ) ) ) ) { + $date['y']++; + } + + $calmod = $this->getCalendarModel( $calendarmodel, $date['y'], $date['m'], $date['d'] ); + + try { + $this->m_dataitem = new DITime( $calmod, $date['y'], $date['m'], $date['d'], $hours, $minutes, $seconds . '.' . $microseconds, $timezone ); + } catch ( SMWDataItemException $e ) { + $this->addErrorMsg( [ 'smw-datavalue-time-invalid', $this->m_wikivalue, $e->getMessage() ] ); + return false; + } + + // Having more than years or specifying a calendar model does + // not make sense for prehistoric dates, and our calendar + // conversion would not be reliable if JD numbers get too huge: + if ( ( $date['y'] <= self::PREHISTORY ) && + ( ( $this->m_dataitem->getPrecision() > DITime::PREC_Y ) || ( $calendarmodel !== false ) ) ) { + $this->addErrorMsg( [ 'smw-datavalue-time-invalid-prehistoric', $this->m_wikivalue ] ); + return false; + } + + if ( $timeoffset != 0 ) { + $newjd = $this->m_dataitem->getJD() - $timeoffset / 24; + try { + $this->m_dataitem = DITime::newFromJD( $newjd, $calmod, $this->m_dataitem->getPrecision(), $timezone ); + } catch ( SMWDataItemException $e ) { + $this->addErrorMsg( [ 'smw-datavalue-time-invalid-jd', $this->m_wikivalue, $e->getMessage() ] ); + return false; + } + } + + return true; + } + + /** + * Check which roles a string component might play in a date, and + * set the call-by-ref parameter to the proper numerical + * representation. The component string has already been normalized to + * be either a plain number, a month name, or a plain number with "d" + * pre-pended. The result is a bit vector to indicate the possible + * interpretations. + * + * @param $component string + * @param $numvalue integer representing the components value + * + * @return integer that encodes a three-digit bit vector + */ + protected static function checkDateComponent( $component, &$numvalue ) { + + if ( $component === '' ) { // should not happen + $numvalue = 0; + return 0; + } elseif ( is_numeric( $component ) ) { + $numvalue = intval( $component ); + if ( ( $numvalue >= 1 ) && ( $numvalue <= 12 ) ) { + return SMW_DAY_MONTH_YEAR; // can be a month, day or year + } elseif ( ( $numvalue >= 1 ) && ( $numvalue <= 31 ) ) { + return SMW_DAY_YEAR; // can be day or year + } else { // number can just be a year + return SMW_YEAR; + } + } elseif ( $component { 0 } == 'd' ) { // already marked as day + if ( is_numeric( substr( $component, 1 ) ) ) { + $numvalue = intval( substr( $component, 1 ) ); + return ( ( $numvalue >= 1 ) && ( $numvalue <= 31 ) ) ? SMW_DAY : 0; + } else { + return 0; + } + } + + $monthnum = array_search( $component, Components::$monthsShort ); + + if ( $monthnum !== false ) { + $numvalue = $monthnum + 1; + return SMW_MONTH; + } else { + return 0; + } + } + + /** + * Determine the calendar model under which an input should be + * interpreted based on the given input data. + * + * @param $presetmodel mixed string related to a user input calendar model (OS, Jl, Gr) or false + * @param $year integer of the given year (adjusted for BC(E), i.e. possibly negative) + * @param $month mixed integer of the month or false + * @param $day mixed integer of the day or false + * + * @return integer either DITime::CM_GREGORIAN or DITime::CM_JULIAN + */ + protected function getCalendarModel( $presetmodel, $year, $month, $day ) { + + // Old Style is a notational convention of Julian dates only + if ( $presetmodel == 'OS' ) { + $presetmodel = 'Jl'; + } + + if ( $presetmodel === 'Gr' || $presetmodel === 'GR' ) { + return DITime::CM_GREGORIAN; + } elseif ( $presetmodel === 'Jl' || $presetmodel === 'JL' ) { + return DITime::CM_JULIAN; + } + + if ( ( $year > 1582 ) || + ( ( $year == 1582 ) && ( $month > 10 ) ) || + ( ( $year == 1582 ) && ( $month == 10 ) && ( $day > 4 ) ) ) { + return DITime::CM_GREGORIAN; + } elseif ( $year > self::PREHISTORY ) { + return DITime::CM_JULIAN; + } + + // Proleptic Julian years at some point deviate from the count of complete + // revolutions of the earth around the sun hence assume that earlier + // date years are Gregorian (where this effect is very weak). This is + // mostly for internal use since we will not allow users to specify + // calendar models at this scale + return DITime::CM_GREGORIAN; + } + + /** + * @see SMWDataValue::loadDataItem + * + * {@inheritDoc} + */ + protected function loadDataItem( SMWDataItem $dataItem ) { + + if ( $dataItem->getDIType() !== SMWDataItem::TYPE_TIME ) { + return false; + } + + $this->m_dataitem = $dataItem; + $this->m_caption = false; + $this->m_wikivalue = false; + + return true; + } + + /** + * @see SMWDataValue::getShortWikiText + * + * {@inheritDoc} + */ + public function getShortWikiText( $linker = null ) { + return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::WIKI_SHORT, $linker ); + } + + /** + * @see SMWDataValue::getShortHTMLText + * + * {@inheritDoc} + */ + public function getShortHTMLText( $linker = null ) { + return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::HTML_SHORT, $linker ); + } + + /** + * @see SMWDataValue::getLongWikiText + * + * {@inheritDoc} + */ + public function getLongWikiText( $linker = null ) { + return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::WIKI_LONG, $linker ); + } + + /** + * @see SMWDataValue::getLongHTMLText + * + * {@inheritDoc} + */ + public function getLongHTMLText( $linker = null ) { + return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::HTML_LONG, $linker ); + } + + /** + * @todo The preferred caption may not be suitable as a wiki value (i.e. not parsable). + * @see SMWDataValue::getLongHTMLText + * + * {@inheritDoc} + */ + public function getWikiValue() { + return $this->m_wikivalue ? $this->m_wikivalue : strip_tags( $this->getLongWikiText() ); + } + + /** + * @see SMWDataValue::isNumeric + * + * {@inheritDoc} + */ + public function isNumeric() { + return true; + } + + /** + * Return the year number in the given calendar model, or false if + * this number is not available (typically when attempting to get + * prehistoric Julian calendar dates). As everywhere in this class, + * there is no year 0. + * + * @param $calendarmodel integer either DITime::CM_GREGORIAN or DITime::CM_JULIAN + * + * @return mixed typically a number but possibly false + */ + public function getYear( $calendarmodel = DITime::CM_GREGORIAN ) { + + $dataItem = $this->getDataItemForCalendarModel( + $calendarmodel + ); + + if ( $dataItem instanceof DITime ) { + return $dataItem->getYear(); + } + + return false; + } + + /** + * Return the month number in the given calendar model, or false if + * this number is not available (typically when attempting to get + * prehistoric Julian calendar dates). + * + * @param $calendarmodel integer either DITime::CM_GREGORIAN or DITime::CM_JULIAN + * @param $default value to return if month is not set at our level of precision + * + * @return mixed typically a number but possibly anything given as $default + */ + public function getMonth( $calendarmodel = DITime::CM_GREGORIAN, $default = 1 ) { + + $dataItem = $this->getDataItemForCalendarModel( + $calendarmodel + ); + + if ( $dataItem instanceof DITime ) { + return ( $dataItem->getPrecision() >= DITime::PREC_YM ) ? $dataItem->getMonth() : $default; + } + + return false; + } + + /** + * Return the day number in the given calendar model, or false if this + * number is not available (typically when attempting to get + * prehistoric Julian calendar dates). + * + * @param $calendarmodel integer either DITime::CM_GREGORIAN or DITime::CM_JULIAN + * @param $default value to return if day is not set at our level of precision + * + * @return mixed typically a number but possibly anything given as $default + */ + public function getDay( $calendarmodel = DITime::CM_GREGORIAN, $default = 1 ) { + + $dataItem = $this->getDataItemForCalendarModel( + $calendarmodel + ); + + if ( $dataItem instanceof DITime ) { + return ( $dataItem->getPrecision() >= DITime::PREC_YMD ) ? $dataItem->getDay() : $default; + } + + return false; + } + + /** + * @see TimeValueFormatter::getTimeStringFromDataItem + * + * @return + */ + public function getTimeString( $default = '00:00:00' ) { + return $this->dataValueServiceFactory->getValueFormatter( $this )->getTimeString( $default ); + } + + /** + * @deprecated This method is now called getISO8601Date(). It will vanish before SMW 1.7. + */ + public function getXMLSchemaDate( $mindefault = true ) { + return $this->getISO8601Date( $mindefault ); + } + + /** + * @see TimeValueFormatter::getISO8601DateFromDataItem + * + * @param $mindefault boolean determining whether values below the + * precision of our input should be completed with minimal or maximal + * conceivable values + * + * @return string + */ + public function getISO8601Date( $mindefault = true ) { + return $this->dataValueServiceFactory->getValueFormatter( $this )->getISO8601Date( $mindefault ); + } + + /** + * @see TimeValueFormatter::getMediaWikiDateFromDataItem + * + * @return string + */ + public function getMediaWikiDate() { + return $this->dataValueServiceFactory->getValueFormatter( $this )->getMediaWikiDate(); + } + + /** + * Get the current data in the specified calendar model. Conversion is + * not done for prehistoric dates (where it might lead to precision + * errors and produce results that are not meaningful). In this case, + * null might be returned if no data in the specified format is + * available. + * + * @param $calendarmodel integer one of DITime::CM_GREGORIAN or DITime::CM_JULIAN + * + * @return DITime + */ + public function getDataItemForCalendarModel( $calendarmodel ) { + if ( $this->m_dataitem->getYear() <= self::PREHISTORY ) { + return ( $this->m_dataitem->getCalendarModel() == $calendarmodel ) ? $this->m_dataitem : null; + } elseif ( $calendarmodel == DITime::CM_GREGORIAN ) { + if ( is_null( $this->m_dataitem_greg ) ) { + $this->m_dataitem_greg = $this->m_dataitem->getForCalendarModel( DITime::CM_GREGORIAN ); + } + return $this->m_dataitem_greg; + } else { + if ( is_null( $this->m_dataitem_jul ) ) { + $this->m_dataitem_jul = $this->m_dataitem->getForCalendarModel( DITime::CM_JULIAN ); + } + return $this->m_dataitem_jul; + } + } + + private function isYear( $value ) { + return strpos( $value, ' ' ) === false && is_numeric( strval( $value ) ) && ( strval( $value ) < 0 || strlen( $value ) < 6 ); + } + + private function isTimestamp( $value ) { + // 1200-11-02T12:03:25 or 20120320055913 + // avoid things like 2458119.500000 (JD) + return ( ( strlen( $value ) > 4 && substr( $value, 10, 1 ) === 'T' ) || ( strlen( $value ) == 14 && strpos( $value, '.' ) === false ) ) && wfTimestamp( TS_MW, $value ) !== false; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_URI.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_URI.php new file mode 100644 index 00000000..94724268 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DV_URI.php @@ -0,0 +1,379 @@ +<?php + +use SMW\Encoder; +use SMW\Message; + +/** + * @ingroup SMWDataValues + */ + +define( 'SMW_URI_MODE_EMAIL', 1 ); +define( 'SMW_URI_MODE_URI', 3 ); +define( 'SMW_URI_MODE_ANNOURI', 4 ); +define( 'SMW_URI_MODE_TEL', 5 ); + +/** + * This datavalue implements URL/URI/ANNURI/PHONE/EMAIL datavalues suitable for + * defining the respective types of properties. + * + * @author Nikolas Iwan + * @author Markus Krötzsch + * @ingroup SMWDataValues + * @bug Correctly create safe HTML and Wiki text. + */ +class SMWURIValue extends SMWDataValue { + + /** + * Raw value without encoding + */ + const VALUE_RAW = 'uri.value.raw'; + + /** + * The value as returned by getWikitext() and getLongText(). + * @var string + */ + protected $m_wikitext; + /** + * One of the basic modes of operation for this class (emails, URL, + * telephone number URI, ...). + * @var integer + */ + private $m_mode; + + /** + * @var boolean + */ + private $showUrlContextInRawFormat = true; + + /** + * @var array + */ + private $schemeList = []; + + public function __construct( $typeid ) { + parent::__construct( $typeid ); + switch ( $typeid ) { + case '_ema': + $this->m_mode = SMW_URI_MODE_EMAIL; + break; + case '_anu': + $this->m_mode = SMW_URI_MODE_ANNOURI; + break; + case '_tel': + $this->m_mode = SMW_URI_MODE_TEL; + break; + case '__spu': + case '_uri': + case '_url': + default: + $this->m_mode = SMW_URI_MODE_URI; + break; + } + + $this->schemeList = array_flip( $GLOBALS['smwgURITypeSchemeList'] ); + } + + protected function parseUserValue( $value ) { + $value = trim( $value ); + $this->m_wikitext = $value; + if ( $this->m_caption === false ) { + $this->m_caption = $this->m_wikitext; + } + + $scheme = $hierpart = $query = $fragment = ''; + if ( $value === '' ) { // do not accept empty strings + $this->addErrorMsg( [ 'smw_emptystring' ] ); + return; + } + + switch ( $this->m_mode ) { + case SMW_URI_MODE_URI: + case SMW_URI_MODE_ANNOURI: + + // Whether the url value was externally encoded or not + if ( strpos( $value, "%" ) === false ) { + $this->showUrlContextInRawFormat = false; + } + + // If somehow the slash was encoded bring into one format + $value = str_replace( "%2F", "/", $value ); + + $parts = explode( ':', $value, 2 ); // try to split "schema:rest" + if ( count( $parts ) == 1 ) { // possibly add "http" as default + $value = 'http://' . $value; + $parts[1] = $parts[0]; + $parts[0] = 'http'; + } + // check against blacklist + $uri_blacklist = explode( "\n", Message::get( 'smw_uri_blacklist', Message::TEXT, Message::CONTENT_LANGUAGE ) ); + foreach ( $uri_blacklist as $uri ) { + $uri = trim( $uri ); + if ( $uri !== '' && $uri == mb_substr( $value, 0, mb_strlen( $uri ) ) ) { // disallowed URI! + $this->addErrorMsg( [ 'smw_baduri', $value ] ); + return; + } + } + // decompose general URI components + $scheme = $parts[0]; + + if ( !$this->getOption( self::OPT_QUERY_CONTEXT ) && !isset( $this->schemeList[$scheme] ) ) { + $this->addErrorMsg( [ 'smw-datavalue-uri-invalid-scheme', $scheme ] ); + return; + } + + $parts = explode( '?', $parts[1], 2 ); // try to split "hier-part?queryfrag" + if ( count( $parts ) == 2 ) { + $hierpart = $parts[0]; + $parts = explode( '#', $parts[1], 2 ); // try to split "query#frag" + $query = $parts[0]; + $fragment = ( count( $parts ) == 2 ) ? $parts[1] : ''; + } else { + $query = ''; + $parts = explode( '#', $parts[0], 2 ); // try to split "hier-part#frag" + $hierpart = $parts[0]; + $fragment = ( count( $parts ) == 2 ) ? $parts[1] : ''; + } + // We do not validate the URI characters (the data item will do this) but we do some escaping: + // encode most characters, but leave special symbols as given by user: + $hierpart = str_replace( [ '%3A', '%2F', '%23', '%40', '%3F', '%3D', '%26', '%25' ], [ ':', '/', '#', '@', '?', '=', '&', '%' ], rawurlencode( $hierpart ) ); + $query = str_replace( [ '%3A', '%2F', '%23', '%40', '%3F', '%3D', '%26', '%25' ], [ ':', '/', '#', '@', '?', '=', '&', '%' ], rawurlencode( $query ) ); + $fragment = str_replace( [ '%3A', '%2F', '%23', '%40', '%3F', '%3D', '%26', '%25' ], [ ':', '/', '#', '@', '?', '=', '&', '%' ], rawurlencode( $fragment ) ); + /// NOTE: we do not support raw [ (%5D) and ] (%5E), although they are needed for ldap:// (but rarely in a wiki) + /// NOTE: "+" gets encoded, as it is interpreted as space by most browsers when part of a URL; + /// this prevents tel: from working directly, but we have a datatype for this anyway. + + if ( substr( $hierpart, 0, 2 ) === '//' ) { + $hierpart = substr( $hierpart, 2 ); + } + + // #3540 + if ( $hierpart !== '' && $hierpart[0] === '/' ) { + return $this->addErrorMsg( [ 'smw-datavalue-uri-invalid-authority-path-component', $value, $hierpart ] ); + } + + break; + case SMW_URI_MODE_TEL: + $scheme = 'tel'; + + if ( substr( $value, 0, 4 ) === 'tel:' ) { // accept optional "tel" + $value = substr( $value, 4 ); + $this->m_wikitext = $value; + } + + $hierpart = preg_replace( '/(?<=[0-9]) (?=[0-9])/', '\1-\2', $value ); + $hierpart = str_replace( ' ', '', $hierpart ); + if ( substr( $hierpart, 0, 2 ) == '00' ) { + $hierpart = '+' . substr( $hierpart, 2 ); + } + + if ( !$this->getOption( self::OPT_QUERY_CONTEXT ) && ( ( strlen( preg_replace( '/[^0-9]/', '', $hierpart ) ) < 6 ) || + ( preg_match( '<[-+./][-./]>', $hierpart ) ) || + ( !self::isValidTelURI( 'tel:' . $hierpart ) ) ) ) { /// TODO: introduce error-message for "bad" phone number + $this->addErrorMsg( [ 'smw_baduri', $this->m_wikitext ] ); + return; + } + break; + case SMW_URI_MODE_EMAIL: + $scheme = 'mailto'; + if ( strpos( $value, 'mailto:' ) === 0 ) { // accept optional "mailto" + $value = substr( $value, 7 ); + $this->m_wikitext = $value; + } + + if ( !$this->getOption( self::OPT_QUERY_CONTEXT ) && !Sanitizer::validateEmail( $value ) ) { + /// TODO: introduce error-message for "bad" email + $this->addErrorMsg( [ 'smw_baduri', $value ] ); + return; + } + $hierpart = str_replace( [ '%3A', '%2F', '%23', '%40', '%3F', '%3D', '%26', '%25' ], [ ':', '/', '#', '@', '?', '=', '&', '%' ], rawurlencode( $value ) ); + } + + // Now create the URI data item: + try { + $this->m_dataitem = new SMWDIUri( $scheme, $hierpart, $query, $fragment, !$this->getOption( self::OPT_QUERY_CONTEXT ) ); + } catch ( SMWDataItemException $e ) { + $this->addErrorMsg( [ 'smw_baduri', $this->m_wikitext ] ); + } + } + + /** + * Returns true if the argument is a valid RFC 3966 phone number. + * Only global phone numbers are supported, and no full validation + * of parameters (appended via ;param=value) is performed. + */ + protected static function isValidTelURI( $s ) { + $tel_uri_regex = '<^tel:\+[0-9./-]*[0-9][0-9./-]*(;[0-9a-zA-Z-]+=(%[0-9a-zA-Z][0-9a-zA-Z]|[0-9a-zA-Z._~:/?#[\]@!$&\'()*+,;=-])*)*$>'; + return (bool) preg_match( $tel_uri_regex, $s ); + } + + /** + * @see SMWDataValue::loadDataItem() + * @param $dataitem SMWDataItem + * @return boolean + */ + protected function loadDataItem( SMWDataItem $dataItem ) { + + if ( $dataItem->getDIType() !== SMWDataItem::TYPE_URI ) { + return false; + } + + $this->m_dataitem = $dataItem; + if ( $this->m_mode == SMW_URI_MODE_EMAIL ) { + $this->m_wikitext = substr( $dataItem->getURI(), 7 ); + } elseif ( $this->m_mode == SMW_URI_MODE_TEL ) { + $this->m_wikitext = substr( $dataItem->getURI(), 4 ); + } else { + $this->m_wikitext = $dataItem->getURI(); + } + + $this->m_caption = $this->m_wikitext; + $this->showUrlContextInRawFormat = false; + + return true; + } + + public function getShortWikiText( $linked = null ) { + + list( $url, $caption ) = $this->decodeUriContext( $this->m_caption, $linked ); + + if ( is_null( $linked ) || ( $linked === false ) || ( $url === '' ) || + ( $this->m_outformat == '-' ) || ( $this->m_caption === '' ) ) { + return $caption; + } elseif ( $this->m_outformat == 'nowiki' ) { + return $this->makeNonlinkedWikiText( $caption ); + } else { + return '[' . $url . ' ' . $caption . ']'; + } + } + + public function getShortHTMLText( $linker = null ) { + + list( $url, $caption ) = $this->decodeUriContext( $this->m_caption, $linker ); + + if ( is_null( $linker ) || ( !$this->isValid() ) || ( $url === '' ) || + ( $this->m_outformat == '-' ) || ( $this->m_outformat == 'nowiki' ) || + ( $this->m_caption === '' ) || $linker === false ) { + return $caption; + } else { + return $linker->makeExternalLink( $url, $caption ); + } + } + + public function getLongWikiText( $linked = null ) { + + if ( !$this->isValid() ) { + return $this->getErrorText(); + } + + list( $url, $wikitext ) = $this->decodeUriContext( $this->m_wikitext, $linked ); + + if ( is_null( $linked ) || ( $linked === false ) || ( $url === '' ) || + ( $this->m_outformat == '-' ) || $linked === false ) { + return $wikitext; + } elseif ( $this->m_outformat == 'nowiki' ) { + return $this->makeNonlinkedWikiText( $wikitext ); + } else { + return '[' . $url . ' ' . $wikitext . ']'; + } + } + + public function getLongHTMLText( $linker = null ) { + + if ( !$this->isValid() ) { + return $this->getErrorText(); + } + + list( $url, $wikitext ) = $this->decodeUriContext( $this->m_wikitext, $linker ); + + if ( is_null( $linker ) || ( !$this->isValid() ) || ( $url === '' ) || + ( $this->m_outformat == '-' ) || ( $this->m_outformat == 'nowiki' ) || $linker === false ) { + return $wikitext; + } else { + return $linker->makeExternalLink( $url, $wikitext ); + } + } + + public function getWikiValue() { + + if ( $this->getOption( self::VALUE_RAW ) ) { + return rawurldecode( $this->m_wikitext ); + } + + return $this->m_wikitext; + } + + public function getURI() { + return $this->getUriDataitem()->getURI(); + } + + protected function getServiceLinkParams() { + // Create links to mapping services based on a wiki-editable message. The parameters + // available to the message are: + // $1: urlencoded version of URI/URL value (includes mailto: for emails) + return [ rawurlencode( $this->getUriDataitem()->getURI() ) ]; + } + + /** + * Get a URL for hyperlinking this URI, or the empty string if this URI + * is not hyperlinked in MediaWiki. + * @return string + */ + public function getURL() { + global $wgUrlProtocols; + + foreach ( $wgUrlProtocols as $prot ) { + if ( ( $prot == $this->getUriDataitem()->getScheme() . ':' ) || ( $prot == $this->getUriDataitem()->getScheme() . '://' ) ) { + return $this->getUriDataitem()->getURI(); + } + } + + return ''; + } + + /** + * Helper function to get the current dataitem, or some dummy URI + * dataitem if the dataitem was not set. This makes it easier to + * write code that avoids errors even if the data was not + * initialized properly. + * @return SMWDIUri + */ + protected function getUriDataitem() { + if ( isset( $this->m_dataitem ) ) { + return $this->m_dataitem; + } else { // note: use "noprotocol" to avoid accidental use in an MW link, see getURL() + return new SMWDIUri( 'noprotocol', 'x', '', '', $this->m_typeid ); + } + } + + /** + * Helper function that changes a URL string in such a way that it + * can be used in wikitext without being turned into a hyperlink, + * while still displaying the same characters. The use of + * <nowiki> is avoided, since the resulting strings may be + * inserted during parsing, after this has been stripped. + * + * @since 1.8 + */ + protected function makeNonlinkedWikiText( $url ) { + return str_replace( ':', ':', $url ); + } + + private function decodeUriContext( $context, $linker ) { + + // Prior to decoding turn any `-` into an internal representation to avoid + // potential breakage + if ( !$this->showUrlContextInRawFormat ) { + $context = Encoder::decode( str_replace( '-', '-2D', $context ) ); + } + + if ( $this->m_mode !== SMW_URI_MODE_EMAIL && $linker !== null ) { + $context = str_replace( '_', ' ', $context ); + } + + // Allow the display without `_` so that URIs can be split + // during the outout by the browser without breaking the URL itself + // as it contains the `_` for spaces + return [ $this->getURL(), $context ]; + } + +} 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; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DataValue.php b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DataValue.php new file mode 100644 index 00000000..a4becb46 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/datavalues/SMW_DataValue.php @@ -0,0 +1,897 @@ +<?php + +/** + * This group contains all parts of SMW that relate to the processing of datavalues + * of various types. + * + * @defgroup SMWDataValues SMWDataValues + * @ingroup SMW + */ + +use SMW\DataValues\InfoLinksProvider; +use SMW\Deserializers\DVDescriptionDeserializerRegistry; +use SMW\DIProperty; +use SMW\Localizer; +use SMW\Message; +use SMW\Options; +use SMW\Query\QueryComparator; +use SMW\Services\DataValueServiceFactory; +use SMW\Utils\CharArmor; + +/** + * Objects of this type represent all that is known about a certain user-provided + * data value, especially its various representations as strings, tooltips, + * numbers, etc. Objects can be created as "empty" containers of a certain type, + * but are then usually filled with data to present one particular data value. + * + * Data values have two chief representation forms: the user-facing syntax and the + * internal representation. In user syntax, every value is (necessarily) a single + * string, however complex the value is. For example, a string such as "Help:editing" + * may represent a wiki page called "Editing" in the namespace for "Help". The + * internal representation may be any numerical array of strings and numbers. In the + * example, it might be array("Editing",12), where 12 is the number used for identifying + * the namespace "Help:". Of course, the internal representation could also use a single + * string value, such as in array("Help:Editing"), but this might be less useful for + * certain operations (e.g. filterng by namespace). Moreover, all values that are + * restored from the database are given in the internal format, so it wise to choose a + * format that allows for very fast and easy processing without unnecessary parsing. + * + * The main functions of data value objects are: + * - setUserValue() which triggers parseUserValue() to process a user-level string. + * + * In addition, there are a number of get-functions that provide useful output versions + * for displaying and serializing the value. + * + * @ingroup SMWDataValues + * + * @author Markus Krötzsch + */ +abstract class SMWDataValue { + + /** + * Contains the user language a user operates in. + */ + const OPT_USER_LANGUAGE = 'user.language'; + + /** + * Contains either the global "site" content language or a specified page + * content language invoked by the context page. + */ + const OPT_CONTENT_LANGUAGE = 'content.language'; + + /** + * Describes a state where a DataValue is part of a query condition and may + * (or not) require a different treatment. + */ + const OPT_QUERY_CONTEXT = 'query.context'; + + /** + * Describes a state where a DataValue is part of a query condition and + * contains a comparator. + */ + const OPT_QUERY_COMP_CONTEXT = 'query.comparator.context'; + + /** + * Option to disable related infolinks + */ + const OPT_DISABLE_INFOLINKS = 'disable.infolinks'; + + /** + * Option to disable service links + */ + const OPT_DISABLE_SERVICELINKS = 'disable.servicelinks'; + + /** + * Option to use compact infolinks + */ + const OPT_COMPACT_INFOLINKS = 'compact.infolinks'; + + /** + * Associated data item. This is the reference to the immutable object + * that represents the current data content. All other data stored here + * is only about presentation and parsing, but is not relevant to the + * actual data that is represented (and stored later on). + * + * This variable must always be set to some data item, even if there + * have been errors in initialising the data. + * @var SMWDataItem + */ + protected $m_dataitem; + + /** + * The property for which this value is constructed or null if none + * given. Property pages are used to make settings that affect parsing + * and display, hence it is sometimes needed to know them. + * + * @var DIProperty + */ + protected $m_property = null; + + /** + * Wiki page in the context of which the value is to be interpreted, or + * null if not given (or not on a page). This information is used to + * parse user values such as "#subsection" which only make sense when + * used on a certain page. + * + * @var SMWDIWikiPage + */ + protected $m_contextPage = null; + + /** + * The text label to be used for output or false if none given. + * @var string + */ + protected $m_caption; + + /** + * The type id for this value object. + * @var string + */ + protected $m_typeid; + + /** + * Output formatting string, false when not set. + * @see setOutputFormat() + * @var mixed + */ + protected $m_outformat = false; + + /** + * Array of error text messages. Private to allow us to track error insertion + * (PHP's count() is too slow when called often) by using $mHasErrors. + * @var array + */ + private $mErrors = []; + + /** + * Boolean indicating if there where any errors. + * Should be modified accordingly when modifying $mErrors. + * @var boolean + */ + private $mHasErrors = false; + + /** + * @var false|array + */ + protected $restrictionError = false; + + /** + * @var Options + */ + private $options; + + /** + * @var InfoLinksProvider + */ + private $infoLinksProvider = null; + + /** + * @var string + */ + private $userValue = ''; + + /** + * @var DataValueServiceFactory + */ + protected $dataValueServiceFactory; + + /** + * Constructor. + * + * @param string $typeid + */ + public function __construct( $typeid ) { + $this->m_typeid = $typeid; + } + + /** + * Return a short string that unambiguously specify the type of this + * value. This value will globally be used to identify the type of a + * value (in spite of the class it actually belongs to, which can still + * implement various types). + */ + public function getTypeID() { + return $this->m_typeid; + } + + /** + * Set the user value (and compute other representations if possible). + * The given value is a string as supplied by some user. An alternative + * label for printout might also be specified. + * + * @param string $value + * @param mixed $caption + */ + public function setUserValue( $value, $caption = false ) { + + $this->m_dataitem = null; + $this->mErrors = []; // clear errors + $this->mHasErrors = false; + $this->m_caption = is_string( $caption ) ? trim( $caption ) : false; + $this->userValue = $value; + + // #2435 + $value = CharArmor::removeControlChars( + CharArmor::removeSpecialChars( $value ) + ); + + // Process may set a caption if not set yet, depending on datavalue + $this->parseUserValue( $value ); + + // The following checks for Strip markers generated by MediaWiki to handle special content, + // from parser and extension tags e.g. <pre>,<nowiki>,<math>,<source>. + // See https://en.wikipedia.org/wiki/Help:Strip_markers + // In general, we are not prepared to handle such content properly, and we + // also have no means of obtaining the user input at this point. Hence the assignment + // just fails, even if parseUserValue() above might not have noticed this issue. + // Note: \x07 was used in MediaWiki 1.11.0, \x7f is used now (backwards compatibility, b/c) + if ( is_string( $value ) && ( ( strpos( $value, "\x7f" ) !== false ) || ( strpos( $value, "\x07" ) !== false ) ) ) { + $this->addErrorMsg( [ 'smw-datavalue-stripmarker-parse-error', $value ] ); + } + + if ( $this->isValid() && !$this->getOption( self::OPT_QUERY_CONTEXT ) ) { + $this->checkAllowedValues(); + } + } + + /** + * Set the actual data contained in this object. The method returns + * true if this was successful (requiring the type of the dataitem + * to match the data value). If false is returned, the data value is + * left unchanged (the data item was rejected). + * + * @note Even if this function returns true, the data value object + * might become invalid if the content of the data item caused errors + * in spite of it being of the right basic type. False is only returned + * if the data item is fundamentally incompatible with the data value. + * + * @param $dataitem SMWDataItem + * @return boolean + */ + public function setDataItem( SMWDataItem $dataItem ) { + $this->m_dataitem = null; + $this->mErrors = []; + $this->mHasErrors = $this->m_caption = false; + return $this->loadDataItem( $dataItem ); + } + + /** + * @since 2.5 + * + * @param DataValueServiceFactory $dataValueServiceFactory + */ + public function setDataValueServiceFactory( DataValueServiceFactory $dataValueServiceFactory ) { + $this->dataValueServiceFactory = $dataValueServiceFactory; + } + + /** + * Specify the property to which this value refers. Property pages are + * used to make settings that affect parsing and display, hence it is + * sometimes needed to know them. + * + * @since 1.6 + * + * @param DIProperty $property + */ + public function setProperty( DIProperty $property ) { + $this->m_property = $property; + } + + /** + * Returns the property to which this value refers. + * + * @since 1.8 + * + * @return DIProperty|null + */ + public function getProperty() { + return $this->m_property; + } + + /** + * Specify the wiki page to which this value refers. This information is + * used to parse user values such as "#subsection" which only make sense + * when used on a certain page. + * + * @since 1.7 + * + * @param SMWDIWikiPage|null $contextPage + */ + public function setContextPage( SMWDIWikiPage $contextPage = null ) { + $this->m_contextPage = $contextPage; + + $this->setOption( + self::OPT_CONTENT_LANGUAGE, + Localizer::getInstance()->getPreferredContentLanguage( $contextPage )->getCode() + ); + } + + /** + * @since 2.4 + * + * @return DIWikiPage|null + */ + public function getContextPage() { + return $this->m_contextPage; + } + + /** + * Change the caption (the text used for displaying this datavalue). The given + * value must be a string. + * + * @param string $caption + */ + public function setCaption( $caption ) { + $this->m_caption = $caption; + } + + /** + * @since 2.4 + * + * @param string $caption + */ + public function getCaption() { + return $this->m_caption; + } + + /** + * Returns a preferred caption and may deviate from the standard caption as + * a subclass is permitted to override this method and provide a more + * contextualized display representation (language or value context etc.). + * + * @since 2.4 + * + * @return string + */ + public function getPreferredCaption() { + return $this->m_caption; + } + + /** + * Define a particular output format. Output formats are user-supplied strings + * that the datavalue may (or may not) use to customise its return value. For + * example, quantities with units of measurement may interpret the string as + * a desired output unit. In other cases, the output format might be built-in + * and subject to internationalisation (which the datavalue has to implement). + * In any case, an empty string resets the output format to the default. + * + * There is one predefined output format that all datavalues should respect: the + * format '-' indicates "plain" output that is most useful for further processing + * the value in a template. It should not use any wiki markup or beautification, + * and it should also avoid localization to the current language. When users + * explicitly specify an empty format string in a query, it is normalized to "-" + * to avoid confusion. Note that empty format strings are not interpreted in + * this way when directly passed to this function. + * + * @param string $formatString + */ + public function setOutputFormat( $formatString ) { + $this->m_outformat = $formatString; // just store it, subclasses may or may not use this + } + + /** + * @since 2.4 + * + * @return string + */ + public function getOutputFormat() { + return $this->m_outformat; + } + + /** + * Add a new error string or array of such strings to the error list. + * + * @note Errors should not be escaped here in any way, in contradiction to what + * the docs used to say here in 1.5 and before. Escaping should happen at the output. + * + * @param mixed $error A single string, or array of strings. + */ + public function addError( $error ) { + if ( is_array( $error ) ) { + $this->mErrors = array_merge( $this->mErrors, $error ); + $this->mHasErrors = $this->mHasErrors || ( count( $error ) > 0 ); + } else { + $this->mErrors[] = $error; + $this->mHasErrors = true; + } + } + + /** + * Messages are not resolved until the output and instead will be kept with the + * message and argument keys (e.g. `[2,"smw_baduri","~*0123*"]`). This allows to + * switch the a representation without requiring language context by the object + * that reports an error. + * + * @since 2.4 + * + * @param $parameters + * @param integer|null $type + * @param integer|null $language + */ + public function addErrorMsg( $parameters, $type = null ) { + $this->mErrors[Message::getHash( $parameters, $type )] = Message::encode( $parameters, $type ); + $this->mHasErrors = true; + } + + /** + * Return a string that displays all error messages as a tooltip, or + * an empty string if no errors happened. + * + * @return string + */ + public function getErrorText() { + return smwfEncodeMessages( $this->mErrors ); + } + + /** + * Return an array of error messages, or an empty array + * if no errors occurred. + * + * @return array + */ + public function getErrors() { + return $this->mErrors; + } + + /** + * @since 3.0 + * + * @return array|false + */ + public function getRestrictionError() { + return $this->restrictionError; + } + + /** + * @since 2.4 + */ + public function clearErrors() { + $this->mErrors = []; + $this->mHasErrors = false; + } + +///// Query support ///// + + /** + * FIXME 3.0, allow NULL as value + * + * @see DataValueDescriptionDeserializer::deserialize + * + * @note Descriptions of values need to know their property to be able to + * create a parsable wikitext version of a query condition again. Thus it + * might be necessary to call setProperty() before using this method. + * + * @param string $value + * + * @return Description + * @throws InvalidArgumentException + */ + public function getQueryDescription( $value ) { + + $descriptionDeserializer = DVDescriptionDeserializerRegistry::getInstance()->getDescriptionDeserializerBy( $this ); + $description = $descriptionDeserializer->deserialize( $value ); + + foreach ( $descriptionDeserializer->getErrors() as $error ) { + $this->addError( $error ); + } + + return $description; + } + + /** + * @deprecated since 2.3 + * + * @see DescriptionDeserializer::prepareValue + * + * This method should no longer be used for direct public access, instead a + * DataValue is expected to register a DescriptionDeserializer with + * DVDescriptionDeserializerRegistry. + */ + static public function prepareValue( &$value, &$comparator ) { + $comparator = QueryComparator::getInstance()->extractComparatorFromString( $value ); + } + +///// Get methods ///// + + /** + * Get the actual data contained in this object or null if the data is + * not defined (due to errors or due to not being set at all). + * @note Most implementations ensure that a data item is always set, + * even if errors occurred, to avoid additional checks for not + * accessing null. Hence, one must not assume that a non-null return + * value here implies that isValid() returns true. + * + * @since 1.6 + * + * @return SMWDataItem|SMWDIError + */ + public function getDataItem() { + + if ( $this->isValid() ) { + return $this->m_dataitem; + } + + return new SMWDIError( $this->mErrors, $this->userValue ); + } + + /** + * @since 2.2 + * + * @return string + */ + public function __toString() { + return $this->getDataItem()->getSerialization(); + } + + /** + * Returns a short textual representation for this data value. If the value + * was initialised from a user supplied string, then this original string + * should be reflected in this short version (i.e. no normalisation should + * normally happen). There might, however, be additional parts such as code + * for generating tooltips. The output is in wiki text. + * + * The parameter $linked controls linking of values such as titles and should + * be non-NULL and non-false if this is desired. + */ + abstract public function getShortWikiText( $linked = null ); + + /** + * Returns a short textual representation for this data value. If the value + * was initialised from a user supplied string, then this original string + * should be reflected in this short version (i.e. no normalisation should + * normally happen). There might, however, be additional parts such as code + * for generating tooltips. The output is in HTML text. + * + * The parameter $linker controls linking of values such as titles and should + * be some Linker object (or NULL for no linking). + */ + abstract public function getShortHTMLText( $linker = null ); + + /** + * Return the long textual description of the value, as printed for + * example in the factbox. If errors occurred, return the error message + * The result always is a wiki-source string. + * + * The parameter $linked controls linking of values such as titles and should + * be non-NULL and non-false if this is desired. + */ + abstract public function getLongWikiText( $linked = null ); + + /** + * Return the long textual description of the value, as printed for + * example in the factbox. If errors occurred, return the error message + * The result always is an HTML string. + * + * The parameter $linker controls linking of values such as titles and should + * be some Linker object (or NULL for no linking). + */ + abstract public function getLongHTMLText( $linker = null ); + + /** + * Return the plain wiki version of the value, or + * FALSE if no such version is available. The returned + * string suffices to reobtain the same DataValue + * when passing it as an input string to setUserValue(). + */ + abstract public function getWikiValue(); + + /** + * Returns a short textual representation for this data value. If the value + * was initialised from a user supplied string, then this original string + * should be reflected in this short version (i.e. no normalisation should + * normally happen). There might, however, be additional parts such as code + * for generating tooltips. The output is in the specified format. + * + * The parameter $linker controls linking of values such as titles and should + * be some Linker object (for HTML output), or NULL for no linking. + */ + public function getShortText( $outputformat, $linker = null ) { + switch ( $outputformat ) { + case SMW_OUTPUT_WIKI: + return $this->getShortWikiText( $linker ); + case SMW_OUTPUT_HTML: + case SMW_OUTPUT_FILE: + default: + return $this->getShortHTMLText( $linker ); + } + } + + /** + * Return the long textual description of the value, as printed for + * example in the factbox. If errors occurred, return the error message. + * The output is in the specified format. + * + * The parameter $linker controls linking of values such as titles and should + * be some Linker object (for HTML output), or NULL for no linking. + */ + public function getLongText( $outputformat, $linker = null ) { + switch ( $outputformat ) { + case SMW_OUTPUT_WIKI: + return $this->getLongWikiText( $linker ); + case SMW_OUTPUT_HTML: + case SMW_OUTPUT_FILE: + default: + return $this->getLongHTMLText( $linker ); + } + } + + /** + * Return text serialisation of info links. Ensures more uniform layout + * throughout wiki (Factbox, Property pages, ...). + * + * @param integer $outputformat Element of the SMW_OUTPUT_ enum + * @param $linker + * + * @return string + */ + public function getInfolinkText( $outputformat, $linker = null ) { + + if ( $this->getOption( self::OPT_DISABLE_INFOLINKS ) === true ) { + return ''; + } + + if ( $this->infoLinksProvider === null ) { + $this->infoLinksProvider = $this->dataValueServiceFactory->newInfoLinksProvider( $this ); + } + + if ( $this->getOption( self::OPT_DISABLE_SERVICELINKS ) === true ) { + $this->infoLinksProvider->disableServiceLinks(); + } + + $this->infoLinksProvider->setCompactLink( + $this->getOption( self::OPT_COMPACT_INFOLINKS, false ) + ); + + return $this->infoLinksProvider->getInfolinkText( $outputformat, $linker ); + } + + /** + * Return an array of SMWLink objects that provide additional resources + * for the given value. Captions can contain some HTML markup which is + * admissible for wiki text, but no more. Result might have no entries + * but is always an array. + */ + public function getInfolinks() { + + if ( $this->infoLinksProvider === null ) { + $this->infoLinksProvider = $this->dataValueServiceFactory->newInfoLinksProvider( $this ); + } + + $this->infoLinksProvider->setServiceLinkParameters( + $this->getServiceLinkParams() + ); + + return $this->infoLinksProvider->createInfoLinks(); + } + + /** + * Return a string that identifies the value of the object, and that can + * be used to compare different value objects. + * Possibly overwritten by subclasses (e.g. to ensure that returned + * value is normalized first) + * + * @return string + */ + public function getHash() { + return $this->isValid() ? $this->m_dataitem->getHash() : implode( "\t", $this->mErrors ); + } + + /** + * Convenience method that checks if the value that is used to sort + * data of this type is numeric. This only works if the value is set. + * + * @return boolean + */ + public function isNumeric() { + if ( isset( $this->m_dataitem ) ) { + return is_numeric( $this->m_dataitem->getSortKey() ); + } else { + return false; + } + } + + /** + * Return true if a value was defined and understood by the given type, + * and false if parsing errors occurred or no value was given. + * + * @return boolean + */ + public function isValid() { + return !$this->mHasErrors && isset( $this->m_dataitem ); + } + + /** + * Whether a datavalue can be used or not (can be made more restrictive then + * isValid). + * + * @note Validity defines a processable state without any technical restrictions + * while usability is determined by its accessibility to a context + * (permission, convention etc.) + * + * @since 2.2 + * + * @return boolean + */ + public function canUse() { + return true; + } + + /** + * @since 3.0 + * + * @return boolean + */ + public function isRestricted() { + return false; + } + + /** + * @since 2.3 + * + * @param string $name + * @param array $parameters + * + * @return mixed + * @throws RuntimeException + */ + public function getExtraneousFunctionFor( $name, array $parameters = [] ) { + return $this->dataValueServiceFactory->newExtraneousFunctionByName( $name, $parameters ); + } + + /** + * @since 3.0 + * + * @param string $key + * @param mixed $data + */ + public function setExtensionData( $key, $data ) { + $this->extenstionData[$key] = $data; + } + + /** + * @since 3.0 + * + * @param string $key + * + * @return mixed + */ + public function getExtensionData( $key ) { + + if ( isset( $this->extenstionData[$key] ) ) { + return $this->extenstionData[$key]; + } + + return null; + } + + /** + * @since 2.4 + * + * @return Options|null $options + */ + public function copyOptions( Options $options = null ) { + + if ( $options === null ) { + return; + } + + foreach ( $options->getOptions() as $key => $value ) { + $this->setOption( $key, $value ); + } + } + + /** + * @since 2.4 + * + * @return string $key + * @param mxied $value + */ + public function setOption( $key, $value ) { + + if ( $this->options === null ) { + $this->options = new Options(); + } + + $this->options->set( $key, $value ); + } + + /** + * @since 2.4 + * + * @param string $key + * + * @return mixed|false + */ + public function getOption( $key, $default = false ) { + + if ( $this->options !== null && $this->options->has( $key ) ) { + return $this->options->get( $key ); + } + + return $default; + } + + /** + * @since 3.0 + * + * @param integer $feature + * + * @return boolean + */ + public function hasFeature( $feature ) { + + if ( $this->options !== null ) { + return $this->options->isFlagSet( 'smwgDVFeatures', (int)$feature ); + } + + return false; + } + + /** + * @deprecated since 3.0, use DataValue::hasFeature + * @since 2.4 + */ + public function isEnabledFeature( $feature ) { + return $this->hasFeature( $feature ); + } + + /** + * @since 2.5 + * + * @return Options + */ + protected function getOptions() { + return $this->options; + } + + /** + * Initialise the datavalue from the given value string. + * The format of this strings might be any acceptable user input + * and especially includes the output of getWikiValue(). + * + * @param string $value + */ + abstract protected function parseUserValue( $value ); + + /** + * Set the actual data contained in this object. The method returns + * true if this was successful (requiring the type of the dataitem + * to match the data value). If false is returned, the data value is + * left unchanged (the data item was rejected). + * + * @note Even if this function returns true, the data value object + * might become invalid if the content of the data item caused errors + * in spite of it being of the right basic type. False is only returned + * if the data item is fundamentally incompatible with the data value. + * + * @since 1.6 + * + * @param SMWDataItem $dataItem + * + * @return boolean + */ + abstract protected function loadDataItem( SMWDataItem $dataItem ); + + /** + * Overwritten by callers to supply an array of parameters that can be used for + * creating servicelinks. The number and content of values in the parameter array + * may vary, depending on the concrete datatype. + */ + protected function getServiceLinkParams() { + return false; + } + + /** + * Check if property is range restricted and, if so, whether the current value is allowed. + * Creates an error if the value is illegal. + */ + protected function checkAllowedValues() { + + if ( $this->dataValueServiceFactory === null ) { + return; + } + + $this->dataValueServiceFactory->getConstraintValueValidator()->validate( $this ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Exp_Data.php b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Exp_Data.php new file mode 100644 index 00000000..e4c81210 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Exp_Data.php @@ -0,0 +1,328 @@ +<?php + +use SMW\Exporter\Element; + +/** + * SMWExpData is a class representing semantic data that is ready for easy + * serialisation in OWL or RDF. + * + * @author Markus Krötzsch + * @ingroup SMW + */ + + + +/** + * SMWExpData is a data container for export-ready semantic content. It is + * organised as a tree-shaped data structure with one root subject and zero + * or more children connected with labelled edges to the root. Children are + * again SMWExpData objects, and edges are annotated with SMWExpNsElements + * specifying properties. + * @note We do not allow property element without namespace abbreviation + * here. Property aabbreviations are mandatory for some serialisations. + * + * @ingroup SMW + */ +class SMWExpData implements Element { + + /** + * @var DataItem|null + */ + private $dataItem; + + /** + * The subject of the data that we store. + * @var SMWExpResource + */ + protected $m_subject; + + /** + * Array mapping property URIs to arrays their values, given as + * SMWExpElement objects. + * @var array of array of SMWElement + */ + protected $m_children = []; + + /** + * Array mapping property URIs to arrays their SMWExpResource + * @var array of SMWExpResource + */ + protected $m_edges = []; + + /** + * @var string|null + */ + private $hash = null; + + /** + * Constructor. $subject is the SMWExpResource for the + * subject about which this SMWExpData is. + */ + public function __construct( SMWExpResource $subject ) { + $this->dataItem = $subject->getDataItem(); + $this->m_subject = $subject; + } + + /** + * @since 2.2 + * + * @return DataItem|null + */ + public function getDataItem() { + return $this->dataItem; + } + + /** + * @since 2.2 + * + * @return string + */ + public function getHash() { + + if ( $this->hash !== null ) { + return $this->hash; + } + + $hashes = []; + $hashes[] = $this->m_subject->getHash(); + + foreach ( $this->getProperties() as $property ) { + + $hashes[] = $property->getHash(); + + foreach ( $this->getValues( $property ) as $child ) { + $hashes[] = $child->getHash(); + } + } + + sort( $hashes ); + + $this->hash = md5( implode( '#', $hashes ) ); + unset( $hashes ); + + return $this->hash; + } + + /** + * Turn an array of SMWExpElements into an RDF collection. + * + * @param $elements array of SMWExpElement + * @return SMWExpData + */ + public static function makeCollection( array $elements ) { + + if ( count( $elements ) == 0 ) { + return new SMWExpData( SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'nil' ) ); + } + + $result = new SMWExpData( new SMWExpResource( '' ) ); // bnode + + $result->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'type' ), + new SMWExpData( SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'List' ) ) + ); + + $result->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'first' ), + array_shift( $elements ) + ); + + $result->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'rest' ), + self::makeCollection( $elements ) + ); + + return $result; + } + + /** + * Return subject to which the stored semantic annotation refer to. + * + * @return SMWExpResource + */ + public function getSubject() { + return $this->m_subject; + } + + /** + * Store a value for a property identified by its title object. No + * duplicate elimination as this is usually done in SMWSemanticData + * already (which is typically used to generate this object). + * + * @param SMWExpNsResource $property + * @param Element $child + */ + public function addPropertyObjectValue( SMWExpNsResource $property, Element $child ) { + + $this->hash = null; + + if ( !array_key_exists( $property->getUri(), $this->m_edges ) ) { + $this->m_children[$property->getUri()] = []; + $this->m_edges[$property->getUri()] = $property; + } + + $this->m_children[$property->getUri()][] = $child; + } + + /** + * Return the list of SMWExpResource objects for all properties for + * which some values have been given. + * + * @return array of SMWExpResource + */ + public function getProperties() { + return $this->m_edges; + } + + /** + * Return the list of SMWExpElement values associated to some property + * (element). + * + * @return array of SMWExpElement + */ + public function getValues( SMWExpResource $property ) { + + if ( array_key_exists( $property->getUri(), $this->m_children ) ) { + return $this->m_children[$property->getUri()]; + } + + return []; + } + + /** + * Return the list of SMWExpData values associated to some property that is + * specified by a standard namespace id and local name. + * + * @param $namespaceId string idetifying a known special namespace (e.g. "rdf") + * @param $localName string of local name (e.g. "type") + * @return array of SMWExpData + */ + public function getSpecialValues( $namespaceId, $localName ) { + $pe = SMWExporter::getInstance()->getSpecialNsResource( $namespaceId, $localName ); + return $this->getValues( $pe ); + } + + /** + * This function finds the main type (class) element of the subject + * based on the current property assignments. It returns this type + * element (SMWExpElement) and removes the according type assignement + * from the data. If no type is assigned, the element for rdf:Resource + * is returned. + * + * @note Under all normal conditions, the result will be an + * SMWExpResource. + * + * @return SMWExpElement + */ + public function extractMainType() { + $pe = SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'type' ); + if ( array_key_exists( $pe->getUri(), $this->m_children ) ) { + $result = array_shift( $this->m_children[$pe->getUri()] ); + if ( count( $this->m_children[$pe->getUri()] ) == 0 ) { + unset( $this->m_edges[$pe->getUri()] ); + unset( $this->m_children[$pe->getUri()] ); + } + return ( $result instanceof SMWExpData ) ? $result->getSubject() : $result; + } else { + return SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'Resource' ); + } + } + + /** + * Check if this element encodes an RDF list, and if yes return an + * array of SMWExpElements corresponding to the collection elements in + * the specified order. Otherwise return false. + * The method only returns lists that can be encoded using + * parseType="Collection" in RDF/XML, i.e. only lists of non-literal + * resources. + * + * @return mixed array of SMWExpElement (but not SMWExpLiteral) or false + */ + public function getCollection() { + $rdftypeUri = SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'type' )->getUri(); + $rdffirstUri = SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'first' )->getUri(); + $rdfrestUri = SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'rest' )->getUri(); + $rdfnilUri = SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'nil' )->getUri(); + // first check if we are basically an RDF List: + if ( ( $this->m_subject->isBlankNode() ) && + ( count( $this->m_children ) == 3 ) && + ( array_key_exists( $rdftypeUri, $this->m_children ) ) && + ( count( $this->m_children[$rdftypeUri] ) == 1 ) && + ( array_key_exists( $rdffirstUri, $this->m_children ) ) && + ( count( $this->m_children[$rdffirstUri] ) == 1 ) && + !( end( $this->m_children[$rdffirstUri] ) instanceof SMWExpLiteral ) && + // (parseType collection in RDF not possible with literals :-/) + ( array_key_exists( $rdfrestUri, $this->m_children ) ) && + ( count( $this->m_children[$rdfrestUri] ) == 1 ) ) { + $typedata = end( $this->m_children[$rdftypeUri] ); + $rdflistUri = SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'List' )->getUri(); + if ( $typedata->getSubject()->getUri() == $rdflistUri ) { + $first = end( $this->m_children[$rdffirstUri] ); + $rest = end( $this->m_children[$rdfrestUri] ); + if ( $rest instanceof SMWExpData ) { + $restlist = $rest->getCollection(); + if ( $restlist === false ) { + return false; + } else { + array_unshift( $restlist, $first ); + return $restlist; + } + } elseif ( ( $rest instanceof SMWExpResource ) && + ( $rest->getUri() == $rdfnilUri ) ) { + return [ $first ]; + } else { + return false; + } + } else { + return false; + } + } elseif ( ( count( $this->m_children ) == 0 ) && ( $this->m_subject->getUri() == $rdfnilUri ) ) { + return []; + } else { + return false; + } + } + + /** + * Return an array of ternary arrays (subject predicate object) of + * SMWExpElements that represents the flattened version of this data. + * + * @return array of array of SMWExpElement + */ + public function getTripleList( Element $subject = null ) { + global $smwgBnodeCount; + if ( !isset( $smwgBnodeCount ) ) { + $smwgBnodeCount = 0; + } + + if ( $subject == null ) { + $subject = $this->m_subject; + } + + $result = []; + + foreach ( $this->m_edges as $key => $edge ) { + foreach ( $this->m_children[$key] as $childElement ) { + if ( $childElement instanceof SMWExpData ) { + $childSubject = $childElement->getSubject(); + } else { + $childSubject = $childElement; + } + + if ( ( $childSubject instanceof SMWExpResource ) && + ( $childSubject->isBlankNode() ) ) { // bnode, rename ID to avoid unifying bnodes of different contexts + // TODO: should we really rename bnodes of the form "_id" here? + $childSubject = new SMWExpResource( '_' . $smwgBnodeCount++, $childSubject->getDataItem() ); + } + + $result[] = [ $subject, $edge, $childSubject ]; + if ( $childElement instanceof SMWExpData ) { // recursively add child's triples + $result = array_merge( $result, $childElement->getTripleList( $childSubject ) ); + } + } + } + + return $result; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_ExportController.php b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_ExportController.php new file mode 100644 index 00000000..3e40c077 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_ExportController.php @@ -0,0 +1,768 @@ +<?php + +use SMW\ApplicationFactory; +use SMW\DIProperty; +use SMW\DIWikiPage; +use SMW\Exporter\Escaper; +use SMW\Query\PrintRequest; +use SMW\SemanticData; +use SMW\Site; + +/** + * File holding the SMWExportController class that provides basic functions for + * exporting pages to RDF and OWL. + * + * @ingroup SMW + * + * @author Markus Krötzsch + */ + +/** + * Class for controlling the export of SMW page data, supporting high-level + * features such as recursive export and backlink inclusion. The class controls + * export independent of the serialisation syntax that is used. + * + * @ingroup SMW + */ +class SMWExportController { + const MAX_CACHE_SIZE = 5000; // do not let cache arrays get larger than this + const CACHE_BACKJUMP = 500; // kill this many cached entries if limit is reached, + // avoids too much array copying; <= MAX_CACHE_SIZE! + /** + * The object used for serialisation. + * @var SMWSerializer + */ + protected $serializer; + /** + * An array that keeps track of the elements for which we still need to + * write auxiliary definitions/declarations. + */ + protected $element_queue; + /** + * An array that keeps track of the recursion depth with which each object + * has been serialised. + */ + protected $element_done; + /** + * Boolean to indicate whether all objects that are exported in full (with + * all data) should also lead to the inclusion of all "inlinks" that they + * receive from other objects. If yes, these other objects are also + * serialised with at least the relevant inlinking properties included. + * Adding such dependencies counts as "recursive serialisation" and whether + * or not inlinking objects are included in full depends on the setting for + * recursion depth. Setting this to true enables "browsable RDF". + */ + protected $add_backlinks; + /** + * Controls how long to wait until flushing content to output. Flushing + * early may reduce the memory footprint of serialization functions. + * Flushing later has some advantages for export formats like RDF/XML where + * global namespace declarations are only possible by modifying the header, + * so that only local declarations are possible after the first flush. + */ + protected $delay_flush; + /** + * File handle for a potential output file to write to, or null if printing + * to standard output. + */ + protected $outputfile; + + /** + * @var DeepRedirectTargetResolver + */ + private $deepRedirectTargetResolver = null; + + /** + * Constructor. + * @param SMWSerializer $serializer defining the object used for syntactic + * serialization. + * @param boolean $enable_backlinks defining if backlinks are included, + * see $add_backlinks for details. + */ + public function __construct( SMWSerializer $serializer, $enable_backlinks = false ) { + $this->serializer = $serializer; + $this->outputfile = null; + $this->add_backlinks = $enable_backlinks; + } + + /** + * Enable or disable inclusion of backlinks into the output. + * @param boolean $enable + */ + public function enableBacklinks( $enable ) { + $this->add_backlinks = $enable; + } + + /** + * Initialize all internal structures to begin with some serialization. + * Returns true if initialization was successful (this means that the + * optional output file is writable). + * @param string $outfilename URL of the file that output should be written + * to, or empty string for writing to the standard output. + * + * @return boolean + */ + protected function prepareSerialization( $outfilename = '' ) { + $this->serializer->clear(); + $this->element_queue = []; + $this->element_done = []; + if ( $outfilename !== '' ) { + $this->outputfile = fopen( $outfilename, 'w' ); + if ( !$this->outputfile ) { // TODO Rather throw an exception here. + print "\nCannot open \"$outfilename\" for writing.\n"; + return false; + } + } + return true; + } + + /** + * Serialize data associated to a specific page. This method works on the + * level of pages, i.e. it serialises parts of SMW content and implements + * features like recursive export or backlinks that are available for this + * type of data. + * + * The recursion depth means the following. Depth of 1 or above means + * the object is serialised with all property values, and referenced + * objects are serialised with depth reduced by 1. Depth 0 means that only + * minimal declarations are serialised, so no dependencies are added. A + * depth of -1 encodes "infinite" depth, i.e. a complete recursive + * serialisation without limit. + * + * @param SMWDIWikiPage $diWikiPage specifying the page to be exported + * @param integer $recursiondepth specifying the depth of recursion + */ + protected function serializePage( SMWDIWikiPage $diWikiPage, $recursiondepth = 1 ) { + + if ( $this->isPageDone( $diWikiPage, $recursiondepth ) ) { + return; // do not export twice + } + + $this->markPageAsDone( $diWikiPage, $recursiondepth ); + $semData = $this->getSemanticData( $diWikiPage, ( $recursiondepth == 0 ) ); + + // Don't try to serialize an empty page that cause an incomplete exp-data set + // (e.g. _REDI as no property page hence DBKey is empty) + if ( $semData === null || $diWikiPage->getDBKey() === '' ) { + return null; + } + + $expData = SMWExporter::getInstance()->makeExportData( $semData ); + $this->serializer->serializeExpData( $expData, $recursiondepth ); + + foreach( $semData->getSubSemanticData() as $subSemanticData ) { + + // Mark SubSemanticData subjects as well to ensure that backlinks to + // the same subject do not create duplicate XML export entities + $this->markPageAsDone( + $subSemanticData->getSubject(), + $recursiondepth + ); + + $expData = SMWExporter::getInstance()->makeExportData( + $subSemanticData + ); + + $this->serializer->serializeExpData( $expData ); + } + + // let other extensions add additional RDF data for this page + $expDataList = []; + + \Hooks::run( + 'SMW::Exporter::Controller::AddExpData', + [ + $diWikiPage, + &$expDataList, + ( $recursiondepth != 0 ), + $this->add_backlinks + ] + ); + + foreach ( $expDataList as $data ) { + + if ( !$data instanceof SMWExpData ) { + continue; + } + + $this->serializer->serializeExpData( $data ); + } + + if ( $recursiondepth != 0 ) { + $subrecdepth = $recursiondepth > 0 ? ( $recursiondepth - 1 ) : + ( $recursiondepth == 0 ? 0 : -1 ); + + foreach ( $expData->getProperties() as $property ) { + if ( $property->getDataItem() instanceof SMWWikiPageValue ) { + $this->queuePage( $property->getDataItem(), 0 ); // no real recursion along properties + } + $wikipagevalues = false; + foreach ( $expData->getValues( $property ) as $valueExpElement ) { + $valueResource = $valueExpElement instanceof SMWExpData ? $valueExpElement->getSubject() : $valueExpElement; + if ( !$wikipagevalues && ( $valueResource->getDataItem() instanceof SMWWikiPageValue ) ) { + $wikipagevalues = true; + } elseif ( !$wikipagevalues ) { + break; + } + $this->queuePage( $valueResource->getDataItem(), $subrecdepth ); + } + } + + // Add backlinks: + // Note: Backlinks are different from recursive serialisations, since + // stub declarations (recdepth==0) still need to have the property that + // links back to the object. So objects that would be exported with + // recdepth 0 cannot be put into the main queue but must be done right + // away. They also might be required many times, if they link back to + // many different objects in many ways (we cannot consider them "Done" + // if they were serialised at recdepth 0 only). + if ( $this->add_backlinks ) { + $inprops = \SMW\StoreFactory::getStore()->getInProperties( $diWikiPage ); + + foreach ( $inprops as $inprop ) { + $propWikiPage = $inprop->getCanonicalDiWikiPage(); + + if ( !is_null( $propWikiPage ) ) { + $this->queuePage( $propWikiPage, 0 ); // no real recursion along properties + } + + $inSubs = \SMW\StoreFactory::getStore()->getPropertySubjects( $inprop, $diWikiPage ); + + foreach ( $inSubs as $inSub ) { + if ( !$this->isPageDone( $inSub, $subrecdepth ) ) { + $semdata = $this->getSemanticData( $inSub, true ); + + if ( !$semdata instanceof SMWSemanticData ) { + continue; + } + + $semdata->addPropertyObjectValue( $inprop, $diWikiPage ); + $expData = SMWExporter::getInstance()->makeExportData( $semdata ); + $this->serializer->serializeExpData( $expData, $subrecdepth ); + } + } + } + + if ( NS_CATEGORY === $diWikiPage->getNamespace() ) { // also print elements of categories + $options = new SMWRequestOptions(); + $options->limit = 100; // Categories can be large, always use limit + $instances = \SMW\StoreFactory::getStore()->getPropertySubjects( new SMW\DIProperty( '_INST' ), $diWikiPage, $options ); + $pinst = new SMW\DIProperty( '_INST' ); + + foreach ( $instances as $instance ) { + if ( !array_key_exists( $instance->getHash(), $this->element_done ) ) { + $semdata = $this->getSemanticData( $instance, true ); + + if ( !$semdata instanceof SMWSemanticData ) { + continue; + } + + $semdata->addPropertyObjectValue( $pinst, $diWikiPage ); + $expData = SMWExporter::getInstance()->makeExportData( $semdata ); + $this->serializer->serializeExpData( $expData, $subrecdepth ); + } + } + } elseif ( SMW_NS_CONCEPT === $diWikiPage->getNamespace() ) { // print concept members (slightly different code) + $desc = new SMWConceptDescription( $diWikiPage ); + $desc->addPrintRequest( new PrintRequest( PrintRequest::PRINT_THIS, '' ) ); + $query = new SMWQuery( $desc ); + $query->setLimit( 100 ); + + $res = \SMW\StoreFactory::getStore()->getQueryResult( $query ); + $resarray = $res->getNext(); + $pinst = new SMW\DIProperty( '_INST' ); + + while ( $resarray !== false ) { + $instance = end( $resarray )->getNextDataItem(); + + if ( !$instance instanceof \SMWDataItem ) { + $resarray = $res->getNext(); + continue; + } + + if ( !array_key_exists( $instance->getHash(), $this->element_done ) ) { + $semdata = $this->getSemanticData( $instance, true ); + + if ( !$semdata instanceof \SMW\SemanticData ) { + $resarray = $res->getNext(); + continue; + } + + $semdata->addPropertyObjectValue( $pinst, $diWikiPage ); + $expData = SMWExporter::getInstance()->makeExportData( $semdata ); + $this->serializer->serializeExpData( $expData ); + } + + $resarray = $res->getNext(); + } + } + } + } + } + + /** + * Add a given SMWDIWikiPage to the export queue if needed. + */ + protected function queuePage( SMWDIWikiPage $diWikiPage, $recursiondepth ) { + if ( !$this->isPageDone( $diWikiPage, $recursiondepth ) ) { + $diWikiPage->recdepth = $recursiondepth; // add a field + $this->element_queue[$diWikiPage->getHash()] = $diWikiPage; + } + } + + /** + * Mark an article as done while making sure that the cache used for this + * stays reasonably small. Input is given as an SMWDIWikiPage object. + */ + protected function markPageAsDone( SMWDIWikiPage $di, $recdepth ) { + $this->markHashAsDone( $di->getHash(), $recdepth ); + } + + /** + * Mark a task as done while making sure that the cache used for this + * stays reasonably small. + */ + protected function markHashAsDone( $hash, $recdepth ) { + if ( count( $this->element_done ) >= self::MAX_CACHE_SIZE ) { + $this->element_done = array_slice( $this->element_done, + self::CACHE_BACKJUMP, + self::MAX_CACHE_SIZE - self::CACHE_BACKJUMP, + true ); + } + if ( !$this->isHashDone( $hash, $recdepth ) ) { + $this->element_done[$hash] = $recdepth; // mark title as done, with given recursion + } + unset( $this->element_queue[$hash] ); // make sure it is not in the queue + } + + /** + * Check if the given object has already been serialised at sufficient + * recursion depth. + * @param SMWDIWikiPage $st specifying the object to check + * + * @return boolean + */ + protected function isPageDone( SMWDIWikiPage $di, $recdepth ) { + return $this->isHashDone( $di->getHash(), $recdepth ); + } + + /** + * Check if the given task has already been completed at sufficient + * recursion depth. + */ + protected function isHashDone( $hash, $recdepth ) { + return ( ( array_key_exists( $hash, $this->element_done ) ) && + ( ( $this->element_done[$hash] == -1 ) || + ( ( $recdepth != -1 ) && ( $this->element_done[$hash] >= $recdepth ) ) ) ); + } + + /** + * Retrieve a copy of the semantic data for a wiki page, possibly filtering + * it so that only essential properties are included (in some cases, we only + * want to export stub information about a page). + * We make a copy of the object since we may want to add more data later on + * and we do not want to modify the store's result which may be used for + * caching purposes elsewhere. + */ + protected function getSemanticData( SMWDIWikiPage $diWikiPage, $core_props_only ) { + + // Issue 619 + // Resolve the redirect target and return a container with information + // about the redirect + if ( $diWikiPage->getTitle() !== null && $diWikiPage->getTitle()->isRedirect() ) { + + try { + $redirectTarget = $this->getDeepRedirectTargetResolver()->findRedirectTargetFor( $diWikiPage->getTitle() ); + } catch ( \Exception $e ) { + $redirectTarget = null; + } + + // Couldn't resolve the redirect which is most likely caused by a + // circular redirect therefore we give up + if ( $redirectTarget === null ) { + return null; + } + + $semData = new SemanticData( $diWikiPage ); + + $semData->addPropertyObjectValue( + new DIProperty( '_REDI' ), + DIWikiPage::newFromTitle( $redirectTarget ) + ); + + return $semData; + } + + $semdata = \SMW\StoreFactory::getStore()->getSemanticData( $diWikiPage, $core_props_only ? [ '__spu', '__typ', '__imp' ] : false ); // advise store to retrieve only core things + if ( $core_props_only ) { // be sure to filter all non-relevant things that may still be present in the retrieved + $result = new SMWSemanticData( $diWikiPage ); + foreach ( [ '_URI', '_TYPE', '_IMPO' ] as $propid ) { + $prop = new SMW\DIProperty( $propid ); + $values = $semdata->getPropertyValues( $prop ); + foreach ( $values as $dv ) { + $result->addPropertyObjectValue( $prop, $dv ); + } + } + } else { + $result = clone $semdata; + } + + + return $result; + } + + /** + * Send to the output what has been serialized so far. The flush might + * be deferred until later unless $force is true. + */ + protected function flush( $force = false ) { + if ( !$force && ( $this->delay_flush > 0 ) ) { + $this->delay_flush -= 1; + } elseif ( !is_null( $this->outputfile ) ) { + fwrite( $this->outputfile, $this->serializer->flushContent() ); + } else { + ob_start(); + print $this->serializer->flushContent(); + // Ship data in small chunks (even though browsers often do not display anything + // before the file is complete -- this might be due to syntax highlighting features + // for app/xml). You may want to sleep(1) here for debugging this. + ob_flush(); + flush(); + ob_get_clean(); + } + } + + /** + * This function prints all selected pages, specified as an array of page + * names (strings with namespace identifiers). + * + * @param array $pages list of page names to export + * @param integer $recursion determines how pages are exported recursively: + * "0" means that referenced resources are only declared briefly, "1" means + * that all referenced resources are also exported recursively (propbably + * retrieving the whole wiki). + * @param string $revisiondate filter page list by including only pages + * that have been changed since this date; format "YmdHis" + * + * @todo Consider dropping the $revisiondate filtering and all associated + * functionality. Is anybody using this? + */ + public function printPages( $pages, $recursion = 1, $revisiondate = false ) { + + $linkCache = LinkCache::singleton(); + $this->prepareSerialization(); + $this->delay_flush = 10; // flush only after (fully) printing 11 objects + + // transform pages into queued short titles + foreach ( $pages as $page ) { + $title = Title::newFromText( $page ); + if ( null === $title ) { + continue; // invalid title name given + } + if ( $revisiondate !== '' ) { // filter page list by revision date + $rev = Revision::getTimeStampFromID( $title, $title->getLatestRevID() ); + if ( $rev < $revisiondate ) { + continue; + } + } + + $diPage = SMWDIWikiPage::newFromTitle( $title ); + $this->queuePage( $diPage, ( $recursion==1 ? -1 : 1 ) ); + } + + $this->serializer->startSerialization(); + + if ( count( $pages ) == 1 ) { // ensure that ontologies that are retrieved as linked data are not confused with their subject! + $ontologyuri = SMWExporter::getInstance()->expandURI( '&export;' ) . '/' . Escaper::encodeUri( end( $pages ) ); + } else { // use empty URI, i.e. "location" as URI otherwise + $ontologyuri = ''; + } + $this->serializer->serializeExpData( SMWExporter::getInstance()->getOntologyExpData( $ontologyuri ) ); + + while ( count( $this->element_queue ) > 0 ) { + $diPage = reset( $this->element_queue ); + $this->serializePage( $diPage, $diPage->recdepth ); + $this->flush(); + $linkCache->clear(); // avoid potential memory leak + } + $this->serializer->finishSerialization(); + $this->flush( true ); + + } + + /** + * Exports semantic data for all pages within the wiki and for all elements + * that are referred to a file resource + * + * @since 2.0 + * + * @param string $outfile the output file URI, or false if printing to stdout + * @param mixed $ns_restriction namespace restriction, see fitsNsRestriction() + * @param integer $delay number of microseconds for which to sleep during + * export to reduce server load in long-running operations + * @param integer $delayeach number of pages to process between two sleeps + */ + public function printAllToFile( $outfile, $ns_restriction = false, $delay, $delayeach ) { + + if ( !$this->prepareSerialization( $outfile ) ) { + return; + } + + $this->printAll( $ns_restriction, $delay, $delayeach ); + } + + /** + * Exports semantic data for all pages within the wiki and for all elements + * that are referred to the stdout + * + * @since 2.0 + * + * @param mixed $ns_restriction namespace restriction, see fitsNsRestriction() + * @param integer $delay number of microseconds for which to sleep during + * export to reduce server load in long-running operations + * @param integer $delayeach number of pages to process between two sleeps + */ + public function printAllToOutput( $ns_restriction = false, $delay, $delayeach ) { + $this->prepareSerialization(); + $this->printAll( $ns_restriction, $delay, $delayeach ); + } + + /** + * @since 2.0 made protected; use printAllToFile or printAllToOutput + */ + protected function printAll( $ns_restriction = false, $delay, $delayeach ) { + $linkCache = LinkCache::singleton(); + $db = wfGetDB( DB_SLAVE ); + + $this->delay_flush = 10; + + $this->serializer->startSerialization(); + $this->serializer->serializeExpData( SMWExporter::getInstance()->getOntologyExpData( '' ) ); + + $end = $db->selectField( 'page', 'max(page_id)', false, __METHOD__ ); + $a_count = 0; // DEBUG + $d_count = 0; // DEBUG + $delaycount = $delayeach; + + for ( $id = 1; $id <= $end; $id += 1 ) { + $title = Title::newFromID( $id ); + if ( is_null( $title ) || !\SMW\NamespaceExaminer::getInstance()->isSemanticEnabled( $title->getNamespace() ) ) { + continue; + } + if ( !self::fitsNsRestriction( $ns_restriction, $title->getNamespace() ) ) { + continue; + } + $a_count += 1; // DEBUG + + $diPage = SMWDIWikiPage::newFromTitle( $title ); + $this->queuePage( $diPage, 1 ); + + while ( count( $this->element_queue ) > 0 ) { + $diPage = reset( $this->element_queue ); + $this->serializePage( $diPage, $diPage->recdepth ); + // resolve dependencies that will otherwise not be printed + foreach ( $this->element_queue as $key => $diaux ) { + if ( !\SMW\NamespaceExaminer::getInstance()->isSemanticEnabled( $diaux->getNamespace() ) || + !self::fitsNsRestriction( $ns_restriction, $diaux->getNamespace() ) ) { + // Note: we do not need to check the cache to guess if an element was already + // printed. If so, it would not be included in the queue in the first place. + $d_count += 1; // DEBUG + } else { // don't carry values that you do not want to export (yet) + unset( $this->element_queue[$key] ); + } + } + // sleep each $delaycount for $delay µs to be nice to the server + if ( ( $delaycount-- < 0 ) && ( $delayeach != 0 ) ) { + usleep( $delay ); + $delaycount = $delayeach; + } + } + + $this->flush(); + $linkCache->clear(); + } + + $this->serializer->finishSerialization(); + $this->flush( true ); + } + + /** + * Print basic definitions a list of pages ordered by their page id. + * Offset and limit refer to the count of existing pages, not to the + * page id. + * @param integer $offset the number of the first (existing) page to + * serialize a declaration for + * @param integer $limit the number of pages to serialize + */ + public function printPageList( $offset = 0, $limit = 30 ) { + global $smwgNamespacesWithSemanticLinks; + + $db = wfGetDB( DB_SLAVE ); + $this->prepareSerialization(); + $this->delay_flush = 35; // don't do intermediate flushes with default parameters + $linkCache = LinkCache::singleton(); + + $this->serializer->startSerialization(); + $this->serializer->serializeExpData( SMWExporter::getInstance()->getOntologyExpData( '' ) ); + + $query = ''; + foreach ( $smwgNamespacesWithSemanticLinks as $ns => $enabled ) { + if ( $enabled ) { + if ( $query !== '' ) { + $query .= ' OR '; + } + $query .= 'page_namespace = ' . $db->addQuotes( $ns ); + } + } + $res = $db->select( $db->tableName( 'page' ), + 'page_id,page_title,page_namespace', $query, + 'SMW::RDF::PrintPageList', [ 'ORDER BY' => 'page_id ASC', 'OFFSET' => $offset, 'LIMIT' => $limit ] ); + $foundpages = false; + + foreach ( $res as $row ) { + $foundpages = true; + try { + $diPage = new SMWDIWikiPage( $row->page_title, $row->page_namespace, '' ); + $this->serializePage( $diPage, 0 ); + $this->flush(); + $linkCache->clear(); + } catch ( SMWDataItemException $e ) { + // strange data, who knows, not our DB table, keep calm and carry on + } + } + + if ( $foundpages ) { // add link to next result page + if ( strpos( SMWExporter::getInstance()->expandURI( '&wikiurl;' ), '?' ) === false ) { // check whether we have title as a first parameter or in URL + $nexturl = SMWExporter::getInstance()->expandURI( '&export;?offset=' ) . ( $offset + $limit ); + } else { + $nexturl = SMWExporter::getInstance()->expandURI( '&export;&offset=' ) . ( $offset + $limit ); + } + + $expData = new SMWExpData( new SMWExpResource( $nexturl ) ); + $ed = new SMWExpData( SMWExporter::getInstance()->getSpecialNsResource( 'owl', 'Thing' ) ); + $expData->addPropertyObjectValue( SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'type' ), $ed ); + $ed = new SMWExpData( new SMWExpResource( $nexturl ) ); + $expData->addPropertyObjectValue( SMWExporter::getInstance()->getSpecialNsResource( 'rdfs', 'isDefinedBy' ), $ed ); + $this->serializer->serializeExpData( $expData ); + } + + $this->serializer->finishSerialization(); + $this->flush( true ); + + } + + + /** + * Print basic information about this site. + */ + public function printWikiInfo() { + + $this->prepareSerialization(); + $this->delay_flush = 35; // don't do intermediate flushes with default parameters + + // assemble export data: + $expData = new SMWExpData( new SMWExpResource( '&wiki;#wiki' ) ); + + $expData->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'type' ), + new SMWExpData( SMWExporter::getInstance()->getSpecialNsResource( 'swivt', 'Wikisite' ) ) + ); + + // basic wiki information + $expData->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'rdfs', 'label' ), + new SMWExpLiteral( Site::name() ) + ); + + $expData->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'swivt', 'siteName' ), + new SMWExpLiteral( Site::name(), 'http://www.w3.org/2001/XMLSchema#string' ) + ); + + $expData->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'swivt', 'pagePrefix' ), + new SMWExpLiteral( SMWExporter::getInstance()->expandURI( '&wikiurl;' ), 'http://www.w3.org/2001/XMLSchema#string' ) + ); + + $expData->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'swivt', 'smwVersion' ), + new SMWExpLiteral( SMW_VERSION, 'http://www.w3.org/2001/XMLSchema#string' ) + ); + + $expData->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'swivt', 'langCode' ), + new SMWExpLiteral( Site::languageCode(), 'http://www.w3.org/2001/XMLSchema#string' ) + ); + + $mainpage = Title::newMainPage(); + + if ( !is_null( $mainpage ) ) { + $ed = new SMWExpData( new SMWExpResource( $mainpage->getFullURL() ) ); + $expData->addPropertyObjectValue( SMWExporter::getInstance()->getSpecialNsResource( 'swivt', 'mainPage' ), $ed ); + } + + // statistical information + foreach ( Site::stats() as $key => $value ) { + $expData->addPropertyObjectValue( + SMWExporter::getInstance()->getSpecialNsResource( 'swivt', $key ), + new SMWExpLiteral( (string)$value, 'http://www.w3.org/2001/XMLSchema#int' ) + ); + } + + $this->serializer->startSerialization(); + $this->serializer->serializeExpData( SMWExporter::getInstance()->getOntologyExpData( '' ) ); + $this->serializer->serializeExpData( $expData ); + + // link to list of existing pages: + if ( strpos( SMWExporter::getInstance()->expandURI( '&wikiurl;' ), '?' ) === false ) { // check whether we have title as a first parameter or in URL + $nexturl = SMWExporter::getInstance()->expandURI( '&export;?offset=0' ); + } else { + $nexturl = SMWExporter::getInstance()->expandURI( '&export;&offset=0' ); + } + $expData = new SMWExpData( new SMWExpResource( $nexturl ) ); + $ed = new SMWExpData( SMWExporter::getInstance()->getSpecialNsResource( 'owl', 'Thing' ) ); + $expData->addPropertyObjectValue( SMWExporter::getInstance()->getSpecialNsResource( 'rdf', 'type' ), $ed ); + $ed = new SMWExpData( new SMWExpResource( $nexturl ) ); + $expData->addPropertyObjectValue( SMWExporter::getInstance()->getSpecialNsResource( 'rdfs', 'isDefinedBy' ), $ed ); + $this->serializer->serializeExpData( $expData ); + + $this->serializer->finishSerialization(); + $this->flush( true ); + + } + + /** + * This function checks whether some article fits into a given namespace + * restriction. Restrictions are encoded as follows: a non-negative number + * requires the namespace to be identical to the given number; "-1" + * requires the namespace to be different from Category, Property, and + * Type; "false" means "no restriction". + * + * @param $res mixed encoding the restriction as described above + * @param $ns integer the namespace constant to be checked + * + * @return boolean + */ + static public function fitsNsRestriction( $res, $ns ) { + if ( $res === false ) { + return true; + } + if ( is_array( $res ) ) { + return in_array( $ns, $res ); + } + if ( $res >= 0 ) { + return ( $res == $ns ); + } + return ( ( $res != NS_CATEGORY ) && ( $res != SMW_NS_PROPERTY ) ); + } + + private function getDeepRedirectTargetResolver() { + + if ( $this->deepRedirectTargetResolver === null ) { + $this->deepRedirectTargetResolver = ApplicationFactory::getInstance()->newMwCollaboratorFactory()->newDeepRedirectTargetResolver(); + } + + return $this->deepRedirectTargetResolver; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Exporter.php b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Exporter.php new file mode 100644 index 00000000..9f97d748 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Exporter.php @@ -0,0 +1,675 @@ +<?php + +use SMW\ApplicationFactory; +use SMW\DataTypeRegistry; +use SMW\DataValueFactory; +use SMW\DIProperty; +use SMW\Site; +use SMW\Exporter\DataItemMatchFinder; +use SMW\Exporter\Element\ExpElement; +use SMW\Exporter\Element\ExpLiteral; +use SMW\Exporter\Element\ExpNsResource; +use SMW\Exporter\ElementFactory; +use SMW\Exporter\Escaper; +use SMW\Exporter\ExpResourceMapper; +use SMW\Exporter\ResourceBuilders\DispatchingResourceBuilder; +use SMW\Localizer; +use SMW\NamespaceUriFinder; + +/** + * SMWExporter is a class for converting internal page-based data (SMWSemanticData) into + * a format for easy serialisation in OWL or RDF. + * + * @author Markus Krötzsch + * @ingroup SMW + */ +class SMWExporter { + + const POOLCACHE_ID = 'exporter.shared'; + + /** + * @var SMWExporter + */ + private static $instance = null; + + /** + * @var ExpResourceMapper + */ + private static $expResourceMapper = null; + + /** + * @var ElementFactory + */ + private static $elementFactory = null; + + /** + * @var DataItemMatchFinder + */ + private static $dataItemMatchFinder = null; + + /** + * @var DispatchingResourceBuilder + */ + private static $dispatchingResourceBuilder = null; + + static protected $m_exporturl = false; + static protected $m_ent_wiki = false; + static protected $m_ent_property = false; + static protected $m_ent_category = false; + static protected $m_ent_wikiurl = false; + + /** + * @since 2.0 + * + * @return SMWExporter + */ + public static function getInstance() { + + if ( self::$instance === null ) { + + self::$instance = new self(); + self::$instance->initBaseURIs(); + + $applicationFactory = ApplicationFactory::getInstance(); + + $poolCache = $applicationFactory->getInMemoryPoolCache(); + $poolCache->resetPoolCacheById( self::POOLCACHE_ID ); + + // FIXME with 3.x + // There is no better way of getting around the static use without BC + self::$elementFactory = new ElementFactory(); + + self::$dispatchingResourceBuilder = new DispatchingResourceBuilder(); + + self::$expResourceMapper = new ExpResourceMapper( + $applicationFactory->getStore() + ); + + self::$expResourceMapper->reset(); + + self::$expResourceMapper->setBCAuxiliaryUse( + $applicationFactory->getSettings()->get( 'smwgExportBCAuxiliaryUse' ) + ); + + self::$dataItemMatchFinder = new DataItemMatchFinder( + $applicationFactory->getStore(), + self::$m_ent_wiki + ); + } + + return self::$instance; + } + + /** + * @since 2.0 + */ + public static function clear() { + self::$instance = null; + self::$m_exporturl = false; + } + + /** + * @since 2.2 + */ + public function resetCacheBy( SMWDIWikiPage $diWikiPage ) { + self::$expResourceMapper->invalidateCache( $diWikiPage ); + } + + /** + * Make sure that necessary base URIs are initialised properly. + */ + static public function initBaseURIs() { + if ( self::$m_exporturl !== false ) { + return; + } + global $wgContLang; + + global $smwgNamespace; // complete namespace for URIs (with protocol, usually http://) + + $resolver = Title::makeTitle( NS_SPECIAL, 'URIResolver' ); + + if ( '' == $smwgNamespace ) { + $smwgNamespace = $resolver->getFullURL() . '/'; + } elseif ( $smwgNamespace[0] == '.' ) { + $smwgNamespace = "http://" . substr( $smwgNamespace, 1 ) . $resolver->getLocalURL() . '/'; + } + + // The article name must be the last part of wiki URLs for proper OWL/RDF export: + self::$m_ent_wikiurl = Site::wikiurl(); + self::$m_ent_wiki = $smwgNamespace; + + $property = $GLOBALS['smwgExportBCNonCanonicalFormUse'] ? urlencode( str_replace( ' ', '_', $wgContLang->getNsText( SMW_NS_PROPERTY ) ) ) : 'Property'; + $category = $GLOBALS['smwgExportBCNonCanonicalFormUse'] ? urlencode( str_replace( ' ', '_', $wgContLang->getNsText( NS_CATEGORY ) ) ) : 'Category'; + + self::$m_ent_property = self::$m_ent_wiki . Escaper::encodeUri( $property . ':' ); + self::$m_ent_category = self::$m_ent_wiki . Escaper::encodeUri( $category . ':' ); + + $title = Title::makeTitle( NS_SPECIAL, 'ExportRDF' ); + self::$m_exporturl = self::$m_ent_wikiurl . $title->getPrefixedURL(); + + // Canonical form, the title object always contains a wgContLang reference + // therefore replace it + if ( !$GLOBALS['smwgExportBCNonCanonicalFormUse'] ) { + $localizer = Localizer::getInstance(); + + self::$m_ent_property = $localizer->getCanonicalizedUrlByNamespace( NS_SPECIAL, self::$m_ent_property ); + self::$m_ent_category = $localizer->getCanonicalizedUrlByNamespace( NS_SPECIAL, self::$m_ent_category ); + self::$m_ent_wiki = $localizer->getCanonicalizedUrlByNamespace( NS_SPECIAL, self::$m_ent_wiki ); + self::$m_exporturl = $localizer->getCanonicalizedUrlByNamespace( NS_SPECIAL, self::$m_exporturl ); + } + } + + /** + * Create exportable data from a given semantic data record. + * + * @param $semdata SMWSemanticData + * @return SMWExpData + */ + static public function makeExportData( SMWSemanticData $semdata ) { + self::initBaseURIs(); + + $subject = $semdata->getSubject(); + + // Make sure to use the canonical form, a localized representation + // should not carry a reference to a subject (e.g invoked as incoming + // property caused by a different user language) + if ( $subject->getNamespace() === SMW_NS_PROPERTY && $subject->getSubobjectName() === '' ) { + $subject = DIProperty::newFromUserLabel( $subject->getDBKey() )->getCanonicalDiWikiPage(); + } + + // #1690 Couldn't match a CanonicalDiWikiPage which is most likely caused + // by an outdated pre-defined property therefore use the original subject + if ( $subject->getDBKey() === '' ) { + $subject = $semdata->getSubject(); + } + + // #649 Alwways make sure to have a least one valid sortkey + if ( !$semdata->getPropertyValues( new DIProperty( '_SKEY' ) ) && $subject->getSortKey() !== '' ) { + + // @see SMWSQLStore3Writers::getSortKey + if ( $semdata->getExtensionData( 'sort.extension' ) !== null ) { + $sortkey = $semdata->getExtensionData( 'sort.extension' ); + } else { + $sortkey = $subject->getSortKey(); + } + + // Extend the subobject sortkey in case no @sortkey was given for an + // entity + if ( $subject->getSubobjectName() !== '' ) { + + // Add sort data from some dedicated containers (of a record or + // reference type etc.) otherwise use the sobj name as extension + // to distinguish each entity + if ( $semdata->getExtensionData( 'sort.data' ) !== null ) { + $sortkey .= '#' . $semdata->getExtensionData( 'sort.data' ); + } else { + $sortkey .= '#' . $subject->getSubobjectName(); + } + } + + // #649 Be consistent about how sortkeys are stored therefore always + // normalize even for usages like {{DEFAULTSORT: Foo_bar }} + $sortkey = str_replace( '_', ' ', $sortkey ); + + $semdata->addPropertyObjectValue( + new DIProperty( '_SKEY' ), + new SMWDIBlob( $sortkey ) + ); + } + + $result = self::makeExportDataForSubject( $subject ); + + foreach ( $semdata->getProperties() as $property ) { + self::addPropertyValues( $property, $semdata->getPropertyValues( $property ), $result, $subject ); + } + + return $result; + } + + /** + * Make an SMWExpData object for the given page, and include the basic + * properties about this subject that are not directly represented by + * SMW property values. The optional parameter $typevalueforproperty + * can be used to pass a particular SMWTypesValue object that is used + * for determining the OWL type for property pages. + * + * @todo Take into account whether the wiki page belongs to a builtin property, and ensure URI alignment/type declaration in this case. + * + * @param $diWikiPage SMWDIWikiPage + * @param $addStubData boolean to indicate if additional data should be added to make a stub entry for this page + * @return SMWExpData + */ + static public function makeExportDataForSubject( SMWDIWikiPage $diWikiPage, $addStubData = false ) { + global $wgContLang; + + $wikiPageExpElement = self::getDataItemExpElement( $diWikiPage ); + $result = new SMWExpData( $wikiPageExpElement ); + + if ( $diWikiPage->getSubobjectName() !== '' ) { + $result->addPropertyObjectValue( + self::getSpecialNsResource( 'rdf', 'type' ), + self::getSpecialNsResource( 'swivt', 'Subject' ) + ); + + $masterPage = new SMWDIWikiPage( + $diWikiPage->getDBkey(), + $diWikiPage->getNamespace(), + $diWikiPage->getInterwiki() + ); + + $result->addPropertyObjectValue( + self::getSpecialNsResource( 'swivt', 'masterPage' ), + self::getDataItemExpElement( $masterPage ) + ); + + // #649 + // Subobjects contain there individual sortkey's therefore + // no need to add them twice + + // #520 + $result->addPropertyObjectValue( + self::getSpecialNsResource( 'swivt', 'wikiNamespace' ), + new ExpLiteral( strval( $diWikiPage->getNamespace() ), 'http://www.w3.org/2001/XMLSchema#integer' ) + ); + + } else { + + // #1410 (use the display title as label when available) + $displayTitle = DataValueFactory::getInstance()->newDataValueByItem( $diWikiPage )->getDisplayTitle(); + + $pageTitle = str_replace( '_', ' ', $diWikiPage->getDBkey() ); + if ( $diWikiPage->getNamespace() !== 0 ) { + $prefixedSubjectTitle = $wgContLang->getNsText( $diWikiPage->getNamespace()) . ":" . $pageTitle; + } else { + $prefixedSubjectTitle = $pageTitle; + } + + $prefixedSubjectUrl = Escaper::encodeUri( str_replace( ' ', '_', $prefixedSubjectTitle ) ); + + switch ( $diWikiPage->getNamespace() ) { + case NS_CATEGORY: case SMW_NS_CONCEPT: + $maintype_pe = self::getSpecialNsResource( 'owl', 'Class' ); + $label = $pageTitle; + break; + case SMW_NS_PROPERTY: + $property = new DIProperty( $diWikiPage->getDBKey() ); + $maintype_pe = self::getSpecialNsResource( 'owl', self::getOWLPropertyType( $property->findPropertyTypeID() ) ); + $label = $pageTitle; + break; + default: + $label = $prefixedSubjectTitle; + $maintype_pe = self::getSpecialNsResource( 'swivt', 'Subject' ); + } + + $result->addPropertyObjectValue( self::getSpecialNsResource( 'rdf', 'type' ), $maintype_pe ); + + if ( !$wikiPageExpElement->isBlankNode() ) { + $ed = new ExpLiteral( $displayTitle !== '' ? $displayTitle : $label ); + $result->addPropertyObjectValue( self::getSpecialNsResource( 'rdfs', 'label' ), $ed ); + $ed = new SMWExpResource( self::$m_exporturl . '/' . $prefixedSubjectUrl ); + $result->addPropertyObjectValue( self::getSpecialNsResource( 'rdfs', 'isDefinedBy' ), $ed ); + $ed = new SMWExpResource( self::getNamespaceUri( 'wikiurl' ) . $prefixedSubjectUrl ); + $result->addPropertyObjectValue( self::getSpecialNsResource( 'swivt', 'page' ), $ed ); + $ed = new ExpLiteral( strval( $diWikiPage->getNamespace() ), 'http://www.w3.org/2001/XMLSchema#integer' ); + $result->addPropertyObjectValue( self::getSpecialNsResource( 'swivt', 'wikiNamespace' ), $ed ); + + if ( $addStubData ) { + // Add a default sort key; for pages that exist in the wiki, + // this is set during parsing + $property = new DIProperty( '_SKEY' ); + $resourceBuilder = self::$dispatchingResourceBuilder->findResourceBuilder( $property ); + $resourceBuilder->addResourceValue( $result, $property, $diWikiPage ); + } + + if ( $diWikiPage->getPageLanguage() ) { + $result->addPropertyObjectValue( + self::getSpecialNsResource( 'swivt', 'wikiPageContentLanguage' ), + new ExpLiteral( strval( $diWikiPage->getPageLanguage() ), 'http://www.w3.org/2001/XMLSchema#string' ) + ); + } + + if ( $diWikiPage->getNamespace() === NS_FILE ) { + + $title = Title::makeTitle( $diWikiPage->getNamespace(), $diWikiPage->getDBkey() ); + $file = wfFindFile( $title ); + + if ( $file !== false ) { + $result->addPropertyObjectValue( + self::getSpecialNsResource( 'swivt', 'file' ), + new SMWExpResource( $file->getFullURL() ) + ); + } + } + } + } + + return $result; + } + + /** + * Extend a given SMWExpData element by adding export data for the + * specified property data itme. This method is called when + * constructing export data structures from SMWSemanticData objects. + * + * @param $property SMWDIProperty + * @param $dataItems array of SMWDataItem objects for the given property + * @param $data SMWExpData to add the data to + */ + static public function addPropertyValues( SMWDIProperty $property, array $dataItems, SMWExpData &$expData ) { + + $resourceBuilder = self::$dispatchingResourceBuilder->findResourceBuilder( $property ); + + if ( $property->isUserDefined() ) { + foreach ( $dataItems as $dataItem ) { + $resourceBuilder->addResourceValue( $expData, $property, $dataItem ); + } + } else { // pre-defined property, only exported if known + $diSubject = $expData->getSubject()->getDataItem(); + // subject wikipage required for disambiguating special properties: + if ( is_null( $diSubject ) || + $diSubject->getDIType() != SMWDataItem::TYPE_WIKIPAGE ) { + return; + } + + $pe = self::getSpecialPropertyResource( $property->getKey(), $diSubject->getNamespace() ); + if ( is_null( $pe ) ) { + return; // unknown special property, not exported + } + // have helper property ready before entering the for loop, even if not needed: + $peHelper = self::getResourceElementForProperty( $property, true ); + + $filterNamespace = ( $property->getKey() == '_REDI' || $property->getKey() == '_URI' ); + + foreach ( $dataItems as $dataItem ) { + // Basic namespace filtering to ensure that types match for redirects etc. + /// TODO: currently no full check for avoiding OWL DL illegal redirects is done (OWL property type ignored) + if ( $filterNamespace && !( $dataItem instanceof SMWDIUri ) && + ( !( $dataItem instanceof SMWDIWikiPage ) ) ) { + continue; + } + + $resourceBuilder->addResourceValue( $expData, $property, $dataItem ); + } + } + } + + /** + * @see ExpResourceMapper::mapPropertyToResourceElement + */ + static public function getResourceElementForProperty( SMWDIProperty $diProperty, $helperProperty = false, $seekImportVocabulary = true ) { + return self::$expResourceMapper->mapPropertyToResourceElement( $diProperty, $helperProperty, $seekImportVocabulary ); + } + + /** + * @see ExpResourceMapper::mapWikiPageToResourceElement + */ + static public function getResourceElementForWikiPage( SMWDIWikiPage $diWikiPage, $markForAuxiliaryUsage = false ) { + return self::$expResourceMapper->mapWikiPageToResourceElement( $diWikiPage, $markForAuxiliaryUsage ); + } + + /** + * Try to find an SMWDataItem that the given ExpElement might + * represent. Returns null if this attempt failed. + * + * @param ExpElement $expElement + * @return SMWDataItem or null + */ + public function findDataItemForExpElement( ExpElement $expElement ) { + return self::$dataItemMatchFinder->matchExpElement( $expElement ); + } + + /** + * Determine what kind of OWL property some SMW property should be exported as. + * The input is an SMWTypesValue object, a typeid string, or empty (use default) + * @todo An improved mechanism for selecting property types here is needed. + */ + static public function getOWLPropertyType( $type = '' ) { + if ( $type instanceof SMWDIWikiPage ) { + $type = DataTypeRegistry::getInstance()->findTypeByLabel( str_replace( '_', ' ', $type->getDBkey() ) ); + } elseif ( $type == false ) { + $type = ''; + } + + switch ( $type ) { + case '_anu': + return 'AnnotationProperty'; + case '': case '_wpg': case '_wpp': case '_wpc': case '_wpf': + case '_uri': case '_ema': case '_tel': case '_rec': case '__typ': + case '__red': case '__spf': case '__spu': + return 'ObjectProperty'; + default: + return 'DatatypeProperty'; + } + } + + /** + * Get an ExpNsResource for a special property of SMW, or null if + * no resource is assigned to the given property key. The optional + * namespace is used to select the proper resource for properties that + * must take the type of the annotated object into account for some + * reason. + * + * @param $propertyKey string the Id of the special property + * @param $forNamespace integer the namespace of the page which has a value for this property + * @return ExpNsResource|null + */ + static public function getSpecialPropertyResource( $propertyKey, $forNamespace = NS_MAIN ) { + switch ( $propertyKey ) { + case '_INST': + return self::getSpecialNsResource( 'rdf', 'type' ); + case '_SUBC': + return self::getSpecialNsResource( 'rdfs', 'subClassOf' ); + case '_CONC': + return self::getSpecialNsResource( 'owl', 'equivalentClass' ); + case '_URI': + if ( $forNamespace == NS_CATEGORY || $forNamespace == SMW_NS_CONCEPT ) { + return self::getSpecialNsResource( 'owl', 'equivalentClass' ); + } elseif ( $forNamespace == SMW_NS_PROPERTY ) { + return self::getSpecialNsResource( 'owl', 'equivalentProperty' ); + } else { + return self::getSpecialNsResource( 'owl', 'sameAs' ); + } + case '_REDI': + return self::getSpecialNsResource( 'swivt', 'redirectsTo' ); + case '_SUBP': + if ( $forNamespace == SMW_NS_PROPERTY ) { + return self::getSpecialNsResource( 'rdfs', 'subPropertyOf' ); + } else { + return null; + } + case '_MDAT': + return self::getSpecialNsResource( 'swivt', 'wikiPageModificationDate' ); + case '_CDAT': + return self::getSpecialNsResource( 'swivt', 'wikiPageCreationDate' ); + case '_LEDT': + return self::getSpecialNsResource( 'swivt', 'wikiPageLastEditor' ); + case '_NEWP': + return self::getSpecialNsResource( 'swivt', 'wikiPageIsNew' ); + case '_SKEY': + return self::getSpecialNsResource( 'swivt', 'wikiPageSortKey' ); + case '_TYPE': + return self::getSpecialNsResource( 'swivt', 'type' ); + case '_IMPO': + return self::getSpecialNsResource( 'swivt', 'specialImportedFrom' ); + default: + return self::getSpecialNsResource( 'swivt', 'specialProperty' . $propertyKey ); + } + } + + + /** + * Create an ExpNsResource for some special element that belongs to + * a known vocabulary. An exception is generated when given parameters + * that do not fit any known vocabulary. + * + * @param $namespaceId string (e.g. "rdf") + * @param $localName string (e.g. "type") + * @return ExpNsResource + */ + static public function getSpecialNsResource( $namespaceId, $localName ) { + $namespace = self::getNamespaceUri( $namespaceId ); + if ( $namespace !== '' ) { + return new ExpNsResource( $localName, $namespace, $namespaceId ); + } else { + throw new InvalidArgumentException( "The vocabulary '$namespaceId' is not a known special vocabulary." ); + } + } + + /** + * This function expands standard XML entities used in some generated + * URIs. Given a string with such entities, it returns a string with + * all entities properly replaced. + * + * @note The function SMWExporter::getInstance()->getNamespaceUri() is often more + * suitable. This XML-specific method might become obsolete. + * + * @param $uri string of the URI to be expanded + * @return string of the expanded URI + */ + static public function expandURI( $uri ) { + self::initBaseURIs(); + $uri = str_replace( [ '&wiki;', '&wikiurl;', '&property;', '&category;', '&owl;', '&rdf;', '&rdfs;', '&swivt;', '&export;' ], + [ self::$m_ent_wiki, self::$m_ent_wikiurl, self::$m_ent_property, self::$m_ent_category, 'http://www.w3.org/2002/07/owl#', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'http://www.w3.org/2000/01/rdf-schema#', 'http://semantic-mediawiki.org/swivt/1.0#', + self::$m_exporturl ], + $uri ); + return $uri; + } + + /** + * @return string + */ + public function decodeURI( $uri ) { + return Escaper::decodeUri( $uri ); + } + + /** + * Get the URI of a standard namespace prefix used in SMW, or the empty + * string if the prefix is not known. + * + * @param $shortName string id (prefix) of the namespace + * @return string of the expanded URI + */ + static public function getNamespaceUri( $shortName ) { + self::initBaseURIs(); + + if ( ( $uri = NamespaceUriFinder::getUri( $shortName ) ) !== false ) { + return $uri; + } + + switch ( $shortName ) { + case 'wiki': + return self::$m_ent_wiki; + case 'wikiurl': + return self::$m_ent_wikiurl; + case 'property': + return self::$m_ent_property; + case 'category': + return self::$m_ent_category; + case 'export': + return self::$m_exporturl; + case 'owl': + return 'http://www.w3.org/2002/07/owl#'; + case 'rdf': + return 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + case 'rdfs': + return 'http://www.w3.org/2000/01/rdf-schema#'; + case 'swivt': + return 'http://semantic-mediawiki.org/swivt/1.0#'; + case 'xsd': + return 'http://www.w3.org/2001/XMLSchema#'; + default: + return ''; + } + } + + /** + * Create an SMWExpData container that encodes the ontology header for an + * SMW exported OWL file. + * + * @param string $ontologyuri specifying the URI of the ontology, possibly + * empty + * + * @return SMWExpData + */ + static public function getOntologyExpData( $ontologyuri ) { + $data = new SMWExpData( new SMWExpResource( $ontologyuri ) ); + $ed = self::getSpecialNsResource( 'owl', 'Ontology' ); + $data->addPropertyObjectValue( self::getSpecialNsResource( 'rdf', 'type' ), $ed ); + $ed = new ExpLiteral( date( DATE_W3C ), 'http://www.w3.org/2001/XMLSchema#dateTime' ); + $data->addPropertyObjectValue( self::getSpecialNsResource( 'swivt', 'creationDate' ), $ed ); + $ed = new SMWExpResource( 'http://semantic-mediawiki.org/swivt/1.0' ); + $data->addPropertyObjectValue( self::getSpecialNsResource( 'owl', 'imports' ), $ed ); + return $data; + } + + /** + * @see ElementFactory::mapDataItemToElement + */ + static public function getDataItemExpElement( SMWDataItem $dataItem ) { + return self::$elementFactory->newFromDataItem( $dataItem ); + } + + /** + * Create an SWMExpElement that encodes auxiliary data for representing + * values of the specified dataitem object in a simplified fashion. + * This is done for types of dataitems that are not supported very well + * in current systems, or that do not match a standard datatype in RDF. + * For example, time points (DITime) are encoded as numbers. The number + * can replace the actual time for all query and ordering purposes (the + * order in either case is linear and maps to the real number line). + * Only data retrieval should better use the real values to avoid that + * rounding errors lead to unfaithful recovery of data. Note that the + * helper values do not maintain any association with their original + * values -- they are a fully redundant alternative representation, not + * an additional piece of information for the main values. Even if + * decoding is difficult, they must be in one-to-one correspondence to + * the original value. + * + * For dataitems that do not have such a simplification, the method + * returns null. + * + * @note If a helper element is used, then it must be the same as + * getDataItemHelperExpElement( $dataItem->getSortKeyDataItem() ). + * Query conditions like ">" use sortkeys for values, and helper + * elements are always preferred in query answering. + * + * @param $dataItem SMWDataItem + * @return ExpElement|null + */ + static public function getDataItemHelperExpElement( SMWDataItem $dataItem ) { + + if ( $dataItem->getDIType() == SMWDataItem::TYPE_TIME ) { + return new ExpLiteral( (string)$dataItem->getSortKey(), 'http://www.w3.org/2001/XMLSchema#double', '', $dataItem ); + } + + if ( $dataItem->getDIType() == SMWDataItem::TYPE_GEO ) { + return new ExpLiteral( (string)$dataItem->getSortKey(), 'http://www.w3.org/2001/XMLSchema#string', '', $dataItem ); + } + + return null; + } + + /** + * Check whether the values of a given type of dataitem have helper + * values in the sense of SMWExporter::getInstance()->getDataItemHelperExpElement(). + * + * @param DIProperty $property + * + * @return boolean + */ + static public function hasHelperExpElement( DIProperty $property ) { + return ( $property->findPropertyTypeID() === '_dat' || $property->findPropertyTypeID() === '_geo' ) || ( !$property->isUserDefined() && !self::hasSpecialPropertyResource( $property ) ); + } + + static protected function hasSpecialPropertyResource( DIProperty $property ) { + return $property->getKey() === '_SKEY' || + $property->getKey() === '_INST' || + $property->getKey() === '_MDAT' || + $property->getKey() === '_SUBC' || + $property->getKey() === '_SUBP' || + $property->getKey() === '_TYPE' || + $property->getKey() === '_IMPO' || + $property->getKey() === '_URI'; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer.php b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer.php new file mode 100644 index 00000000..775d1b42 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer.php @@ -0,0 +1,333 @@ +<?php + +/** + * File holding the SMWSerializer class that provides basic functions for + * serialising data in OWL and RDF syntaxes. + * + * @ingroup SMW + * + * @author Markus Krötzsch + */ + +define( 'SMW_SERIALIZER_DECL_CLASS', 1 ); +define( 'SMW_SERIALIZER_DECL_OPROP', 2 ); +define( 'SMW_SERIALIZER_DECL_APROP', 4 ); + +/** + * Abstract class for serializing exported data (encoded as SMWExpData object) + * in a concrete syntactic format such as Turtle or RDF/XML. The serializer + * adds object serialisations to an internal string that can be retrieved for + * pushing it to an output. This abstract class does not define this string as + * implementations may want to use their own scheme (e.g. using two buffers as + * in the case of SMWRDFXMLSerializer). The function flushContent() returns the + * string serialized so far so as to enable incremental serialization. + * + * RDF and OWL have two types of dependencies that are managed by this class: + * namespaces (and similar abbreviation schemes) and element declarations. + * The former need to be defined before being used, while the latter can occur + * at some later point in the serialization. Declarations are relevant to the + * OWL data model, being one of Class, DatatypeProperty, and ObjectProperty + * (only the latter two are mutually exclusive). This class determines the + * required declaration from the context in which an element is used. + * + * @ingroup SMW + */ +abstract class SMWSerializer { + /** + * The current working string is obtained by concatenating the strings + * $pre_ns_buffer and $post_ns_buffer. The split between the two is such + * that one can append additional namespace declarations to $pre_ns_buffer + * so that they affect all current elements. The buffers are flushed during + * output in order to achieve "streaming" RDF export for larger files. + * @var string + */ + protected $pre_ns_buffer; + /** + * See documentation for $pre_ns_buffer. + * @var string + */ + protected $post_ns_buffer; + /** + * Array for recording required declarations; format: + * resourcename => decl-flag, where decl-flag is a sum of flags + * SMW_SERIALIZER_DECL_CLASS, SMW_SERIALIZER_DECL_OPROP, + * SMW_SERIALIZER_DECL_APROP. + * @var array of integer + */ + protected $decl_todo; + /** + * Array for recording previous declarations; format: + * resourcename => decl-flag, where decl-flag is a sum of flags + * SMW_SERIALIZER_DECL_CLASS, SMW_SERIALIZER_DECL_OPROP, + * SMW_SERIALIZER_DECL_APROP. + * @var array of integer + */ + protected $decl_done; + /** + * Array of additional namespaces (abbreviation => URI), flushed on + * closing the current namespace tag. Since we export in a streamed + * way, it is not always possible to embed additional namespaces into + * a syntactic block (e.g. an RDF/XML tag) which might have been sent to + * the client already. But we wait with printing the current block so that + * extra namespaces from this array can still be printed (note that one + * never know which extra namespaces you encounter during export). + * @var array of string + */ + protected $extra_namespaces; + /** + * Array of namespaces that have been declared globally already. Contains + * entries of format 'namespace abbreviation' => true, assuming that the + * same abbreviation always refers to the same URI. + * @var array of string + */ + protected $global_namespaces; + + /** + * Constructor. + */ + public function __construct() { + $this->clear(); + } + + /** + * Clear internal states to start a new serialization. + */ + public function clear() { + $this->pre_ns_buffer = ''; + $this->post_ns_buffer = ''; + $this->decl_todo = []; + $this->decl_done = []; + $this->global_namespaces = []; + $this->extra_namespaces = []; + } + + /** + * Start a new serialization, resetting all internal data and serializing + * necessary header elements. + */ + public function startSerialization() { + $this->clear(); + $this->serializeHeader(); + } + + /** + * Complete the serialization so that calling flushContent() will return + * the final part of the output, leading to a complete serialization with + * all necessary declarations. No further serialization functions must be + * called after this. + */ + public function finishSerialization() { + $this->serializeDeclarations(); + $this->serializeFooter(); + } + + /** + * Serialize the header (i.e. write it to the internal buffer). May + * include standard syntax to start output but also declare some common + * namespaces globally. + */ + abstract protected function serializeHeader(); + + /** + * Serialise the footer (i.e. write it to the internal buffer). + */ + abstract protected function serializeFooter(); + + /** + * Serialize any declarations that have been found to be missing while + * serializing other elements. + */ + public function serializeDeclarations() { + foreach ( $this->decl_todo as $name => $flag ) { + $types = []; + if ( $flag & SMW_SERIALIZER_DECL_CLASS ) { + $types[] = 'owl:Class'; + } + if ( $flag & SMW_SERIALIZER_DECL_OPROP ) { + $types[] = 'owl:ObjectProperty'; + } + if ( $flag & SMW_SERIALIZER_DECL_APROP ) { + $types[] = 'owl:DatatypeProperty'; + } + foreach ( $types as $typename ) { + $this->serializeDeclaration( $name, $typename ); + } + $curdone = array_key_exists( $name, $this->decl_done ) ? $this->decl_done[$name] : 0; + $this->decl_done[$name] = $curdone | $flag; + } + $this->decl_todo = []; // reset all + } + + /** + * Serialize a single declaration for the given $uri (expanded) and type + * (given as a QName). + * @param $uri string URI of the thing to declare + * @param $typename string one of owl:Class, owl:ObjectProperty, and + * owl:datatypeProperty + */ + abstract public function serializeDeclaration( $uri, $typename ); + + /** + * Serialise the given SMWExpData object. The method must not assume that + * the exported data refers to wiki pages or other SMW data, and it must + * ensure that all required auxiliary declarations for obtaining proper OWL + * are included in any case (this can be done using requireDeclaration()). + * + * @param $data SMWExpData containing the data to be serialised. + */ + abstract public function serializeExpData( SMWExpData $data ); + + /** + * Get the string that has been serialized so far. This function also + * resets the internal buffers for serilized strings and namespaces + * (what is flushed is gone). + */ + public function flushContent() { + if ( ( $this->pre_ns_buffer === '' ) && ( $this->post_ns_buffer === '' ) ) { + return ''; + } + $this->serializeNamespaces(); + $result = $this->pre_ns_buffer . $this->post_ns_buffer; + $this->pre_ns_buffer = ''; + $this->post_ns_buffer = ''; + return $result; + } + + /** + * Include collected namespace information into the serialization. + */ + protected function serializeNamespaces() { + foreach ( $this->extra_namespaces as $nsshort => $nsuri ) { + $this->serializeNamespace( $nsshort, $nsuri ); + } + $this->extra_namespaces = []; + } + + /** + * Serialize a single namespace. + * Namespaces that were serialized in such a way that they remain + * available for all following output should be added to + * $global_namespaces. + * @param $shortname string abbreviation/prefix to declare + * @param $uri string URI prefix that the namespace encodes + */ + abstract protected function serializeNamespace( $shortname, $uri ); + + /** + * Require an additional namespace to be declared in the serialization. + * The function checks whether the required namespace is available globally + * and add it to the list of required namespaces otherwise. + */ + protected function requireNamespace( $nsshort, $nsuri ) { + if ( !array_key_exists( $nsshort, $this->global_namespaces ) ) { + $this->extra_namespaces[$nsshort] = $nsuri; + } + } + + /** + * State that a certain declaration is needed. The method checks if the + * declaration is already available, and records a todo otherwise. + */ + protected function requireDeclaration( SMWExpResource $resource, $decltype ) { + // Do not declare predefined OWL language constructs: + if ( $resource instanceof SMWExpNsResource ) { + $nsId = $resource->getNamespaceId(); + if ( ( $nsId == 'owl' ) || ( $nsId == 'rdf' ) || ( $nsId == 'rdfs' ) ) { + return; + } + } + // Do not declare blank nodes: + if ( $resource->isBlankNode() ) { + return; + } + + $name = $resource->getUri(); + if ( array_key_exists( $name, $this->decl_done ) && ( $this->decl_done[$name] & $decltype ) ) { + return; + } + if ( !array_key_exists( $name, $this->decl_todo ) ) { + $this->decl_todo[$name] = $decltype; + } else { + $this->decl_todo[$name] = $this->decl_todo[$name] | $decltype; + } + } + + /** + * Update the declaration "todo" and "done" lists for the case that the + * given data has been serialized with the type information it provides. + * + * @param $expData specifying the type data upon which declarations are based + */ + protected function recordDeclarationTypes( SMWExpData $expData ) { + foreach ( $expData->getSpecialValues( 'rdf', 'type') as $typeresource ) { + if ( $typeresource instanceof SMWExpNsResource ) { + switch ( $typeresource->getQName() ) { + case 'owl:Class': $typeflag = SMW_SERIALIZER_DECL_CLASS; + break; + case 'owl:ObjectProperty': $typeflag = SMW_SERIALIZER_DECL_OPROP; + break; + case 'owl:DatatypeProperty': $typeflag = SMW_SERIALIZER_DECL_APROP; + break; + default: $typeflag = 0; + } + if ( $typeflag != 0 ) { + $this->declarationDone( $expData->getSubject(), $typeflag ); + } + } + } + } + + /** + * Update the declaration "todo" and "done" lists to reflect the fact that + * the given element has been declared to has the given type. + * + * @param $element SMWExpResource specifying the element to update + * @param $typeflag integer specifying the type (e.g. SMW_SERIALIZER_DECL_CLASS) + */ + protected function declarationDone( SMWExpResource $element, $typeflag ) { + $name = $element->getUri(); + $curdone = array_key_exists( $name, $this->decl_done ) ? $this->decl_done[$name] : 0; + $this->decl_done[$name] = $curdone | $typeflag; + if ( array_key_exists( $name, $this->decl_todo ) ) { + $this->decl_todo[$name] = $this->decl_todo[$name] & ( ~$typeflag ); + if ( $this->decl_todo[$name] == 0 ) { + unset( $this->decl_todo[$name] ); + } + } + } + + /** + * Check if the given property is one of the special properties of the OWL + * language that require their values to be classes or RDF lists of + * classes. In these cases, it is necessary to declare this in the exported + * data. + * + * @note The list of properties checked here is not complete for the OWL + * language but covers what is used in SMW. + * @note OWL 2 allows URIs to refer to both classes and individual elements + * in different contexts. We only need declarations for classes that are + * used as such, hence it is enough to check the property. Moreover, we do + * not use OWL Datatypes in SMW, so rdf:type, rdfs:domain, etc. always + * refer to classes. + * + * @param SMWExpNsResource $property + * + * @return boolean + */ + protected function isOWLClassTypeProperty( SMWExpNsResource $property ) { + $locname = $property->getLocalName(); + if ( $property->getNamespaceID() == 'rdf' ) { + return ( $locname == 'type' ); + } elseif ( $property->getNamespaceID() == 'owl' ) { + return ( $locname == 'intersectionOf' ) || ( $locname == 'unionOf' ) || + ( $locname == 'equivalentClass' ) || + ( $locname == 'complementOf' ) || ( $locname == 'someValuesFrom' ) || + ( $locname == 'allValuesFrom' ) || ( $locname == 'onClass' ); + } elseif ( $property->getNamespaceID() == 'rdfs' ) { + return ( $locname == 'subClassOf' ) || ( $locname == 'range' ) || ( $locname == 'domain' ); + } else { + return false; + } + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer_RDFXML.php b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer_RDFXML.php new file mode 100644 index 00000000..4848dcf0 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer_RDFXML.php @@ -0,0 +1,277 @@ +<?php + +/** + * File holding the SMWRDFXMLSerializer class that provides basic functions for + * serialising OWL data in RDF/XML syntax. + * + * @ingroup SMW + * + * @author Markus Krötzsch + */ + +/** + * Class for serializing exported data (encoded as SMWExpData object) in + * RDF/XML. + * + * @ingroup SMW + */ +class SMWRDFXMLSerializer extends SMWSerializer { + /** + * True if the $pre_ns_buffer contains the beginning of a namespace + * declaration block to which further declarations for the current + * context can be appended. + */ + protected $namespace_block_started; + /** + * True if the namespaces that are added at the current serialization stage + * become global, i.e. remain available for all later contexts. This is the + * case in RDF/XML only as long as the header has not been streamed to the + * client (reflected herein by calling flushContent()). Later, namespaces + * can only be added locally to individual elements, thus requiring them to + * be re-added multiple times if used in many elements. + */ + protected $namespaces_are_global; + + public function clear() { + parent::clear(); + $this->namespaces_are_global = false; + $this->namespace_block_started = false; + } + + protected function serializeHeader() { + $this->namespaces_are_global = true; + $this->namespace_block_started = true; + $this->pre_ns_buffer = + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" . + "<!DOCTYPE rdf:RDF[\n" . + "\t<!ENTITY rdf " . $this->makeValueEntityString( SMWExporter::getInstance()->expandURI( '&rdf;' ) ) . ">\n" . + "\t<!ENTITY rdfs " . $this->makeValueEntityString( SMWExporter::getInstance()->expandURI( '&rdfs;' ) ) . ">\n" . + "\t<!ENTITY owl " . $this->makeValueEntityString( SMWExporter::getInstance()->expandURI( '&owl;' ) ) . ">\n" . + "\t<!ENTITY swivt " . $this->makeValueEntityString( SMWExporter::getInstance()->expandURI( '&swivt;' ) ) . ">\n" . + // A note on "wiki": this namespace is crucial as a fallback when it would be illegal to start e.g. with a number. + // In this case, one can always use wiki:... followed by "_" and possibly some namespace, since _ is legal as a first character. + "\t<!ENTITY wiki " . $this->makeValueEntityString( SMWExporter::getInstance()->expandURI( '&wiki;' ) ) . ">\n" . + "\t<!ENTITY category " . $this->makeValueEntityString( SMWExporter::getInstance()->expandURI( '&category;' ) ) . ">\n" . + "\t<!ENTITY property " . $this->makeValueEntityString( SMWExporter::getInstance()->expandURI( '&property;' ) ) . ">\n" . + "\t<!ENTITY wikiurl " . $this->makeValueEntityString( SMWExporter::getInstance()->expandURI( '&wikiurl;' ) ) . ">\n" . + "]>\n\n" . + "<rdf:RDF\n" . + "\txmlns:rdf=\"&rdf;\"\n" . + "\txmlns:rdfs=\"&rdfs;\"\n" . + "\txmlns:owl =\"&owl;\"\n" . + "\txmlns:swivt=\"&swivt;\"\n" . + "\txmlns:wiki=\"&wiki;\"\n" . + "\txmlns:category=\"&category;\"\n" . + "\txmlns:property=\"&property;\""; + $this->global_namespaces = [ 'rdf' => true, 'rdfs' => true, 'owl' => true, 'swivt' => true, 'wiki' => true, 'property' => true, 'category' => true ]; + $this->post_ns_buffer .= ">\n\n"; + } + + protected function serializeFooter() { + $this->post_ns_buffer .= "\t<!-- Created by Semantic MediaWiki, https://www.semantic-mediawiki.org/ -->\n"; + $this->post_ns_buffer .= '</rdf:RDF>'; + } + + public function serializeDeclaration( $uri, $typename ) { + $this->post_ns_buffer .= "\t<$typename rdf:about=\"$uri\" />\n"; + } + + public function serializeExpData( SMWExpData $expData ) { + $this->serializeNestedExpData( $expData, '' ); + $this->serializeNamespaces(); + if ( !$this->namespaces_are_global ) { + $this->pre_ns_buffer .= $this->post_ns_buffer; + $this->post_ns_buffer = ''; + $this->namespace_block_started = false; + } + } + + public function flushContent() { + $result = parent::flushContent(); + $this->namespaces_are_global = false; // must not be done before calling the parent method (which may declare namespaces) + $this->namespace_block_started = false; + return $result; + } + + protected function serializeNamespace( $shortname, $uri ) { + if ( $this->namespaces_are_global ) { + $this->global_namespaces[$shortname] = true; + $this->pre_ns_buffer .= "\n\t"; + } else { + $this->pre_ns_buffer .= ' '; + } + $this->pre_ns_buffer .= "xmlns:$shortname=\"$uri\""; + } + + /** + * Serialize the given SMWExpData object, possibly recursively with + * increased indentation. + * + * @param $expData SMWExpData containing the data to be serialised. + * @param $indent string specifying a prefix for indentation (usually a sequence of tabs) + */ + protected function serializeNestedExpData( SMWExpData $expData, $indent ) { + $this->recordDeclarationTypes( $expData ); + + $type = $expData->extractMainType()->getQName(); + if ( !$this->namespace_block_started ) { // start new ns block + $this->pre_ns_buffer .= "\t$indent<$type"; + $this->namespace_block_started = true; + } else { // continue running block + $this->post_ns_buffer .= "\t$indent<$type"; + } + + if ( ( $expData->getSubject() instanceof SMWExpResource ) && + !$expData->getSubject()->isBlankNode() ) { + $this->post_ns_buffer .= ' rdf:about="' . $expData->getSubject()->getUri() . '"'; + } // else: blank node, no "rdf:about" + + if ( count( $expData->getProperties() ) == 0 ) { // nothing else to export + $this->post_ns_buffer .= " />\n"; + } else { // process data + $this->post_ns_buffer .= ">\n"; + + foreach ( $expData->getProperties() as $property ) { + $prop_decl_queued = false; + $isClassTypeProp = $this->isOWLClassTypeProperty( $property ); + + foreach ( $expData->getValues( $property ) as $valueElement ) { + $this->requireNamespace( $property->getNamespaceID(), $property->getNamespace() ); + + if ( $valueElement instanceof SMWExpLiteral ) { + $prop_decl_type = SMW_SERIALIZER_DECL_APROP; + $this->serializeExpLiteral( $property, $valueElement, "\t\t$indent" ); + } elseif ( $valueElement instanceof SMWExpResource ) { + $prop_decl_type = SMW_SERIALIZER_DECL_OPROP; + $this->serializeExpResource( $property, $valueElement, "\t\t$indent", $isClassTypeProp ); + } elseif ( $valueElement instanceof SMWExpData ) { + $prop_decl_type = SMW_SERIALIZER_DECL_OPROP; + + $collection = $valueElement->getCollection(); + if ( $collection !== false ) { // RDF-style collection (list) + $this->serializeExpCollection( $property, $collection, "\t\t$indent", $isClassTypeProp ); + } elseif ( count( $valueElement->getProperties() ) > 0 ) { // resource with data + $this->post_ns_buffer .= "\t\t$indent<" . $property->getQName() . ">\n"; + $this->serializeNestedExpData( $valueElement, "\t\t$indent" ); + $this->post_ns_buffer .= "\t\t$indent</" . $property->getQName() . ">\n"; + } else { // resource without data + $this->serializeExpResource( $property, $valueElement->getSubject(), "\t\t$indent", $isClassTypeProp ); + } + } // else: no other types of export elements + + if ( !$prop_decl_queued ) { + $this->requireDeclaration( $property, $prop_decl_type ); + $prop_decl_queued = true; + } + } + } + $this->post_ns_buffer .= "\t$indent</" . $type . ">\n"; + } + } + + /** + * Add to the output a serialization of a property assignment where an + * SMWExpLiteral is the object. It is assumed that a suitable subject + * block has already been openend. + * + * @param $expResourceProperty SMWExpNsResource the property to use + * @param $expLiteral SMWExpLiteral the data value to use + * @param $indent string specifying a prefix for indentation (usually a sequence of tabs) + */ + protected function serializeExpLiteral( SMWExpNsResource $expResourceProperty, SMWExpLiteral $expLiteral, $indent ) { + $this->post_ns_buffer .= $indent . '<' . $expResourceProperty->getQName(); + + // https://www.w3.org/TR/rdf-syntax-grammar/#section-Syntax-languages + // "... to indicate that the included content is in the given language. + // Typed literals which includes XML literals are not affected by this + // attribute. The most specific in-scope language present (if any) is + // applied to property element string literal ..." + if ( $expLiteral->getDatatype() !== '' && $expLiteral->getLang() !== '' ) { + $this->post_ns_buffer .= ' xml:lang="' . $expLiteral->getLang() . '"'; + } elseif ( $expLiteral->getDatatype() !== '' ) { + $this->post_ns_buffer .= ' rdf:datatype="' . $expLiteral->getDatatype() . '"'; + } + + $this->post_ns_buffer .= '>' . $this->makeAttributeValueString( $expLiteral->getLexicalForm() ); + $this->post_ns_buffer .= '</' . $expResourceProperty->getQName() . ">\n"; + } + + /** + * Add to the output a serialization of a property assignment where an + * SMWExpResource is the object. It is assumed that a suitable subject + * block has already been openend. + * + * @param $expResourceProperty SMWExpNsResource the property to use + * @param $expResource SMWExpResource the data value to use + * @param $indent string specifying a prefix for indentation (usually a sequence of tabs) + * @param $isClassTypeProp boolean whether the resource must be declared as a class + */ + protected function serializeExpResource( SMWExpNsResource $expResourceProperty, SMWExpResource $expResource, $indent, $isClassTypeProp ) { + $this->post_ns_buffer .= $indent . '<' . $expResourceProperty->getQName(); + if ( !$expResource->isBlankNode() ) { + if ( ( $expResource instanceof SMWExpNsResource ) && ( $expResource->getNamespaceID() == 'wiki' ) ) { + // very common case, reduce bandwidth + $this->post_ns_buffer .= ' rdf:resource="&wiki;' . $expResource->getLocalName() . '"'; + } else { + $uriValue = $this->makeAttributeValueString( $expResource->getUri() ); + $this->post_ns_buffer .= ' rdf:resource="' . $uriValue . '"'; + } + } + $this->post_ns_buffer .= "/>\n"; + if ( $isClassTypeProp ) { + $this->requireDeclaration( $expResource, SMW_SERIALIZER_DECL_CLASS ); + } + } + + /** + * Add a serialization of the given SMWExpResource to the output, + * assuming that an opening property tag is alerady there. + * + * @param $expResourceProperty SMWExpNsResource the property to use + * @param $expResource array of (SMWExpResource or SMWExpData) + * @param $indent string specifying a prefix for indentation (usually a sequence of tabs) + * @param $isClassTypeProp boolean whether the resource must be declared as a class + * + * @bug The $isClassTypeProp parameter is not properly taken into account. + * @bug Individual resources are not serialised properly. + */ + protected function serializeExpCollection( SMWExpNsResource $expResourceProperty, array $collection, $indent, $isClassTypeProp ) { + $this->post_ns_buffer .= $indent . '<' . $expResourceProperty->getQName() . " rdf:parseType=\"Collection\">\n"; + foreach ( $collection as $expElement ) { + if ( $expElement instanceof SMWExpData ) { + $this->serializeNestedExpData( $expElement, $indent ); + } else { + // FIXME: the below is not the right thing to do here + //$this->serializeExpResource( $expResourceProperty, $expElement, $indent ); + } + if ( $isClassTypeProp ) { + // FIXME: $expResource is undefined + //$this->requireDeclaration( $expResource, SMW_SERIALIZER_DECL_CLASS ); + } + } + $this->post_ns_buffer .= "$indent</" . $expResourceProperty->getQName() . ">\n"; + } + + /** + * Escape a string in the special form that is required for values in + * DTD entity declarations in XML. Namely, this require the percent sign + * to be replaced. + * + * @param $string string to be escaped + * @return string + */ + protected function makeValueEntityString( $string ) { + return "'" . str_replace( '%', '%', $string ) . "'"; + } + + /** + * Escape a string as required for using it in XML attribute values. + * + * @param $string string to be escaped + * @return string + */ + protected function makeAttributeValueString( $string ) { + return str_replace( [ '&', '>', '<' ], [ '&', '>', '<' ], $string ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer_Turtle.php b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer_Turtle.php new file mode 100644 index 00000000..d7a6ff2a --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/export/SMW_Serializer_Turtle.php @@ -0,0 +1,294 @@ +<?php + +use SMW\InMemoryPoolCache; + +/** + * File holding the SMWTurtleSerializer class that provides basic functions for + * serialising OWL data in Turtle syntax. + * + * @ingroup SMW + * + * @author Markus Krötzsch + */ + +/** + * Class for serializing exported data (encoded as SMWExpData object) in + * Turtle syntax. + * + * @ingroup SMW + */ +class SMWTurtleSerializer extends SMWSerializer { + /** + * Array of non-trivial sub-SMWExpData elements that cannot be nested while + * serializing some SMWExpData. The elements of the array are serialized + * later during the same serialization step (so this is not like another + * queue for declarations or the like; it just unfolds an SMWExpData + * object). + * + * @var array of SMWExpData + */ + protected $subexpdata; + + /** + * If true, do not serialize namespace declarations and record them in + * $sparql_namespaces instead for later retrieval. + * @var boolean + */ + protected $sparqlmode; + + /** + * Array of retrieved namespaces (abbreviation => URI) for later use. + * @var array of string + */ + protected $sparql_namespaces; + + public function __construct( $sparqlMode = false ) { + parent::__construct(); + $this->sparqlmode = $sparqlMode; + } + + public function clear() { + parent::clear(); + $this->sparql_namespaces = []; + } + + /** + * @since 2.3 + */ + public static function reset() { + InMemoryPoolCache::getInstance()->resetPoolCacheById( 'turtle.serializer' ); + } + + /** + * Get an array of namespace prefixes used in SPARQL mode. + * Namespaces are not serialized among triples in SPARQL mode but are + * collected separately. This method returns the prefixes and empties + * the collected list afterwards. + * + * @return array shortName => namespace URI + */ + public function flushSparqlPrefixes() { + $result = $this->sparql_namespaces; + $this->sparql_namespaces = []; + return $result; + } + + protected function serializeHeader() { + if ( $this->sparqlmode ) { + $this->pre_ns_buffer = ''; + $this->sparql_namespaces = [ + "rdf" => SMWExporter::getInstance()->expandURI( '&rdf;' ), + "rdfs" => SMWExporter::getInstance()->expandURI( '&rdfs;' ), + "owl" => SMWExporter::getInstance()->expandURI( '&owl;' ), + "swivt" => SMWExporter::getInstance()->expandURI( '&swivt;' ), + "wiki" => SMWExporter::getInstance()->expandURI( '&wiki;' ), + "category" => SMWExporter::getInstance()->expandURI( '&category;' ), + "property" => SMWExporter::getInstance()->expandURI( '&property;' ), + "xsd" => "http://www.w3.org/2001/XMLSchema#" , + "wikiurl" => SMWExporter::getInstance()->expandURI( '&wikiurl;' ) + ]; + } else { + $this->pre_ns_buffer = + "@prefix rdf: <" . SMWExporter::getInstance()->expandURI( '&rdf;' ) . "> .\n" . + "@prefix rdfs: <" . SMWExporter::getInstance()->expandURI( '&rdfs;' ) . "> .\n" . + "@prefix owl: <" . SMWExporter::getInstance()->expandURI( '&owl;' ) . "> .\n" . + "@prefix swivt: <" . SMWExporter::getInstance()->expandURI( '&swivt;' ) . "> .\n" . + // A note on "wiki": this namespace is crucial as a fallback when it would be illegal to start e.g. with a number. + // In this case, one can always use wiki:... followed by "_" and possibly some namespace, since _ is legal as a first character. + "@prefix wiki: <" . SMWExporter::getInstance()->expandURI( '&wiki;' ) . "> .\n" . + "@prefix category: <" . SMWExporter::getInstance()->expandURI( '&category;' ) . "> .\n" . + "@prefix property: <" . SMWExporter::getInstance()->expandURI( '&property;' ) . "> .\n" . + "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n" . // note that this XSD URI is hardcoded below (its unlikely to change, of course) + "@prefix wikiurl: <" . SMWExporter::getInstance()->expandURI( '&wikiurl;' ) . "> .\n"; + } + $this->global_namespaces = [ 'rdf' => true, 'rdfs' => true, 'owl' => true, 'swivt' => true, 'wiki' => true, 'property' => true, 'category' => true ]; + $this->post_ns_buffer = "\n"; + } + + protected function serializeFooter() { + if ( !$this->sparqlmode ) { + $this->post_ns_buffer .= "\n# Created by Semantic MediaWiki, https://www.semantic-mediawiki.org/\n"; + } + } + + public function serializeDeclaration( $uri, $typename ) { + $this->post_ns_buffer .= "<" . SMWExporter::getInstance()->expandURI( $uri ) . "> rdf:type $typename .\n"; + } + + public function serializeExpData( SMWExpData $expData ) { + + $this->subExpData = [ $expData ]; + + while ( count( $this->subExpData ) > 0 ) { + $this->serializeNestedExpData( array_pop( $this->subExpData ), '' ); + } + + $this->serializeNamespaces(); + } + + protected function serializeNamespace( $shortname, $uri ) { + $this->global_namespaces[$shortname] = true; + if ( $this->sparqlmode ) { + $this->sparql_namespaces[$shortname] = $uri; + } else { + $this->pre_ns_buffer .= "@prefix $shortname: <$uri> .\n"; + } + } + + /** + * Serialize the given SMWExpData object, possibly recursively with + * increased indentation. + * + * @param $data SMWExpData containing the data to be serialised. + * @param $indent string specifying a prefix for indentation (usually a sequence of tabs) + */ + protected function serializeNestedExpData( SMWExpData $data, $indent ) { + if ( count( $data->getProperties() ) == 0 ) { + return; // nothing to export + } + + // Avoid posting turtle property declarations already known for the + // subject more than once + if ( $data->getSubject()->getDataItem() !== null && $data->getSubject()->getDataItem()->getNamespace() === SMW_NS_PROPERTY ) { + + $hash = $data->getHash(); + $poolCache = InMemoryPoolCache::getInstance()->getPoolCacheById( 'turtle.serializer' ); + + if ( $poolCache->contains( $hash ) && $poolCache->fetch( $hash ) ) { + return; + } + + $poolCache->save( $hash, true ); + } + + $this->recordDeclarationTypes( $data ); + + $bnode = false; + $this->post_ns_buffer .= $indent; + if ( !$data->getSubject()->isBlankNode() ) { + $this->serializeExpResource( $data->getSubject() ); + } else { // blank node + $bnode = true; + $this->post_ns_buffer .= "["; + } + + if ( ( $indent !== '' ) && ( !$bnode ) ) { // called to generate a nested descripion; but Turtle cannot nest non-bnode descriptions, do this later + $this->subexpdata[] = $data; + return; + } elseif ( !$bnode ) { + $this->post_ns_buffer .= "\n "; + } + + $firstproperty = true; + foreach ( $data->getProperties() as $property ) { + $this->post_ns_buffer .= $firstproperty ? "\t" : " ;\n $indent\t"; + $firstproperty = false; + $prop_decl_queued = false; + $class_type_prop = $this->isOWLClassTypeProperty( $property ); + $this->serializeExpResource( $property ); + $firstvalue = true; + + foreach ( $data->getValues( $property ) as $value ) { + $this->post_ns_buffer .= $firstvalue ? ' ' : ' , '; + $firstvalue = false; + + if ( $value instanceof SMWExpLiteral ) { + $prop_decl_type = SMW_SERIALIZER_DECL_APROP; + $this->serializeExpLiteral( $value ); + } elseif ( $value instanceof SMWExpResource ) { + $prop_decl_type = SMW_SERIALIZER_DECL_OPROP; + $this->serializeExpResource( $value ); + } elseif ( $value instanceof SMWExpData ) { // resource (maybe blank node), could have subdescriptions + $prop_decl_type = SMW_SERIALIZER_DECL_OPROP; + $collection = $value->getCollection(); + if ( $collection !== false ) { // RDF-style collection (list) + $this->post_ns_buffer .= "( "; + foreach ( $collection as $subvalue ) { + $this->serializeNestedExpData( $subvalue, $indent . "\t\t" ); + if ( $class_type_prop ) { + $this->requireDeclaration( $subvalue->getSubject(), SMW_SERIALIZER_DECL_CLASS ); + } + } + $this->post_ns_buffer .= " )"; + } else { + if ( $class_type_prop ) { + $this->requireDeclaration( $value->getSubject(), SMW_SERIALIZER_DECL_CLASS ); + } + if ( count( $value->getProperties() ) > 0 ) { // resource with data: serialise + $this->post_ns_buffer .= "\n"; + $this->serializeNestedExpData( $value, $indent . "\t\t" ); + } else { // resource without data: may need to be queued + $this->serializeExpResource( $value->getSubject() ); + } + } + } + + if ( !$prop_decl_queued ) { + $this->requireDeclaration( $property, $prop_decl_type ); + $prop_decl_queued = true; + } + } + } + $this->post_ns_buffer .= ( $bnode ? " ]" : " ." ) . ( $indent === '' ? "\n\n" : '' ); + } + + protected function serializeExpLiteral( SMWExpLiteral $element ) { + $this->post_ns_buffer .= self::getTurtleNameForExpElement( $element ); + } + + protected function serializeExpResource( SMWExpResource $element ) { + if ( $element instanceof SMWExpNsResource ) { + $this->requireNamespace( $element->getNamespaceID(), $element->getNamespace() ); + } + $this->post_ns_buffer .= self::getTurtleNameForExpElement( $element ); + } + + /** + * Get the Turtle serialization string for the given SMWExpElement. The + * method just computes a name, and does not serialize triples, so the + * parameter must be an SMWExpResource or SMWExpLiteral, no SMWExpData. + * + * @param $expElement SMWExpElement being SMWExpLiteral or SMWExpResource + * @return string + */ + public static function getTurtleNameForExpElement( SMWExpElement $expElement ) { + if ( $expElement instanceof SMWExpResource ) { + if ( $expElement->isBlankNode() ) { + return '[]'; + } elseif ( ( $expElement instanceof SMWExpNsResource ) && ( $expElement->hasAllowedLocalName() ) ) { + return $expElement->getQName(); + } else { + return '<' . str_replace( '>', '\>', SMWExporter::getInstance()->expandURI( $expElement->getUri() ) ) . '>'; + } + } elseif ( $expElement instanceof SMWExpLiteral ) { + $dataType = $expElement->getDatatype(); + $lexicalForm = self::getCorrectLexicalForm( $expElement ); + + if ( ( $dataType !== '' ) && ( $dataType != 'http://www.w3.org/2001/XMLSchema#string' ) ) { + $count = 0; + $newdt = str_replace( 'http://www.w3.org/2001/XMLSchema#', 'xsd:', $dataType, $count ); + return ( $count == 1 ) ? "$lexicalForm^^$newdt" : "$lexicalForm^^<$dataType>"; + } else { + return $lexicalForm; + } + } else { + throw new InvalidArgumentException( 'The method can only serialize atomic elements of type SMWExpResource or SMWExpLiteral.' ); + } + } + + private static function getCorrectLexicalForm( $expElement ) { + + $lexicalForm = str_replace( [ '\\', "\n", '"' ], [ '\\\\', "\\n", '\"' ], $expElement->getLexicalForm() ); + + if ( $expElement->getLang() !== '' && ( $expElement->getDatatype() === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString' ) ) { + $lexicalForm = '"' . $lexicalForm . '@' . $expElement->getLang() . '"'; + } elseif ( $expElement->getLang() !== '' ) { + $lexicalForm = '"' . $lexicalForm . '"'. '@' . $expElement->getLang(); + } else { + $lexicalForm = '"' . $lexicalForm . '"'; + } + + return $lexicalForm; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/formatters/MessageFormatter.php b/www/wiki/extensions/SemanticMediaWiki/includes/formatters/MessageFormatter.php new file mode 100644 index 00000000..642ee9da --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/formatters/MessageFormatter.php @@ -0,0 +1,295 @@ +<?php + +namespace SMW; + +use Html; +use Language; + +/** + * Class implementing message output formatting + * + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ + +/** + * This class is implementing message output formatting to avoid having + * classes to invoke a language object that is not a direct dependency (which + * means that context relevant information is mostly missing from the invoking + * class) therefore it is more appropriate to collect Message objects from the + * source and initiate an output formatting only when necessary and requested. + * + * @ingroup Formatter + */ +class MessageFormatter { + + /** @var array */ + protected $messages = []; + + /** @var string */ + protected $type = 'warning'; + + /** @var string */ + protected $separator = ' <!--br-->'; + + /** @var boolean */ + protected $escape = true; + + /** + * @since 1.9 + * + * @param Language $language + */ + public function __construct( Language $language ) { + $this->language = $language; + } + + /** + * Convenience factory method to invoke a message array together with + * a language object + * + * @par Example: + * @code + * MessageFormatter::newFromArray( $language, array( 'Foo' ) )->getHtml(); + * @endcode + * + * @since 1.9 + * + * @param Language $language + * @param array|null $messages + * + * @return MessageFormatter + */ + public static function newFromArray( Language $language, array $messages = [] ) { + $instance = new self( $language ); + return $instance->addFromArray( $messages ); + } + + /** + * Creates a Message object from a key and adds it to an internal array + * + * @since 1.9 + * + * @param string $key message key + * + * @return MessageFormatter + */ + public function addFromKey( $key /*...*/ ) { + $params = func_get_args(); + array_shift( $params ); + $this->addFromArray( [ new \Message( $key, $params ) ] ); + return $this; + } + + /** + * Adds an arbitrary array of messages which can either contain text + * or/and Message objects + * + * @par Example: + * @code + * $msgFormatter = new MessageFormatter( $language ); + * $msgFormatter->addFromArray( array( 'Foo', new Message( 'Bar' ) ) )->getHtml() + * @endcode + * + * @since 1.9 + * + * @param array $messages + * + * @return MessageFormatter + */ + public function addFromArray( array $messages ) { + + $messages = ProcessingErrorMsgHandler::normalizeAndDecodeMessages( $messages ); + + foreach ( $messages as $message ) { + if ( is_string( $message ) ) { + $this->messages[md5( $message )] = $message; + } else{ + $this->messages[] = $message; + } + } + + return $this; + } + + /** + * Returns unformatted invoked messages + * + * @since 1.9 + * + * @return array + */ + public function getMessages() { + return $this->messages; + } + + /** + * Used in connection with the html output to invoke a specific display + * type + * + * @see Highlighter::getTypeId + * + * @since 1.9 + * + * @return MessageFormatter + */ + public function setType( $type ) { + $this->type = $type; + return $this; + } + + /** + * Enables/disables escaping for the output representation + * + * @note Escaping is generally enabled but in cases of special pages or + * with messages already being escaped this option can be circumvent by + * invoking escape( false ) + * + * @since 1.9 + * + * @param boolean $escape + * + * @return MessageFormatter + */ + public function escape( $escape ) { + $this->escape = (bool)$escape; + return $this; + } + + /** + * Clears the internal message array + * + * @since 1.9 + * + * @return MessageFormatter + */ + public function clear() { + $this->messages = []; + return $this; + } + + /** + * Returns if the internal message array does contain messages + * + * @since 1.9 + * + * @return boolean + */ + public function exists() { + return $this->messages !== []; + } + + /** + * Overrides invoked language object + * + * @since 1.9 + * + * @param Language $language + * + * @return MessageFormatter + */ + public function setLanguage( Language $language ) { + $this->language = $language; + return $this; + } + + /** + * Formatting and normalization of an array + * + * @note The array is being recursively resolved in order to ensure that + * the returning representation is a 1-n array where duplicate entries + * have been eliminated already while Message objects being transformed + * into a simple text representation using the invoked language + * + * @since 1.9 + * + * @param array $messages + * + * @return array + */ + protected function doFormat( array $messages ) { + $newArray = []; + + foreach ( $messages as $msg ) { + + if ( $msg instanceof \Message ) { + $text = $msg->inLanguage( $this->language )->text(); + $newArray[md5( $text )] = $text; + } elseif ( (array)$msg === $msg ) { + foreach ( $this->doFormat( $msg ) as $m ) { + $newArray[md5( $m )] = $m; + } + } elseif ( (string)$msg === $msg ) { + $newArray[md5( $msg )] = $msg; + } + } + + return $newArray; + } + + /** + * Converts the message array into a string representation + * + * @since 1.9 + * + * @param boolean $escape + * @param boolean $html + * + * @return string + */ + protected function getString( $html = true ) { + + if ( $this->escape ) { + $messages = array_map( 'htmlspecialchars', array_values( $this->doFormat( $this->messages ) ) ); + } else { + $messages = array_values( $this->doFormat( $this->messages ) ); + } + + if ( count( $messages ) == 1 ) { + $messageString = $messages[0]; + } else { + foreach ( $messages as &$message ) { + $message = $html ? Html::rawElement( 'li', [], $message ) : $message; + } + + $messageString = implode( $this->separator, $messages ); + $messageString = $html ? Html::rawElement( 'ul', [], $messageString ) : $messageString; + } + + return $messageString; + } + + /** + * Returns html representation of the formatted messages + * + * @since 1.9 + * + * @return string + */ + public function getHtml() { + + if ( $this->exists() ) { + + $highlighter = Highlighter::factory( $this->type ); + $highlighter->setContent( [ 'content' => $this->getString( true ) ] ); + + return $highlighter->getHtml(); + } + + return ''; + } + + /** + * Returns plain text representation of the formatted messages + * + * @since 1.9 + * + * @return string + */ + public function getPlain() { + return $this->exists() ? $this->getString( false ) : ''; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/params/SMW_ParamFormat.php b/www/wiki/extensions/SemanticMediaWiki/includes/params/SMW_ParamFormat.php new file mode 100644 index 00000000..935fd4e5 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/params/SMW_ParamFormat.php @@ -0,0 +1,171 @@ +<?php + +use ParamProcessor\Definition\StringParam; +use ParamProcessor\IParam; +use SMW\Query\PrintRequest; + +/** + * Definition for the format parameter. + * + * @since 1.6.2 + * @deprecated since 1.9 + * + * @ingroup SMW + * @ingroup ParamDefinition + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class SMWParamFormat extends StringParam { + + /** + * List of the queries print requests, used to determine the format + * when it's not provided. Set with setPrintRequests before passing + * to Validator. + * + * @since 1.6.2 + * + * @var PrintRequest[] + */ + protected $printRequests = []; + + protected $showMode = false; + + /** + * Takes a format name, which can be an alias and returns a format name + * which will be valid for sure. Aliases are resolved. If the given + * format name is invalid, the predefined default format will be returned. + * + * @since 1.6.2 + * + * @param string $value + * + * @return string + */ + protected function getValidFormatName( $value ) { + global $smwgResultFormats; + + $value = strtolower( trim( $value ) ); + + if ( !array_key_exists( $value, $smwgResultFormats ) ) { + $isAlias = self::resolveFormatAliases( $value ); + + if ( !$isAlias ) { + $value = $this->getDefaultFormat(); + self::resolveFormatAliases( $value ); + } + } + + return $value; + } + + /** + * Turns format aliases into main formats. + * + * @since 1.6.2 + * + * @param string $format + * + * @return boolean Indicates if the passed format was an alias, and thus was changed. + */ + public static function resolveFormatAliases( &$format ) { + global $smwgResultAliases; + + $isAlias = false; + + foreach ( $smwgResultAliases as $mainFormat => $aliases ) { + if ( in_array( $format, $aliases ) ) { + $format = $mainFormat; + $isAlias = true; + break; + } + } + + return $isAlias; + } + + /** + * Determines and returns the default format, based on the queries print + * requests, if provided. + * + * @since 1.6.2 + * + * @return string Array key in $smwgResultFormats + */ + protected function getDefaultFormat() { + + if ( empty( $this->printRequests ) ) { + return 'table'; + } + + $format = false; + + /** + * This hook allows extensions to override SMWs implementation of default result + * format handling. + * + * @since 1.5.2 + */ + \Hooks::run( 'SMWResultFormat', [ &$format, $this->printRequests, [] ] ); + + if ( $format !== false ) { + return $format; + } + + // If no default was set by an extension, use a table, plainlist or list, depending on showMode and column count. + if ( count( $this->printRequests ) > 1 ) { + return 'table'; + } + + return 'plainlist'; + } + + /** + * Sets the print requests of the query, used for determining + * the default format if none is provided. + * + * @since 1.6.2 + * + * @param PrintRequest[] $printRequests + */ + public function setPrintRequests( array $printRequests ) { + $this->printRequests = $printRequests; + } + + /** + * + * @since 3.0 + * + * @param bool $showMode + */ + public function setShowMode( $showMode ) { + $this->showMode = $showMode; + } + + /** + * Formats the parameter value to it's final result. + * + * @since 1.8 + * + * @param mixed $value + * @param IParam $param + * @param IParamDefinition[] $definitions + * @param IParam[] $params + * + * @return mixed + */ + protected function formatValue( $value, IParam $param, array &$definitions, array $params ) { + $value = parent::formatValue( $value, $param, $definitions, $params ); + + // Make sure the format value is valid. + $value = self::getValidFormatName( $value ); + + // Add the formats parameters to the parameter list. + $queryPrinter = SMWQueryProcessor::getResultPrinter( $value ); + + $definitions = $queryPrinter->getParamDefinitions( $definitions ); + + return $value; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/query/SMW_Query.php b/www/wiki/extensions/SemanticMediaWiki/includes/query/SMW_Query.php new file mode 100644 index 00000000..eadf97b0 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/query/SMW_Query.php @@ -0,0 +1,552 @@ +<?php + +use SMW\DIWikiPage; +use SMW\Message; +use SMW\Query\Language\Description; +use SMW\Query\PrintRequest; +use SMW\Query\QueryContext; +use SMW\Query\QueryStringifier; +use SMW\Query\QueryToken; + +/** + * This file contains the class for representing queries in SMW, each + * consisting of a query description and possible query parameters. + * @ingroup SMWQuery + * @author Markus Krötzsch + */ + +/** + * This group contains all parts of SMW that relate to processing semantic queries. + * SMW components that relate to plain storage access (for querying or otherwise) + * have their own group. + * @defgroup SMWQuery SMWQuery + * @ingroup SMW + */ + +/** + * Representation of queries in SMW, each consisting of a query + * description and various parameters. Some settings might also lead to + * changes in the query description. + * + * Most additional query parameters (limit, sort, ascending, ...) are + * interpreted as in SMWRequestOptions (though the latter contains some + * additional settings). + * @ingroup SMWQuery + */ +class SMWQuery implements QueryContext { + + const ID_PREFIX = '_QUERY'; + + /** + * The time the QueryEngine required to answer a query condition + */ + const PROC_QUERY_TIME = 'proc.query.time'; + + /** + * The time a ResultPrinter required to build the final result including all + * PrintRequests + */ + const PROC_PRINT_TIME = 'proc.print.time'; + + /** + * The processing context in which the query is being executed + */ + const PROC_CONTEXT = 'proc.context'; + + /** + * Status code information + */ + const PROC_STATUS_CODE = 'proc.status.code'; + + /** + * The processing parameters + */ + const OPT_PARAMETERS = 'proc.parameters'; + + /** + * Suppress a possible cache request + */ + const NO_CACHE = 'no.cache'; + + /** + * Indicates no dependency trace + */ + const NO_DEPENDENCY_TRACE = 'no.dependency.trace'; + + /** + * Sort by score if the query engine supports it. + */ + const SCORE_SORT = 'score.sort'; + + public $sort = false; + public $sortkeys = []; // format: "Property key" => "ASC" / "DESC" (note: order of entries also matters) + public $querymode = self::MODE_INSTANCES; + + private $limit; + private $offset = 0; + private $description; + private $errors = []; // keep any errors that occurred so far + private $queryString = false; // string (inline query) version (if fixed and known) + private $isInline; // query used inline? (required for finding right default parameters) + private $isUsedInConcept; // query used in concept? (required for finding right default parameters) + + /** + * @var PrintRequest[] + */ + private $m_extraprintouts = []; // SMWPrintoutRequest objects supplied outside querystring + private $m_mainlabel = ''; // Since 1.6 + + /** + * @var DIWikiPage|null + */ + private $contextPage; + + /** + * Describes a non-local (remote) query source + * + * @var string|null + */ + private $querySource = null; + + /** + * @var QueryToken|null + */ + private $queryToken; + + /** + * @var array + */ + private $options = []; + + /** + * @since 1.6 + * + * @param Description $description + * @param integer|boolean $context + */ + public function __construct( Description $description = null, $context = false ) { + $inline = false; + $concept = false; + + // stating whether this query runs in an inline context; used to + // determine proper default parameters (e.g. the default limit) + if ( $context === self::INLINE_QUERY || $context === self::DEFERRED_QUERY ) { + $inline = true; + } + + // stating whether this query belongs to a concept; used to determine + // proper default parameters (concepts usually have less restrictions) + if ( $context === self::CONCEPT_DESC ) { + $concept = true; + } + + $this->limit = $inline ? $GLOBALS['smwgQMaxInlineLimit'] : $GLOBALS['smwgQMaxLimit']; + $this->isInline = $inline; + $this->isUsedInConcept = $concept; + $this->description = $description; + $this->applyRestrictions(); + } + + /** + * @since 3.0 + * + * @param boolean + */ + public function isEmbedded() { + return $this->isInline; + } + + /** + * @since 2.5 + * + * @param integer + */ + public function setQueryMode( $queryMode ) { + // FIXME 3.0; $this->querymode is a public property + // declare it private and rename it to $this->queryMode + $this->querymode = $queryMode; + } + + /** + * @since 2.5 + * + * @param integer + */ + public function getQueryMode() { + return $this->querymode; + } + + /** + * @since 2.3 + * + * @param DIWikiPage|null $contextPage + */ + public function setContextPage( DIWikiPage $contextPage = null ) { + $this->contextPage = $contextPage; + } + + /** + * @since 2.3 + * + * @return DIWikiPage|null + */ + public function getContextPage() { + return $this->contextPage; + } + + /** + * @since 2.4 + * + * @param string + */ + public function setQuerySource( $querySource ) { + $this->querySource = $querySource; + } + + /** + * @since 2.4 + * + * @return string + */ + public function getQuerySource() { + return $this->querySource; + } + + /** + * @since 2.5 + * + * @param QueryToken|null $queryToken + */ + public function setQueryToken( QueryToken $queryToken = null ) { + $this->queryToken = $queryToken; + } + + /** + * @since 2.5 + * + * @return QueryToken|null + */ + public function getQueryToken() { + return $this->queryToken; + } + + /** + * Sets the mainlabel. + * + * @since 1.6. + * + * @param string $mainlabel + */ + public function setMainLabel( $mainlabel ) { + $this->m_mainlabel = $mainlabel; + } + + /** + * Gets the mainlabel. + * + * @since 1.6. + * + * @return string + */ + public function getMainLabel() { + return $this->m_mainlabel; + } + + public function setDescription( SMWDescription $description ) { + $this->description = $description; + $this->queryString = false; + + foreach ( $this->m_extraprintouts as $printout ) { + $this->description->addPrintRequest( $printout ); + } + $this->applyRestrictions(); + } + + public function getDescription() { + return $this->description; + } + + public function setExtraPrintouts( $extraprintouts ) { + $this->m_extraprintouts = $extraprintouts; + + if ( !is_null( $this->description ) ) { + foreach ( $extraprintouts as $printout ) { + $this->description->addPrintRequest( $printout ); + } + } + } + + /** + * @return PrintRequest[] + */ + public function getExtraPrintouts() { + return $this->m_extraprintouts; + } + + /** + * @since 3.0 + */ + public function clearErrors() { + $this->errors = []; + } + + public function getErrors() { + return $this->errors; + } + + public function addErrors( $errors ) { + $this->errors = array_merge( $this->errors, $errors ); + } + + public function setQueryString( $querystring ) { + $this->queryString = $querystring; + } + + /** + * @since 2.5 + * + * @param string|integer $key + * @param mixed $value + */ + public function setOption( $key, $value ) { + $this->options[$key] = $value; + } + + /** + * @since 2.5 + * + * @param string|integer $key + * + * @return mixed + */ + public function getOption( $key ) { + return isset( $this->options[$key] ) ? $this->options[$key] : false; + } + + /** + * @since 1.7 + * + * @param boolean $fresh + * + * @return string + */ + public function getQueryString( $fresh = false ) { + + // Mostly relevant on requesting a further results link to + // ensure that localized values are transformed into a canonical + // representation + if ( $fresh && $this->description !== null ) { + return $this->description->getQueryString(); + } + + if ( $this->queryString !== false ) { + return $this->queryString; + } elseif ( !is_null( $this->description ) ) { + return $this->description->getQueryString(); + } else { + return ''; + } + } + + public function getOffset() { + return $this->offset; + } + + /** + * Set an offset for the returned query results. No offset beyond the maximal query + * limit will be set, and the current query limit might be reduced in order to ensure + * that no results beyond the maximal limit are returned. + * The function returns the chosen offset. + * @todo The function should be extended to take into account whether or not we + * are in inline mode (not critical, since offsets are usually not applicable inline). + */ + public function setOffset( $offset ) { + global $smwgQMaxLimit; + $this->offset = min( $smwgQMaxLimit, $offset ); // select integer between 0 and maximal limit; + $this->limit = min( $smwgQMaxLimit - $this->offset, $this->limit ); // note that limit might become 0 here + return $this->offset; + } + + public function getLimit() { + return $this->limit; + } + + /** + * Set a limit for number of query results. The set limit might be restricted by the + * current offset so as to ensure that the number of the last considered result does not + * exceed the maximum amount of supported results. + * The function returns the chosen limit. + * @note It makes sense to have limit==0, e.g. to only show a link to the search special + */ + public function setLimit( $limit, $restrictinline = true ) { + global $smwgQMaxLimit, $smwgQMaxInlineLimit; + $maxlimit = ( $this->isInline && $restrictinline ) ? $smwgQMaxInlineLimit : $smwgQMaxLimit; + $this->limit = min( $smwgQMaxLimit - $this->offset, $limit, $maxlimit ); + return $this->limit; + } + + /** + * @note Sets an unbound limit that is independent from GLOBAL settings + * + * @since 2.0 + * + * @param integer $limit + */ + public function setUnboundLimit( $limit ) { + $this->limit = (int)$limit; + } + + /** + * @note format: "Property key" => "ASC" / "DESC" (note: order of entries also matters) + * + * @since 2.2 + * + * @param array $sortKeys + */ + public function setSortKeys( array $sortKeys ) { + $this->sortkeys = $sortKeys; + } + + /** + * @since 2.2 + * + * @return array + */ + public function getSortKeys() { + return $this->sortkeys; + } + + /** + * Apply structural restrictions to the current description. + */ + public function applyRestrictions() { + global $smwgQMaxSize, $smwgQMaxDepth, $smwgQConceptMaxSize, $smwgQConceptMaxDepth; + + if ( !is_null( $this->description ) ) { + if ( $this->isUsedInConcept ) { + $maxsize = $smwgQConceptMaxSize; + $maxdepth = $smwgQConceptMaxDepth; + } else { + $maxsize = $smwgQMaxSize; + $maxdepth = $smwgQMaxDepth; + } + + $log = []; + $this->description = $this->description->prune( $maxsize, $maxdepth, $log ); + + if ( count( $log ) > 0 ) { + $this->errors[] = Message::encode( [ + 'smw_querytoolarge', + str_replace( '[', '[', implode( ', ', $log ) ), + count( $log ) + ] ); + } + } + } + + /** + * Returns serialized query details + * + * The output is following the askargs api module convention + * + * conditions The query conditions (requirements for a subject to be included) + * printouts The query printouts (which properties to show per subject) + * parameters The query parameters (non-condition and non-printout arguments) + * + * @since 1.9 + * + * @return array + */ + public function toArray() { + $serialized = []; + + $serialized['conditions'] = $this->getQueryString(); + + // This can be extended but for the current use cases that is + // sufficient since most printer related parameters have to be sourced + // in the result printer class + $serialized['parameters'] = [ + 'limit' => $this->limit, + 'offset' => $this->offset, + 'sortkeys' => $this->sortkeys, + 'mainlabel' => $this->m_mainlabel, + 'querymode' => $this->querymode + ]; + + // @2.4 Keep the queryID stable with previous versions unless + // a query source is selected. The "same" query executed on different + // remote systems requires a different queryID + if ( $this->querySource !== null && $this->querySource !== '' ) { + $serialized['parameters']['source'] = $this->querySource; + } + + foreach ( $this->getExtraPrintouts() as $printout ) { + if ( ( $serialisation = $printout->getSerialisation() ) !== '' ) { + $serialized['printouts'][] = $serialisation; + } + } + + return $serialized; + } + + /** + * @note Before 2.5, toArray was used to generate the content, as of 2.5 + * only parameters that influence the result of an query is included. + * + * @since 2.1 + * + * @return string + */ + public function getHash() { + + // Only use elements that directly influence the result list + $serialized = []; + + // Don't use the QueryString, use the canonized fingerprint to ensure that + // [[Foo::123]][[Bar::abc]] returns the same ID as [[Bar::abc]][[Foo::123]] + // given that limit, offset, and sort/order are the same + if ( $this->description !== null ) { + $serialized['fingerprint'] = $this->description->getFingerprint(); + } else { + $serialized['conditions'] = $this->getQueryString(); + } + + $serialized['parameters'] = [ + 'limit' => $this->limit, + 'offset' => $this->offset, + 'sortkeys' => $this->sortkeys, + + // COUNT, DEBUG ... + 'querymode' => $this->querymode + ]; + + // Make to sure to distinguish queries and results from a foreign repository + if ( $this->querySource !== null && $this->querySource !== '' ) { + $serialized['parameters']['source'] = $this->querySource; + } + + // Printouts are avoided as part of the hash as they not influence the + // list of entities and are only resolved after the query result has + // been retrieved + return md5( json_encode( $serialized ) ); + } + + /** + * @since 2.5 + * + * @return string + */ + public function toString() { + return QueryStringifier::toString( $this ); + } + + /** + * @since 2.3 + * + * @return string + */ + public function getQueryId() { + return self::ID_PREFIX . $this->getHash(); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/query/SMW_QueryProcessor.php b/www/wiki/extensions/SemanticMediaWiki/includes/query/SMW_QueryProcessor.php new file mode 100644 index 00000000..ed851db5 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/query/SMW_QueryProcessor.php @@ -0,0 +1,530 @@ +<?php + +use ParamProcessor\Options; +use ParamProcessor\Param; +use ParamProcessor\ParamDefinition; +use ParamProcessor\Processor; +use SMW\ApplicationFactory; +use SMW\Message; +use SMW\Parser\RecursiveTextProcessor; +use SMW\Query\Deferred; +use SMW\Query\PrintRequest; +use SMW\Query\Processor\ParamListProcessor; +use SMW\Query\Processor\DefaultParamDefinition; +use SMW\Query\QueryContext; +use SMW\Query\Exception\ResultFormatNotFoundException; + +/** + * This file contains a static class for accessing functions to generate and execute + * semantic queries and to serialise their results. + * + * @ingroup SMWQuery + * @author Markus Krötzsch + */ + +/** + * Static class for accessing functions to generate and execute semantic queries + * and to serialise their results. + * @ingroup SMWQuery + */ +class SMWQueryProcessor implements QueryContext { + + /** + * @var RecursiveTextProcessor + */ + private static $recursiveTextProcessor; + + /** + * @since 3.0 + * + * @param RecursiveTextProcessor|null $recursiveTextProcessor + */ + public static function setRecursiveTextProcessor( RecursiveTextProcessor $recursiveTextProcessor = null ) { + self::$recursiveTextProcessor = $recursiveTextProcessor; + } + + /** + * Takes an array of unprocessed parameters, processes them using + * Validator, and returns them. + * + * Both input and output arrays are + * param name (string) => param value (mixed) + * + * @since 1.6.2 + * The return value changed in SMW 1.8 from an array with result values + * to an array with Param objects. + * + * @param array $params + * @param array $printRequests + * @param boolean $unknownInvalid + * + * @return Param[] + */ + public static function getProcessedParams( array $params, array $printRequests = [], $unknownInvalid = true, $context = null, $showMode = false ) { + $validator = self::getValidatorForParams( $params, $printRequests, $unknownInvalid, $context, $showMode ); + $validator->processParameters(); + $parameters = $validator->getParameters(); + + // Negates some weird behaviour of ParamDefinition::setDefault used in + // an individual printer. + // Applying $smwgQMaxLimit, $smwgQMaxInlineLimit will happen at a later + // stage. + if ( isset( $params['limit'] ) && isset( $parameters['limit'] ) ) { + $parameters['limit']->setValue( (int)$params['limit'] ); + } + + return $parameters; + } + + /** + * Parse a query string given in SMW's query language to create + * an SMWQuery. Parameters are given as key-value-pairs in the + * given array. The parameter $context defines in what context the + * query is used, which affects ceretain general settings. + * An object of type SMWQuery is returned. + * + * The format string is used to specify the output format if already + * known. Otherwise it will be determined from the parameters when + * needed. This parameter is just for optimisation in a common case. + * + * @param string $queryString + * @param array $params These need to be the result of a list fed to getProcessedParams + * @param $context + * @param string $format + * @param array $extraPrintouts + * + * @return SMWQuery + */ + static public function createQuery( $queryString, array $params, $context = self::INLINE_QUERY, $format = '', array $extraPrintouts = [], $contextPage = null ) { + + if ( $format === '' || is_null( $format ) ) { + $format = $params['format']->getValue(); + } + + $defaultSort = 'ASC'; + + if ( $format == 'count' ) { + $queryMode = SMWQuery::MODE_COUNT; + } elseif ( $format == 'debug' ) { + $queryMode = SMWQuery::MODE_DEBUG; + } else { + $printer = self::getResultPrinter( $format, $context ); + $queryMode = $printer->getQueryMode( $context ); + $defaultSort = $printer->getDefaultSort(); + } + + // set mode, limit, and offset: + $offset = 0; + $limit = $GLOBALS['smwgQDefaultLimit']; + + if ( ( array_key_exists( 'offset', $params ) ) && ( is_int( $params['offset']->getValue() + 0 ) ) ) { + $offset = $params['offset']->getValue(); + } + + if ( ( array_key_exists( 'limit', $params ) ) && ( is_int( trim( $params['limit']->getValue() ) + 0 ) ) ) { + $limit = $params['limit']->getValue(); + + // limit < 0: always show further results link only + if ( ( trim( $params['limit']->getValue() ) + 0 ) < 0 ) { + $queryMode = SMWQuery::MODE_NONE; + } + } + + // largest possible limit for "count", even inline + if ( $queryMode == SMWQuery::MODE_COUNT ) { + $offset = 0; + $limit = $GLOBALS['smwgQMaxLimit']; + } + + $queryCreator = ApplicationFactory::getInstance()->singleton( 'QueryCreator' ); + + $params = [ + 'extraPrintouts' => $extraPrintouts, + 'queryMode' => $queryMode, + 'context' => $context, + 'contextPage' => $contextPage, + 'offset' => $offset, + 'limit' => $limit, + 'source' => $params['source']->getValue(), + 'mainLabel' => $params['mainlabel']->getValue(), + 'sort' => $params['sort']->getValue(), + 'order' => $params['order']->getValue(), + 'defaultSort' => $defaultSort + ]; + + return $queryCreator->create( $queryString, $params ); + } + + /** + * Add the subject print request, unless mainlabel is set to "-". + * + * @since 1.7 + * + * @param array $printRequests + * @param array $rawParams + */ + public static function addThisPrintout( array &$printRequests, array $rawParams ) { + + if ( $printRequests === null ) { + return; + } + + // If THIS is already registered, bail-out! + foreach ( $printRequests as $printRequest ) { + if ( $printRequest->isMode( PrintRequest::PRINT_THIS ) ) { + return; + } + } + + $hasMainlabel = array_key_exists( 'mainlabel', $rawParams ); + + if ( !$hasMainlabel || trim( $rawParams['mainlabel'] ) !== '-' ) { + $printRequest = new PrintRequest( + PrintRequest::PRINT_THIS, + $hasMainlabel ? $rawParams['mainlabel'] : '' + ); + + // Signal to any post-processing that THIS was added outside of + // the normal processing chain + $printRequest->isDisconnected( true ); + + array_unshift( $printRequests, $printRequest ); + } + } + + /** + * Preprocess a query as given by an array of parameters as is typically + * produced by the #ask parser function. The parsing results in a querystring, + * an array of additional parameters, and an array of additional SMWPrintRequest + * objects, which are filled into call-by-ref parameters. + * $showMode is true if the input should be treated as if given by #show + * + * @param array $rawParams + * @param string $querystring + * @param array $params + * @param array $printouts array of SMWPrintRequest + * @param boolean $showMode + * @deprecated Will vanish after SMW 1.8 is released. + * Use getComponentsFromFunctionParams which has a cleaner interface. + */ + static public function processFunctionParams( array $rawParams, &$querystring, &$params, &$printouts, $showMode = false ) { + list( $querystring, $params, $printouts ) = self::getComponentsFromFunctionParams( $rawParams, $showMode ); + } + + + /** + * Preprocess a query as given by an array of parameters as is + * typically produced by the #ask parser function or by Special:Ask. + * The parsing results in a querystring, an array of additional + * parameters, and an array of additional SMWPrintRequest objects, + * which are returned in an array with three components. If + * $showMode is true then the input will be processed as if for #show. + * This uses a slightly different way to get the query, and different + * default labels (empty) for additional print requests. + * + * @param array $rawParams + * @param boolean $showMode + * @return array( string, array( string => string ), array( SMWPrintRequest ) ) + */ + static public function getComponentsFromFunctionParams( array $rawParams, $showMode ) { + + $paramListProcessor = ApplicationFactory::getInstance()->singleton( 'ParamListProcessor' ); + + return $paramListProcessor->format( + $paramListProcessor->preprocess( $rawParams, $showMode ), + ParamListProcessor::FORMAT_LEGACY + ); + } + + /** + * Process and answer a query as given by an array of parameters as is + * typically produced by the #ask parser function. The parameter + * $context defines in what context the query is used, which affects + * certain general settings. + * + * The main task of this function is to preprocess the raw parameters + * to obtain actual parameters, printout requests, and the query string + * for further processing. + * + * @since 1.8 + * @param array $rawParams user-provided list of unparsed parameters + * @param integer $outputMode SMW_OUTPUT_WIKI, SMW_OUTPUT_HTML, ... + * @param integer $context INLINE_QUERY, SPECIAL_PAGE, CONCEPT_DESC + * @param boolean $showMode process like #show parser function? + * @return array( SMWQuery, array of IParam ) + */ + static public function getQueryAndParamsFromFunctionParams( array $rawParams, $outputMode, $context, $showMode, $contextPage = null ) { + list( $queryString, $params, $printouts ) = self::getComponentsFromFunctionParams( $rawParams, $showMode ); + + if ( !$showMode ) { + self::addThisPrintout( $printouts, $params ); + } + + $params = self::getProcessedParams( $params, $printouts, true, $context, $showMode ); + + $query = self::createQuery( $queryString, $params, $context, '', $printouts, $contextPage ); + + // For convenience keep parameters and options to be available for immediate + // processing + if ( $context === self::DEFERRED_QUERY ) { + $query->setOption( Deferred::QUERY_PARAMETERS, implode( '|', $rawParams ) ); + $query->setOption( Deferred::SHOW_MODE, $showMode ); + $query->setOption( Deferred::CONTROL_ELEMENT, isset( $params['@control'] ) ? $params['@control']->getValue() : '' ); + } + + return [ $query, $params ]; + } + + /** + * Process and answer a query as given by an array of parameters as is + * typically produced by the #ask parser function. The result is formatted + * according to the specified $outputformat. The parameter $context defines + * in what context the query is used, which affects ceretain general settings. + * + * The main task of this function is to preprocess the raw parameters to + * obtain actual parameters, printout requests, and the query string for + * further processing. + * + * @note Consider using getQueryAndParamsFromFunctionParams() and + * getResultFromQuery() instead. + * @deprecated Will vanish after release of SMW 1.8. + * See SMW_Ask.php for example code on how to get query results from + * #ask function parameters. + */ + static public function getResultFromFunctionParams( array $rawParams, $outputMode, $context = self::INLINE_QUERY, $showMode = false ) { + list( $queryString, $params, $printouts ) = self::getComponentsFromFunctionParams( $rawParams, $showMode ); + + if ( !$showMode ) { + self::addThisPrintout( $printouts, $params ); + } + + $params = self::getProcessedParams( $params, $printouts ); + + return self::getResultFromQueryString( $queryString, $params, $printouts, SMW_OUTPUT_WIKI, $context ); + } + + /** + * Process a query string in SMW's query language and return a formatted + * result set as specified by $outputmode. A parameter array of key-value-pairs + * constrains the query and determines the serialisation mode for results. The + * parameter $context defines in what context the query is used, which affects + * certain general settings. Finally, $extraprintouts supplies additional + * printout requests for the query results. + * + * @param string $queryString + * @param array $params These need to be the result of a list fed to getProcessedParams + * @param $extraPrintouts + * @param $outputMode + * @param $context + * + * @return string + * @deprecated Will vanish after release of SMW 1.8. + * See SMW_Ask.php for example code on how to get query results from + * #ask function parameters. + */ + static public function getResultFromQueryString( $queryString, array $params, $extraPrintouts, $outputMode, $context = self::INLINE_QUERY ) { + + $query = self::createQuery( $queryString, $params, $context, '', $extraPrintouts ); + $result = self::getResultFromQuery( $query, $params, $outputMode, $context ); + + + return $result; + } + + /** + * Create a fully formatted result string from a query and its + * parameters. The method takes care of processing various types of + * query result. Most cases are handled by printers, but counting and + * debugging uses special code. + * + * @param SMWQuery $query + * @param array $params These need to be the result of a list fed to getProcessedParams + * @param integer $outputMode + * @param integer $context + * @since before 1.7, but public only since 1.8 + * + * @return string + */ + public static function getResultFromQuery( SMWQuery $query, array $params, $outputMode, $context ) { + + $printer = self::getResultPrinter( + $params['format']->getValue(), + $context + ); + + if ( $printer->isDeferrable() && $context === self::DEFERRED_QUERY && $query->getLimit() > 0 ) { + + // Halt processing that is not `DEFERRED_DATA` as it is expected the + // process is picked-up by the `deferred.js` loader that will + // initiate an API request to finalize the query after MW has build + // the page. + if ( $printer->isDeferrable() !== $printer::DEFERRED_DATA ) { + return Deferred::buildHTML( $query ); + } + + // `DEFERRED_DATA` is interpret as "execute the query with limit=0" (i.e. + // no query execution) but allow the printer to setup the HTML so that + // the data can be loaded after MW has finished the page build including + // the pre-rendered query HTML representation. This mode deferrers the + // actual query execution and data load to after the page build. + // + // Each printer that uses this mode has to handle the required parameters + // and data load accordingly. + $query->querymode = SMWQuery::MODE_INSTANCES; + $query->setOption( 'deferred.limit', $query->getLimit() ); + $query->setLimit( 0 ); + } + + $res = self::getStoreFromParams( $params )->getQueryResult( $query ); + $start = microtime( true ); + + if ( $res instanceof SMWQueryResult && $query->getOption( 'calc.result_hash' ) ) { + $query->setOption( 'result_hash', $res->getHash( 'quick' ) ); + } + + if ( ( $query->querymode == SMWQuery::MODE_INSTANCES ) || + ( $query->querymode == SMWQuery::MODE_NONE ) ) { + + $result = $printer->getResult( $res, $params, $outputMode ); + + $query->setOption( SMWQuery::PROC_PRINT_TIME, microtime( true ) - $start ); + return $result; + } else { // result for counting or debugging is just a string or number + + if ( $res instanceof SMWQueryResult ) { + $res = $res->getCountValue(); + } + + if ( is_numeric( $res ) ) { + $res = strval( $res ); + } + + if ( is_string( $res ) ) { + $result = str_replace( '_', ' ', $params['intro']->getValue() ) + . $res + . str_replace( '_', ' ', $params['outro']->getValue() ) + . smwfEncodeMessages( $query->getErrors() ); + } else { + // When no valid result was obtained, $res will be a SMWQueryResult. + $result = smwfEncodeMessages( $query->getErrors() ); + } + + $query->setOption( SMWQuery::PROC_PRINT_TIME, microtime( true ) - $start ); + + return $result; + } + } + + private static function getStoreFromParams( array $params ) { + return ApplicationFactory::getInstance()->getQuerySourceFactory()->get( $params['source']->getValue() ); + } + + /** + * Find suitable SMWResultPrinter for the given format. The context in + * which the query is to be used determines some basic settings of the + * returned printer object. Possible contexts are + * SMWQueryProcessor::SPECIAL_PAGE, SMWQueryProcessor::INLINE_QUERY, + * SMWQueryProcessor::CONCEPT_DESC. + * + * @param string $format + * @param $context + * + * @return SMWResultPrinter + * @throws MissingResultFormatException + */ + static public function getResultPrinter( $format, $context = self::SPECIAL_PAGE ) { + global $smwgResultFormats; + + SMWParamFormat::resolveFormatAliases( $format ); + + if ( !array_key_exists( $format, $smwgResultFormats ) ) { + throw new ResultFormatNotFoundException( "There is no result format for '$format'." ); + } + + $formatClass = $smwgResultFormats[$format]; + + $printer = new $formatClass( $format, ( $context != self::SPECIAL_PAGE ) ); + + if ( self::$recursiveTextProcessor === null ) { + self::$recursiveTextProcessor = new RecursiveTextProcessor(); + } + + $printer->setRecursiveTextProcessor( + self::$recursiveTextProcessor + ); + + return $printer; + } + + /** + * Produces a list of default allowed parameters for a result printer. Most + * query printers should override this function. + * + * @since 1.6.2, return element type changed in 1.8 + * + * @param integer|null $context + * @param ResultPrinter|null $resultPrinter + * + * @return IParamDefinition[] + */ + public static function getParameters( $context = null, $resultPrinter = null ) { + return DefaultParamDefinition::getParamDefinitions( $context, $resultPrinter ); + } + + /** + * Returns the definitions of all parameters supported by the specified format. + * + * @since 1.8 + * + * @param string $format + * + * @return ParamDefinition[] + */ + public static function getFormatParameters( $format ) { + SMWParamFormat::resolveFormatAliases( $format ); + + if ( !array_key_exists( $format, $GLOBALS['smwgResultFormats'] ) ) { + return []; + } + + $resultPrinter = self::getResultPrinter( $format ); + + if ( $resultPrinter instanceof \SMW\Query\ResultPrinters\NullResultPrinter ) { + return []; + } + + return ParamDefinition::getCleanDefinitions( + $resultPrinter->getParamDefinitions( self::getParameters( null, $resultPrinter ) ) + ); + } + + /** + * Takes an array of unprocessed parameters, + * and sets them on a new Validator object, + * which is returned and ready to process the parameters. + * + * @since 1.8 + * + * @param array $params + * @param array $printRequests + * @param boolean $unknownInvalid + * + * @return Processor + */ + private static function getValidatorForParams( array $params, array $printRequests = [], $unknownInvalid = true, $context = null, $showMode = false ) { + $paramDefinitions = self::getParameters( $context ); + + $paramDefinitions['format']->setPrintRequests( $printRequests ); + $paramDefinitions['format']->setShowMode( $showMode ); + + $processorOptions = new Options(); + $processorOptions->setUnknownInvalid( $unknownInvalid ); + + $validator = Processor::newFromOptions( $processorOptions ); + + $validator->setParameters( $params, $paramDefinitions, false ); + + return $validator; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/querypages/PropertiesQueryPage.php b/www/wiki/extensions/SemanticMediaWiki/includes/querypages/PropertiesQueryPage.php new file mode 100644 index 00000000..737fce97 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/querypages/PropertiesQueryPage.php @@ -0,0 +1,275 @@ +<?php + +namespace SMW; + +use Html; +use SMW\DataValues\ValueFormatters\DataValueFormatter; +use SMW\Exception\PropertyNotFoundException; +use SMWDIError; +use SMWTypesValue; + +/** + * Query class that provides content for the Special:Properties page + * + * @ingroup QueryPage + * + * @licence GNU GPL v2+ + * @since 1.9 + * + * @author Markus Krötzsch + * @author mwjames + */ +class PropertiesQueryPage extends QueryPage { + + /** @var Store */ + protected $store; + + /** @var Settings */ + protected $settings; + + /** + * @var ListLookup + */ + private $listLookup; + + /** + * @since 1.9 + * + * @param Store $store + * @param Settings $settings + */ + public function __construct( Store $store, Settings $settings ) { + $this->store = $store; + $this->settings = $settings; + } + + /** + * @codeCoverageIgnore + * Returns available cache information (takes into account user preferences) + * + * @since 1.9 + * + * @return string + */ + public function getCacheInfo() { + + if ( $this->listLookup->isFromCache() ) { + return $this->msg( 'smw-sp-properties-cache-info', $this->getLanguage()->userTimeAndDate( $this->listLookup->getTimestamp(), $this->getUser() ) )->parse(); + } + + return ''; + } + + /** + * @return string + */ + function getPageHeader() { + return Html::rawElement( + 'p', + [ 'class' => 'smw-sp-properties-docu' ], + $this->msg( 'smw-sp-properties-docu' )->parse() + ) . $this->getSearchForm( $this->getRequest()->getVal( 'property' ), $this->getCacheInfo() ) . + Html::element( + 'h2', + [], + $this->msg( 'smw-sp-properties-header-label' )->text() + ); + } + + /** + * @codeCoverageIgnore + * @return string + */ + function getName() { + return 'Properties'; + } + + /** + * Format a result in the list of results as a string. We expect the + * result to be an array with one object of type SMWDIProperty + * (normally) or maybe SMWDIError (if something went wrong), followed + * by a number (how often the property is used). + * + * @param Skin $skin provided by MediaWiki, not needed here + * @param mixed $result + * @return String + * @throws PropertyNotFoundException if the result was not of a supported type + */ + function formatResult( $skin, $result ) { + + list ( $dataItem, $useCount ) = $result; + + if ( $dataItem instanceof DIProperty ) { + return $this->formatPropertyItem( $dataItem, $useCount ); + } elseif ( $dataItem instanceof SMWDIError ) { + return $this->getMessageFormatter()->clear() + ->setType( 'warning' ) + ->addFromArray( [ $dataItem->getErrors(), 'ID: ' . ( isset( $dataItem->id ) ? $dataItem->id : 'N/A' ) ] ) + ->getHtml(); + } + + throw new PropertyNotFoundException( 'PropertiesQueryPage expects results that are properties or errors.' ); + } + + /** + * Produce a formatted string representation for showing a property and + * its usage count in the list of used properties. + * + * @since 1.8 + * + * @param DIProperty $property + * @param integer $useCount + * @return string + */ + protected function formatPropertyItem( DIProperty $property, $useCount ) { + + // Clear formatter before invoking messages + $this->getMessageFormatter()->clear(); + + $diWikiPage = $property->getDiWikiPage(); + $title = $diWikiPage !== null ? $diWikiPage->getTitle() : null; + + if ( $useCount == 0 && !$this->settings->get( 'smwgPropertyZeroCountDisplay' ) ) { + return ''; + } + + if ( $property->isUserDefined() ) { + + if ( $title === null ) { + // Show even messed up property names. + $typestring = ''; + $proplink = $property->getLabel(); + $this->getMessageFormatter() + ->addFromArray( [ 'ID: ' . ( isset( $property->id ) ? $property->id : 'N/A' ) ] ) + ->addFromKey( 'smw_notitle', $proplink ); + } else { + list( $typestring, $proplink ) = $this->getUserDefinedPropertyInfo( $title, $property, $useCount ); + } + + $infoLink = ''; + + // Add a link to SearchByProperty to hopefully identify the + // "hidden" reference + if ( $useCount < 1 ) { + $infoLink = ' ' . \SMWInfolink::newPropertySearchLink( '+', $property->getLabel(), '' )->getHTML( $this->getLinker() ); + } + + $proplink .= $infoLink; + + } else { + list( $typestring, $proplink ) = $this->getPredefinedPropertyInfo( $property ); + } + + if ( $typestring === '' ) { // Built-ins have no type + + // @todo Should use numParams for $useCount? + return $this->msg( 'smw_property_template_notype' ) + ->rawParams( $proplink )->numParams( $useCount )->text() . ' ' . + $this->getMessageFormatter() + ->setType( 'warning' ) + ->escape( false )->getHtml(); + + } else { + + // @todo Should use numParams for $useCount? + return $this->msg( 'smw_property_template' ) + ->rawParams( $proplink, $typestring )->numParams( $useCount )->escaped() . ' ' . + $this->getMessageFormatter() + ->setType( 'warning' ) + ->escape( false )->getHtml(); + + } + } + + /** + * Returns information related to user-defined properties + * + * @since 1.9 + * + * @param Title $title + * @param DIProperty $property + * @param integer $useCount + * + * @return array + */ + private function getUserDefinedPropertyInfo( $title, $property, $useCount ) { + + if ( $useCount <= $this->settings->get( 'smwgPropertyLowUsageThreshold' ) ) { + $this->getMessageFormatter()->addFromKey( 'smw_propertyhardlyused' ); + } + + // User defined types default to Page + $typestring = SMWTypesValue::newFromTypeId( $this->settings->get( 'smwgPDefaultType' ) )->getLongHTMLText( $this->getLinker() ); + + $label = htmlspecialchars( $property->getLabel() ); + $linkAttributes = []; + + if ( isset( $property->id ) ) { + $linkAttributes['title'] = 'ID: ' . $property->id; + } + + $dataValue = DataValueFactory::getInstance()->newDataValueByItem( $property ); + $dataValue->setLinkAttributes( $linkAttributes ); + + $proplink = $dataValue->getFormattedLabel( + DataValueFormatter::HTML_SHORT, + $this->getLinker() + ); + + if ( !$title->exists() ) { + $this->getMessageFormatter()->addFromKey( 'smw_propertylackspage' ); + } + + $typeProperty = new DIProperty( '_TYPE' ); + $types = $this->store->getPropertyValues( $property->getDiWikiPage(), $typeProperty ); + + if ( is_array( $types ) && count( $types ) >= 1 ) { + $typeDataValue = DataValueFactory::getInstance()->newDataValueByItem( current( $types ), $typeProperty ); + $typestring = $typeDataValue->getLongHTMLText( $this->getLinker() ); + } else { + $this->getMessageFormatter()->addFromKey( 'smw_propertylackstype', $typestring ); + } + + return [ $typestring, $proplink ]; + } + + /** + * Returns information related to predefined properties + * + * @since 1.9 + * + * @param DIProperty $property + * + * @return array + */ + private function getPredefinedPropertyInfo( DIProperty $property ) { + + $dataValue = DataValueFactory::getInstance()->newDataValueByItem( $property, null ); + + $dataValue->setLinkAttributes( [ + 'title' => 'ID: ' . ( isset( $property->id ) ? $property->id : 'N/A' ) . ' (' . $property->getKey() . ')' + ] ); + + $label = $dataValue->getFormattedLabel( + DataValueFormatter::HTML_SHORT, + $this->getLinker() + ); + + return [ + SMWTypesValue::newFromTypeId( $property->findPropertyTypeID() )->getLongHTMLText( $this->getLinker() ), + $label + ]; + } + + /** + * Get the list of results. + * + * @param SMWRequestOptions $requestOptions + * @return array of array( SMWDIProperty|SMWDIError, integer ) + */ + function getResults( $requestOptions ) { + $this->listLookup = $this->store->getPropertiesSpecial( $requestOptions ); + return $this->listLookup->fetchList(); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/querypages/QueryPage.php b/www/wiki/extensions/SemanticMediaWiki/includes/querypages/QueryPage.php new file mode 100644 index 00000000..79cecae8 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/querypages/QueryPage.php @@ -0,0 +1,250 @@ +<?php + +namespace SMW; + +use Html; +use SMWRequestOptions; +use SMWStringCondition; +use Xml; + +/** + * An abstract query page base class that supports array-based + * data retrieval instead of the SQL-based access used by MW. + * + * + * @license GNU GPL v2+ + * @since ?? + * + * @author Markus Krötzsch + */ + +/** + * Abstract base class for SMW's variant of the MW QueryPage. + * Subclasses must implement getResults() and formatResult(), as + * well as some other standard functions of QueryPage. + * + * @ingroup SMW + * @ingroup QueryPage + */ +abstract class QueryPage extends \QueryPage { + + /** @var MessageFormatter */ + protected $msgFormatter; + + /** @var Linker */ + protected $linker = null; + + /** @var array */ + protected $selectOptions = []; + + /** @var array */ + protected $useSerchForm = false; + + /** + * Implemented by subclasses to provide concrete functions. + */ + abstract function getResults( $requestoptions ); + + /** + * Clear the cache and save new results + * @todo Implement caching for SMW query pages + */ + function recache( $limit, $ignoreErrors = true ) { + /// TODO + } + + function isExpensive() { + return false; // Disables caching for now + } + + function isSyndicated() { + return false; // TODO: why not? + } + + /** + * @see QueryPage::linkParameters + * + * @since 1.9 + * + * @return array + */ + public function linkParameters() { + + $parameters = []; + $property = $this->getRequest()->getVal( 'property' ); + + if ( $property !== null && $property !== '' ) { + $parameters['property'] = $property; + } + + $filter = $this->getRequest()->getVal( 'filter' ); + + if ( $filter !== null && $filter !== '' ) { + $parameters['filter'] = $filter; + } + + return $parameters; + } + + /** + * Returns a MessageFormatter object + * + * @since 1.9 + * + * @return MessageFormatter + */ + public function getMessageFormatter() { + + if ( !isset( $this->msgFormatter ) ) { + $this->msgFormatter = new MessageFormatter( $this->getLanguage() ); + } + + return $this->msgFormatter; + } + + /** + * Returns a Linker object + * + * @since 1.9 + * + * @return Linker + */ + public function getLinker() { + + if ( $this->linker === null ) { + $this->linker = smwfGetLinker(); + } + + return $this->linker; + } + + /** + * Generates a search form + * + * @since 1.9 + * + * @param string $property + * + * @return string + */ + public function getSearchForm( $property = '', $cacheDate = '', $propertySearch = true, $filter = '' ) { + + $this->useSerchForm = true; + $this->getOutput()->addModules( 'ext.smw.autocomplete.property' ); + + // No need to verify $this->selectOptions because its values are set + // during doQuery() which is processed before this form is generated + $resultCount = wfShowingResults( $this->selectOptions['offset'], $this->selectOptions['count'] ); + + $selection = $this->getLanguage()->viewPrevNext( + $this->getContext()->getTitle(), + $this->selectOptions['offset'], + $this->selectOptions['limit'], + $this->linkParameters(), + $this->selectOptions['end'] + ); + + if ( $cacheDate !== '' ) { + $cacheDate = Xml::tags( 'p', [], $cacheDate ); + } + + if ( $propertySearch ) { + $propertySearch = Xml::tags( 'hr', [ 'style' => 'margin-bottom:10px;' ], '' ) . + Xml::inputLabel( $this->msg( 'smw-special-property-searchform' )->text(), 'property', 'smw-property-input', 20, $property ) . ' ' . + Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ); + } + + if ( $filter !== '' ) { + $filter = Xml::tags( 'hr', [ 'style' => 'margin-bottom:10px;' ], '' ) . $filter; + } + + return Xml::tags( 'form', [ + 'method' => 'get', + 'action' => htmlspecialchars( $GLOBALS['wgScript'] ), + 'class' => 'plainlinks' + ], Html::hidden( 'title', $this->getContext()->getTitle()->getPrefixedText() ) . + Xml::fieldset( $this->msg( 'smw-special-property-searchform-options' )->text(), + Xml::tags( 'p', [], $resultCount ) . + Xml::tags( 'p', [], $selection ) . + $cacheDate . + $propertySearch . + $filter + ) + ); + } + + /** + * This is the actual workhorse. It does everything needed to make a + * real, honest-to-gosh query page. + * Alas, we need to overwrite the whole beast since we do not assume + * an SQL-based storage backend. + * + * @param $offset database query offset + * @param $limit database query limit + * @param $property database string query + */ + function doQuery( $offset = false, $limit = false, $property = false ) { + $out = $this->getOutput(); + $sk = $this->getSkin(); + + $options = new SMWRequestOptions(); + $options->limit = $limit; + $options->offset = $offset; + $options->sort = true; + + if ( $property ) { + $options->addStringCondition( $property, SMWStringCondition::STRCOND_MID ); + } + + if ( ( $filter = $this->getRequest()->getVal( 'filter' ) ) === 'unapprove' ) { + $options->addExtraCondition( [ 'filter.unapprove' => true ] ); + } + + $res = $this->getResults( $options ); + $num = count( $res ); + + // often disable 'next' link when we reach the end + $atend = $num < $limit; + + $this->selectOptions = [ + 'offset' => $offset, + 'limit' => $limit, + 'end' => $atend, + 'count' => $num + ]; + + $out->addHTML( $this->getPageHeader() ); + + // if list is empty, show it + if ( $num == 0 ) { + $out->addHTML( '<p>' . $this->msg( 'specialpage-empty' )->escaped() . '</p>' ); + return; + } + + if ( $num > 0 ) { + $s = []; + if ( ! $this->listoutput ) { + $s[] = $this->openList( $offset ); + } + + foreach ( $res as $r ) { + $format = $this->formatResult( $sk, $r ); + if ( $format ) { + $s[] = $this->listoutput ? $format : "<li>{$format}</li>\n"; + } + } + + if ( ! $this->listoutput ) { + $s[] = $this->closeList(); + } + $str = $this->listoutput ? $this->getLanguage()->listToText( $s ) : implode( '', $s ); + $out->addHTML( $str ); + } + + if ( !$this->useSerchForm ) { + $out->addHTML( "<p>{$sl}</p>\n" ); + } + + return $num; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/querypages/UnusedPropertiesQueryPage.php b/www/wiki/extensions/SemanticMediaWiki/includes/querypages/UnusedPropertiesQueryPage.php new file mode 100644 index 00000000..878acf98 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/querypages/UnusedPropertiesQueryPage.php @@ -0,0 +1,186 @@ +<?php + +namespace SMW; + +use Html; +use SMW\Exception\PropertyNotFoundException; +use SMWDIError; +use SMWTypesValue; + +/** + * Query page that provides content to Special:UnusedProperties + * + * @ingroup QueryPage + * + * @licence GNU GPL v2+ + * @since 1.9 + * + * @author Markus Krötzsch + * @author mwjames + */ +class UnusedPropertiesQueryPage extends QueryPage { + + /** @var Store */ + protected $store; + + /** @var Settings */ + protected $settings; + + /** + * @var ListLookup + */ + private $listLookup; + + /** + * @since 1.9 + * + * @param Store $store + * @param Settings $settings + */ + public function __construct( Store $store, Settings $settings ) { + $this->store = $store; + $this->settings = $settings; + } + + /** + * @codeCoverageIgnore + * @return string + */ + function getName() { + return "UnusedProperties"; + } + + /** + * @codeCoverageIgnore + * @return boolean + */ + function isExpensive() { + return false; // Disables caching for now + } + + /** + * @codeCoverageIgnore + * @return boolean + */ + function isSyndicated() { + return false; // TODO: why not? + } + + /** + * @codeCoverageIgnore + * Returns available cache information (takes into account user preferences) + * + * @since 1.9 + * + * @return string + */ + public function getCacheInfo() { + + if ( $this->listLookup->isFromCache() ) { + return $this->msg( 'smw-sp-properties-cache-info', $this->getLanguage()->userTimeAndDate( $this->listLookup->getTimestamp(), $this->getUser() ) )->parse(); + } + + return ''; + } + + /** + * @codeCoverageIgnore + * @return string + */ + function getPageHeader() { + + return Html::rawElement( + 'p', + [ 'class' => 'smw-unusedproperties-docu' ], + $this->msg( 'smw-unusedproperties-docu' )->parse() + ) . $this->getSearchForm( $this->getRequest()->getVal( 'property' ), $this->getCacheInfo() ) . + Html::element( + 'h2', + [], + $this->msg( 'smw-sp-properties-header-label' )->text() + ); + } + + /** + * Format a result in the list of results as a string. We expect the + * result to be an object of type SMWDIProperty (normally) or maybe + * SMWDIError (if something went wrong). + * + * @param Skin $skin provided by MediaWiki, not needed here + * @param mixed $result + * + * @return String + * @throws InvalidResultException if the result was not of a supported type + */ + function formatResult( $skin, $result ) { + + if ( $result instanceof DIProperty ) { + return $this->formatPropertyItem( $result ); + } elseif ( $result instanceof SMWDIError ) { + return $this->getMessageFormatter()->clear() + ->setType( 'warning' ) + ->addFromArray( [ $result->getErrors() ] ) + ->getHtml(); + } + + throw new PropertyNotFoundException( 'UnusedPropertiesQueryPage expects results that are properties or errors.' ); + } + + /** + * Produce a formatted string representation for showing a property in + * the list of unused properties. + * + * @since 1.8 + * + * @param DIProperty $property + * + * @return string + */ + protected function formatPropertyItem( DIProperty $property ) { + + // Clear formatter before invoking messages and + // avoid having previous data to be present + $this->getMessageFormatter()->clear(); + + if ( $property->isUserDefined() ) { + + $title = $property->getDiWikiPage()->getTitle(); + + if ( !$title instanceof \Title ) { + return ''; + } + + $propertyLink = $this->getLinker()->link( + $title, + $property->getLabel() + ); + + $types = $this->store->getPropertyValues( $property->getDiWikiPage(), new DIProperty( '_TYPE' ) ); + + if ( is_array( $types ) && count( $types ) >= 1 ) { + $typeDataValue = DataValueFactory::getInstance()->newDataValueByItem( current( $types ), new DIProperty( '_TYPE' ) ); + } else { + $typeDataValue = SMWTypesValue::newFromTypeId( '_wpg' ); + $this->getMessageFormatter()->addFromKey( 'smw_propertylackstype', $typeDataValue->getLongHTMLText() ); + } + + } else { + $typeDataValue = SMWTypesValue::newFromTypeId( $property->findPropertyTypeID() ); + $propertyLink = DataValueFactory::getInstance()->newDataValueByItem( $property, null )->getShortHtmlText( $this->getLinker() ); + } + + return $this->msg( 'smw-unusedproperty-template', $propertyLink, $typeDataValue->getLongHTMLText( $this->getLinker() ) )->text() . ' ' . + $this->getMessageFormatter()->getHtml(); + } + + /** + * Get the list of results. + * + * @param SMWRequestOptions $requestOptions + * @return array of SMWDIProperty|SMWDIError + */ + function getResults( $requestOptions ) { + $this->listLookup = $this->store->getUnusedPropertiesSpecial( $requestOptions ); + return $this->listLookup->fetchList(); + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/querypages/WantedPropertiesQueryPage.php b/www/wiki/extensions/SemanticMediaWiki/includes/querypages/WantedPropertiesQueryPage.php new file mode 100644 index 00000000..0daf7608 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/querypages/WantedPropertiesQueryPage.php @@ -0,0 +1,187 @@ +<?php + +namespace SMW; + +use Html; + +/** + * Query class that provides content for the Special:WantedProperties page + * + * @ingroup QueryPage + * + * @licence GNU GPL v2+ + * @since 1.9 + * + * @author Markus Krötzsch + * @author mwjames + */ +class WantedPropertiesQueryPage extends QueryPage { + + /** @var Store */ + protected $store; + + /** @var Settings */ + protected $settings; + + /** + * @var ListLookup + */ + private $listLookup; + + /** + * @since 1.9 + * + * @param Store $store + * @param Settings $settings + */ + public function __construct( Store $store, Settings $settings ) { + $this->store = $store; + $this->settings = $settings; + } + + /** + * @codeCoverageIgnore + * @return string + */ + public function setTitle( $title ) { + $this->title = $title; + } + + /** + * @codeCoverageIgnore + * @return string + */ + function getName() { + return "WantedProperties"; + } + + /** + * @codeCoverageIgnore + * @return boolean + */ + function isExpensive() { + return false; /// disables caching for now + } + + /** + * @codeCoverageIgnore + * @return boolean + */ + function isSyndicated() { + return false; ///TODO: why not? + } + + /** + * @codeCoverageIgnore + * Returns available cache information (takes into account user preferences) + * + * @since 1.9 + * + * @return string + */ + public function getCacheInfo() { + + if ( $this->listLookup->isFromCache() ) { + return $this->msg( 'smw-sp-properties-cache-info', $this->getLanguage()->userTimeAndDate( $this->listLookup->getTimestamp(), $this->getUser() ) )->parse(); + } + + return ''; + } + + /** + * @codeCoverageIgnore + * @return string + */ + function getPageHeader() { + + $filer = $this->getRequest()->getVal( 'filter', '' ); + + if ( $filer !== 'unapprove' ) { + $label = $this->msg( 'smw-special-wantedproperties-filter-unapproved' )->text(); + $title = $this->msg( 'smw-special-wantedproperties-filter-unapproved-desc' )->text(); + } else { + $label = $this->msg( 'smw-special-wantedproperties-filter-none' )->text(); + $title = ''; + } + + $filter = Html::rawElement( + 'div', + [ + 'class' => 'smw-special-filter' + ], + $this->msg( 'smw-special-wantedproperties-filter-label' )->text() . + ' ' . + Html::rawElement( + 'span', + [ + 'class' => 'smw-special-filter-button', + 'title' => $title + ], + Html::element( + 'a', + [ + 'href' => $this->title->getLocalURL( [ 'filter' => $filer !== '' ? '' : 'unapprove' ] ), + 'rel' => 'nofollow' + ], + $label + ) + ) + ); + + return Html::rawElement( + 'p', + [ 'class' => 'smw-wantedproperties-docu plainlinks' ], + $this->msg( 'smw-special-wantedproperties-docu' )->parse() + ) . $this->getSearchForm( $this->getRequest()->getVal( 'property' ), $this->getCacheInfo(), false, $filter ) . + Html::element( + 'h2', + [], + $this->msg( 'smw-sp-properties-header-label' )->text() + ); + } + + /** + * @param $skin + * @param array $result First item is SMWDIProperty, second item is int + * + * @return string + */ + function formatResult( $skin, $result ) { + // Only display user-defined properties because it can happen that + // custom predefined (fixed) properties are mixed within the result + // (did not use their own fixedProperty table and therefore were + // selected as well e.g _SF_PDF etc.) + if ( !$result[0] instanceof DIProperty || !$result[0]->isUserDefined() ) { + return ''; + } + + $title = $result[0]->getDiWikiPage()->getTitle(); + + if ( !$title instanceof \Title ) { + return ''; + } + + $proplink = $this->getLinker()->link( + $title, + htmlspecialchars( $result[0]->getLabel() ), + [], + [ 'action' => 'view' ] + ); + + return $this->msg( 'smw-special-wantedproperties-template' ) + ->rawParams( $proplink ) + ->params( $result[1] ) + ->escaped(); + } + + /** + * Get the list of results. + * + * @param SMWRequestOptions $requestOptions + * @return array of SMWDIProperty|SMWDIError + */ + function getResults( $requestoptions ) { + $this->listLookup = $this->store->getWantedPropertiesSpecial( $requestoptions ); + return $this->listLookup->fetchList(); + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/AggregatablePrinter.php b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/AggregatablePrinter.php new file mode 100644 index 00000000..7439d2f8 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/AggregatablePrinter.php @@ -0,0 +1,280 @@ +<?php + +namespace SMW; + +use SMWDataItem; +use SMWQueryResult; + +/** + * Abstract class that supports the aggregation and distributive calculation + * of numerical data. + * + * @since 1.9 + * + * + * @license GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ + +/** + * Abstract class that supports the aggregation and distributive calculation + * of numerical data. Supports the distribution parameter, and related + * parameters that allows the user to choose between regular behavior or + * generating a distribution of values. + * + * For example, this result set: foo bar baz foo bar bar ohi + * Will be turned into + * * bar (3) + * * foo (2) + * * baz (1) + * * ohi (1) + * + * @ingroup QueryPrinter + */ +abstract class AggregatablePrinter extends ResultPrinter { + + /** + * Create the formats output given the result data and return it. + * + * @since 1.7 + * + * @param array $data label => value + */ + protected abstract function getFormatOutput( array $data ); + + /** + * Method gets called right before the result is returned + * in case there are values to display. It is meant for + * adding resources such as JS and CSS only needed for this + * format when it has actual values. + * + * @since 1.7 + */ + protected function addResources() { + } + + /** + * (non-PHPdoc) + * @see SMWResultPrinter::getResultText() + */ + protected function getResultText( SMWQueryResult $queryResult, $outputMode ) { + $data = $this->getResults( $queryResult, $outputMode ); + + if ( $data === [] ) { + $queryResult->addErrors( [ + $this->msg( 'smw-qp-empty-data' )->inContentLanguage()->text() + ] ); + return ''; + } else { + $this->applyDistributionParams( $data ); + $this->addResources(); + return $this->getFormatOutput( $data ); + } + } + + /** + * Apply the distribution specific parameters. + * + * @since 1.7 + * + * @param array $data + */ + protected function applyDistributionParams( array &$data ) { + if ( $this->params['distributionsort'] == 'asc' ) { + asort( $data, SORT_NUMERIC ); + } + elseif ( $this->params['distributionsort'] == 'desc' ) { + arsort( $data, SORT_NUMERIC ); + } + + if ( $this->params['distributionlimit'] !== false ) { + $data = array_slice( $data, 0, $this->params['distributionlimit'], true ); + } + } + + /** + * Gets and processes the results so they can be fed directly to the + * getFormatOutput method. They are returned as an array with the keys + * being the labels and the values being their corresponding (numeric) values. + * + * @since 1.7 + * + * @param SMWQueryResult $result + * @param $outputMode + * + * @return array label => value + */ + protected function getResults( SMWQueryResult $result, $outputMode ) { + if ( $this->params['distribution'] ) { + return $this->getDistributionResults( $result, $outputMode ); + } + else { + return $this->getNumericResults( $result, $outputMode ); + } + } + + /** + * Counts all the occurrences of all values in the query result, + * and returns an array with as key the value and as value the count. + * + * @since 1.7 + * + * @param SMWQueryResult $result + * @param $outputMode + * + * @return array label => value + */ + protected function getDistributionResults( SMWQueryResult $result, $outputMode ) { + $values = []; + + while ( /* array of SMWResultArray */ $row = $result->getNext() ) { // Objects (pages) + for ( $i = 0, $n = count( $row ); $i < $n; $i++ ) { // SMWResultArray for a sinlge property + while ( ( /* SMWDataValue */ $dataValue = $row[$i]->getNextDataValue() ) !== false ) { // Data values + + // Get the HTML for the tag content. Pages are linked, other stuff is just plaintext. + if ( $dataValue->getTypeID() == '_wpg' ) { + $value = $dataValue->getTitle()->getText(); + } + else { + $value = $dataValue->getShortText( $outputMode, $this->getLinker( false ) ); + } + + if ( !array_key_exists( $value, $values ) ) { + $values[$value] = 0; + } + + $values[$value]++; + } + } + } + + return $values; + } + + /** + * Returns an array with the numerical data in the query result. + * + * @since 1.7 + * + * @param SMWQueryResult $res + * @param $outputMode + * + * @return array label => value + */ + protected function getNumericResults( SMWQueryResult $res, $outputMode ) { + $values = []; + + // print all result rows + while ( $subject = $res->getNext() ) { + $dataValue = $subject[0]->getNextDataValue(); + + if ( $dataValue !== false ) { + $name = $dataValue->getShortWikiText(); + + foreach ( $subject as $field ) { + + // Use the aggregation parameter to determine the source of + // the number composition + if ( $this->params['aggregation'] === 'property' ) { + $name = $field->getPrintRequest()->getLabel(); + } + + // Aggregated array key depends on the mainlabel which is + // either the subject or a printout property + if ( $this->params['mainlabel'] === '-' ) { + + // In case of '-', getNextDataValue() already shifted the + // array forward which means the first column + // ( $subject[0] == $field ) contains a possible value + // and has to be collected as well + if ( ( $subject[0] == $field ) && $dataValue->getDataItem()->getDIType() === SMWDataItem::TYPE_NUMBER ) { + $value = $dataValue->getDataItem( )->getNumber(); + $values[$name] = isset( $values[$name] ) ? $values[$name] + $value : $value; + } + } + + while ( ( /* SMWDataItem */ $dataItem = $field->getNextDataItem() ) !== false ) { + $this->addNumbersForDataItem( $dataItem, $values, $name ); + } + } + } + } + + return $values; + } + + /** + * Adds all numbers contained in a dataitem to the list. + * + * @since 1.7 + * + * @param SMWDataItem $dataItem + * @param array $values + * @param string $name + */ + protected function addNumbersForDataItem( SMWDataItem $dataItem, array &$values, $name ) { + switch ( $dataItem->getDIType() ) { + case SMWDataItem::TYPE_NUMBER: + // Collect and aggregate values for the same array key + $value = $dataItem->getNumber(); + if ( !isset( $values[$name] ) ) { + $values[$name] = 0; + } + $values[$name] += $value; + break; + case SMWDataItem::TYPE_CONTAINER: + foreach ( $dataItem->getDataItems() as $di ) { + $this->addNumbersForDataItem( $di, $values, $name ); + } + break; + default: + } + } + + /** + * @codeCoverageIgnore + * @see SMWResultPrinter::getParamDefinitions + * + * @since 1.8 + * + * @param ParamDefinition[] $definitions + * + * @return array + */ + public function getParamDefinitions( array $definitions ) { + $definitions = parent::getParamDefinitions( $definitions ); + + $definitions['distribution'] = [ + 'name' => 'distribution', + 'type' => 'boolean', + 'default' => false, + 'message' => 'smw-paramdesc-distribution', + ]; + + $definitions['distributionsort'] = [ + 'name' => 'distributionsort', + 'type' => 'string', + 'default' => 'none', + 'message' => 'smw-paramdesc-distributionsort', + 'values' => [ 'asc', 'desc', 'none' ], + ]; + + $definitions['distributionlimit'] = [ + 'name' => 'distributionlimit', + 'type' => 'integer', + 'default' => false, + 'manipulatedefault' => false, + 'message' => 'smw-paramdesc-distributionlimit', + 'lowerbound' => 1, + ]; + + $definitions['aggregation'] = [ + 'message' => 'smw-paramdesc-aggregation', + 'default' => 'subject', + 'values' => [ 'property', 'subject' ], + ]; + + return $definitions; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/DsvResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/DsvResultPrinter.php new file mode 100644 index 00000000..f472132e --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/DsvResultPrinter.php @@ -0,0 +1,210 @@ +<?php + +namespace SMW; + +use Sanitizer; +use SMWQueryResult; + +/** + * Result printer to print results in UNIX-style DSV (deliminter separated value) format. + * + * @license GNU GPL v2+ + * @since 1.6 + * + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class DsvResultPrinter extends FileExportPrinter { + + protected $separator = ':'; + protected $fileName = 'result.dsv'; + + /** + * @see SMWResultPrinter::handleParameters + * + * @since 1.6 + * + * @param array $params + * @param $outputmode + */ + protected function handleParameters( array $params, $outputmode ) { + parent::handleParameters( $params, $outputmode ); + + // Do not allow backspaces as delimiter, as they'll break stuff. + if ( trim( $params['separator'] ) != '\\' ) { + $this->separator = trim( $params['separator'] ); + } + + $this->fileName = str_replace( ' ', '_', $params['filename'] ); + } + + /** + * @see SMWIExportPrinter::getMimeType + * + * @since 1.8 + * + * @param SMWQueryResult $queryResult + * + * @return string + */ + public function getMimeType( SMWQueryResult $queryResult ) { + return 'text/dsv'; + } + + /** + * @see SMWIExportPrinter::getFileName + * + * @since 1.8 + * + * @param SMWQueryResult $queryResult + * + * @return string|boolean + */ + public function getFileName( SMWQueryResult $queryResult ) { + return $this->fileName; + } + + public function getName() { + return wfMessage( 'smw_printername_dsv' )->text(); + } + + protected function getResultText( SMWQueryResult $res, $outputMode ) { + if ( $outputMode == SMW_OUTPUT_FILE ) { // Make the DSV file. + return $this->getResultFileContents( $res ); + } + else { // Create a link pointing to the DSV file. + return $this->getLinkToFile( $res, $outputMode ); + } + } + + /** + * Returns the query result in DSV. + * + * @since 1.6 + * + * @param SMWQueryResult $res + * + * @return string + */ + protected function getResultFileContents( SMWQueryResult $queryResult ) { + $lines = []; + + if ( $this->mShowHeaders ) { + $headerItems = []; + + foreach ( $queryResult->getPrintRequests() as $printRequest ) { + $headerItems[] = $printRequest->getLabel(); + } + + $lines[] = $this->getDSVLine( $headerItems ); + } + + // Loop over the result objects (pages). + while ( $row = $queryResult->getNext() ) { + $rowItems = []; + + /** + * Loop over their fields (properties). + * @var SMWResultArray $field + */ + foreach ( $row as $field ) { + $itemSegments = []; + + // Loop over all values for the property. + while ( ( $object = $field->getNextDataValue() ) !== false ) { + $itemSegments[] = Sanitizer::decodeCharReferences( $object->getWikiValue() ); + } + + // Join all values into a single string, separating them with comma's. + $rowItems[] = implode( ',', $itemSegments ); + } + + $lines[] = $this->getDSVLine( $rowItems ); + } + + return implode( "\n", $lines ); + } + + /** + * Returns a single DSV line. + * + * @since 1.6 + * + * @param array $fields + * + * @return string + */ + protected function getDSVLine( array $fields ) { + return implode( $this->separator, array_map( [ $this, 'encodeDSV' ], $fields ) ); + } + + /** + * Encodes a single DSV. + * + * @since 1.6 + * + * @param string $value + * + * @return string + */ + protected function encodeDSV( $value ) { + // TODO + // \nnn or \onnn or \0nnn for the character with octal value nnn + // \xnn for the character with hexadecimal value nn + // \dnnn for the character with decimal value nnn + // \unnnn for a hexadecimal Unicode literal. + return str_replace( + [ '\n', '\r', '\t', '\b', '\f', '\\', $this->separator ], + [ "\n", "\r", "\t", "\b", "\f", '\\\\', "\\$this->separator" ], + $value + ); + } + + /** + * Returns html for a link to a query that returns the DSV file. + * + * @since 1.6 + * + * @param SMWQueryResult $res + * @param $outputMode + * + * @return string + */ + protected function getLinkToFile( SMWQueryResult $res, $outputMode ) { + // yes, our code can be viewed as HTML if requested, no more parsing needed + $this->isHTML = ( $outputMode == SMW_OUTPUT_HTML ); + return $this->getLink( $res, $outputMode )->getText( $outputMode, $this->mLinker ); + } + + /** + * @see SMWResultPrinter::getParamDefinitions + * + * @since 1.8 + * + * @param ParamDefinition[] $definitions + * + * @return array + */ + public function getParamDefinitions( array $definitions ) { + $params = parent::getParamDefinitions( $definitions ); + + $params['searchlabel']->setDefault( wfMessage( 'smw_dsv_link' )->text() ); + + $params['limit']->setDefault( 100 ); + + $params[] = [ + 'name' => 'separator', + 'message' => 'smw-paramdesc-dsv-separator', + 'default' => $this->separator, + 'aliases' => 'sep', + ]; + + $params[] = [ + 'name' => 'filename', + 'message' => 'smw-paramdesc-dsv-filename', + 'default' => $this->fileName, + ]; + + return $params; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/EmbeddedResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/EmbeddedResultPrinter.php new file mode 100644 index 00000000..d067a255 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/EmbeddedResultPrinter.php @@ -0,0 +1,155 @@ +<?php + +namespace SMW; + +use SMWQueryResult; +use Title; + +/** + * Printer for embedded data. + * + * Embeds in the page output the contents of the pages in the query result set. + * Printouts are ignored: it only matters which pages were returned by the query. + * The optional "titlestyle" formatting parameter can be used to apply a format to + * the headings for the page titles. If "titlestyle" is not specified, a <h1> tag is + * used. + * + * @license GNU GPL v2+ + * @since 1.7 + * + * @author Fernando Correia + * @author Markus Krötzsch + */ +class EmbeddedResultPrinter extends ResultPrinter { + + protected $m_showhead; + protected $m_embedformat; + + /** + * @see SMWResultPrinter::handleParameters + * + * @since 1.7 + * + * @param array $params + * @param $outputmode + */ + protected function handleParameters( array $params, $outputmode ) { + parent::handleParameters( $params, $outputmode ); + + $this->m_showhead = !$params['embedonly']; + $this->m_embedformat = $params['embedformat']; + } + + public function getName() { + return wfMessage( 'smw_printername_embedded' )->text(); + } + + /** + * @see ResultPrinter::isDeferrable + * + * {@inheritDoc} + */ + public function isDeferrable() { + return true; + } + + protected function getResultText( SMWQueryResult $res, $outputMode ) { + + // Ensure that there is an annotation block in place before starting the + // parse and transclution process. Unfortunately we are unable to block + // the inclusion of categories which are attached to a MediaWiki + // object we have no immediate access or control. + $this->transcludeAnnotation = false; + + global $wgParser; + // No page should embed itself, find out who we are: + if ( $wgParser->getTitle() instanceof Title ) { + $title = $wgParser->getTitle()->getPrefixedText(); + } else { // this is likely to be in vain -- this case is typical if we run on special pages + global $wgTitle; + $title = $wgTitle->getPrefixedText(); + } + + // print header + $result = ''; + $footer = ''; + $embstart = ''; + $embend = ''; + $headstart = ''; + $headend = ''; + $this->hasTemplates = true; + + switch ( $this->m_embedformat ) { + case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': + $headstart = '<' . $this->m_embedformat . '>'; + $headend = '</' . $this->m_embedformat . ">\n"; + break; + case 'ul': case 'ol': + $result .= '<' . $this->m_embedformat . '>'; + $footer = '</' . $this->m_embedformat . '>'; + $embstart = '<li>'; + $headend = "<br />\n"; + $embend = "</li>\n"; + break; + } + + // Print all result rows: + foreach ( $res->getResults() as $diWikiPage ) { + if ( $diWikiPage instanceof DIWikiPage ) { // ensure that we deal with title-likes + $dvWikiPage = DataValueFactory::getInstance()->newDataValueByItem( $diWikiPage, null ); + $result .= $embstart; + + if ( $this->m_showhead ) { + $result .= $headstart . $dvWikiPage->getLongWikiText( $this->mLinker ) . $headend; + } + + if ( $dvWikiPage->getLongWikiText() != $title ) { + if ( $diWikiPage->getNamespace() == NS_MAIN ) { + $result .= '{{:' . $diWikiPage->getDBkey() . '}}'; + } else { + $result .= '{{' . $dvWikiPage->getLongWikiText() . '}}'; + } + } else { // block recursion + $result .= '<b>' . $dvWikiPage->getLongWikiText() . '</b>'; + } + + $result .= $embend; + } + } + + // show link to more results + if ( $this->linkFurtherResults( $res ) ) { + $result .= $embstart + . $this->getFurtherResultsLink( $res, $outputMode )->getText( SMW_OUTPUT_WIKI, $this->mLinker ) + . $embend; + } + + $result .= $footer; + + return $result; + } + + /** + * @inheritdoc + */ + public function getParamDefinitions( array $definitions ) { + $definitions = parent::getParamDefinitions( $definitions ); + + $definitions[] = [ + 'name' => 'embedformat', + 'message' => 'smw-paramdesc-embedformat', + 'default' => 'h1', + 'values' => [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul' ], + ]; + + $definitions[] = [ + 'name' => 'embedonly', + 'type' => 'boolean', + 'message' => 'smw-paramdesc-embedonly', + 'default' => false, + ]; + + + return $definitions; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/JsonResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/JsonResultPrinter.php new file mode 100644 index 00000000..990e11d6 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/JsonResultPrinter.php @@ -0,0 +1,189 @@ +<?php + +namespace SMW; + +use FormatJson; +use SMWQueryResult; + +/** + * Print links to JSON files representing query results. + * + * @see http://www.semantic-mediawiki.org/wiki/Help:JSON_format + * + * @since 1.5.3 + * + * + * @license GNU GPL v2 or later + * @author mwjames + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author Fabian Howahl + */ + +/** + * Print links to JSON files representing query results. + * + * @ingroup QueryPrinter + */ +class JsonResultPrinter extends FileExportPrinter { + + /** + * Returns human readable label for this printer + * @codeCoverageIgnore + * + * @return string + */ + public function getName() { + return $this->msg( 'smw_printername_json' )->text(); + } + + /** + * @see SMWIExportPrinter::getMimeType + * + * @since 1.8 + * + * @param SMWQueryResult $queryResult + * + * @return string + */ + public function getMimeType( SMWQueryResult $queryResult ) { + return 'application/json'; + } + + /** + * @see SMWIExportPrinter::getFileName + * + * @since 1.8 + * + * @param SMWQueryResult $queryResult + * + * @return string|boolean + */ + public function getFileName( SMWQueryResult $queryResult ) { + + if ( $this->getSearchLabel( SMW_OUTPUT_WIKI ) !== '' ) { + return str_replace( ' ', '_', $this->getSearchLabel( SMW_OUTPUT_WIKI ) ) . '.json'; + } + + return 'result.json'; + } + + /** + * Returns a filename that is to be sent to the caller + * + * @param SMWQueryResult $res + * @param $outputMode integer + * + * @return string + */ + protected function getResultText( SMWQueryResult $res, $outputMode ) { + + if ( $outputMode == SMW_OUTPUT_FILE ) { + + // No results, just bailout + if ( $res->getCount() == 0 ){ + return $this->params['default'] !== '' ? $this->params['default'] : ''; + } + + $flags = $this->params['prettyprint'] ? JSON_PRETTY_PRINT : 0; + $flags = $flags | ( $this->params['unescape'] ? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES : 0 ); + + // Serialize queryResult + if ( isset( $this->params['type'] ) && $this->params['type'] === 'simple' ) { + $result = $this->serializeAsSimpleList( $res ); + } else { + $result = array_merge( + $res->serializeToArray(), + [ 'rows' => $res->getCount() ] + ); + } + + $result = json_encode( + $result, + $flags + ); + + } else { + // Create a link that points to the JSON file + $result = $this->getLink( $res, $outputMode )->getText( $outputMode, $this->mLinker ); + + // Code can be viewed as HTML if requested, no more parsing needed + $this->isHTML = $outputMode == SMW_OUTPUT_HTML; + } + + return $result; + } + + /** + * @see SMWResultPrinter::getParamDefinitions + * @codeCoverageIgnore + * + * @since 1.8 + * + * @param ParamDefinition[] $definitions + * + * @return array + */ + public function getParamDefinitions( array $definitions ) { + $params = parent::getParamDefinitions( $definitions ); + + $params['searchlabel']->setDefault( $this->msg( 'smw_json_link' )->inContentLanguage()->text() ); + + $params['limit']->setDefault( 100 ); + + $params['type'] = [ + 'values' => [ 'simple', 'full' ], + 'default' => 'full', + 'message' => 'smw-paramdesc-json-type', + ]; + + $params['prettyprint'] = [ + 'type' => 'boolean', + 'default' => '', + 'message' => 'smw-paramdesc-prettyprint', + ]; + + $params['unescape'] = [ + 'type' => 'boolean', + 'default' => '', + 'message' => 'smw-paramdesc-json-unescape', + ]; + + return $params; + } + + private function serializeAsSimpleList( $res ) { + + $result = []; + + while ( $row = $res->getNext() ) { + $item = []; + $subject = ''; + + foreach ( $row as /* SMWResultArray */ $field ) { + $label = $field->getPrintRequest()->getLabel(); + + if ( $label === '' ) { + continue; + } + + $values = []; + $subject = $field->getResultSubject()->getHash(); + + while ( ( $dataValue = $field->getNextDataValue() ) !== false ) { + $values[] = $dataValue->getWikiValue(); + } + + $item[$label] = $values; + } + + if ( $this->params['mainlabel'] === '-' || $subject === '' ) { + $result[] = $item; + } else { + $result[$subject] = $item; + } + } + + return $result; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/RawResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/RawResultPrinter.php new file mode 100644 index 00000000..1e975162 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/RawResultPrinter.php @@ -0,0 +1,130 @@ +<?php + +namespace SMW; + +use FormatJson; +use Html; +use SMWOutputs; +use SMWQueryResult; + +/** + * Base class for result printers that use the serialized results + * + * @since 1.9 + * + * @license GNU GPL v2 or later + * @author mwjames + */ +abstract class RawResultPrinter extends ResultPrinter { + + /** + * Returns html output. + * + * @since 1.9 + */ + abstract protected function getHtml( array $data ); + + /** + * Convenience method to register resources + * + * @since 1.9 + * + * @param string $resource + */ + protected function addResources( $resource ) { + SMWOutputs::requireResource( $resource ); + } + + /** + * Convenience method to create a unique id + * + * @since 1.9 + */ + protected function getId( ) { + return 'smw-' . uniqid(); + } + + /** + * Convenience method generating a visual placeholder before any + * JS is registered to indicate that resources (JavaScript, CSS) + * are being loaded and once ready ensure to set + * ( '.smw-spinner' ).hide() + * + * @since 1.9 + */ + protected function createLoadingHtmlPlaceholder() { + $this->addResources( 'ext.smw.style' ); + + return Html::rawElement( + 'div', + [ 'class' => 'smw-spinner left mw-small-spinner' ], + Html::element( + 'p', + [ 'class' => 'text' ], + $this->msg( 'smw-livepreview-loading' )->inContentLanguage()->text() + ) + ); + } + + /** + * @deprecated since 2.0 + */ + protected function loading() { + return $this->createLoadingHtmlPlaceholder(); + } + + /** + * Convenience method to encode output data + * + * @since 1.9 + * + * @param string $id + * @param array $data + */ + protected function encodeToJsonForId( $id, $data ) { + SMWOutputs::requireHeadItem( + $id, + $this->getSkin()->makeVariablesScript( [ $id => FormatJson::encode( $data ) ] + ) ); + + return $this; + } + + /** + * @deprecated since 2.0 + */ + protected function encode( $id, $data ) { + return $this->encodeToJsonForId( $id, $data ); + } + + /** + * Returns serialised content + * + * @see SMWResultPrinter::getResultText() + * + * @param SMWQueryResult $queryResult + * @param $outputMode + * + * @return string + */ + protected function getResultText( SMWQueryResult $queryResult, $outputMode ) { + + // Add parameters that are only known to the specific printer + $ask = $queryResult->getQuery()->toArray(); + foreach ( $this->params as $key => $value ) { + if ( is_string( $value ) || is_integer( $value ) || is_bool( $value ) ) { + $ask['parameters'][$key] = $value; + } + } + + // Combine all data into one object + $data = [ + 'query' => [ + 'result' => $queryResult->toArray(), + 'ask' => $ask + ] + ]; + + return $this->getHtml( $data ); + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/RdfResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/RdfResultPrinter.php new file mode 100644 index 00000000..24006dbe --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/queryprinters/RdfResultPrinter.php @@ -0,0 +1,141 @@ +<?php + +namespace SMW; + +use SMW\Query\PrintRequest; +use SMWExporter; +use SMWQueryResult; +use SMWRDFXMLSerializer; +use SMWTurtleSerializer; + +/** + * Printer class for generating RDF output + * + * @license GNU GPL v2+ + * @since 1.6 + * + * @author Markus Krötzsch + */ +class RdfResultPrinter extends FileExportPrinter { + + /** + * The syntax to be used for export. May be 'rdfxml' or 'turtle'. + */ + protected $syntax; + + /** + * @see SMWResultPrinter::handleParameters + * + * @since 1.7 + * + * @param array $params + * @param $outputmode + */ + protected function handleParameters( array $params, $outputmode ) { + parent::handleParameters( $params, $outputmode ); + $this->syntax = $params['syntax']; + } + + /** + * @see SMWIExportPrinter::getMimeType + * + * @since 1.8 + * + * @param SMWQueryResult $queryResult + * + * @return string + */ + public function getMimeType( SMWQueryResult $queryResult ) { + return $this->syntax == 'turtle' ? 'application/x-turtle' : 'application/xml'; + } + + /** + * @see SMWIExportPrinter::getFileName + * + * @since 1.8 + * + * @param SMWQueryResult $queryResult + * + * @return string|boolean + */ + public function getFileName( SMWQueryResult $queryResult ) { + return $this->syntax == 'turtle' ? 'result.ttl' : 'result.rdf'; + } + + public function getName() { + return wfMessage( 'smw_printername_rdf' )->text(); + } + + protected function getResultText( SMWQueryResult $res, $outputMode ) { + if ( $outputMode == SMW_OUTPUT_FILE ) { // make RDF file + $serializer = $this->syntax == 'turtle' ? new SMWTurtleSerializer() : new SMWRDFXMLSerializer(); + $serializer->startSerialization(); + $serializer->serializeExpData( SMWExporter::getInstance()->getOntologyExpData( '' ) ); + + while ( $row = $res->getNext() ) { + $subjectDi = reset( $row )->getResultSubject(); + $data = SMWExporter::getInstance()->makeExportDataForSubject( $subjectDi ); + + foreach ( $row as $resultarray ) { + $printreq = $resultarray->getPrintRequest(); + $property = null; + + switch ( $printreq->getMode() ) { + case PrintRequest::PRINT_PROP: + $property = $printreq->getData()->getDataItem(); + break; + case PrintRequest::PRINT_CATS: + $property = new DIProperty( '_TYPE' ); + break; + case PrintRequest::PRINT_CCAT: + // not serialised right now + break; + case PrintRequest::PRINT_THIS: + // ignored here (object is always included in export) + break; + } + + if ( !is_null( $property ) ) { + SMWExporter::getInstance()->addPropertyValues( $property, $resultarray->getContent(), $data, $subjectDi ); + } + } + $serializer->serializeExpData( $data ); + } + + $serializer->finishSerialization(); + + return $serializer->flushContent(); + } else { // just make link to feed + $this->isHTML = ( $outputMode == SMW_OUTPUT_HTML ); // yes, our code can be viewed as HTML if requested, no more parsing needed + + return $this->getLink( $res, $outputMode )->getText( $outputMode, $this->mLinker ); + } + } + + /** + * @see SMWResultPrinter::getParamDefinitions + * + * @since 1.8 + * + * @param ParamDefinition[] $definitions + * + * @return array + */ + public function getParamDefinitions( array $definitions ) { + $definitions = parent::getParamDefinitions( $definitions ); + + $definitions['limit']->setDefault( 100 ); + + $definitions['searchlabel']->setDefault( wfMessage( 'smw_rdf_link' )->inContentLanguage()->text() ); + + $definitions[] = [ + 'name' => 'syntax', + 'message' => 'smw-paramdesc-rdfsyntax', + 'values' => [ 'rdfxml', 'turtle' ], + 'default' => 'rdfxml', + ]; + + return $definitions; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/specials/SMW_SpecialOWLExport.php b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SMW_SpecialOWLExport.php new file mode 100644 index 00000000..06884ffd --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SMW_SpecialOWLExport.php @@ -0,0 +1,196 @@ +<?php + +/** + * This special page (Special:ExportRDF) for MediaWiki implements an OWL-export of semantic data, + * gathered both from the annotations in articles, and from metadata already + * present in the database. + * + * @ingroup SMWSpecialPage + * @ingroup SpecialPage + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + */ +class SMWSpecialOWLExport extends SpecialPage { + + /// Export controller object to be used for serializing data + protected $export_controller; + + public function __construct() { + parent::__construct( 'ExportRDF' ); + } + + public function execute( $page ) { + $this->setHeaders(); + global $wgOut, $wgRequest; + + $wgOut->setPageTitle( wfMessage( 'exportrdf' )->text() ); + + // see if we can find something to export: + $page = is_null( $page ) ? $wgRequest->getVal( 'page' ) : rawurldecode( $page ); + $pages = false; + + if ( !is_null( $page ) || $wgRequest->getCheck( 'page' ) ) { + $page = is_null( $page ) ? $wgRequest->getCheck( 'text' ) : $page; + + if ( $page !== '' ) { + $pages = [ $page ]; + } + } + + if ( $pages === false && $wgRequest->getCheck( 'pages' ) ) { + $pageBlob = $wgRequest->getText( 'pages' ); + + if ( $pageBlob !== '' ) { + $pages = explode( "\n", $wgRequest->getText( 'pages' ) ); + } + } + + if ( $pages !== false ) { + $this->exportPages( $pages ); + return; + } else { + $offset = $wgRequest->getVal( 'offset' ); + + if ( isset( $offset ) ) { + $this->startRDFExport(); + $this->export_controller->printPageList( $offset ); + return; + } else { + $stats = $wgRequest->getVal( 'stats' ); + + if ( isset( $stats ) ) { + $this->startRDFExport(); + $this->export_controller->printWikiInfo(); + return; + } + } + } + + // Nothing exported yet; show user interface: + $this->showForm(); + } + + /** + * Create the HTML user interface for this special page. + */ + protected function showForm() { + global $wgOut, $wgUser, $smwgAllowRecursiveExport, $smwgExportBacklinks, $smwgExportAll; + + $html = '<form name="tripleSearch" action="" method="POST">' . "\n" . + '<p>' . wfMessage( 'smw_exportrdf_docu' )->text() . "</p>\n" . + '<input type="hidden" name="postform" value="1"/>' . "\n" . + '<textarea name="pages" cols="40" rows="10"></textarea><br />' . "\n"; + + if ( $wgUser->isAllowed( 'delete' ) || $smwgAllowRecursiveExport ) { + $html .= '<input type="checkbox" name="recursive" value="1" id="rec"> <label for="rec">' . wfMessage( 'smw_exportrdf_recursive' )->text() . '</label></input><br />' . "\n"; + } + + if ( $wgUser->isAllowed( 'delete' ) || $smwgExportBacklinks ) { + $html .= '<input type="checkbox" name="backlinks" value="1" default="true" id="bl"> <label for="bl">' . wfMessage( 'smw_exportrdf_backlinks' )->text() . '</label></input><br />' . "\n"; + } + + if ( $wgUser->isAllowed( 'delete' ) || $smwgExportAll ) { + $html .= '<br />'; + $html .= '<input type="text" name="date" value="' . date( DATE_W3C, mktime( 0, 0, 0, 1, 1, 2000 ) ) . '" id="date"> <label for="ea">' . wfMessage( 'smw_exportrdf_lastdate' )->text() . '</label></input><br />' . "\n"; + } + + $html .= '<br /><input type="submit" value="' . wfMessage( 'smw_exportrdf_submit' )->text() . "\"/>\n</form>"; + + $wgOut->addHTML( $html ); + } + + /** + * Prepare $wgOut for printing non-HTML data. + */ + protected function startRDFExport() { + global $wgOut, $wgRequest; + + $syntax = $wgRequest->getText( 'syntax' ); + + if ( $syntax === '' ) { + $syntax = $wgRequest->getVal( 'syntax' ); + } + + $wgOut->disable(); + ob_start(); + + if ( $syntax == 'turtle' ) { + $mimetype = 'application/x-turtle'; // may change to 'text/turtle' at some time, watch Turtle development + $serializer = new SMWTurtleSerializer(); + } else { // rdfxml as default + // Only use rdf+xml mimetype if explicitly requested (browsers do + // not support it by default). + // We do not add this parameter to RDF links within the export + // though; it is only meant to help some tools to see that HTML + // included resources are RDF (from there on they should be fine). + $mimetype = ( $wgRequest->getVal( 'xmlmime' ) == 'rdf' ) ? 'application/rdf+xml' : 'application/xml'; + $serializer = new SMWRDFXMLSerializer(); + } + + header( "Content-type: $mimetype; charset=UTF-8" ); + + $this->export_controller = new SMWExportController( $serializer ); + } + + /** + * Export the given pages to RDF. + * @param array $pages containing the string names of pages to be exported + */ + protected function exportPages( $pages ) { + global $wgRequest, $smwgExportBacklinks, $wgUser, $smwgAllowRecursiveExport; + + // Effect: assume "no" from missing parameters generated by checkboxes. + $postform = $wgRequest->getText( 'postform' ) == 1; + + $recursive = 0; // default, no recursion + $rec = $wgRequest->getText( 'recursive' ); + + if ( $rec === '' ) { + $rec = $wgRequest->getVal( 'recursive' ); + } + + if ( ( $rec == '1' ) && ( $smwgAllowRecursiveExport || $wgUser->isAllowed( 'delete' ) ) ) { + $recursive = 1; // users may be allowed to switch it on + } + + $backlinks = $smwgExportBacklinks; // default + $bl = $wgRequest->getText( 'backlinks' ); + + if ( $bl === '' ) { + // TODO: wtf? this does not make a lot of sense... + $bl = $wgRequest->getVal( 'backlinks' ); + } + + if ( ( $bl == '1' ) && ( $wgUser->isAllowed( 'delete' ) ) ) { + $backlinks = true; // admins can always switch on backlinks + } elseif ( ( $bl == '0' ) || ( '' == $bl && $postform ) ) { + $backlinks = false; // everybody can explicitly switch off backlinks + } + + $date = $wgRequest->getText( 'date' ); + if ( $date === '' ) { + $date = $wgRequest->getVal( 'date' ); + } + + if ( $date !== '' ) { + $timeint = strtotime( $date ); + $stamp = date( "YmdHis", $timeint ); + $date = $stamp; + } + + // If it is a redirect then we don't want to generate triples other than + // the redirect target information + if ( isset( $pages[0] ) && ( $title = Title::newFromText( $pages[0] ) ) !== null && $title->isRedirect() ) { + $backlinks = false; + } + + $this->startRDFExport(); + $this->export_controller->enableBacklinks( $backlinks ); + $this->export_controller->printPages( $pages, $recursive, $date ); + } + + protected function getGroupName() { + return 'smw_group'; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/specials/SMW_SpecialTypes.php b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SMW_SpecialTypes.php new file mode 100644 index 00000000..1fe7ad8e --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SMW_SpecialTypes.php @@ -0,0 +1,387 @@ +<?php + +use SMW\ApplicationFactory; +use SMW\DataTypeRegistry; +use SMW\DataValueFactory; +use SMW\Utils\HtmlTabs; +use SMW\Page\ListPager; +use SMW\Utils\HtmlColumns; +use SMWDataItem as DataItem; +use SMW\MediaWiki\Collator; +use SMW\TypesRegistry; +use SMW\DIProperty; +use SMW\DIWikiPage; +use SMW\RequestOptions; +use SMWInfolink as Infolink; +use SMW\DataValues\TypesValue; +use SMWErrorValue as ErrorValue; +use SMW\Page\ListBuilder; + +/** + * This special page for MediaWiki provides information about available types + * and those related properties. + * + * @license GNU GPL v2+ + * @since 3.0 + * + * @author mwjames + * @author Markus Krötzsch + */ +class SMWSpecialTypes extends SpecialPage { + + /** + * @see SpecialPage::execute + */ + public function __construct() { + parent::__construct( 'Types' ); + } + + /** + * @see SpecialPage::execute + */ + public function execute( $param ) { + $this->setHeaders(); + $out = $this->getOutput(); + + $out->addModuleStyles( 'ext.smw.page.styles' ); + $out->addModules( 'smw.property.page' ); + + $params = Infolink::decodeParameters( $param, false ); + $typeLabel = reset( $params ); + + if ( $typeLabel == false ) { + $out->setPageTitle( wfMessage( 'types' )->text() ); + $html = $this->getTypesList(); + } else { + $typeLabel = str_replace( '%', '-', $typeLabel ); + $typeName = str_replace( '_', ' ', $typeLabel ); + $out->setPageTitle( wfMessage( 'smw-types-title', $typeName )->text() ); + $out->prependHTML( $this->getBreadcrumbLink() ); + $html = $this->getPropertiesByType( $typeLabel ); + } + + $out->addHTML( $html ); + } + + /** + * @since 3.0 + * + * @param DataValue $dataValue + * @param Link $linker + * + * @return string + */ + public function formatItem( $dataValue, $linker ) { + + // Outdated property? Predefined property definition no longer exists? + if ( $dataValue->getDataItem()->getInterwiki() === SMW_SQL3_SMWIW_OUTDATED ) { + $dataItem = $dataValue->getDataItem(); + + $dataValue = DataValueFactory::getInstance()->newDataValueByItem( + new DIWikiPage( $dataItem->getDBKey(), SMW_NS_PROPERTY ), + null + ); + + $dataValue->setOption( $dataValue::NO_TEXT_TRANSFORMATION, true ); + $dataValue->setOption( $dataValue::SHORT_FORM, true ); + + return $dataValue->getWikiValue(); + } + + $searchlink = Infolink::newBrowsingLink( + '+', $dataValue->getWikiValue() + ); + + return $dataValue->getLongHTMLText( $linker ) . ' ' . $searchlink->getHTML( $linker ); + } + + /** + * @see SpecialPage::getGroupName + */ + protected function getGroupName() { + return 'pages'; + } + + private function getTypesList() { + + $this->addHelpLink( wfMessage( 'smw-specials-types-helplink' )->escaped(), true ); + + $typeLabels = DataTypeRegistry::getInstance()->getKnownTypeLabels(); + $linker = smwfGetLinker(); + asort( $typeLabels, SORT_STRING ); + + $primitive = TypesRegistry::getTypesByGroup( 'primitive' ); + $compound = TypesRegistry::getTypesByGroup( 'compound' ); + + $pr_text = wfMessage( 'smw-type-primitive' )->text(); + $cx_text = wfMessage( 'smw-type-contextual' )->text(); + $cp_text = wfMessage( 'smw-type-compound' )->text(); + + $contents = [ + $pr_text => [], + $cx_text => [], + $cp_text => [] + ]; + + foreach ( $typeLabels as $typeId => $label ) { + $typeValue = TypesValue::newFromTypeId( $typeId ); + $msgKey = 'smw-type' . str_replace( '_', '-', strtolower( $typeId ) ); + + $text = $typeValue->getLongHTMLText( $linker ); + + if ( wfMessage( $msgKey )->exists() ) { + $msg = wfMessage( $msgKey, '' )->parse(); + $text .= Html::rawElement( + 'span' , + [ + 'class' => 'plainlinks', + 'style' => 'font-size:85%' + ], + + // Remove the first two chars which are a localized + // diacritical, quotation mark + str_replace( mb_substr( $msg, 0, 2 ), '', $msg ) + ); + } + + if ( isset( $primitive[$typeId] ) ) { + $contents[$pr_text][] = $typeValue->getLongHTMLText( $linker ); + } elseif ( isset( $compound[$typeId] ) ) { + $contents[$cp_text][] = $text; + } else { + $contents[$cx_text][] = $text; + } + } + + $htmlColumns = new HtmlColumns(); + $htmlColumns->setColumnClass( 'smw-column-responsive' ); + + $htmlColumns->setContinueAbbrev( + wfMessage( 'listingcontinuesabbrev' )->text() + ); + + $htmlColumns->setColumns( 2 ); + $htmlColumns->addContents( $contents, HtmlColumns::INDX_CONTENT ); + $html = $htmlColumns->getHtml(); + + $htmlTabs = new HtmlTabs(); + $htmlTabs->setGroup( 'types' ); + + $htmlTabs->tab( 'smw-type-list', $this->msg( 'smw-type-tab-types' ) ); + $htmlTabs->content( 'smw-type-list', "<div class='smw-page-navigation'>$html</div>" ); + + $html = $htmlTabs->buildHTML( + [ + 'id' => 'smw-types', + 'class' => 'smw-types' + ] + ); + + return Html::rawElement( + 'p', + [ + 'class' => 'plainlinks smw-types-intro' + ], + wfMessage( 'smw_types_docu' )->parse() + ) . $html; + } + + private function getPropertiesByType( $typeLabel ) { + + $typeValue = DataValueFactory::getInstance()->newTypeIDValue( + TypesValue::TYPE_ID, + $typeLabel + ); + + if ( !$typeValue->isValid() ) { + return Html::rawElement( + 'div', + [ + 'class' => 'plainlinks smw-type-unknown' + ], + $this->msg( 'smw-special-types-no-such-type', $typeLabel )->escaped() + ); + } + + $this->addHelpLink( wfMessage( 'smw-specials-bytype-helplink', $typeLabel )->escaped(), true ); + $applicationFactory = ApplicationFactory::getInstance(); + + $pagingLimit = $applicationFactory->getSettings()->dotGet( 'smwgPagingLimit.type' ); + + // not too useful, but we comply to this request + if ( $pagingLimit <= 0 ) { + return ''; + } + + $limit = $this->getRequest()->getVal( 'limit', $pagingLimit ); + $offset = $this->getRequest()->getVal( 'offset', 0 ); + + $requestOptions = new RequestOptions(); + $requestOptions->sort = true; + $requestOptions->setLimit( $limit + 1 ); + $requestOptions->setOffset( $offset ); + + $dataItems = $applicationFactory->getStore()->getPropertySubjects( + new DIProperty( '_TYPE' ), + $typeValue->getDataItem(), + $requestOptions + ); + + if ( $dataItems instanceof \Iterator ) { + $dataItems = iterator_to_array( $dataItems ); + } + + if ( !$requestOptions->ascending ) { + $dataItems = array_reverse( $dataItems ); + } + + $typeId = $typeValue->getDataItem()->getFragment(); + + $dataValue = DataValueFactory::getInstance()->newTypeIDValue( + $typeId + ); + + $label = htmlspecialchars( $typeValue->getWikiValue() ); + $typeKey = 'smw-type' . str_replace( '_', '-', strtolower( $typeId ) ); + $msgKey = wfMessage( $typeKey )->exists() ? $typeKey : 'smw-types-default'; + + $result = \Html::rawElement( + 'div', + [ + 'class' => 'plainlinks smw-types-intro '. $typeKey + ], + wfMessage( $msgKey, str_replace( '_', ' ', $label ) )->parse() . + $this->find_extras( $dataValue, $typeId, $label ) + ); + + $count = count( $dataItems ); + + if ( $count == 0 ) { + return $result; + } + + $html = Html::rawElement( + 'div' , + [ + 'class' => 'smw-page-navigation' + ], + ListPager::pagination( + $this->getTitleFor( 'Types', $typeLabel ), + $limit, + $offset, + $count, + [ '_target' => '#smw-list' ] + ) . Html::rawElement( + 'div' , + [ + 'class' => 'smw-page-nav-note' + ], + wfMessage( 'smw_typearticlecount' )->numParams( min( $limit, $count ) )->text() + ) + ); + + $listBuilder = new ListBuilder( + ApplicationFactory::getInstance()->getStore() + ); + + $listBuilder->setItemFormatter( [ $this, 'formatItem' ] ); + + $html .= $listBuilder->getColumnList( + $dataItems + ); + + $errors = $this->find_errors( $dataValue, $typeId, $label ); + + $htmlTabs = new HtmlTabs(); + $htmlTabs->setGroup( 'type' ); + + $htmlTabs->setActiveTab( + $errors !== null ? 'smw-type-errors' : 'smw-type-list' + ); + + $htmlTabs->tab( 'smw-type-list', $this->msg( 'smw-type-tab-properties' ) ); + $htmlTabs->content( 'smw-type-list', "<div>$html</div>" ); + + $htmlTabs->tab( + 'smw-type-errors', + $this->msg( 'smw-type-tab-errors' ), + [ + 'hide' => $errors === null, + 'class' => 'smw-tab-warning' + ] + ); + + $htmlTabs->content( 'smw-type-errors', "<div>$errors</div>" ); + + return $result . $htmlTabs->buildHTML( + [ + 'id' => 'smw-list', + 'class' => 'smw-types' + ] + ); + } + + private function find_errors( $dataValue, $typeId, $label ) { + + $errors = []; + + if ( $typeId === '_geo' && $dataValue instanceof ErrorValue ) { + $errors[] = Html::rawElement( + 'li', + [], + wfMessage( 'smw-types-extra-geo-not-available', $label )->parse() + ); + } + + if ( $errors !== [] ) { + return Html::rawElement( + 'ul', + [ + 'class' => 'smw-page-content plainlinks' + ], + implode( '', $errors) + ); + } + } + + private function find_extras( $dataValue, $typeId, $label ) { + + $html = ''; + + if ( $typeId === '_mlt_rec' ) { + $option = $dataValue->hasFeature( SMW_DV_MLTV_LCODE ) ? 1 : 2; + $html = ' ' . wfMessage( 'smw-types-extra-mlt-lcode', $label, $option )->parse(); + } + + $extra = 'smw-type-extra' . str_replace( '_', '-', $typeId ); + + if ( wfMessage( $extra )->exists() ) { + $html = ' ' . wfMessage( $extra )->parse(); + } + + return $html; + } + + private function getBreadcrumbLink() { + return Html::rawElement( + 'div', + [ + 'class' => 'smw-breadcrumb-link' + ], + Html::rawElement( + 'span', + [ + 'class' => 'smw-breadcrumb-arrow-right' + ] + ) . + Html::rawElement( + 'a', + [ + 'href' => SpecialPage::getTitleFor( 'Types')->getFullURL() + ], + $this->msg( 'types' )->escaped() + ) + ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialConcepts.php b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialConcepts.php new file mode 100644 index 00000000..9e5bea44 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialConcepts.php @@ -0,0 +1,177 @@ +<?php + +namespace SMW; + +use Html; +use SMW\Page\ListPager; +use SMW\Page\ListBuilder; +use SMW\SQLStore\SQLStore; +use SMW\Utils\HtmlTabs; +use SMW\ApplicationFactory; +use SMW\MediaWiki\Collator; + +/** + * Special page that lists available concepts + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ +class SpecialConcepts extends \SpecialPage { + + /** + * @var Store + */ + private $store; + + /** + * @see SpecialPage::__construct + */ + public function __construct() { + parent::__construct( 'Concepts' ); + } + + /** + * @see SpecialPage::execute + */ + public function execute( $param ) { + + $this->setHeaders(); + $out = $this->getOutput(); + $out->addModuleStyles( 'ext.smw.page.styles' ); + + $limit = $this->getRequest()->getVal( 'limit', 50 ); + $offset = $this->getRequest()->getVal( 'offset', 0 ); + + $this->store = ApplicationFactory::getInstance()->getStore(); + + $diWikiPages = $this->fetchFromTable( $limit, $offset ); + $html = $this->getHtml( $diWikiPages, $limit, $offset ); + + $this->addHelpLink( wfMessage( 'smw-helplink-concepts' )->escaped(), true ); + + $out->setPageTitle( $this->msg( 'concepts' )->text() ); + $out->addHTML( $html ); + } + + /** + * @since 1.9 + * + * @param integer $limit + * @param integer $offset + * + * @return DIWikiPage[] + */ + public function fetchFromTable( $limit, $offset ) { + + $connection = $this->store->getConnection( 'mw.db' ); + $results = []; + + $fields = [ + 'smw_id', + 'smw_title' + ]; + + $conditions = [ + 'smw_namespace' => SMW_NS_CONCEPT, + 'smw_iw' => '', + 'smw_subobject' => '', + 'smw_proptable_hash IS NOT NULL', + 'concept_features > 0' + ]; + + $options = [ + 'LIMIT' => $limit + 1, + 'OFFSET' => $offset, + ]; + + $res = $connection->select( + [ + $connection->tableName( SQLStore::ID_TABLE ), + $connection->tableName( SQLStore::CONCEPT_TABLE ) + ], + $fields, + $conditions, + __METHOD__, + $options, + [ + $connection->tableName( SQLStore::ID_TABLE ) => [ 'INNER JOIN', [ 'smw_id=s_id' ] ] + ] + ); + + foreach ( $res as $row ) { + $results[] = new DIWikiPage( $row->smw_title, SMW_NS_CONCEPT ); + } + + return $results; + } + + /** + * @since 1.9 + * + * @param DIWikiPage[] $dataItems + * @param integer $limit + * @param integer $offset + * + * @return string + */ + public function getHtml( $dataItems, $limit, $offset ) { + + if ( $this->store === null ) { + $this->store = ApplicationFactory::getInstance()->getStore(); + } + + $count = count( $dataItems ); + $resultNumber = min( $limit, $count ); + + if ( $resultNumber == 0 ) { + $key = 'smw-special-concept-empty'; + } else { + $key = 'smw-special-concept-count'; + } + + $listBuilder = new ListBuilder( + $this->store, + Collator::singleton() + ); + + $htmlTabs = new HtmlTabs(); + $htmlTabs->setGroup( 'concept' ); + + $html = Html::rawElement( + 'div', + [ 'id' => 'mw-pages'], + Html::rawElement( + 'div', + [ 'class' => 'smw-page-navigation' ], + ListPager::pagination( $this->getPageTitle(), $limit, $offset, $count ) + ) . Html::element( + 'div', + [ 'class' => $key, 'style' => 'margin-top:10px;margin-bottom:10px;' ], + $this->msg( $key, $resultNumber )->parse() + ) . $listBuilder->getColumnList( $dataItems ) + ); + + $htmlTabs->tab( 'smw-concept-list', $this->msg( 'smw-concept-tab-list' ) ); + $htmlTabs->content( 'smw-concept-list', $html ); + + $html = $htmlTabs->buildHTML( + [ 'class' => 'smw-concept clearfix' ] + ); + + return Html::rawElement( + 'p', + [ 'class' => 'smw-special-concept-docu plainlinks' ], + $this->msg( 'smw-special-concept-docu' )->parse() + ) . $html; + } + + /** + * @see SpecialPage::getGroupName + */ + protected function getGroupName() { + return 'pages'; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialPage.php b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialPage.php new file mode 100644 index 00000000..5bc84de0 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialPage.php @@ -0,0 +1,93 @@ +<?php + +namespace SMW; + +/** + * Semantic MediaWiki SpecialPage base class + * + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author mwjames + */ + +/** + * Semantic MediaWiki SpecialPage base class + * + * @ingroup SpecialPage + * @codeCoverageIgnore + */ +class SpecialPage extends \SpecialPage { + + /** @var Store */ + protected $store = null; + + /** @var Settings */ + protected $settings = null; + + /** + * @see SpecialPage::__construct + * + * @since 1.9 + * + * @param $name + * @param $restriction + */ + public function __construct( $name = '', $restriction = '' ) { + parent::__construct( $name, $restriction ); + $this->store = StoreFactory::getStore(); + } + + /** + * Sets store instance + * + * @since 1.9 + * + * @param Store $store + */ + public function setStore( Store $store ) { + $this->store = $store; + return $this; + } + + /** + * Returns store object + * + * @since 1.9 + * + * @return Store + */ + public function getStore() { + return $this->store; + } + + /** + * Sets Settings object + * + * @since 1.9 + * + * @param Settings $settings + */ + public function setSettings( Settings $settings ) { + $this->settings = $settings; + return $this; + } + + /** + * Returns Settings object + * + * @since 1.9 + * + * @return Store + */ + public function getSettings() { + + if ( $this->settings === null ) { + $this->settings = Settings::newFromGlobals(); + } + + return $this->settings; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialProperties.php b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialProperties.php new file mode 100644 index 00000000..8a4ab4c6 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialProperties.php @@ -0,0 +1,62 @@ +<?php + +namespace SMW; + +use SMWOutputs; + +/** + * Special page (Special:Properties) for MediaWiki shows all + * used properties + * + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + * @author mwjames + */ + +/** + * This special page for MediaWiki shows all used properties. + * + * @ingroup SpecialPage + */ +class SpecialProperties extends SpecialPage { + + /** + * @see SpecialPage::__construct + * @codeCoverageIgnore + */ + public function __construct() { + parent::__construct( 'Properties' ); + } + + /** + * @see SpecialPage::execute + */ + public function execute( $param ) { + $this->setHeaders(); + $out = $this->getOutput(); + + $out->setPageTitle( $this->msg( 'properties' )->text() ); + + $page = new PropertiesQueryPage( $this->getStore(), $this->getSettings() ); + $page->setContext( $this->getContext() ); + + list( $limit, $offset ) = $this->getLimitOffset(); + $page->doQuery( $offset, $limit, $this->getRequest()->getVal( 'property' ) ); + + // Ensure locally collected output data is pushed to the output! + SMWOutputs::commitToOutputPage( $out ); + + } + + private function getLimitOffset() { + return $this->getRequest()->getLimitOffset(); + } + + protected function getGroupName() { + return 'pages'; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialUnusedProperties.php b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialUnusedProperties.php new file mode 100644 index 00000000..23987442 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialUnusedProperties.php @@ -0,0 +1,63 @@ +<?php + +namespace SMW; + +use SMWOutputs; + +/** + * Special page (Special:UnusedProperties) for MediaWiki shows all + * unused properties + * + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + * @author mwjames + */ + +/** + * This special page (Special:UnusedProperties) for MediaWiki shows all unused + * properties. + * + * @ingroup SpecialPage + */ +class SpecialUnusedProperties extends SpecialPage { + + /** + * @see SpecialPage::__construct + * @codeCoverageIgnore + */ + public function __construct() { + parent::__construct( 'UnusedProperties' ); + } + + /** + * @see SpecialPage::execute + */ + public function execute( $param ) { + $this->setHeaders(); + + $out = $this->getOutput(); + + $out->setPageTitle( $this->msg( 'unusedproperties' )->text() ); + + $page = new UnusedPropertiesQueryPage( $this->getStore(), $this->getSettings() ); + $page->setContext( $this->getContext() ); + + list( $limit, $offset ) = $this->getLimitOffset(); + $page->doQuery( $offset, $limit, $this->getRequest()->getVal( 'property' ) ); + + // Ensure locally collected output data is pushed to the output! + SMWOutputs::commitToOutputPage( $out ); + } + + private function getLimitOffset() { + return $this->getRequest()->getLimitOffset(); + } + + protected function getGroupName() { + return 'maintenance'; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialWantedProperties.php b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialWantedProperties.php new file mode 100644 index 00000000..79a9f681 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/specials/SpecialWantedProperties.php @@ -0,0 +1,69 @@ +<?php + +namespace SMW; + +use SMWOutputs; + +/** + * Special page (Special:WantedProperties) for MediaWiki shows all + * wanted properties + * + * + * @license GNU GPL v2+ + * @since 1.9 + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + * @author mwjames + */ + +/** + * This special page (Special:WantedProperties) for MediaWiki shows all wanted + * properties (used but not having a page). + * + * @ingroup SpecialPage + */ +class SpecialWantedProperties extends SpecialPage { + + /** + * @see SpecialPage::__construct + * @codeCoverageIgnore + */ + public function __construct() { + parent::__construct( 'WantedProperties' ); + } + + /** + * @see SpecialPage::execute + */ + public function execute( $param ) { + $this->setHeaders(); + + $out = $this->getOutput(); + + $out->addModuleStyles( [ + 'ext.smw.special.style' + ] ); + + $out->setPageTitle( $this->msg( 'wantedproperties' )->text() ); + + $page = new WantedPropertiesQueryPage( $this->getStore(), $this->getSettings() ); + $page->setContext( $this->getContext() ); + $page->setTitle( $this->getPageTitle() ); + + list( $limit, $offset ) = $this->getLimitOffset(); + $page->doQuery( $offset, $limit ); + + // Ensure locally collected output data is pushed to the output! + // ?? still needed !! + SMWOutputs::commitToOutputPage( $out ); + } + + private function getLimitOffset() { + return $this->getRequest()->getLimitOffset(); + } + + protected function getGroupName() { + return 'maintenance'; + } +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/storage/SMW_QueryResult.php b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SMW_QueryResult.php new file mode 100644 index 00000000..17f760d7 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SMW_QueryResult.php @@ -0,0 +1,462 @@ +<?php + +use SMW\Query\Excerpts; +use SMW\Query\PrintRequest; +use SMW\Query\QueryLinker; +use SMW\Query\Result\ResolverJournal; +use SMW\Query\ScoreSet; +use SMW\SerializerFactory; + +/** + * Objects of this class encapsulate the result of a query in SMW. They + * provide access to the query result and printed data, and to some + * relevant query parameters that were used. + * + * Standard access is provided through the iterator function getNext(), + * which returns an array ("table row") of SMWResultArray objects ("table cells"). + * It is also possible to access the set of result pages directly using + * getResults(). This is useful for printers that disregard printouts and + * only are interested in the actual list of pages. + * + * + * @ingroup SMWQuery + * + * @licence GNU GPL v2 or later + * @author Markus Krötzsch + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class SMWQueryResult { + + /** + * Array of SMWDIWikiPage objects that are the basis for this result + * @var SMWDIWikiPage[] + */ + protected $mResults; + + /** + * Array of SMWPrintRequest objects, indexed by their natural hash keys + * + * @var PrintRequest[] + */ + protected $mPrintRequests; + + /** + * Are there more results than the ones given? + * @var boolean + */ + protected $mFurtherResults; + + /** + * The query object for which this is a result, must be set on create and is the source of + * data needed to create further result links. + * @var SMWQuery + */ + protected $mQuery; + + /** + * The SMWStore object used to retrieve further data on demand. + * @var SMWStore + */ + protected $mStore; + + /** + * Holds a value that belongs to a count query result + * @var integer|null + */ + private $countValue; + + /** + * Indicates whether results have been retrieved from cache or not + * + * @var boolean + */ + private $isFromCache = false; + + /** + * @var ResolverJournal + */ + private $resolverJournal; + + /** + * @var integer + */ + private $serializer_version = 2; + + /** + * @var ScoreSet + */ + private $scoreSet; + + /** + * @var Excerpts + */ + private $excerpts; + + /** + * Initialise the object with an array of SMWPrintRequest objects, which + * define the structure of the result "table" (one for each column). + * + * TODO: Update documentation + * + * @param PrintRequest[] $printRequests + * @param SMWQuery $query + * @param SMWDIWikiPage[] $results + * @param SMWStore $store + * @param boolean $furtherRes + */ + public function __construct( array $printRequests, SMWQuery $query, array $results, SMWStore $store, $furtherRes = false ) { + $this->mResults = $results; + reset( $this->mResults ); + $this->mPrintRequests = $printRequests; + $this->mFurtherResults = $furtherRes; + $this->mQuery = $query; + $this->mStore = $store; + $this->resolverJournal = new ResolverJournal(); + } + + /** + * @since 3.0 + * + * @param ResolverJournal $resolverJournal + */ + public function setResolverJournal( ResolverJournal $ResolverJournal ) { + $this->resolverJournal = $ResolverJournal; + } + + /** + * @since 2.4 + * + * @return ResolverJournal + */ + public function getResolverJournal() { + return $this->resolverJournal; + } + + /** + * @since 2.4 + * + * @param boolean $isFromCache + */ + public function setFromCache( $isFromCache ) { + $this->isFromCache = (bool)$isFromCache; + } + + /** + * Only available by some stores that support the computation of scores. + * + * @since 3.0 + * + * @param ScoreSet $scoreSet + */ + public function setScoreSet( ScoreSet $scoreSet ) { + $this->scoreSet = $scoreSet; + } + + /** + * @since 3.0 + * + * @return ScoreSet|null + */ + public function getScoreSet() { + return $this->scoreSet; + } + + /** + * Only available by some stores that support the retrieval of excerpts. + * + * @since 3.0 + * + * @param Excerpts $excerpts + */ + public function setExcerpts( Excerpts $excerpts ) { + $this->excerpts = $excerpts; + } + + /** + * @since 3.0 + * + * @return Excerpts|null + */ + public function getExcerpts() { + return $this->excerpts; + } + + /** + * @since 2.4 + * + * @return boolean + */ + public function isFromCache() { + return $this->isFromCache; + } + + /** + * Get the SMWStore object that this result is based on. + * + * @return SMWStore + */ + public function getStore() { + return $this->mStore; + } + + /** + * Return the next result row as an array of SMWResultArray objects, and + * advance the internal pointer. + * + * @return SMWResultArray[]|false + */ + public function getNext() { + $page = current( $this->mResults ); + next( $this->mResults ); + + if ( $page === false ) { + return false; + } + + $row = []; + + foreach ( $this->mPrintRequests as $p ) { + $resultArray = new SMWResultArray( $page, $p, $this->mStore ); + $resultArray->setResolverJournal( $this->resolverJournal ); + $resultArray->setQueryToken( $this->mQuery->getQueryToken() ); + $row[] = $resultArray; + } + + return $row; + } + + /** + * Return number of available results. + * + * @return integer + */ + public function getCount() { + return count( $this->mResults ); + } + + /** + * Return an array of SMWDIWikiPage objects that make up the + * results stored in this object. + * + * @return SMWDIWikiPage[] + */ + public function getResults() { + return $this->mResults; + } + + /** + * @since 2.3 + */ + public function reset() { + return reset( $this->mResults ); + } + + /** + * Returns the query object of the current result set + * + * @since 1.8 + * + * @return SMWQuery + */ + public function getQuery() { + return $this->mQuery; + } + + /** + * Return the number of columns of result values that each row + * in this result set contains. + * + * @return integer + */ + public function getColumnCount() { + return count( $this->mPrintRequests ); + } + + /** + * Return array of print requests (needed for printout since they contain + * property labels). + * + * @return PrintRequest[] + */ + public function getPrintRequests() { + return $this->mPrintRequests; + } + + /** + * Returns the query string defining the conditions for the entities to be + * returned. + * + * @return string + */ + public function getQueryString() { + return $this->mQuery->getQueryString(); + } + + /** + * Would there be more query results that were not shown due to a limit? + * + * @return boolean + */ + public function hasFurtherResults() { + return $this->mFurtherResults; + } + + /** + * @since 2.0 + * + * @param integer $countValue + */ + public function setCountValue( $countValue ) { + $this->countValue = (int)$countValue; + } + + /** + * @since 2.0 + * + * @return integer|null + */ + public function getCountValue() { + return $this->countValue; + } + + /** + * Return error array, possibly empty. + * + * @return array + */ + public function getErrors() { + // Just use query errors, as no errors generated in this class at the moment. + return $this->mQuery->getErrors(); + } + + /** + * Adds an array of erros. + * + * @param array $errors + */ + public function addErrors( array $errors ) { + $this->mQuery->addErrors( $errors ); + } + + /** + * Create an SMWInfolink object representing a link to further query results. + * This link can then be serialised or extended by further params first. + * The optional $caption can be used to set the caption of the link (though this + * can also be changed afterwards with SMWInfolink::setCaption()). If empty, the + * message 'smw_iq_moreresults' is used as a caption. + * + * @param string|false $caption + * + * @return SMWInfolink + */ + public function getQueryLink( $caption = false ) { + + $link = QueryLinker::get( $this->mQuery ); + + $link->setCaption( $caption ); + $link->setParameter( $this->mQuery->getOffset() + count( $this->mResults ), 'offset' ); + + return $link; + } + + /** + * @deprecated since 2.5, use QueryResult::getQueryLink + * + * Returns an SMWInfolink object with the QueryResults print requests as parameters. + * + * @since 1.8 + * + * @return SMWInfolink + */ + public function getLink() { + return $this->getQueryLink(); + } + + /** + * @private + * + * @since 3.0 + */ + public function setSerializerVersion( $version ) { + $this->serializer_version = $version; + } + + /** + * @see DISerializer::getSerializedQueryResult + * @since 1.7 + * @return array + */ + public function serializeToArray() { + + $serializerFactory = new SerializerFactory(); + $serializer = $serializerFactory->newQueryResultSerializer(); + $serializer->version( $this->serializer_version ); + + $serialized = $serializer->serialize( $this ); + reset( $this->mResults ); + + return $serialized; + } + + /** + * Returns a serialized SMWQueryResult object with additional meta data + * + * This methods extends the serializeToArray() for additional meta + * that are useful when handling data via the api + * + * @note should be used instead of SMWQueryResult::serializeToArray() + * as this method contains additional informaion + * + * @since 1.9 + * + * @return array + */ + public function toArray() { + + $time = microtime( true ); + + // @note micro optimization: We call getSerializedQueryResult() + // only once and create the hash here instead of calling getHash() + // to avoid getSerializedQueryResult() being called again + // @note count + offset equals total therefore we deploy both values + $serializeArray = $this->serializeToArray(); + + return array_merge( $serializeArray, [ + 'meta'=> [ + 'hash' => md5( json_encode( $serializeArray ) ), + 'count' => $this->getCount(), + 'offset' => $this->mQuery->getOffset(), + 'source' => $this->mQuery->getQuerySource(), + 'time' => number_format( ( microtime( true ) - $time ), 6, '.', '' ) + ] + ] + ); + } + + /** + * Returns result hash value + * + * @since 1.9 + * + * @return string + */ + public function getHash( $type = null ) { + + // Just iterate over available subjects to create a "quick" hash given + // that resolving the entire object tree is costly due to recursive + // processing of all data items including its printouts + if ( $type === 'quick' ) { + $hash = []; + + foreach ( $this->mResults as $dataItem ) { + $hash[] = $dataItem->getHash(); + } + + reset( $this->mResults ); + return 'q:' . md5( json_encode( $hash ) ); + } + + return md5( json_encode( $this->serializeToArray() ) ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/storage/SMW_ResultArray.php b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SMW_ResultArray.php new file mode 100644 index 00000000..438edf5a --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SMW_ResultArray.php @@ -0,0 +1,282 @@ +<?php + +use SMW\DataValueFactory; +use SMW\Query\PrintRequest; +use SMW\Query\QueryToken; +use SMW\Query\Result\ResolverJournal; +use SMW\Query\Result\ResultFieldMatchFinder; +use SMWDataItem as DataItem; + +/** + * Container for the contents of a single result field of a query result, + * i.e. basically an array of SMWDataItems with some additional parameters. + * The content of the array is fetched on demand only. + * + * @ingroup SMWQuery + * + * @author Markus Krötzsch + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class SMWResultArray { + + /** + * @var PrintRequest + */ + private $mPrintRequest; + + /** + * @var SMWDIWikiPage + */ + private $mResult; + + /** + * @var SMWStore + */ + private $mStore; + + /** + * @var SMWDataItem[]|false + */ + private $mContent; + + /** + * @var ResolverJournal + */ + private $resolverJournal; + + /** + * @var ResultFieldMatchFinder + */ + private $resultFieldMatchFinder; + + /** + * @var QueryToken + */ + private $queryToken; + + /** + * Constructor. + * + * @param SMWDIWikiPage $resultPage + * @param PrintRequest $printRequest + * @param SMWStore $store + */ + public function __construct( SMWDIWikiPage $resultPage, PrintRequest $printRequest, SMWStore $store ) { + $this->mResult = $resultPage; + $this->mPrintRequest = $printRequest; + $this->mStore = $store; + $this->mContent = false; + + // FIXME 3.0; Inject the object + $this->resultFieldMatchFinder = new ResultFieldMatchFinder( $store, $printRequest ); + } + + /** + * Get the SMWStore object that this result is based on. + * + * @return SMWStore + */ + public function getStore() { + return $this->mStore; + } + + /** + * Returns the SMWDIWikiPage object to which this SMWResultArray refers. + * If you only care for those objects, consider using SMWQueryResult::getResults() + * directly. + * + * @return SMWDIWikiPage + */ + public function getResultSubject() { + return $this->mResult; + } + + /** + * Temporary track what entities are used while being instantiated, so an external + * service can have access to the list without requiring to resolve the objects + * independently. + * + * @since 2.4 + * + * @param ResolverJournal $resolverJournal + */ + public function setResolverJournal( ResolverJournal $resolverJournal ) { + $this->resolverJournal = $resolverJournal; + } + + /** + * @since 2.5 + * + * @param QueryToken|null $queryToken + */ + public function setQueryToken( QueryToken $queryToken = null ) { + $this->queryToken = $queryToken; + } + + /** + * Returns an array of SMWDataItem objects that contain the results of + * the given print request for the given result object. + * + * @return SMWDataItem[]|false + */ + public function getContent() { + $this->loadContent(); + return $this->mContent; + } + + /** + * Return a PrintRequest object describing what is contained in this + * result set. + * + * @return PrintRequest + */ + public function getPrintRequest() { + return $this->mPrintRequest; + } + + /** + * Return the next SMWDataItem object or false if no further object exists. + * + * @since 1.6 + * + * @return SMWDataItem|false + */ + public function getNextDataItem() { + $this->loadContent(); + $result = current( $this->mContent ); + + if ( $this->resolverJournal !== null && $result instanceof DataItem ) { + $this->resolverJournal->recordItem( $result ); + } + + next( $this->mContent ); + return $result; + } + + /** + * Set the internal pointer of the array of SMWDataItem objects to its first + * element. Return the first SMWDataItem object or false if the array is + * empty. + * + * @since 1.7.1 + * + * @return SMWDataItem|false + */ + public function reset() { + $this->loadContent(); + return reset( $this->mContent ); + } + + /** + * Return an SMWDataValue object for the next SMWDataItem object or + * false if no further object exists. + * + * @since 1.6 + * + * @return SMWDataValue|false + */ + public function getNextDataValue() { + $dataItem = $this->getNextDataItem(); + + if ( $dataItem === false ) { + return false; + } + + if ( $this->mPrintRequest->getMode() == PrintRequest::PRINT_PROP && + strpos( $this->mPrintRequest->getTypeID(), '_rec' ) !== false && + $this->mPrintRequest->getParameter( 'index' ) !== false ) { + + $recordValue = DataValueFactory::getInstance()->newDataValueByItem( + $dataItem, + $this->mPrintRequest->getData()->getDataItem() + ); + + $diProperty = $recordValue->getPropertyDataItemByIndex( + $this->mPrintRequest->getParameter( 'index' ) + ); + } elseif ( $this->mPrintRequest->isMode( PrintRequest::PRINT_PROP ) ) { + $diProperty = $this->mPrintRequest->getData()->getDataItem(); + } elseif ( $this->mPrintRequest->isMode( PrintRequest::PRINT_CHAIN ) ) { + $diProperty = $this->mPrintRequest->getData()->getLastPropertyChainValue()->getDataItem(); + } else { + $diProperty = null; + } + + $dataValue = DataValueFactory::getInstance()->newDataValueByItem( + $dataItem, + $diProperty + ); + + $dataValue->setContextPage( + $this->mResult + ); + + if ( $this->mPrintRequest->getOutputFormat() ) { + $dataValue->setOutputFormat( $this->mPrintRequest->getOutputFormat() ); + } + + if ( $this->resolverJournal !== null && $dataItem instanceof DataItem ) { + $this->resolverJournal->recordItem( $dataItem ); + $this->resolverJournal->recordProperty( $diProperty ); + } + + return $dataValue; + } + + /** + * Return the main text representation of the next SMWDataItem object + * in the specified format, or false if no further object exists. + * + * The parameter $linker controls linking of title values and should + * be some Linker object (or NULL for no linking). + * + * @param integer $outputMode + * @param mixed $linker + * + * @return string|false + */ + public function getNextText( $outputMode, $linker = null ) { + $dataValue = $this->getNextDataValue(); + if ( $dataValue !== false ) { // Print data values. + return $dataValue->getShortText( $outputMode, $linker ); + } else { + return false; + } + } + + /** + * Load results of the given print request and result subject. This is only + * done when needed. + */ + protected function loadContent() { + + if ( $this->mContent !== false ) { + return; + } + + $this->resultFieldMatchFinder->setQueryToken( + $this->queryToken + ); + + $this->mContent = $this->resultFieldMatchFinder->findAndMatch( + $this->mResult + ); + + return reset( $this->mContent ); + } + + /** + * Make a request option object based on the given parameters, and + * return NULL if no such object is required. The parameter defines + * if the limit should be taken into account, which is not always desired + * (especially if results are to be cached for future use). + * + * @param boolean $useLimit + * + * @return SMWRequestOptions|null + */ + protected function getRequestOptions( $useLimit = true ) { + return $this->resultFieldMatchFinder->getRequestOptions( $useLimit ); + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3.php b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3.php new file mode 100644 index 00000000..cfc21271 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3.php @@ -0,0 +1,653 @@ +<?php + +use SMW\DIConcept; +use SMW\DIProperty; +use SMW\DIWikiPage; +use SMW\SemanticData; +use SMW\SQLStore\PropertyTableInfoFetcher; +use SMW\SQLStore\RequestOptionsProc; +use SMW\SQLStore\SQLStoreFactory; +use SMW\SQLStore\TableBuilder\FieldType; +use SMW\SQLStore\TableDefinition; + +/** + * SQL-based implementation of SMW's storage abstraction layer. + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + * @author Nischay Nahata + * + * @since 1.8 + * + * @ingroup SMWStore + */ + +// The use of the following constants is explained in SMWSQLStore3::setup(): +define( 'SMW_SQL3_SMWIW_OUTDATED', ':smw' ); // virtual "interwiki prefix" for old-style special SMW objects (no longer used) +define( 'SMW_SQL3_SMWREDIIW', ':smw-redi' ); // virtual "interwiki prefix" for SMW objects that are redirected +define( 'SMW_SQL3_SMWBORDERIW', ':smw-border' ); // virtual "interwiki prefix" separating very important pre-defined properties from the rest +define( 'SMW_SQL3_SMWINTDEFIW', ':smw-intprop' ); // virtual "interwiki prefix" marking internal (invisible) predefined properties +define( 'SMW_SQL3_SMWDELETEIW', ':smw-delete' ); // virtual "interwiki prefix" marking a deleted subject, see #1100 + +/** + * Storage access class for using the standard MediaWiki SQL database for + * keeping semantic data. + * + * @note Regarding the use of interwiki links in the store, there is currently + * no support for storing semantic data about interwiki objects, and hence + * queries that involve interwiki objects really make sense only for them + * occurring in object positions. Most methods still use the given input + * interwiki text as a simple way to filter out results that may be found if an + * interwiki object is given but a local object of the same name exists. It is + * currently not planned to support things like interwiki reuse of properties. + * + * @since 1.8 + * @ingroup SMWStore + */ +class SMWSQLStore3 extends SMWStore { + + /** + * Specifies the border limit (upper bound) for pre-defined properties used + * in the ID_TABLE + * + * When changing the upper bound, please make sure to copy the current upper + * bound as legcy to the TableIntegrityExaminer::checkPredefinedPropertyUpperbound + */ + const FIXED_PROPERTY_ID_UPPERBOUND = 500; + + /** + * Name of the table to store the concept cache in. + * + * @note This should never change. If it is changed, the concept caches + * will appear empty until they are recomputed. + */ + const CONCEPT_CACHE_TABLE = 'smw_concept_cache'; + const CONCEPT_TABLE = 'smw_fpt_conc'; + + /** + * Name of the table to store the concept cache in. + * + * @note This should never change, but if it does then its contents can + * simply be rebuilt by running the setup. + */ + const PROPERTY_STATISTICS_TABLE = 'smw_prop_stats'; + + /** + * Name of the table that manages the query dependency links + */ + const QUERY_LINKS_TABLE = 'smw_query_links'; + + /** + * Name of the table that manages the fulltext index + */ + const FT_SEARCH_TABLE = 'smw_ft_search'; + + /** + * Name of the table that manages the Store IDs + */ + const ID_TABLE = 'smw_object_ids'; + + /** + * @var SQLStoreFactory + */ + private $factory; + + /** + * @var PropertyTableInfoFetcher|null + */ + private $propertyTableInfoFetcher = null; + + /** + * @var PropertyTableIdReferenceFinder + */ + private $propertyTableIdReferenceFinder; + + /** + * @var DataItemHandlerDispatcher + */ + private $dataItemHandlerDispatcher; + + /** + * @var EntityLookup + */ + private $entityLookup; + + /** + * @var ServicesContainer + */ + protected $servicesContainer; + + /** + * Object to access the SMW IDs table. + * + * @since 1.8 + * @var SMWSql3SmwIds + */ + public $smwIds; + + /** + * The reader object used by this store. Initialized by getReader() + * Always access using getReader() + * + * @since 1.8 + * @var SMWSQLStore3Readers + */ + protected $reader = false; + + /** + * The writer object used by this store. Initialized by getWriter(), + * which is the only way in which it should be accessed. + * + * @since 1.8 + * @var SMWSQLStore3Writers + */ + protected $writer = false; + + /** + * @since 1.8 + */ + public function __construct() { + $this->factory = new SQLStoreFactory( $this, $this->messageReporter ); + $this->smwIds = $this->factory->newEntityTable(); + } + + /** + * Get an object of the dataitem handler from the dataitem provided. + * + * @since 1.8 + * @param integer $diType + * + * @return SMWDIHandler + * @throws RuntimeException if no handler exists for the given type + */ + public function getDataItemHandlerForDIType( $diType ) { + + if ( $this->dataItemHandlerDispatcher === null ) { + $this->dataItemHandlerDispatcher = $this->factory->newDataItemHandlerDispatcher( $this ); + } + + return $this->dataItemHandlerDispatcher->getHandlerByType( $diType ); + } + +///// Reading methods ///// + + public function getReader() { + if( $this->reader == false ) { + $this->reader = new SMWSQLStore3Readers( $this, $this->factory );//Initialize if not done already + } + + return $this->reader; + } + + public function getSemanticData( DIWikiPage $subject, $filter = false ) { + return $this->getEntityLookup()->getSemanticData( $subject, $filter ); + } + + /** + * @param mixed $subject + * @param DIProperty $property + * @param null $requestOptions + * + * @return SMWDataItem[] + */ + public function getPropertyValues( $subject, DIProperty $property, $requestOptions = null ) { + return $this->getEntityLookup()->getPropertyValues( $subject, $property, $requestOptions ); + } + + public function getProperties( DIWikiPage $subject, $requestOptions = null ) { + return $this->getEntityLookup()->getProperties( $subject, $requestOptions ); + } + + public function getPropertySubjects( DIProperty $property, $dataItem, $requestOptions = null ) { + return $this->getEntityLookup()->getPropertySubjects( $property, $dataItem, $requestOptions ); + } + + public function getAllPropertySubjects( DIProperty $property, $requestoptions = null ) { + return $this->getEntityLookup()->getAllPropertySubjects( $property, $requestoptions ); + } + + public function getInProperties( SMWDataItem $value, $requestoptions = null ) { + return $this->getEntityLookup()->getInProperties( $value, $requestoptions ); + } + + +///// Writing methods ///// + + + public function getWriter() { + if( $this->writer == false ) { + $this->writer = new SMWSQLStore3Writers( $this, $this->factory ); + } + + return $this->writer; + } + + public function deleteSubject( Title $title ) { + + $subject = DIWikiPage::newFromTitle( $title ); + + $this->getEntityLookup()->invalidateCache( + $subject + ); + + $this->getWriter()->deleteSubject( $title ); + + $this->doDeferredCachedListLookupUpdate( + $subject + ); + } + + protected function doDataUpdate( SemanticData $semanticData ) { + + $this->getEntityLookup()->invalidateCache( + $semanticData->getSubject() + ); + + $this->getWriter()->doDataUpdate( $semanticData ); + + $this->doDeferredCachedListLookupUpdate( + $semanticData->getSubject() + ); + } + + public function changeTitle( Title $oldtitle, Title $newtitle, $pageid, $redirid = 0 ) { + + $this->getEntityLookup()->invalidateCache( + DIWikiPage::newFromTitle( $oldtitle ) + ); + + $this->getEntityLookup()->invalidateCache( + DIWikiPage::newFromTitle( $newtitle ) + ); + + $this->getWriter()->changeTitle( $oldtitle, $newtitle, $pageid, $redirid ); + + $this->doDeferredCachedListLookupUpdate( + DIWikiPage::newFromTitle( $oldtitle ) + ); + } + + private function doDeferredCachedListLookupUpdate( DIWikiPage $subject ) { + + if ( $subject->getNamespace() !== SMW_NS_PROPERTY ) { + return null; + } + + $deferredCallableUpdate = $this->factory->newDeferredCallableCachedListLookupUpdate(); + $deferredCallableUpdate->setOrigin( __METHOD__ ); + $deferredCallableUpdate->waitOnTransactionIdle(); + $deferredCallableUpdate->pushUpdate(); + } + +///// Query answering ///// + + /** + * @note Move hooks to the base class in 3.* + * + * @see SMWStore::fetchQueryResult + * + * @since 1.8 + * @param SMWQuery $query + * @return SMWQueryResult|string|integer depends on $query->querymode + */ + public function getQueryResult( SMWQuery $query ) { + + $result = null; + $start = microtime( true ); + + if ( \Hooks::run( 'SMW::Store::BeforeQueryResultLookupComplete', [ $this, $query, &$result, $this->factory->newSlaveQueryEngine() ] ) ) { + $result = $this->fetchQueryResult( $query ); + } + + \Hooks::run( 'SMW::SQLStore::AfterQueryResultLookupComplete', [ $this, &$result ] ); + \Hooks::run( 'SMW::Store::AfterQueryResultLookupComplete', [ $this, &$result ] ); + + $query->setOption( SMWQuery::PROC_QUERY_TIME, microtime( true ) - $start ); + + return $result; + } + + protected function fetchQueryResult( SMWQuery $query ) { + return $this->factory->newSlaveQueryEngine()->getQueryResult( $query ); + } + +///// Special page functions ///// + + /** + * @param RequestOptions|null $requestOptions + * + * @return CachedListLookup + */ + public function getPropertiesSpecial( $requestOptions = null ) { + return $this->factory->newPropertyUsageCachedListLookup( $requestOptions ); + } + + /** + * @param RequestOptions|null $requestOptions + * + * @return CachedListLookup + */ + public function getUnusedPropertiesSpecial( $requestOptions = null ) { + return $this->factory->newUnusedPropertyCachedListLookup( $requestOptions ); + } + + /** + * @param RequestOptions|null $requestOptions + * + * @return CachedListLookup + */ + public function getWantedPropertiesSpecial( $requestOptions = null ) { + return $this->factory->newUndeclaredPropertyCachedListLookup( $requestOptions ); + } + + public function getStatistics() { + return $this->factory->newUsageStatisticsCachedListLookup()->fetchList(); + } + + +///// Setup store ///// + + /** + * @see Store::service + * + * {@inheritDoc} + */ + public function service( $service, ...$args ) { + + if ( $this->servicesContainer === null ) { + $this->servicesContainer = $this->newServicesContainer(); + } + + return $this->servicesContainer->get( $service, ...$args ); + } + + /** + * @since 1.8 + * + * {@inheritDoc} + */ + public function setup( $verbose = true ) { + + $installer = $this->factory->newInstaller(); + $installer->setMessageReporter( $this->messageReporter ); + + return $installer->install( $verbose ); + } + + /** + * @since 1.8 + * + * {@inheritDoc} + */ + public function drop( $verbose = true ) { + + $installer = $this->factory->newInstaller(); + $installer->setMessageReporter( $this->messageReporter ); + + return $installer->uninstall( $verbose ); + } + + public function refreshData( &$id, $count, $namespaces = false, $usejobs = true ) { + + $entityRebuildDispatcher = $this->factory->newEntityRebuildDispatcher(); + + $entityRebuildDispatcher->setDispatchRangeLimit( $count ); + $entityRebuildDispatcher->setRestrictionToNamespaces( $namespaces ); + + $entityRebuildDispatcher->setOptions( + [ + 'use-job' => $usejobs + ] + ); + + return $entityRebuildDispatcher; + } + + +///// Concept caching ///// + + /** + * Refresh the concept cache for the given concept. + * + * @since 1.8 + * @param Title $concept + * @return array of error strings (empty if no errors occurred) + */ + public function refreshConceptCache( Title $concept ) { + return $this->factory->newMasterConceptCache()->refreshConceptCache( $concept ); + } + + /** + * Delete the concept cache for the given concept. + * + * @since 1.8 + * @param Title $concept + */ + public function deleteConceptCache( $concept ) { + $this->factory->newMasterConceptCache()->deleteConceptCache( $concept ); + } + + /** + * Return status of the concept cache for the given concept as an array + * with key 'status' ('empty': not cached, 'full': cached, 'no': not + * cacheable). If status is not 'no', the array also contains keys 'size' + * (query size), 'depth' (query depth), 'features' (query features). If + * status is 'full', the array also contains keys 'date' (timestamp of + * cache), 'count' (number of results in cache). + * + * @since 1.8 + * @param Title|SMWWikiPageValue $concept + * + * @return DIConcept|null + */ + public function getConceptCacheStatus( $concept ) { + return $this->factory->newSlaveConceptCache()->getStatus( $concept ); + } + + +///// Helper methods, mostly protected ///// + + /** + * @see RequestOptionsProc::getSQLOptions + * + * @since 1.8 + * + * @param SMWRequestOptions|null $requestOptions + * @param string $valuecol + * + * @return array + */ + public function getSQLOptions( SMWRequestOptions $requestOptions = null, $valueCol = '' ) { + return RequestOptionsProc::getSQLOptions( $requestOptions, $valueCol ); + } + + /** + * @see RequestOptionsProc::getSQLConditions + * + * @since 1.8 + * + * @param SMWRequestOptions|null $requestOptions + * @param string $valueCol name of SQL column to which conditions apply + * @param string $labelCol name of SQL column to which string conditions apply, if any + * @param boolean $addAnd indicate whether the string should begin with " AND " if non-empty + * + * @return string + */ + public function getSQLConditions( SMWRequestOptions $requestOptions = null, $valueCol = '', $labelCol = '', $addAnd = true ) { + return RequestOptionsProc::getSQLConditions( $this, $requestOptions, $valueCol, $labelCol, $addAnd ); + } + + /** + * @see RequestOptionsProc::applyRequestOptions + * + * @since 1.8 + * + * @param array $data array of SMWDataItem objects + * @param SMWRequestOptions|null $requestOptions + * + * @return SMWDataItem[] + */ + public function applyRequestOptions( array $data, SMWRequestOptions $requestOptions = null ) { + return RequestOptionsProc::applyRequestOptions( $this, $data, $requestOptions ); + } + + /** + * PropertyTableInfoFetcher::findTableIdForDataTypeTypeId + * + * @param string $typeid + * + * @return string + */ + public function findTypeTableId( $typeid ) { + return $this->getPropertyTableInfoFetcher()->findTableIdForDataTypeTypeId( $typeid ); + } + + /** + * PropertyTableInfoFetcher::findTableIdForDataItemTypeId + * + * @param integer $dataItemId + * + * @return string + */ + public function findDiTypeTableId( $dataItemId ) { + return $this->getPropertyTableInfoFetcher()->findTableIdForDataItemTypeId( $dataItemId ); + } + + /** + * PropertyTableInfoFetcher::findTableIdForProperty + * + * @param DIProperty $property + * + * @return string + */ + public function findPropertyTableID( DIProperty $property ) { + return $this->getPropertyTableInfoFetcher()->findTableIdForProperty( $property ); + } + + /** + * PropertyTableInfoFetcher::getPropertyTableDefinitions + * + * @return TableDefinition[] + */ + public function getPropertyTables() { + return $this->getPropertyTableInfoFetcher()->getPropertyTableDefinitions(); + } + + /** + * Returns SMW Id object + * + * @since 1.9 + * + * @return SMWSql3SmwIds + */ + public function getObjectIds() { + return $this->smwIds; + } + + /** + * Returns the statics table + * + * @since 1.9 + * + * @return string + */ + public function getStatisticsTable() { + return self::PROPERTY_STATISTICS_TABLE; + } + + /** + * Resets internal objects + * + * @since 1.9.1.1 + */ + public function clear() { + parent::clear(); + $this->factory->newSemanticDataLookup()->clear(); + $this->propertyTableInfoFetcher = null; + $this->servicesContainer = null; + $this->getObjectIds()->initCache(); + } + + /** + * @since 3.0 + * + * @param string|null $type + * + * @return array + */ + public function getInfo( $type = null ) { + + if ( $type === 'store' ) { + return 'SMWSQLStore'; + } + + $connection = $this->getConnection( 'mw.db' ); + + if ( $type === 'db' ) { + return $connection->getInfo(); + } + + return [ + 'SMWSQLStore' => $connection->getInfo() + ]; + } + + /** + * @since 2.1 + * + * @param string $type + * + * @return Database + */ + public function getConnection( $type = 'mw.db' ) { + return parent::getConnection( $type ); + } + + /** + * @since 2.2 + * + * @return PropertyTableInfoFetcher + */ + public function getPropertyTableInfoFetcher() { + + if ( $this->propertyTableInfoFetcher === null ) { + $this->propertyTableInfoFetcher = $this->factory->newPropertyTableInfoFetcher(); + } + + return $this->propertyTableInfoFetcher; + } + + /** + * @since 2.4 + * + * @return PropertyTableIdReferenceFinder + */ + public function getPropertyTableIdReferenceFinder() { + + if ( $this->propertyTableIdReferenceFinder === null ) { + $this->propertyTableIdReferenceFinder = $this->factory->newPropertyTableIdReferenceFinder(); + } + + return $this->propertyTableIdReferenceFinder; + } + + /** + * @return ServicesContainer + */ + protected function newServicesContainer() { + return $this->factory->newServicesContainer(); + } + + /** + * @return EntityLookup + */ + private function getEntityLookup() { + + if ( $this->entityLookup === null ) { + $this->entityLookup = $this->factory->newEntityLookup(); + } + + return $this->entityLookup; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3_Readers.php b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3_Readers.php new file mode 100644 index 00000000..f2968edb --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3_Readers.php @@ -0,0 +1,501 @@ +<?php + +use SMW\ApplicationFactory; +use SMW\DataTypeRegistry; +use SMW\Enum; +use SMW\DIProperty; +use SMW\DIWikiPage; +use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException; +use SMW\SQLStore\TableDefinition; + +/** + * Class to provide all basic read methods for SMWSQLStore3. + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + * @author Nischay Nahata + * + * @since 1.8 + * @ingroup SMWStore + */ +class SMWSQLStore3Readers { + + /** + * The store used by this store reader + * + * @since 1.8 + * @var SMWSQLStore3 + */ + private $store; + + /** + * @var SQLStoreFactory + */ + private $factory; + + /** + * @var TraversalPropertyLookup + */ + private $traversalPropertyLookup; + + /** + * @var PropertySubjectsLookup + */ + private $propertySubjectsLookup; + + /** + * @var PropertiesLookup + */ + private $propertiesLookup; + + /** + * @var SemanticDataLookup + */ + private $semanticDataLookup; + + public function __construct( SMWSQLStore3 $parentStore, $factory ) { + $this->store = $parentStore; + $this->factory = $factory; + $this->traversalPropertyLookup = $this->factory->newTraversalPropertyLookup(); + $this->propertySubjectsLookup = $this->factory->newPropertySubjectsLookup(); + $this->propertiesLookup = $this->factory->newPropertiesLookup(); + $this->semanticDataLookup = $this->factory->newSemanticDataLookup(); + } + + /** + * @see SMWStore::getSemanticData() + * @since 1.8 + * + * @param DIWikiPage $subject + * @param string[]|bool $filter + */ + public function getSemanticData( DIWikiPage $subject, $filter = false ) { + + // *** Find out if this subject exists ***// + $sortKey = ''; + + $sid = $this->store->smwIds->getSMWPageIDandSort( + $subject->getDBkey(), + $subject->getNamespace(), + $subject->getInterwiki(), + $subject->getSubobjectName(), + $sortKey, + true, + true + ); + + // Ensures that a cached item to contain an expected sortKey when + // for example the ID was just created and the sortKey from the DB + // is empty otherwise the DB wins over the invoked sortKey + if ( !$sortKey ) { + $sortKey = $subject->getSortKey(); + } + + $subject->setSortKey( $sortKey ); + + if ( $sid == 0 ) { + // We consider redirects for getting $sid, + // so $sid == 0 also means "no redirects". + return new SMWSemanticData( $subject ); + } + + $propertyTableHashes = $this->store->smwIds->getPropertyTableHashes( $sid ); + $opts = null; + $semanticData = null; + + if ( $filter instanceof SMWRequestOptions ) { + $semanticData = $this->semanticDataLookup->newStubSemanticData( $subject ); + } + + foreach ( $this->store->getPropertyTables() as $tid => $proptable ) { + if ( !array_key_exists( $proptable->getName(), $propertyTableHashes ) ) { + continue; + } + + if ( $filter instanceof SMWRequestOptions ) { + $opts = $filter; + } elseif ( $filter !== false ) { + $relevant = false; + foreach ( $filter as $typeId ) { + $diType = DataTypeRegistry::getInstance()->getDataItemId( $typeId ); + $relevant = $relevant || ( $proptable->getDiType() == $diType ); + if ( $relevant ) { + break; + } + } + if ( !$relevant ) { + continue; + } + } + + $data = $this->semanticDataLookup->getSemanticDataFromTable( $sid, $subject, $proptable, $opts ); + + if ( $semanticData !== null ) { + $semanticData->importDataFrom( $data ); + } + } + + if ( $semanticData === null ) { + // Note: the sortkey is always set but belongs to no property table, + // hence no entry in $this->store->m_sdstate[$sid] is made. + $this->semanticDataLookup->lockCache(); + $this->semanticDataLookup->initLookupCache( $sid, $subject ); + + $semanticData = $this->semanticDataLookup->getSemanticDataById( + $sid + ); + + $this->semanticDataLookup->unlockCache(); + } + + // Avoid adding a sortkey for an already extended stub + if ( !$semanticData->hasProperty( new DIProperty( '_SKEY' ) ) ) { + $semanticData->addPropertyStubValue( '_SKEY', [ '', $sortKey ] ); + } + + $this->store->smwIds->warmUpCache( + $semanticData->getProperties() + ); + + $this->store->smwIds->warmUpCache( + $semanticData->getPropertyValues( new DIProperty( '_INST' ) ) + ); + + return $semanticData; + } + + /** + * @see SMWStore::getPropertyValues + * + * @todo Retrieving all sortkeys (values for _SKEY with $subject null) + * is not supported. An empty array will be given. + * + * @since 1.8 + * + * @param $subject mixed DIWikiPage or null + * @param $property SMWDIProperty + * @param $requestOptions SMWRequestOptions + * + * @return SMWDataItem[] + */ + public function getPropertyValues( $subject, SMWDIProperty $property, $requestOptions = null ) { + + if ( $property->isInverse() ) { // inverses are working differently + $noninverse = new SMW\DIProperty( $property->getKey(), false ); + $result = $this->getPropertySubjects( $noninverse, $subject, $requestOptions ); + } elseif ( !is_null( $subject ) ) { // subject given, use semantic data cache + $sid = $this->store->smwIds->getSMWPageID( $subject->getDBkey(), + $subject->getNamespace(), $subject->getInterwiki(), + $subject->getSubobjectName(), true ); + if ( $sid == 0 ) { + $result = []; + } elseif ( $property->getKey() == '_SKEY' ) { + $this->store->smwIds->getSMWPageIDandSort( $subject->getDBkey(), + $subject->getNamespace(), $subject->getInterwiki(), + $subject->getSubobjectName(), $sortKey, true ); + $sortKeyDi = new SMWDIBlob( $sortKey ); + $result = $this->store->applyRequestOptions( [ $sortKeyDi ], $requestOptions ); + } else { + $propTableId = $this->store->findPropertyTableID( $property ); + $proptables = $this->store->getPropertyTables(); + + if ( !isset( $proptables[$propTableId] ) ) { + return []; + } + + $propertyTableDef = $proptables[$propTableId]; + + $opts = $this->semanticDataLookup->newRequestOptions( + $propertyTableDef, + $property, + $requestOptions + ); + + $semanticData = $this->semanticDataLookup->getSemanticDataFromTable( + $sid, + $subject, + $propertyTableDef, + $opts + ); + + $pv = $semanticData->getPropertyValues( $property ); + $this->store->smwIds->warmUpCache( $pv ); + + $result = $this->store->applyRequestOptions( + $pv, + $requestOptions + ); + } + } else { // no subject given, get all values for the given property + $pid = $this->store->smwIds->getSMWPropertyID( $property ); + $tableid = $this->store->findPropertyTableID( $property ); + + if ( ( $pid == 0 ) || ( $tableid === '' ) ) { + return []; + } + + $proptables = $this->store->getPropertyTables(); + $data = $this->semanticDataLookup->fetchSemanticData( + $pid, + $property, + $proptables[$tableid], + $requestOptions + ); + + $result = []; + $propertyTypeId = $property->findPropertyTypeID(); + $propertyDiId = DataTypeRegistry::getInstance()->getDataItemId( $propertyTypeId ); + + foreach ( $data as $dbkeys ) { + try { + $diHandler = $this->store->getDataItemHandlerForDIType( $propertyDiId ); + $result[] = $diHandler->dataItemFromDBKeys( $dbkeys ); + } catch ( SMWDataItemException $e ) { + // maybe type assignment changed since data was stored; + // don't worry, but we can only drop the data here + } + } + } + + $this->store->smwIds->warmUpCache( $result ); + + return $result; + } + + /** + * @see SMWStore::getPropertySubjects + * + * @todo This method cannot retrieve subjects for sortkeys, i.e., for + * property _SKEY. Only empty arrays will be returned there. + * + * @param SMWDIProperty $property + * @param SMWDataItem|null $value + * @param SMWRequestOptions|null $requestOptions + * + * @return array of DIWikiPage + */ + public function getPropertySubjects( SMWDIProperty $property, SMWDataItem $dataItem = null, SMWRequestOptions $requestOptions = null ) { + + // inverses are working differently + if ( $property->isInverse() ) { + $noninverse = new SMW\DIProperty( $property->getKey(), false ); + $result = $this->getPropertyValues( $dataItem, $noninverse, $requestOptions ); + return $result; + } + + $type = DataTypeRegistry::getInstance()->getDataItemByType( + $property->findPropertyTypeID() + ); + + // #1222, Filter those where types don't match (e.g property = _txt + // and value = _wpg) + if ( $dataItem !== null && $type !== $dataItem->getDIType() ) { + return []; + } + + // First build $select, $from, and $where for the DB query + $pid = $this->store->smwIds->getSMWPropertyID( $property ); + $tableid = $this->store->findPropertyTableID( $property ); + + if ( ( $pid == 0 ) || ( $tableid === '' ) ) { + return []; + } + + $proptables = $this->store->getPropertyTables(); + $proptable = $proptables[$tableid]; + + $result = $this->propertySubjectsLookup->fetchFromTable( + $pid, + $proptable, + $dataItem, + $requestOptions + ); + + // Keep the result as iterator which is normally advised when the result + // size is expected to be larger than 1000 or results are retrieved through + // a job which may process them in batches. + if ( $requestOptions !== null && $requestOptions->getOption( Enum::SUSPEND_CACHE_WARMUP ) ) { + return $result; + } + + $this->store->smwIds->warmUpCache( $result ); + + return $result; + } + + + /** + * Helper function to compute from and where strings for a DB query so that + * only rows of the given value object match. The parameter $tableindex + * counts that tables used in the query to avoid duplicate table names. The + * parameter $proptable provides the SMWSQLStore3Table object that is + * queried. + * + * @todo Maybe do something about redirects. The old code was + * $oid = $this->store->smwIds->getSMWPageID($value->getDBkey(),$value->getNamespace(),$value->getInterwiki(),false); + * + * @note This method cannot handle DIContainer objects with sortkey + * properties correctly. This should never occur, but it would be good + * to fail in a more controlled way if it ever does. + * + * @param string $from + * @param string $where + * @param TableDefinition $propTable + * @param SMWDataItem $value + * @param integer $tableIndex + */ + private function prepareValueQuery( &$from, &$where, TableDefinition $propTable, $value, $tableIndex = 1 ) { + $db = $this->store->getConnection(); + + if ( $value instanceof SMWDIContainer ) { // recursive handling of containers + $keys = array_keys( $propTable->getFields( $this->store ) ); + $joinfield = "t$tableIndex." . reset( $keys ); // this must be a type 'p' object + $proptables = $this->store->getPropertyTables(); + $semanticData = $value->getSemanticData(); + + foreach ( $semanticData->getProperties() as $subproperty ) { + $tableid = $this->store->findPropertyTableID( $subproperty ); + $subproptable = $proptables[$tableid]; + + foreach ( $semanticData->getPropertyValues( $subproperty ) as $subvalue ) { + $tableIndex++; + + if ( $subproptable->usesIdSubject() ) { // simply add property table to check values + $from .= " INNER JOIN " . $db->tableName( $subproptable->getName() ) . " AS t$tableIndex ON t$tableIndex.s_id=$joinfield"; + } else { // exotic case with table that uses subject title+namespace in container object (should never happen in SMW core) + $from .= " INNER JOIN " . $db->tableName( SMWSql3SmwIds::TABLE_NAME ) . " AS ids$tableIndex ON ids$tableIndex.smw_id=$joinfield" . + " INNER JOIN " . $db->tableName( $subproptable->getName() ) . " AS t$tableIndex ON " . + "t$tableIndex.s_title=ids$tableIndex.smw_title AND t$tableIndex.s_namespace=ids$tableIndex.smw_namespace"; + } + + if ( !$subproptable->isFixedPropertyTable() ) { // the ID we get should be !=0, so no point in filtering the converse + $where .= ( $where ? ' AND ' : '' ) . "t$tableIndex.p_id=" . $db->addQuotes( $this->store->smwIds->getSMWPropertyID( $subproperty ) ); + } + + $this->prepareValueQuery( $from, $where, $subproptable, $subvalue, $tableIndex ); + } + } + } elseif ( !is_null( $value ) ) { // add conditions for given value + $diHandler = $this->store->getDataItemHandlerForDIType( $value->getDIType() ); + foreach ( $diHandler->getWhereConds( $value ) as $fieldname => $value ) { + $where .= ( $where ? ' AND ' : '' ) . "t$tableIndex.$fieldname=" . $db->addQuotes( $value ); + } + } + } + + /** + * @see SMWStore::getAllPropertySubjects + * + * @param SMWDIProperty $property + * @param SMWRequestOptions $requestOptions + * + * @return array of DIWikiPage + */ + public function getAllPropertySubjects( SMWDIProperty $property, SMWRequestOptions $requestOptions = null ) { + return $this->getPropertySubjects( $property, null, $requestOptions ); + } + + /** + * @see Store::getProperties + * + * @param DIWikiPage $subject + * @param SMWRequestOptions|null $requestOptions + * + * @return SMWDataItem[] + */ + public function getProperties( DIWikiPage $subject, SMWRequestOptions $requestOptions = null ) { + + $sid = $this->store->smwIds->getSMWPageID( + $subject->getDBkey(), + $subject->getNamespace(), + $subject->getInterwiki(), + $subject->getSubobjectName() + ); + + // no id, no page, no properties + if ( $sid == 0 ) { + return []; + } + + $subject->setId( $sid ); + $result = []; + + // Potentially need to get more results, since options apply to union + $lookupOptions = $this->propertiesLookup->newRequestOptions( + $requestOptions + ); + + $propertyTableHashes = $this->store->smwIds->getPropertyTableHashes( $sid ); + + foreach ( $this->store->getPropertyTables() as $tid => $propertyTable ) { + + if ( !array_key_exists( $propertyTable->getName(), $propertyTableHashes ) ) { + continue; + } + + $res = $this->propertiesLookup->fetchFromTable( + $subject, + $propertyTable, + $lookupOptions + ); + + foreach ( $res as $row ) { + $result[] = new DIProperty( + isset( $row->smw_title ) ? $row->smw_title : $row + ); + } + } + + // apply options to overall result + $result = $this->store->applyRequestOptions( $result, $requestOptions ); + $this->store->smwIds->warmUpCache( $result ); + + return $result; + } + + /** + * Implementation of SMWStore::getInProperties(). This function is meant to + * be used for finding properties that link to wiki pages. + * + * @since 1.8 + * @see SMWStore::getInProperties + * + * @param SMWDataItem $value + * @param SMWRequestOptions|null $requestOptions + * + * @return DIProperty[] + */ + public function getInProperties( SMWDataItem $value, SMWRequestOptions $requestOptions = null ) { + + $result = []; + $diType = $value->getDIType(); + + foreach ( $this->store->getPropertyTables() as $proptable ) { + + if ( $diType != $proptable->getDiType() ) { + continue; + } + + $res = $this->traversalPropertyLookup->fetchFromTable( + $proptable, + $value, + $requestOptions + ); + + foreach ( $res as $row ) { + try { + $result[] = new SMW\DIProperty( $row->smw_title ); + } catch (SMWDataItemException $e) { + // has been observed to happen (empty property title); cause unclear; ignore this data + } + } + } + + // Apply options to overall result + $result = $this->store->applyRequestOptions( $result, $requestOptions ); + $this->store->smwIds->warmUpCache( $result ); + + return $result; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3_Writers.php b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3_Writers.php new file mode 100644 index 00000000..bc8ef57e --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_SQLStore3_Writers.php @@ -0,0 +1,825 @@ +<?php + +use SMW\ApplicationFactory; +use SMW\ChangePropListener; +use SMW\DIProperty; +use SMW\DIWikiPage; +use SMW\MediaWiki\Jobs\UpdateJob; +use SMW\MediaWiki\Deferred\ChangeTitleUpdate; +use SMW\SemanticData; +use SMW\Parameters; +use SMW\SQLStore\PropertyStatisticsTable; +use SMW\SQLStore\PropertyTableRowDiffer; + +/** + * Class Handling all the write and update methods for SMWSQLStore3. + * + * @note Writing may also require some reading operations. Operations that are + * only needed in helper methods of this class should be implemented here, not + * in SMWSQLStore3Readers. + * + * @author Markus Krötzsch + * @author Jeroen De Dauw + * @author Nischay Nahata + * + * @since 1.8 + * @ingroup SMWStore + */ +class SMWSQLStore3Writers { + + /** + * The store used by this store writer. + * + * @since 1.8 + * @var SMWSQLStore3 + */ + protected $store; + + /** + * @var SQLStoreFactory + */ + private $factory; + + /** + * @var PropertyTableRowDiffer + */ + private $propertyTableRowDiffer; + + /** + * @var PropertyTableUpdater + */ + private $propertyTableUpdater; + + /** + * @var SemanticDataLookup + */ + private $semanticDataLookup; + + /** + * @var IdChanger + */ + private $idChanger; + + /** + * @since 1.8 + * + * @param SMWSQLStore3 $parentStore + * @param SQLStoreFactory $factory + */ + public function __construct( SMWSQLStore3 $parentStore, $factory ) { + $this->store = $parentStore; + $this->factory = $factory; + $this->propertyTableRowDiffer = $this->factory->newPropertyTableRowDiffer(); + $this->propertyTableUpdater = $this->factory->newPropertyTableUpdater(); + $this->semanticDataLookup = $this->factory->newSemanticDataLookup(); + $this->idChanger = $this->factory->newIdChanger(); + } + + /** + * @see SMWStore::deleteSubject + * + * @since 1.8 + * @param Title $title + */ + public function deleteSubject( Title $title ) { + + // @deprecated since 2.1, use 'SMW::SQLStore::BeforeDeleteSubjectComplete' + \Hooks::run( 'SMWSQLStore3::deleteSubjectBefore', [ $this->store, $title ] ); + + \Hooks::run( 'SMW::SQLStore::BeforeDeleteSubjectComplete', [ $this->store, $title ] ); + + // Fetch all possible matches (including any duplicates created by + // incomplete rollback or DB deadlock) + $idList = $this->store->getObjectIds()->findAllEntitiesThatMatch( + $title->getDBkey(), + $title->getNamespace() + ); + + $extensionList = array_flip( $idList ); + $subject = DIWikiPage::newFromTitle( $title ); + + $emptySemanticData = new SemanticData( $subject ); + $emptySemanticData->setOption( SemanticData::PROC_DELETE, true ); + + $subobjectListFinder = $this->factory->newSubobjectListFinder(); + + foreach ( $idList as $id ) { + $this->doDelete( $id, $subject, $subobjectListFinder, $extensionList ); + $this->doDataUpdate( $emptySemanticData ); + + if ( $this->store->service( 'PropertyTableIdReferenceFinder' )->hasResidualPropertyTableReference( $id ) === false ) { + // Mark subject/subobjects with a special IW, the final removal is being + // triggered by the `EntityRebuildDispatcher` + $this->store->getObjectIds()->updateInterwikiField( + $id, + $subject, + SMW_SQL3_SMWDELETEIW + ); + } else { + // Convert the subject into a simple object instance + $this->store->getObjectIds()->setPropertyTableHashes( + $id, + null + ); + } + } + + $extensionList = array_keys( $extensionList ); + + $this->store->extensionData['delete.list'] = $extensionList; + + // @deprecated since 2.1, use 'SMW::SQLStore::AfterDeleteSubjectComplete' + \Hooks::run( 'SMWSQLStore3::deleteSubjectAfter', [ $this->store, $title ] ); + + \Hooks::run( 'SMW::SQLStore::AfterDeleteSubjectComplete', [ $this->store, $title ] ); + } + + private function doDelete( $id, $subject, $subobjectListFinder, &$extensionList ) { + + $this->semanticDataLookup->invalidateCache( $id ); + + if ( $subject->getNamespace() === SMW_NS_CONCEPT ) { // make sure to clear caches + $db = $this->store->getConnection(); + + $db->delete( + SMWSQLStore3::CONCEPT_TABLE, + [ 's_id' => $id ], + 'SMW::deleteSubject::Conc' + ); + + $db->delete( + SMWSQLStore3::CONCEPT_CACHE_TABLE, + [ 'o_id' => $id ], + 'SMW::deleteSubject::Conccache' + ); + } + + $subject->setId( $id ); + + foreach( $subobjectListFinder->find( $subject ) as $subobject ) { + $extensionList[$subobject->getId()] = true; + + $this->store->getObjectIds()->updateInterwikiField( + $subobject->getId(), + $subobject, + SMW_SQL3_SMWDELETEIW + ); + } + } + + /** + * @see SMWStore::doDataUpdate + * + * @since 1.8 + * @param SMWSemanticData $data + */ + public function doDataUpdate( SMWSemanticData $semanticData ) { + \Hooks::run( 'SMWSQLStore3::updateDataBefore', [ $this->store, $semanticData ] ); + + $subject = $semanticData->getSubject(); + $connection = $this->store->getConnection( 'mw.db' ); + + $subobjectListFinder = $this->factory->newSubobjectListFinder(); + + $changeOp = $this->factory->newChangeOp( + $subject + ); + + $this->propertyTableRowDiffer->setChangeOp( + $changeOp + ); + + $changePropListener = $this->factory->newChangePropListener(); + $hierarchyLookup = $this->factory->newHierarchyLookup(); + + // #2698 + $hierarchyLookup->addListenersTo( + $changePropListener + ); + + $changePropListener->loadListeners( + $this->store + ); + + // Update data about our main subject + $this->doFlatDataUpdate( $semanticData ); + $sid = $subject->getId(); + + // Update data about our subobjects + $subSemanticData = $semanticData->getSubSemanticData(); + $connection = $this->store->getConnection( 'mw.db' ); + + $connection->beginAtomicTransaction( __METHOD__ ); + + foreach( $subSemanticData as $subobjectData ) { + $this->doFlatDataUpdate( $subobjectData ); + } + + $deleteList = []; + + // Mark subobjects without reference to be deleted + foreach( $subobjectListFinder->find( $subject ) as $subobject ) { + if( !$semanticData->hasSubSemanticData( $subobject->getSubobjectName() ) ) { + + $this->doFlatDataUpdate( new SemanticData( $subobject ) ); + $deleteList[] = $subobject->getId(); + + $this->store->getObjectIds()->updateInterwikiField( + $subobject->getId(), + $subobject, + SMW_SQL3_SMWDELETEIW + ); + } + } + + if ( ( $rev_id = $semanticData->getExtensionData( 'revision_id' ) ) !== null ) { + $this->store->getObjectIds()->updateRevField( $sid, $rev_id ); + } + + $connection->endAtomicTransaction( __METHOD__ ); + $connection->beginAtomicTransaction( __METHOD__ ); + + // Store the diff in cache so any post processing has a chance to find + // what entities and values were changed + $changeDiff = $changeOp->newChangeDiff(); + $changeDiff->save( ApplicationFactory::getInstance()->getCache() ); + + $changePropListener->callListeners(); + + $this->store->extensionData['delete.list'] = $deleteList; + $this->store->extensionData['change.diff'] = $changeDiff; + + // Deprecated since 2.3, use SMW::SQLStore::AfterDataUpdateComplete + \Hooks::run( 'SMWSQLStore3::updateDataAfter', [ $this->store, $semanticData ] ); + + \Hooks::run( 'SMW::SQLStore::AfterDataUpdateComplete', [ + $this->store, + $semanticData, + $changeOp + ] ); + + $connection->endAtomicTransaction( __METHOD__ ); + } + + /** + * Update the store to contain the given data, without taking any + * subobject data into account. + * + * @since 1.8 + * @param SMWSemanticData $data + */ + protected function doFlatDataUpdate( SMWSemanticData $data ) { + $subject = $data->getSubject(); + + // Take care of redirects + $redirects = $data->getPropertyValues( new SMW\DIProperty( '_REDI' ) ); + + if ( count( $redirects ) > 0 ) { + $redirect = end( $redirects ); // at most one redirect per page + $this->updateRedirects( + $subject->getDBkey(), + $subject->getNamespace(), + $redirect->getDBkey(), + $redirect->getNameSpace() + ); + // Stop here: + // * no support for annotations on redirect pages + // * updateRedirects takes care of deleting any previous data + return; + } else { + $this->updateRedirects( + $subject->getDBkey(), + $subject->getNamespace() + ); + } + + // Find an approriate sortkey, the field is influenced by various + // elements incl. DEFAULTSORT and can be altered without modifying + // any other annotation. + $sortKey = $this->makeSortKey( $subject, $data ); + + // Always fetch an ID which is either recalled from cache or is created. + // Doing so ensures that the sortkey and namespace data are updated + // to both the DB and the cache. + $sid = $this->store->getObjectIds()->makeSMWPageID( + $subject->getDBkey(), + $subject->getNamespace(), + $subject->getInterwiki(), + $subject->getSubobjectName(), + true, + $sortKey, + true + ); + + $subject->setSortKey( $sortKey ); + $subject->setId( $sid ); + + // Find any potential duplicate entries for the current subject and + // if matched, mark them as to be deleted + $idList = $this->store->getObjectIds()->findAllEntitiesThatMatch( + $subject->getDBkey(), + $subject->getNamespace(), + $subject->getInterwiki(), + $subject->getSubobjectName() + ); + + foreach ( $idList as $id ) { + if ( $id != $sid ) { + $this->store->getObjectIds()->updateInterwikiField( + $id, + $subject, + SMW_SQL3_SMWDELETEIW + ); + } + } + + // Take care of all remaining property table data + list( $insertRows, $deleteRows, $newHashes ) = $this->propertyTableRowDiffer->computeTableRowDiff( + $sid, + $data + ); + + $params = new Parameters( + [ + 'insert_rows' => $insertRows, + 'delete_rows' => $deleteRows, + 'new_hashes' => $newHashes + ] + ); + + $this->propertyTableUpdater->update( $sid, $params ); + + if ( $redirects === [] && $subject->getSubobjectName() === '' ) { + + $dataItemFromId = $this->store->getObjectIds()->getDataItemById( $sid ); + + // If for some reason the internal redirect marker is still set but no + // redirect annotations are known then do update the interwiki field + if ( $dataItemFromId !== null && $dataItemFromId->getInterwiki() === SMW_SQL3_SMWREDIIW ) { + $this->store->getObjectIds()->updateInterwikiField( $sid, $subject ); + } + } + + // Update caches (may be important if jobs are directly following this call) + $this->semanticDataLookup->setLookupCache( $sid, $data ); + } + + private function makeSortKey( $subject, $data ) { + + // Don't mind the delete process + if ( $data->getOption( SemanticData::PROC_DELETE ) ) { + return ''; + } + + $property = new DIProperty( '_SKEY' ); + + // Take care of the sortkey + $pv = $data->getPropertyValues( $property ); + $dataItem = end( $pv ); + + if ( $dataItem instanceof SMWDIBlob ) { + $sortkey = $dataItem->getString(); + } elseif ( $data->getExtensionData( 'sort.extension' ) !== null ) { + $sortkey = $data->getExtensionData( 'sort.extension' ); + } else { + $sortkey = $subject->getSortKey(); + } + + // Extend the subobject sortkey in case no @sortkey was given for an + // entity + if ( $subject->getSubobjectName() !== '' && !$dataItem instanceof SMWDIBlob ) { + + // Add sort data from some dedicated containers (of a record or + // reference type etc.) otherwise use the sobj name as extension + // to distinguish each entity + if ( $data->getExtensionData( 'sort.data' ) !== null ) { + $sortkey .= '#' . $data->getExtensionData( 'sort.data' ); + } else { + $sortkey .= '#' . $subject->getSubobjectName(); + } + } + + // #649 Be consistent about how sortkeys are stored therefore always + // normalize even for usages like {{DEFAULTSORT: Foo_bar }} + $sortkey = str_replace( '_', ' ', $sortkey ); + + return $sortkey; + } + + /** + * Implementation of SMWStore::changeTitle(). In contrast to + * updateRedirects(), this function does not simply write a redirect + * from the old page to the new one, but also deletes all data that may + * already be stored for the new title (normally the new title should + * belong to an empty page that has no data but at least it could have a + * redirect to the old page), and moves all data that exists for the old + * title to the new location. Thus, the function executes three steps: + * delete data at newtitle, move data from oldtitle to newtitle, and set + * redirect from oldtitle to newtitle. In some cases, the goal can be + * achieved more efficiently, e.g. if the new title does not occur in SMW + * yet: then we can just change the ID records for the titles instead of + * changing all data tables + * + * Note that the implementation ignores the MediaWiki IDs since this + * store has its own ID management. Also, the function requires that both + * titles are local, i.e. have empty interwiki prefix. + * + * @todo Currently the sortkey is not moved with the remaining data. It is + * not possible to move it reliably in all cases: we cannot distinguish an + * unset sortkey from one that was set to the name of oldtitle. Maybe use + * update jobs right away? + * + * @since 1.8 + * + * @param Title $oldTitle + * @param Title $newTitle + * @param integer $pageId + * @param integer $redirectId + */ + public function changeTitle( Title $oldTitle, Title $newTitle, $pageId, $redirectId = 0 ) { + global $smwgQEqualitySupport; + + \Hooks::run( + 'SMW::SQLStore::BeforeChangeTitleComplete', + [ $this->store, $oldTitle, $newTitle, $pageId, $redirectId ] + ); + + $db = $this->store->getConnection(); + + // get IDs but do not resolve redirects: + $sid = $this->store->getObjectIds()->getSMWPageID( + $oldTitle->getDBkey(), + $oldTitle->getNamespace(), + '', + '', + false + ); + + $tid = $this->store->getObjectIds()->getSMWPageID( + $newTitle->getDBkey(), + $newTitle->getNamespace(), + '', + '', + false + ); + + // Easy case: target not used anywhere yet, just hijack its title for our current id + if ( ( $tid == 0 ) && ( $smwgQEqualitySupport != SMW_EQ_NONE ) ) { + // This condition may not hold even if $newtitle is + // currently unused/non-existing since we keep old IDs. + // If equality support is off, then this simple move + // does too much; fall back to general case below. + if ( $sid != 0 ) { // change id entry to refer to the new title + // Note that this also changes the reference for internal objects (subobjects) + $db->update( + SMWSql3SmwIds::TABLE_NAME, + [ + 'smw_title' => $newTitle->getDBkey(), + 'smw_namespace' => $newTitle->getNamespace(), + 'smw_iw' => '' + ], + [ + 'smw_title' => $oldTitle->getDBkey(), + 'smw_namespace' => $oldTitle->getNamespace(), + 'smw_iw' => '' + ], + __METHOD__ + ); + + $this->store->getObjectIds()->moveSubobjects( + $oldTitle->getDBkey(), + $oldTitle->getNamespace(), + $newTitle->getDBkey(), + $newTitle->getNamespace() + ); + + $this->store->getObjectIds()->setCache( + $oldTitle->getDBkey(), + $oldTitle->getNamespace(), + '', + '', + 0, + '' + ); + + // We do not know the new sortkey, so just clear the cache: + $this->store->getObjectIds()->deleteCache( + $newTitle->getDBkey(), + $newTitle->getNamespace(), + '', + '' + ); + + } else { // make new (target) id for use in redirect table + $sid = $this->store->getObjectIds()->makeSMWPageID( + $newTitle->getDBkey(), + $newTitle->getNamespace(), + '', + '' + ); + } // at this point, $sid is the id of the target page (according to the IDs table) + + // make redirect id for oldtitle: + $this->store->getObjectIds()->makeSMWPageID( + $oldTitle->getDBkey(), + $oldTitle->getNamespace(), + SMW_SQL3_SMWREDIIW, + '' + ); + + $this->store->getObjectIds()->addRedirect( + $sid, + $oldTitle->getDBkey(), + $oldTitle->getNamespace() + ); + + $propertyStatisticsStore = $this->factory->newPropertyStatisticsStore(); + + $propertyStatisticsStore->addToUsageCount( + $this->store->getObjectIds()->getSMWPropertyID( new SMW\DIProperty( '_REDI' ) ), + 1 + ); + + /// NOTE: there is the (bad) case that the moved page is a redirect. As chains of + /// redirects are not supported by MW or SMW, the above is maximally correct in this case too. + /// NOTE: this temporarily leaves existing redirects to oldtitle point to newtitle as well, which + /// will be lost after the next update. Since double redirects are an error anyway, this is not + /// a bad behavior: everything will continue to work until the existing redirects are updated, + /// which will hopefully be done to fix the double redirect. + } else { // General move method: should always be correct + // (equality support respected when updating redirects) + + // Delete any existing data (including redirects) from old title + $emptyNewSemanticData = new SMWSemanticData( SMWDIWikiPage::newFromTitle( $oldTitle ) ); + $this->doDataUpdate( $emptyNewSemanticData ); + + // Move all data of old title to new position: + if ( $sid != 0 ) { + $this->idChanger->change( + $sid, + $tid, + $oldTitle->getNamespace(), + $newTitle->getNamespace(), + true, + false + ); + } + + // Associate internal objects (subobjects) with the new title: + $table = $db->tableName( SMWSql3SmwIds::TABLE_NAME ); + + $values = [ + 'smw_title' => $newTitle->getDBkey(), + 'smw_namespace' => $newTitle->getNamespace(), + 'smw_iw' => '' + ]; + + $sql = "UPDATE $table SET " . $db->makeList( $values, LIST_SET ) . + ' WHERE smw_title = ' . $db->addQuotes( $oldTitle->getDBkey() ) . ' AND ' . + 'smw_namespace = ' . $db->addQuotes( $oldTitle->getNamespace() ) . ' AND ' . + 'smw_iw = ' . $db->addQuotes( '' ) . ' AND ' . + 'smw_subobject != ' . $db->addQuotes( '' ); // The "!=" is why we cannot use MW array syntax here + + $db->query( $sql, __METHOD__ ); + + $this->store->getObjectIds()->moveSubobjects( + $oldTitle->getDBkey(), + $oldTitle->getNamespace(), + $newTitle->getDBkey(), + $newTitle->getNamespace() + ); + + // $redirid == 0 means that the oldTitle was not supposed to be a redirect + // (oldTitle is delete from the db) but instead of deleting all + // references we will still copy data from old to new during updateRedirects() + // and clear the semantic data container for the oldTitle instance + // to ensure that no ghost references exists for an deleted oldTitle + // @see Title::moveTo(), createRedirect + if ( $redirectId == 0 ) { + + // Delete any existing data (including redirects) from old title + $this->updateRedirects( + $oldTitle->getDBkey(), + $oldTitle->getNamespace() + ); + + } else { + + // Write a redirect from old title to new one: + // (this also updates references in other tables as needed.) + // TODO: may not be optimal for the standard case that newtitle + // existed and redirected to oldtitle (PERFORMANCE) + $this->updateRedirects( + $oldTitle->getDBkey(), + $oldTitle->getNamespace(), + $newTitle->getDBkey(), + $newTitle->getNamespace() + ); + + } + + } + + if ( $redirectId == 0 ) { + $oldTitle = null; + } + + ChangeTitleUpdate::addUpdate( $oldTitle, $newTitle ); + } + + /** + * Helper method to write information about some redirect. Various updates + * can be necessary if redirects are resolved as identities in SMW. The + * title and namespace of the affected page and of its updated redirect + * target are given. The target can be empty ('') to delete any redirect. + * Returns the canonical ID that is now to be used for the subject. + * + * This method does not change the ids of the affected pages, and thus it + * is not concerned with updates of the data that is currently stored for + * the subject. Normally, a subject that is a redirect will not have other + * data, but this method does not depend on this. + * + * @note Please make sure you fully understand this code before making any + * changes here. Keeping the redirect structure consistent is important, + * and errors in this code can go unnoticed for quite some time. + * + * @note This method merely handles the addition or deletion of a redirect + * statement in the wiki. It does not assume that any page contents has + * been changed (e.g. moved). See changeTitle() for additional handling in + * this case. + * + * @todo Clean up this code. + * + * @since 1.8 + * @param string $subject_t + * @param integer $subject_ns + * @param string $curtarget_t + * @param integer $curtarget_ns + * @return integer the new canonical ID of the subject + */ + protected function updateRedirects( $subject_t, $subject_ns, $curtarget_t = '', $curtarget_ns = -1 ) { + global $smwgQEqualitySupport; + + $count = 0; //track count changes for redi property + $db = $this->store->getConnection(); + + // *** First get id of subject, old redirect target, and current (new) redirect target ***// + + $sid_sort = ''; + + // find real id of subject, if any + $sid = $this->store->getObjectIds()->getSMWPageIDandSort( + $subject_t, + $subject_ns, + '', + '', + $sid_sort, + false + ); + + /// NOTE: $sid can be 0 here; this is useful to know since it means that fewer table updates are needed + $new_tid = $curtarget_t ? ( $this->store->getObjectIds()->makeSMWPageID( $curtarget_t, $curtarget_ns, '', '', false ) ) : 0; // real id of new target, if given + + $old_tid = $this->store->getObjectIds()->findRedirect( + $subject_t, + $subject_ns + ); + + /// NOTE: $old_tid and $new_tid both (intentionally) ignore further redirects: no redirect chains + + if ( $old_tid == $new_tid ) { // no change, all happy + return ( $new_tid == 0 ) ? $sid : $new_tid; + } // note that this means $old_tid != $new_tid in all cases below + + // *** Make relevant changes in property tables (don't write the new redirect yet) ***// + $jobs = []; + + if ( ( $old_tid == 0 ) && ( $sid != 0 ) && ( $smwgQEqualitySupport != SMW_EQ_NONE ) ) { // new redirect + // $smwgQEqualitySupport requires us to change all tables' page references from $sid to $new_tid. + // Since references must not be 0, we don't have to do this is $sid == 0. + $this->idChanger->change( + $sid, + $new_tid, + $subject_ns, + $curtarget_ns, + false, + true + ); + + } elseif ( $old_tid != 0 ) { // existing redirect is changed or deleted + + $count--; + + $this->store->getObjectIds()->updateRedirect( + $old_tid, + $subject_t, + $subject_ns + ); + } + + // *** Finally, write the new redirect data ***// + + if ( $new_tid != 0 ) { // record a new redirect + // Redirecting done right: + // (1) make a new ID with iw SMW_SQL3_SMWREDIIW or + // change iw field of current ID in this way, + // (2) write smw_fpt_redi table, + // (3) update canonical cache. + // This order must be obeyed unless you really understand what you are doing! + + if ( ( $old_tid == 0 ) && ( $smwgQEqualitySupport != SMW_EQ_NONE ) ) { + // mark subject as redirect (if it was no redirect before) + if ( $sid == 0 ) { // every redirect page must have an ID + $sid = $this->store->getObjectIds()->makeSMWPageID( + $subject_t, + $subject_ns, + SMW_SQL3_SMWREDIIW, + '', + false + ); + } else { + $db->update( + SMWSql3SmwIds::TABLE_NAME, + [ 'smw_iw' => SMW_SQL3_SMWREDIIW ], + [ 'smw_id' => $sid ], + __METHOD__ + ); + + $this->store->getObjectIds()->setCache( + $subject_t, + $subject_ns, + '', + '', + 0, + '' + ); + + $this->store->getObjectIds()->setCache( + $subject_t, + $subject_ns, + SMW_SQL3_SMWREDIIW, + '', + $sid, + $sid_sort + ); + } + } + + $this->store->getObjectIds()->addRedirect( + $new_tid, + $subject_t, + $subject_ns + ); + + $count++; + + } else { // delete old redirect + // This case implies $old_tid != 0 (or we would have new_tid == old_tid above). + // Therefore $subject had a redirect, and it must also have an ID. + // This shows that $sid != 0 here. + if ( $smwgQEqualitySupport != SMW_EQ_NONE ) { // mark subject as non-redirect + + $db->update( + SMWSql3SmwIds::TABLE_NAME, + [ 'smw_iw' => '' ], + [ 'smw_id' => $sid ], + __METHOD__ + ); + + $this->store->getObjectIds()->setCache( + $subject_t, + $subject_ns, + SMW_SQL3_SMWREDIIW, + '', + 0, + '' + ); + + $this->store->getObjectIds()->setCache( + $subject_t, + $subject_ns, + '', + '', + $sid, + $sid_sort + ); + } + } + + // *** Flush some caches to be safe, though they are not essential in runs with redirect updates ***// + $this->semanticDataLookup->invalidateCache( $sid ); + $this->semanticDataLookup->invalidateCache( $new_tid ); + $this->semanticDataLookup->invalidateCache( $old_tid ); + + // *** Update reference count for _REDI property ***// + $propertyStatisticsStore = $this->factory->newPropertyStatisticsStore(); + + $propertyStatisticsStore->addToUsageCount( + $this->store->getObjectIds()->getSMWPropertyID( new SMW\DIProperty( '_REDI' ) ), + $count + ); + + return ( $new_tid == 0 ) ? $sid : $new_tid; + } + +} diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_Sql3SmwIds.php b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_Sql3SmwIds.php new file mode 100644 index 00000000..b8c402b0 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/storage/SQLStore/SMW_Sql3SmwIds.php @@ -0,0 +1,1296 @@ +<?php + +use SMW\DIProperty; +use SMW\DIWikiPage; +use SMW\MediaWiki\Collator; +use SMW\PropertyRegistry; +use SMW\RequestOptions; +use SMW\SQLStore\EntityStore\IdCacheManager; +use SMW\SQLStore\IdToDataItemMatchFinder; +use SMW\SQLStore\RedirectStore; +use SMW\SQLStore\SQLStore; +use SMW\SQLStore\SQLStoreFactory; +use SMW\SQLStore\TableFieldUpdater; +use SMWDataItem as DataItem; +use SMW\MediaWiki\Jobs\UpdateJob; +use SMW\MediaWiki\Connection\Sequence; +use SMW\TypesRegistry; + +/** + * @ingroup SMWStore + * @since 1.8 + * @author Markus Krötzsch + */ + +/** + * Class to access the SMW IDs table in SQLStore3. + * Provides transparent in-memory caching facilities. + * + * Documentation for the SMW IDs table: This table is a dictionary that + * assigns integer IDs to pages, properties, and other objects used by SMW. + * All tables that refer to such objects store these IDs instead. If the ID + * information is lost (e.g., table gets deleted), then the data stored in SMW + * is no longer meaningful: all tables need to be dropped, recreated, and + * refreshed to get back to a working database. + * + * The table has a column for storing interwiki prefixes, used to refer to + * pages on external sites (like in MediaWiki). This column is also used to + * mark some special objects in the table, using "interwiki prefixes" that + * cannot occur in MediaWiki: + * + * - Rows with iw SMW_SQL3_SMWREDIIW are similar to normal entries for + * (internal) wiki pages, but the iw indicates that the page is a redirect, the + * (target of which should be sought using the smw_fpt_redi table. + * + * - The (unique) row with iw SMW_SQL3_SMWBORDERIW just marks the border + * between predefined ids (rows that are reserved for hardcoded ids built into + * SMW) and normal entries. It is no object, but makes sure that SQL's auto + * increment counter is high enough to not add any objects before that marked + * "border". + * + * @note Do not call the constructor of SMWDIWikiPage using data from the SMW + * IDs table; use SMWDIHandlerWikiPage::dataItemFromDBKeys() instead. The table + * does not always contain data as required wiki pages. Especially predefined + * properties are represented by language-independent keys rather than proper + * titles. SMWDIHandlerWikiPage takes care of this. + * + * @since 1.8 + * + * @ingroup SMWStore + */ +class SMWSql3SmwIds { + + /** + * Specifies the border limit for pre-defined properties declared + * in SMWSql3SmwIds::special_ids + */ + const FXD_PROP_BORDER_ID = SMWSQLStore3::FIXED_PROPERTY_ID_UPPERBOUND; + + /** + * Name of the table to store IDs in. + * + * @note This should never change. Existing wikis will have to drop and + * rebuild their SMW tables completely to recover from any change here. + */ + const TABLE_NAME = SMWSQLStore3::ID_TABLE; + + const MAX_CACHE_SIZE = 1000; + const POOLCACHE_ID = 'smw.sqlstore'; + + /** + * Parent SMWSQLStore3. + * + * @since 1.8 + * @var SMWSQLStore3 + */ + public $store; + + /** + * @var SQLStoreFactory + */ + private $factory; + + /** + * @var IdToDataItemMatchFinder + */ + private $idMatchFinder; + + /** + * @var RedirectStore + */ + private $redirectStore; + + /** + * @var TableFieldUpdater + */ + private $tableFieldUpdater; + + /** + * @var array + */ + public static $special_ids = []; + + /** + * @var IdCacheManager + */ + private $idCacheManager; + + /** + * @var IdEntityFinder + */ + private $idEntityFinder; + + /** + * @var IdChanger + */ + private $idChanger; + + /** + * @var UniquenessLookup + */ + private $uniquenessLookup; + + /** + * @since 1.8 + * @param SMWSQLStore3 $store + */ + public function __construct( SMWSQLStore3 $store, SQLStoreFactory $factory ) { + $this->store = $store; + $this->factory = $factory; + $this->initCache(); + + $this->idEntityFinder = $this->factory->newIdEntityFinder( + $this->idCacheManager + ); + + $this->redirectStore = $this->factory->newRedirectStore(); + $this->uniquenessLookup = $this->factory->newUniquenessLookup(); + + $this->tableFieldUpdater = $this->factory->newTableFieldUpdater(); + $this->idChanger = $this->factory->newIdChanger(); + + self::$special_ids = TypesRegistry::getFixedPropertyIdList(); + } + + /** + * @since 2.1 + * + * @param DIWikiPage $subject + * + * @return boolean + */ + public function isRedirect( DIWikiPage $subject ) { + return $this->redirectStore->isRedirect( $subject->getDBKey(), $subject->getNamespace() ); + } + + /** + * @since 3.0 + * + * @param DataItem $dataItem + * + * @return boolean + */ + public function isUnique( DataItem $dataItem ) { + return $this->uniquenessLookup->isUnique( $dataItem ); + } + + /** + * @see RedirectStore::findRedirect + * + * @since 2.1 + * + * @param string $title DB key + * @param integer $namespace + * + * @return integer + */ + public function findRedirect( $title, $namespace ) { + return $this->redirectStore->findRedirect( $title, $namespace ); + } + + /** + * @see RedirectStore::addRedirect + * + * @since 2.1 + * + * @param integer $id + * @param string $title + * @param integer $namespace + */ + public function addRedirect( $id, $title, $namespace ) { + $this->redirectStore->addRedirect( $id, $title, $namespace ); + } + + /** + * @see RedirectStore::updateRedirect + * + * @since 3.0 + * + * @param integer $id + * @param string $title + * @param integer $namespace + */ + public function updateRedirect( $id, $title, $namespace ) { + $this->redirectStore->updateRedirect( $id, $title, $namespace ); + } + + /** + * @see RedirectStore::deleteRedirect + * + * @since 2.1 + * + * @param string $title + * @param integer $namespace + */ + public function deleteRedirect( $title, $namespace ) { + $this->redirectStore->deleteRedirect( $title, $namespace ); + } + + /** + * Find the numeric ID used for the page of the given title, + * namespace, interwiki, and subobject. If $canonical is set to true, + * redirects are taken into account to find the canonical alias ID for + * the given page. If no such ID exists, 0 is returned. The Call-By-Ref + * parameter $sortkey is set to the current sortkey, or to '' if no ID + * exists. + * + * If $fetchhashes is true, the property table hash blob will be + * retrieved in passing if the opportunity arises, and cached + * internally. This will speed up a subsequent call to + * getPropertyTableHashes() for this id. This should only be done + * if such a call is intended, both to safe the previous cache and + * to avoid extra work (even if only a little) to fill it. + * + * @since 1.8 + * @param string $title DB key + * @param integer $namespace namespace + * @param string $iw interwiki prefix + * @param string $subobjectName name of subobject + * @param string $sortkey call-by-ref will be set to sortkey + * @param boolean $canonical should redirects be resolved? + * @param boolean $fetchHashes should the property hashes be obtained and cached? + * @return integer SMW id or 0 if there is none + */ + public function getSMWPageIDandSort( $title, $namespace, $iw, $subobjectName, &$sortkey, $canonical, $fetchHashes = false ) { + $id = $this->getPredefinedData( $title, $namespace, $iw, $subobjectName, $sortkey ); + if ( $id != 0 ) { + return (int)$id; + } else { + return (int)$this->getDatabaseIdAndSort( $title, $namespace, $iw, $subobjectName, $sortkey, $canonical, $fetchHashes ); + } + } + + /** + * Find the numeric ID used for the page of the given normalized title, + * namespace, interwiki, and subobjectName. Predefined IDs are not + * taken into account (however, they would still be found correctly by + * an avoidable database read if they are stored correctly in the + * database; this should always be the case). In all other aspects, the + * method works just like getSMWPageIDandSort(). + * + * @since 1.8 + * @param string $title DB key + * @param integer $namespace namespace + * @param string $iw interwiki prefix + * @param string $subobjectName name of subobject + * @param string $sortkey call-by-ref will be set to sortkey + * @param boolean $canonical should redirects be resolved? + * @param boolean $fetchHashes should the property hashes be obtained and cached? + * @return integer SMW id or 0 if there is none + */ + protected function getDatabaseIdAndSort( $title, $namespace, $iw, $subobjectName, &$sortkey, $canonical, $fetchHashes ) { + global $smwgQEqualitySupport; + + $db = $this->store->getConnection( 'mw.db' ); + + // Integration test "query-04-02-subproperty-dc-import-marc21.json" + // showed a deterministic failure (due to a wrong cache id during querying + // for redirects) hence we force to read directly from the RedirectStore + // for objects marked as redirect + if ( $iw === SMW_SQL3_SMWREDIIW && $canonical && + $smwgQEqualitySupport !== SMW_EQ_NONE && $subobjectName === '' ) { + $id = $this->findRedirect( $title, $namespace ); + } else { + $id = $this->idCacheManager->getId( [ $title, (int)$namespace, $iw, $subobjectName ] ); + } + + if ( $id !== false && $id != 0 ) { // cache hit + $sortkey = $this->idCacheManager->getSort( [ $title, (int)$namespace, $iw, $subobjectName ] ); + } elseif ( $iw == SMW_SQL3_SMWREDIIW && $canonical && + $smwgQEqualitySupport != SMW_EQ_NONE && $subobjectName === '' ) { + $id = $this->findRedirect( $title, $namespace ); + if ( $id != 0 ) { + + if ( $fetchHashes ) { + $select = [ 'smw_sortkey', 'smw_sort', 'smw_proptable_hash' ]; + } else { + $select = [ 'smw_sortkey', 'smw_sort' ]; + } + + $row = $db->selectRow( + self::TABLE_NAME, + $select, + [ 'smw_id' => $id ], + __METHOD__ + ); + + if ( $row !== false ) { + // Make sure that smw_sort is being re-computed in case it is null + $sortkey = $row->smw_sort === null ? '' : $row->smw_sortkey; + if ( $fetchHashes ) { + $this->setPropertyTableHashesCache( $id, $row->smw_proptable_hash ); + } + } else { // inconsistent DB; just recover somehow + $sortkey = str_replace( '_', ' ', $title ); + } + } else { + $sortkey = ''; + } + $this->setCache( $title, $namespace, $iw, $subobjectName, $id, $sortkey ); + } else { + + if ( $fetchHashes ) { + $select = [ 'smw_id', 'smw_sortkey', 'smw_sort', 'smw_proptable_hash' ]; + } else { + $select = [ 'smw_id', 'smw_sortkey', 'smw_sort' ]; + } + + // #2001 + // In cases where title components are excessively long (beyond the + // field limit) it has been observed that at least on MySQL/MariaDB no + // appropriate matches are found even though a row with a truncated + // representation exists in the table. + // + // `postgres` has no field limit and a divergent behaviour has not + // been observed + if ( $subobjectName !== '' && !$db->isType( 'postgres' ) ) { + $subobjectName = mb_substr( $subobjectName, 0, 255 ); + } + + $row = $db->selectRow( + self::TABLE_NAME, + $select, + [ + 'smw_title' => $title, + 'smw_namespace' => $namespace, + 'smw_iw' => $iw, + 'smw_subobject' => $subobjectName + ], + __METHOD__ + ); + + //$this->selectrow_sort_debug++; + + if ( $row !== false ) { + $id = $row->smw_id; + // Make sure that smw_sort is being re-computed in case it is null + $sortkey = $row->smw_sort === null ? '' : $row->smw_sortkey; + if ( $fetchHashes ) { + $this->setPropertyTableHashesCache( $id, $row->smw_proptable_hash); + } + } else { + $id = 0; + $sortkey = ''; + } + + $this->setCache( + $title, + $namespace, + $iw, + $subobjectName, + $id, + $sortkey + ); + } + + if ( $id == 0 && $subobjectName === '' && $iw === '' ) { // could be a redirect; check + $id = $this->getSMWPageIDandSort( + $title, + $namespace, + SMW_SQL3_SMWREDIIW, + $subobjectName, + $sortkey, + $canonical, + $fetchHashes + ); + } + + return $id; + } + + /** + * @since 3.0 + * + * @return [] + */ + public function findDuplicates() { + return $this->uniquenessLookup->findDuplicates(); + } + + /** + * @since 2.3 + * + * @param string $title DB key + * @param integer $namespace namespace + * @param string|null $iw interwiki prefix + * @param string $subobjectName name of subobject + * + * @param array + */ + public function findAllEntitiesThatMatch( $title, $namespace, $iw = null, $subobjectName = '' ) { + + $matches = []; + $query = []; + + $query['fields'] = ['smw_id']; + + $query['conditions'] = [ + 'smw_title' => $title, + 'smw_namespace' => $namespace, + 'smw_iw' => $iw, + 'smw_subobject' => $subobjectName + ]; + + // Null means select all (incl. those marked delete, redi etc.) + if ( $iw === null ) { + unset( $query['conditions']['smw_iw'] ); + } + + $connection = $this->store->getConnection( 'mw.db' ); + + $rows = $connection->select( + $connection->tableName( self::TABLE_NAME ), + $query['fields'], + $query['conditions'], + __METHOD__ + ); + + if ( $rows === false ) { + return $matches; + } + + foreach ( $rows as $row ) { + $matches[] = (int)$row->smw_id; + } + + return $matches; + } + + /** + * @since 2.4 + * + * @param DIWikiPage $subject + * + * @param boolean + */ + public function exists( DIWikiPage $subject ) { + return $this->getId( $subject ) > 0; + } + + /** + * @note SMWSql3SmwIds::getSMWPageID has some issues with the cache as it returned + * 0 even though an object was matchable, using this method is safer then trying + * to encipher getSMWPageID related methods. + * + * It uses the PoolCache which means Lru is in place to avoid memory leakage. + * + * @since 2.4 + * + * @param DIWikiPage $subject + * + * @param integer + */ + public function getId( DIWikiPage $subject ) { + + // Try to match a predefined property + if ( $subject->getNamespace() === SMW_NS_PROPERTY && $subject->getInterWiki() === '' ) { + $property = DIProperty::newFromUserLabel( $subject->getDBKey() ); + $key = $property->getKey(); + + // Has a fixed ID? + if ( isset( self::$special_ids[$key] ) && $subject->getSubobjectName() === '' ) { + return self::$special_ids[$key]; + } + + // Switch title for fixed properties without a fixed ID (e.g. _MIME is the smw_title) + if ( !$property->isUserDefined() ) { + $subject = new DIWikiPage( + $key, + SMW_NS_PROPERTY, + $subject->getInterWiki(), + $subject->getSubobjectName() + ); + } + } + + if ( ( $id = $this->idCacheManager->getId( $subject ) ) !== false ) { + return $id; + } + + $id = 0; + + $row = $this->store->getConnection( 'mw.db' )->selectRow( + self::TABLE_NAME, + [ 'smw_id' ], + [ + 'smw_title' => $subject->getDBKey(), + 'smw_namespace' => $subject->getNamespace(), + 'smw_iw' => $subject->getInterWiki(), + 'smw_subobject' => $subject->getSubobjectName() + ], + __METHOD__ + ); + + if ( $row !== false ) { + $id = $row->smw_id; + + // Legacy + $this->setCache( + $subject->getDBKey(), + $subject->getNamespace(), + $subject->getInterWiki(), + $subject->getSubobjectName(), + $id, + $subject->getSortKey() + ); + } + + return $id; + } + + /** + * Convenience method for calling getSMWPageIDandSort without + * specifying a sortkey (if not asked for). + * + * @since 1.8 + * @param string $title DB key + * @param integer $namespace namespace + * @param string $iw interwiki prefix + * @param string $subobjectName name of subobject + * @param boolean $canonical should redirects be resolved? + * @param boolean $fetchHashes should the property hashes be obtained and cached? + * @return integer SMW id or 0 if there is none + */ + public function getSMWPageID( $title, $namespace, $iw, $subobjectName, $canonical = true, $fetchHashes = false ) { + $sort = ''; + return $this->getSMWPageIDandSort( $title, $namespace, $iw, $subobjectName, $sort, $canonical, $fetchHashes ); + } + + /** + * Find the numeric ID used for the page of the given title, namespace, + * interwiki, and subobjectName. If $canonical is set to true, + * redirects are taken into account to find the canonical alias ID for + * the given page. If no such ID exists, a new ID is created and + * returned. In any case, the current sortkey is set to the given one + * unless $sortkey is empty. + * + * @note Using this with $canonical==false can make sense, especially when + * the title is a redirect target (we do not want chains of redirects). + * But it is of no relevance if the title does not have an id yet. + * + * @since 1.8 + * @param string $title DB key + * @param integer $namespace namespace + * @param string $iw interwiki prefix + * @param string $subobjectName name of subobject + * @param boolean $canonical should redirects be resolved? + * @param string $sortkey call-by-ref will be set to sortkey + * @param boolean $fetchHashes should the property hashes be obtained and cached? + * @return integer SMW id or 0 if there is none + */ + public function makeSMWPageID( $title, $namespace, $iw, $subobjectName, $canonical = true, $sortkey = '', $fetchHashes = false ) { + $id = $this->getPredefinedData( $title, $namespace, $iw, $subobjectName, $sortkey ); + if ( $id != 0 ) { + return (int)$id; + } else { + return (int)$this->makeDatabaseId( $title, $namespace, $iw, $subobjectName, $canonical, $sortkey, $fetchHashes ); + } + } + + /** + * Find the numeric ID used for the page of the given normalized title, + * namespace, interwiki, and subobjectName. Predefined IDs are not + * taken into account (however, they would still be found correctly by + * an avoidable database read if they are stored correctly in the + * database; this should always be the case). In all other aspects, the + * method works just like makeSMWPageID(). Especially, if no ID exists, + * a new ID is created and returned. + * + * @since 1.8 + * @param string $title DB key + * @param integer $namespace namespace + * @param string $iw interwiki prefix + * @param string $subobjectName name of subobject + * @param boolean $canonical should redirects be resolved? + * @param string $sortkey call-by-ref will be set to sortkey + * @param boolean $fetchHashes should the property hashes be obtained and cached? + * @return integer SMW id or 0 if there is none + */ + protected function makeDatabaseId( $title, $namespace, $iw, $subobjectName, $canonical, $sortkey, $fetchHashes ) { + + $oldsort = ''; + $id = $this->getDatabaseIdAndSort( $title, $namespace, $iw, $subobjectName, $oldsort, $canonical, $fetchHashes ); + $db = $this->store->getConnection( 'mw.db' ); + $collator = Collator::singleton(); + + // Safeguard to ensure that no duplicate IDs are created + if ( $id == 0 ) { + $id = $this->getId( new DIWikiPage( $title, $namespace, $iw, $subobjectName ) ); + } + + $db->beginAtomicTransaction( __METHOD__ ); + + if ( $id == 0 ) { + $sortkey = $sortkey ? $sortkey : ( str_replace( '_', ' ', $title ) ); + + // Bug 42659 + $sequenceValue = $db->nextSequenceValue( + Sequence::makeSequence( SQLStore::ID_TABLE, 'smw_id' ) + ); + + // #2089 (MySQL 5.7 complained with "Data too long for column") + $sortkey = mb_substr( $sortkey, 0, 254 ); + + $db->insert( + self::TABLE_NAME, + [ + 'smw_id' => $sequenceValue, + 'smw_title' => $title, + 'smw_namespace' => $namespace, + 'smw_iw' => $iw, + 'smw_subobject' => $subobjectName, + 'smw_sortkey' => $sortkey, + 'smw_sort' => $collator->getSortKey( $sortkey ), + 'smw_hash' => $this->computeSha1( [ $title, (int)$namespace, $iw, $subobjectName ] ) + ], + __METHOD__ + ); + + $id = (int)$db->insertId(); + + // Properties also need to be in the property statistics table + if( $namespace === SMW_NS_PROPERTY ) { + + $propertyStatisticsStore = $this->factory->newPropertyStatisticsStore( + $db + ); + + $propertyStatisticsStore->insertUsageCount( $id, 0 ); + } + + $this->setCache( $title, $namespace, $iw, $subobjectName, $id, $sortkey ); + + if ( $fetchHashes ) { + $this->setPropertyTableHashesCache( $id, null ); + } + + } elseif ( $sortkey !== '' && ( $sortkey != $oldsort || !$collator->isIdentical( $oldsort, $sortkey ) ) ) { + $this->tableFieldUpdater->updateSortField( $id, $sortkey ); + $this->setCache( $title, $namespace, $iw, $subobjectName, $id, $sortkey ); + } + + $db->endAtomicTransaction( __METHOD__ ); + + return $id; + } + + /** + * Properties have a mechanisms for being predefined (i.e. in PHP instead + * of in wiki). Special "interwiki" prefixes separate the ids of such + * predefined properties from the ids for the current pages (which may, + * e.g., be moved, while the predefined object is not movable). + * + * @todo This documentation is out of date. Right now, the special + * interwiki is used only for special properties without a label, i.e., + * which cannot be shown to a user. This allows us to filter such cases + * from all queries that retrieve lists of properties. It should be + * checked that this is really the only use that this has throughout + * the code. + * + * @since 1.8 + * @param SMWDIProperty $property + * @return string + */ + public function getPropertyInterwiki( SMWDIProperty $property ) { + return ( $property->getLabel() !== '' ) ? '' : SMW_SQL3_SMWINTDEFIW; + } + + /** + * @since 2.1 + * + * @param integer $sid + * @param DIWikiPage $subject + * @param integer|string|null $interwiki + */ + public function updateInterwikiField( $sid, DIWikiPage $subject, $interwiki = null ) { + + $connection = $this->store->getConnection( 'mw.db' ); + + if ( $interwiki === null ) { + $interwiki = $subject->getInterWiki(); + } + + $hash = [ + $subject->getDBKey(), + (int)$subject->getNamespace(), + $interwiki, + $subject->getSubobjectName() + ]; + + $connection->update( + self::TABLE_NAME, + [ + 'smw_iw' => $interwiki, + 'smw_hash' => $this->computeSha1( $hash ) + ], + [ 'smw_id' => $sid ], + __METHOD__ + ); + + $this->setCache( + $subject->getDBKey(), + $subject->getNamespace(), + $subject->getInterWiki(), + $subject->getSubobjectName(), + $sid, + $subject->getSortKey() + ); + } + + /** + * @since 3.0 + * + * @param string $title + * @param integer $namespace + * @param string $iw + */ + public function findAssociatedRev( $title, $namespace, $iw = '' ) { + $connection = $this->store->getConnection( 'mw.db' ); + + $row = $connection->selectRow( + self::TABLE_NAME, + 'smw_rev', + [ + "smw_title =" . $connection->addQuotes( $title ), + "smw_namespace =" . $connection->addQuotes( $namespace ), + "smw_iw =" . $connection->addQuotes( $iw ), + "smw_subobject =''" + ], + __METHOD__ + ); + + return $row === false ? 0 : $row->smw_rev; + } + + /** + * @since 3.0 + * + * @param integer $sid + * @param integer $sid + */ + public function updateRevField( $sid, $rev_id ) { + $this->tableFieldUpdater->updateRevField( $sid, $rev_id ); + } + + /** + * Fetch the ID for an SMWDIProperty object. This method achieves the + * same as getSMWPageID(), but avoids additional normalization steps + * that have already been performed when creating an SMWDIProperty + * object. + * + * @note There is no distinction between properties and inverse + * properties here. A property and its inverse have the same ID in SMW. + * + * @param SMWDIProperty $property + * @return integer + */ + public function getSMWPropertyID( SMWDIProperty $property ) { + if ( array_key_exists( $property->getKey(), self::$special_ids ) ) { + return self::$special_ids[$property->getKey()]; + } else { + $sortkey = ''; + return $this->getDatabaseIdAndSort( $property->getKey(), SMW_NS_PROPERTY, $this->getPropertyInterwiki( $property ), '', $sortkey, true, false ); + } + } + + /** + * Fetch and possibly create the ID for an SMWDIProperty object. The + * method achieves the same as getSMWPageID() but avoids additional + * normalization steps that have already been performed when creating + * an SMWDIProperty object. + * + * @see getSMWPropertyID + * @param SMWDIProperty $property + * @return integer + */ + public function makeSMWPropertyID( SMWDIProperty $property ) { + if ( array_key_exists( $property->getKey(), self::$special_ids ) ) { + return (int)self::$special_ids[$property->getKey()]; + } else { + return (int)$this->makeDatabaseId( + $property->getKey(), + SMW_NS_PROPERTY, + $this->getPropertyInterwiki( $property ), + '', + true, + $property->getLabel(), + false + ); + } + } + + /** + * Normalize the information for an SMW object (page etc.) and return + * the predefined ID if any. All parameters are call-by-reference and + * will be changed to perform any kind of built-in normalization that + * SMW requires. This mainly applies to predefined properties that + * should always use their property key as a title, have fixed + * sortkeys, etc. Some very special properties also have fixed IDs that + * do not require any DB lookups. In such cases, the method returns + * this ID; otherwise it returns 0. + * + * @note This function could be extended to account for further kinds + * of normalization and predefined ID. However, both getSMWPropertyID + * and makeSMWPropertyID must then also be adjusted to do the same. + * + * @since 1.8 + * @param string $title DB key + * @param integer $namespace namespace + * @param string $iw interwiki prefix + * @param string $subobjectName + * @param string $sortkey + * @return integer predefined id or 0 if none + */ + protected function getPredefinedData( &$title, &$namespace, &$iw, &$subobjectName, &$sortkey ) { + if ( $namespace == SMW_NS_PROPERTY && + ( $iw === '' || $iw == SMW_SQL3_SMWINTDEFIW ) && $title != '' ) { + + // Check if this is a predefined property: + if ( $title{0} != '_' ) { + // This normalization also applies to + // subobjects of predefined properties. + $newTitle = PropertyRegistry::getInstance()->findPropertyIdByLabel( str_replace( '_', ' ', $title ) ); + if ( $newTitle ) { + $title = $newTitle; + $sortkey = PropertyRegistry::getInstance()->findPropertyLabelById( $title ); + if ( $sortkey === '' ) { + $iw = SMW_SQL3_SMWINTDEFIW; + } + } + } + + // Check if this is a property with a fixed SMW ID: + if ( $subobjectName === '' && array_key_exists( $title, self::$special_ids ) ) { + return self::$special_ids[$title]; + } + } + + return 0; + } + + /** + * Change an internal id to another value. If no target value is given, the + * value is changed to become the last id entry (based on the automatic id + * increment of the database). Whatever currently occupies this id will be + * moved consistently in all relevant tables. Whatever currently occupies + * the target id will be ignored (it should be ensured that nothing is + * moved to an id that is still in use somewhere). + * + * @since 1.8 + * @param integer $curid + * @param integer $targetid + */ + public function moveSMWPageID( $curid, $targetid = 0 ) { + $db = $this->store->getConnection(); + + $row = $db->selectRow( + self::TABLE_NAME, + '*', + [ 'smw_id' => $curid ], + __METHOD__ + ); + + if ( $row === false ) { + return; // no id at current position, ignore + } + + $db->beginAtomicTransaction( __METHOD__ ); + + if ( $targetid == 0 ) { // append new id + + // Bug 42659 + $sequenceValue = $db->nextSequenceValue( + Sequence::makeSequence( SQLStore::ID_TABLE, 'smw_id' ) + ); + + $db->insert( + self::TABLE_NAME, + [ + 'smw_id' => $sequenceValue, + 'smw_title' => $row->smw_title, + 'smw_namespace' => $row->smw_namespace, + 'smw_iw' => $row->smw_iw, + 'smw_subobject' => $row->smw_subobject, + 'smw_sortkey' => $row->smw_sortkey, + 'smw_sort' => $row->smw_sort + ], + __METHOD__ + ); + + $targetid = $db->insertId(); + } else { // change to given id + $db->insert( + self::TABLE_NAME, + [ 'smw_id' => $targetid, + 'smw_title' => $row->smw_title, + 'smw_namespace' => $row->smw_namespace, + 'smw_iw' => $row->smw_iw, + 'smw_subobject' => $row->smw_subobject, + 'smw_sortkey' => $row->smw_sortkey, + 'smw_sort' => $row->smw_sort + ], + __METHOD__ + ); + } + + $db->delete( + self::TABLE_NAME, + [ + 'smw_id' => $curid + ], + __METHOD__ + ); + + $this->setCache( + $row->smw_title, + $row->smw_namespace, + $row->smw_iw, + $row->smw_subobject, + $targetid, + $row->smw_sortkey + ); + + $this->idChanger->change( + $curid, + $targetid, + $row->smw_namespace, + $row->smw_namespace + ); + + $db->endAtomicTransaction( __METHOD__ ); + + if ( ( $title = \Title::newFromText( $row->smw_title, $row->smw_namespace ) ) !== null ) { + $updateJob = new UpdateJob( + $title + ); + + $updateJob->insert(); + } + } + + /** + * @since 3.0 + * + * @param string|array $args + * + * @return string + */ + public function computeSha1( $args = '' ) { + return IdCacheManager::computeSha1( $args ); + } + + /** + * @since 3.0 + * + * @param array $list + */ + public function warmUpCache( $list = [] ) { + + $hashList = []; + + if ( $list instanceof \SMWQueryResult ) { + $list = $list->getResults(); + } + + if ( !$list instanceof \Iterator && !is_array( $list ) ) { + return; + } + + foreach ( $list as $item ) { + + $hash = null; + + if ( $item instanceof DIWikiPage ) { + $hash = [ + $item->getDBKey(), + (int)$item->getNamespace(), + $item->getInterwiki(), + $item->getSubobjectName() + ]; + } + + if ( $item instanceof DIProperty ) { + + // Avoid _SKEY as it is not used during an entity lookup to + // match an ID + if ( $item->getKey() === '_SKEY' ) { + continue; + } + + $hash = [ $item->getKey(), SMW_NS_PROPERTY, '', '' ]; + } + + if ( $hash === null ) { + continue; + } + + $hash = IdCacheManager::computeSha1( $hash ); + + if ( !$this->idCacheManager->hasCache( $hash ) ) { + $hashList[] = $hash; + } + } + + if ( $hashList === [] ) { + return; + } + + $connection = $this->store->getConnection( 'mw.db' ); + + $rows = $connection->select( + SQLStore::ID_TABLE, + [ + 'smw_id', + 'smw_title', + 'smw_namespace', + 'smw_iw', + 'smw_subobject', + 'smw_sortkey', + 'smw_sort' + ], + [ + 'smw_hash' => $hashList + ], + __METHOD__ + ); + + foreach ( $rows as $row ) { + $sortkey = $row->smw_sort === null ? '' : $row->smw_sortkey; + + $this->idCacheManager->setCache( + $row->smw_title, + $row->smw_namespace, + $row->smw_iw, + $row->smw_subobject, + $row->smw_id, + $sortkey + ); + } + } + + /** + * Add or modify a cache entry. The key consists of the + * parameters $title, $namespace, $interwiki, and $subobject. The + * cached data is $id and $sortkey. + * + * @since 1.8 + * @param string $title + * @param integer $namespace + * @param string $interwiki + * @param string $subobject + * @param integer $id + * @param string $sortkey + */ + public function setCache( $title, $namespace, $interwiki, $subobject, $id, $sortkey ) { + $this->idCacheManager->setCache( $title, $namespace, $interwiki, $subobject, $id, $sortkey ); + } + + /** + * @since 2.1 + * + * @param integer $id + * + * @return DIWikiPage|null + */ + public function getDataItemById( $id ) { + return $this->idEntityFinder->getDataItemById( $id ); + } + + /** + * @since 2.3 + * + * @param integer $id + * @param RequestOptions|null $requestOptions + * + * @return string[] + */ + public function getDataItemsFromList( array $idlist, RequestOptions $requestOptions = null ) { + return $this->idEntityFinder->getDataItemsFromList( $idlist, $requestOptions ); + } + + /** + * @deprecated since 3.0, use SMWSql3SmwIds::getDataItemsFromList + */ + public function getDataItemPoolHashListFor( array $idlist, RequestOptions $requestOptions = null ) { + return $this->idEntityFinder->getDataItemsFromList( $idlist, $requestOptions ); + } + + /** + * Remove any cache entry for the given data. The key consists of the + * parameters $title, $namespace, $interwiki, and $subobject. The + * cached data is $id and $sortkey. + * + * @since 1.8 + * @param string $title + * @param integer $namespace + * @param string $interwiki + * @param string $subobject + */ + public function deleteCache( $title, $namespace, $interwiki, $subobject ) { + $this->idCacheManager->deleteCache( $title, $namespace, $interwiki, $subobject ); + } + + /** + * Move all cached information about subobjects. + * + * @todo This method is neither efficient nor very convincing + * architecturally; it should be redesigned. + * + * @since 1.8 + * @param string $oldtitle + * @param integer $oldnamespace + * @param string $newtitle + * @param integer $newnamespace + */ + public function moveSubobjects( $oldtitle, $oldnamespace, $newtitle, $newnamespace ) { + // Currently we have no way to change title and namespace across all entries. + // Best we can do is clear the cache to avoid wrong hits: + if ( $oldnamespace != SMW_NS_PROPERTY || $newnamespace != SMW_NS_PROPERTY ) { + $this->idCacheManager->deleteCache( $oldtitle, $oldnamespace, '', '' ); + $this->idCacheManager->deleteCache( $newtitle, $newnamespace, '', '' ); + } + } + + /** + * @since 3.0 + */ + public function initCache() { + + // Tests indicate that it is more memory efficient to have two + // arrays (IDs and sortkeys) than to have one array that stores both + // values in some data structure (other than a single string). + $this->idCacheManager = $this->factory->newIdCacheManager( + self::POOLCACHE_ID, + [ + 'entity.id' => self::MAX_CACHE_SIZE, + 'entity.sort' => self::MAX_CACHE_SIZE, + 'entity.lookup' => 2000, + 'table.hash' => self::MAX_CACHE_SIZE, + ] + ); + } + + /** + * Return an array of hashes with table names as keys. These + * hashes are used to compare new data with old data for each + * property-value table when updating data + * + * @since 1.8 + * + * @param integer $subjectId ID of the page as stored in the SMW IDs table + * + * @return array + */ + public function getPropertyTableHashes( $sid ) { + + if ( $sid == 0 ) { + return []; + } + + $hash = null; + $cache = $this->idCacheManager->get( 'table.hash' ); + + if ( ( $hash = $cache->fetch( $sid ) ) !== false ) { + return $hash; + } + + $connection = $this->store->getConnection( 'mw.db' ); + + $row = $connection->selectRow( + self::TABLE_NAME, + [ 'smw_proptable_hash' ], + 'smw_id=' . $sid, + __METHOD__ + ); + + if ( $row !== false ) { + $hash = $row->smw_proptable_hash; + } + + if ( $hash !== null && $hash !== false && $connection->isType( 'postgres' ) ) { + $hash = pg_unescape_bytea( $hash ); + } + + $hash = $hash === null || $hash === false ? [] : unserialize( $hash ); + $cache->save( $sid, $hash ); + + return $hash; + } + + /** + * Update the proptable_hash for a given page. + * + * @since 1.8 + * @param integer $sid ID of the page as stored in SMW IDs table + * @param string[] of hash values with table names as keys + */ + public function setPropertyTableHashes( $sid, $hash = null ) { + + $connection = $this->store->getConnection( 'mw.db' ); + $update = []; + + if ( $hash === null ) { + $update = [ 'smw_proptable_hash' => $hash, 'smw_rev' => null ]; + } elseif ( is_array( $hash ) ) { + $update = [ 'smw_proptable_hash' => serialize( $hash ) ]; + } else { + throw new RuntimeException( "Expected a null or an array as value!"); + } + + $connection->update( + self::TABLE_NAME, + $update, + [ 'smw_id' => $sid ], + __METHOD__ + ); + + $this->setPropertyTableHashesCache( $sid, $hash ); + + if ( $hash === null ) { + $this->idCacheManager->deleteCacheById( $sid ); + } + } + + /** + * Temporarily cache a property tablehash that has been retrieved for + * the given SMW ID. + * + * @since 1.8 + * @param $id integer + * @param $propertyTableHash string + */ + /** + * Temporarily cache a property tablehash that has been retrieved for + * the given SMW ID. + * + * @since 1.8 + * @param $id integer + * @param $propertyTableHash string + */ + protected function setPropertyTableHashesCache( $sid, $hash ) { + + // never cache 0 + if ( $sid == 0 ) { + return; + } + + if ( $hash === null ) { + $hash = []; + } elseif ( is_string( $hash ) ) { + $hash = unserialize( $hash ); + } + + $cache = $this->idCacheManager->get( 'table.hash' ); + $cache->save( $sid, $hash ); + } + + /** + * Returns store Id table name + * + * @return string + */ + public function getIdTable() { + return self::TABLE_NAME; + } + +} |