summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/SemanticMediaWiki/src
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/SemanticMediaWiki/src
first commit
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/src')
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Aliases.php101
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ApplicationFactory.php591
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/CacheFactory.php169
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/CachedPropertyValuesPrefetcher.php177
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ChangePropListener.php138
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/CompatibilityMode.php75
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Connection/CallbackConnectionProvider.php53
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionManager.php73
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionProvider.php30
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionProviderRef.php66
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataItemFactory.php169
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataModel/ContainerSemanticData.php156
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataModel/SubSemanticData.php287
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataTypeRegistry.php616
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataUpdater.php430
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValueFactory.php403
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/AbstractMultiValue.php171
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsListValue.php107
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsPatternValue.php113
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsValue.php27
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/BooleanValue.php256
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ErrorMsgTextValue.php111
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ExternalFormatterUriValue.php83
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ExternalIdentifierValue.php185
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ImportValue.php231
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/InfoLinksProvider.php284
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/KeywordValue.php287
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/LanguageCodeValue.php63
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/MonolingualTextValue.php363
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php384
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/UnitConverter.php261
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/PropertyChainValue.php220
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/PropertyValue.php501
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ReferenceValue.php294
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/StringValue.php188
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/TelephoneUriValue.php24
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/TemperatureValue.php221
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/CalendarModel.php29
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/Components.php77
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/IntlTimeFormatter.php217
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/JulianDay.php200
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/Timezone.php388
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/TypesValue.php247
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/UniquenessConstraintValue.php41
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/CodeStringValueFormatter.php85
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/DataValueFormatter.php133
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/DispatchingDataValueFormatter.php78
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/MonolingualTextValueFormatter.php142
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/NoValueFormatter.php39
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/NumberValueFormatter.php162
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/PropertyValueFormatter.php337
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/ReferenceValueFormatter.php178
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/StringValueFormatter.php131
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/TimeValueFormatter.php402
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/ValueFormatter.php24
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/AllowsListValueParser.php127
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/AllowsPatternValueParser.php94
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/ImportValueParser.php184
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/MonolingualTextValueParser.php59
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/PropertyValueParser.php200
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/TimeValueParser.php369
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/ValueParser.php29
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/AllowsListConstraintValueValidator.php269
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/CompoundConstraintValueValidator.php70
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/ConstraintValueValidator.php27
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/PatternConstraintValueValidator.php116
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/PropertySpecificationConstraintValueValidator.php105
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/UniquenessConstraintValueValidator.php205
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Defines.php300
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/DescriptionDeserializer.php180
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/DispatchingDescriptionDeserializer.php71
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/MonolingualTextValueDescriptionDeserializer.php126
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/NumberValueDescriptionDeserializer.php92
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/RecordValueDescriptionDeserializer.php133
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/SomeValueDescriptionDeserializer.php112
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/TimeValueDescriptionDeserializer.php122
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializerRegistry.php108
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/ExpDataDeserializer.php78
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Deserializers/SemanticDataDeserializer.php198
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/ElasticClientTaskHandler.php240
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/IndicesInfoProvider.php102
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/InfoProviderHandler.php82
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/MappingsInfoProvider.php196
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/NodesInfoProvider.php78
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/SettingsInfoProvider.php89
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Config.php67
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/Client.php889
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/ConnectionProvider.php135
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/DummyClient.php266
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/ElasticFactory.php426
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/ElasticStore.php353
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/ClientBuilderNotFoundException.php22
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/InvalidJSONException.php23
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/NoConnectionException.php24
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/ReplicationException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/AttachmentAnnotator.php134
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Bulk.php108
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/FileIndexer.php479
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/FileIngestJob.php118
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Indexer.php777
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/IndexerRecoveryJob.php144
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Rebuilder.php421
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/ReplicationStatus.php128
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Rollover.php170
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/Lookup/ProximityPropertyValueLookup.php310
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Aggregations.php114
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Condition.php126
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/ConditionBuilder.php464
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php97
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php139
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ConjunctionInterpreter.php59
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/DisjunctionInterpreter.php102
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php57
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php464
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/SomeValueInterpreter.php538
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php241
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Excerpts.php85
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/FieldMapper.php676
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/QueryEngine.php369
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/SearchResult.php219
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/SortBuilder.php185
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup.php37
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/CachingTermsLookup.php393
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/Parameters.php80
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/TermsLookup.php415
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Elastic/README.md519
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Encoder.php104
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/EntityLookup.php116
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Enum.php28
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/EventHandler.php103
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/EventListenerRegistry.php190
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/DataItemDeserializationException.php13
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/DataItemException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/DataTypeLookupException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotFoundException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotReadableException.php24
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotWritableException.php24
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/ParameterNotFoundException.php38
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/PredefinedPropertyLabelMismatchException.php13
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/PropertyLabelNotResolvedException.php13
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/PropertyNotFoundException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/RedirectTargetUnresolvableException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/SemanticDataImportException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/SettingNotFoundException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/StoreNotFoundException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exception/SubSemanticDataException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ConceptMapper.php306
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/DataItemMatchFinder.php180
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element.php46
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpElement.php137
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpLiteral.php145
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpNsResource.php158
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpResource.php117
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ElementFactory.php176
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/Escaper.php99
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ExpResourceMapper.php295
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilder.php37
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/AuxiliaryPropertyValueResourceBuilder.php70
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ConceptPropertyValueResourceBuilder.php58
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/DispatchingResourceBuilder.php131
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ExternalIdentifierPropertyValueResourceBuilder.php62
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ImportFromPropertyValueResourceBuilder.php75
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/KeywordPropertyValueResourceBuilder.php66
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/MonolingualTextPropertyValueResourceBuilder.php82
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PredefinedPropertyValueResourceBuilder.php66
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PreferredPropertyLabelResourceBuilder.php68
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PropertyDescriptionValueResourceBuilder.php72
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PropertyValueResourceBuilder.php133
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/RedirectPropertyValueResourceBuilder.php51
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/SortPropertyValueResourceBuilder.php76
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/UniquenessConstraintPropertyValueResourceBuilder.php48
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Exporter/XsdValueMapper.php125
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Factbox/CachedFactbox.php321
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Factbox/Factbox.php461
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Factbox/FactboxFactory.php73
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/GlobalFunctions.php282
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/HashBuilder.php166
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/HierarchyLookup.php355
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreator.php31
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/DispatchingContentCreator.php82
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/TextContentCreator.php150
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/XmlContentCreator.php121
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentIterator.php29
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentModeller.php113
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/ImportContents.php234
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/Importer.php138
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/JsonContentIterator.php72
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/JsonImportContentsFileDirReader.php144
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Importer/README.md151
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/InMemoryPoolCache.php168
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/IteratorFactory.php77
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Iterators/AppendIterator.php67
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Iterators/ChunkedIterator.php97
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Iterators/CsvFileIterator.php157
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Iterators/MappingIterator.php76
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Iterators/ResultIterator.php144
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Lang/FallbackFinder.php100
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Lang/JsonContentsFileReader.php185
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Lang/Lang.php600
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Lang/LanguageContents.php172
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Lang/README.md103
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Localizer.php394
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/ConceptCacheRebuilder.php273
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DataRebuilder.php532
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DistinctEntityDataRebuilder.php294
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DuplicateEntitiesDisposer.php138
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/ExceptionFileLogger.php132
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceFactory.php156
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceHelper.php102
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceLogger.php72
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Maintenance/PropertyStatisticsRebuilder.php195
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/ApiQueryResultFormatter.php214
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/ApiRequestParameterFormatter.php153
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Ask.php117
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/AskArgs.php124
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse.php396
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ArticleAugmentor.php68
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ArticleLookup.php193
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/CachingLookup.php94
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ListAugmentor.php147
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ListLookup.php252
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/Lookup.php29
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/PSubjectLookup.php219
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/PValueLookup.php145
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/SubjectLookup.php144
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/BrowseByProperty.php197
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/BrowseBySubject.php238
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Info.php181
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/PropertyListByApiRequest.php297
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Query.php93
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Task.php431
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Collator.php138
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/ConnectionProvider.php154
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Database.php907
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/LoadBalancerConnectionProvider.php85
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/OptionsBuilder.php152
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Query.php411
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Sequence.php87
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/TransactionProfiler.php58
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/DeepRedirectTargetResolver.php94
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/CallableUpdate.php339
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/ChangeTitleUpdate.php93
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php275
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/EditInfoProvider.php130
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Exception/ExtendedPermissionsError.php28
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleDelete.php143
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleFromTitle.php64
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleProtectComplete.php134
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticlePurge.php75
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleViewHeader.php113
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BaseTemplateToolbox.php91
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BeforeDisplayNoArticleText.php49
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BeforePageDisplay.php93
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/EditPageForm.php102
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ExtensionSchemaUpdates.php104
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ExtensionTypes.php38
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/FileUpload.php128
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/GetPreferences.php106
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookHandler.php75
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookListener.php840
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookRegistry.php383
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/InternalParseBeforeLinks.php153
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/LinksUpdateConstructed.php173
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/NewRevisionFromEditComplete.php140
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/OutputPageParserOutput.php138
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ParserAfterTidy.php258
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/PersonalUrls.php110
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/README.md39
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/RejectParserCacheValue.php49
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderGetConfigVars.php58
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderTestModules.php93
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SkinAfterContent.php75
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SkinTemplateNavigation.php59
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SpecialSearchResultsPrepend.php148
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SpecialStatsAddExtra.php87
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleIsAlwaysKnown.php70
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleIsMovable.php60
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleMoveComplete.php120
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/UserChange.php82
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Job.php259
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/JobFactory.php224
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/JobQueue.php213
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationClassUpdateJob.php33
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationDispatchJob.php424
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationUpdateJob.php62
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/EntityIdDisposerJob.php124
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/FulltextSearchTableRebuildJob.php73
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/FulltextSearchTableUpdateJob.php52
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/NullJob.php41
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ParserCachePurgeJob.php264
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/PropertyStatisticsRebuildJob.php66
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/RefreshJob.php135
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/UpdateDispatcherJob.php368
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/UpdateJob.php322
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/LocalTime.php104
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MagicWordsFinder.php107
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/ManualEntryLogger.php85
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MediaWikiNsContentReader.php78
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MessageBuilder.php129
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MwCollaboratorFactory.php229
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageCreator.php39
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageInfoProvider.php158
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageUpdater.php357
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/RedirectTargetFinder.php77
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlColumnListRenderer.php296
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlFormRenderer.php513
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlTableRenderer.php290
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlTemplateRenderer.php71
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/WikitextTemplateRenderer.php62
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/CustomForm.php189
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/Field.php242
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsBuilder.php358
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsFactory.php59
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsFinder.php87
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/NamespaceForm.php192
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/OpenForm.php173
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/SortForm.php127
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/QueryBuilder.php290
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/README.md161
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Search.php474
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchProfileForm.php433
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchResult.php82
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchResultSet.php204
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/CacheStatisticsListTaskHandler.php135
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/ConfigurationListTaskHandler.php141
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DataRefreshJobTaskHandler.php195
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DeprecationNoticeTaskHandler.php281
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DisposeJobTaskHandler.php174
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DuplicateLookupTaskHandler.php153
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/EntityLookupTaskHandler.php325
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/FulltextSearchTableRebuildJobTaskHandler.php162
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/OperationalStatisticsListTaskHandler.php204
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/OutputFormatter.php188
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/PropertyStatsRebuildJobTaskHandler.php162
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/SupportListTaskHandler.php124
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TableSchemaTaskHandler.php184
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TaskHandler.php131
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TaskHandlerFactory.php240
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/DownloadLinksWidget.php98
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ErrorWidget.php134
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/FormatListWidget.php153
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/HelpWidget.php103
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/HtmlForm.php381
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/LinksWidget.php375
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/NavigationLinksWidget.php218
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParameterInput.php326
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParametersProcessor.php273
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParametersWidget.php359
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/QueryInputWidget.php53
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/SortWidget.php185
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/UrlArgs.php76
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/FieldBuilder.php125
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/GroupFormatter.php237
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/HtmlBuilder.php947
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/ValueFormatter.php212
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/PageProperty/PageBuilder.php180
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/PropertyLabelSimilarity/ContentsBuilder.php143
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/PageBuilder.php395
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/PageRequestOptions.php172
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/QueryResultLookup.php212
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialAdmin.php260
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialAsk.php714
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialBrowse.php224
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialDeferredRequestDispatcher.php169
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialPageProperty.php173
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialProcessingErrorList.php68
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialPropertyLabelSimilarity.php86
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialSearchByProperty.php91
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialURIResolver.php79
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/StripMarkerDecoder.php119
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/TitleFactory.php66
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/TitleLookup.php202
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Message.php254
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/NamespaceExaminer.php119
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/NamespaceManager.php268
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/NamespaceUriFinder.php45
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Options.php181
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Page/ConceptPage.php207
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder.php178
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder/ListBuilder.php190
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder/ValueListBuilder.php392
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Page/ListPager.php195
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Page/Page.php247
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Page/PageFactory.php137
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Page/PropertyPage.php380
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PageInfo.php78
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParameterListDocBuilder.php119
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParameterProcessorFactory.php41
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Parameters.php80
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Parser/InTextAnnotationParser.php462
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Parser/LinksEncoder.php235
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Parser/LinksProcessor.php206
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Parser/RecursiveTextProcessor.php360
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Parser/SemanticLinksParser.php63
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserData.php512
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctionFactory.php521
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/AskParserFunction.php450
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ConceptParserFunction.php198
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/DeclareParserFunction.php122
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/DocumentationParserFunction.php135
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ExpensiveFuncExecutionWatcher.php113
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/InfoParserFunction.php98
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/RecurringEventsParserFunction.php96
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SectionTag.php97
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SetParserFunction.php146
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ShowParserFunction.php60
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SubobjectParserFunction.php336
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ParserParameterProcessor.php336
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PermissionPthValidator.php178
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PostProcHandler.php362
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/ProcessingErrorMsgHandler.php246
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAliasFinder.php191
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotator.php35
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotatorFactory.php226
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/CategoryPropertyAnnotator.php185
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/DisplayTitlePropertyAnnotator.php80
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/EditProtectedPropertyAnnotator.php159
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/MandatoryTypePropertyAnnotator.php99
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/NullPropertyAnnotator.php53
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/PredefinedPropertyAnnotator.php114
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/PropertyAnnotatorDecorator.php71
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/RedirectPropertyAnnotator.php51
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/SchemaPropertyAnnotator.php66
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/SortKeyPropertyAnnotator.php48
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/TranslationPropertyAnnotator.php106
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyChangePropagationNotifier.php252
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyLabelFinder.php242
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyRegistry.php499
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertyRestrictionExaminer.php199
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationLookup.php507
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationReqExaminer.php295
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationReqMsgBuilder.php323
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Protection/EditProtectionUpdater.php207
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Protection/ProtectionValidator.php222
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/DebugFormatter.php261
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Deferred.php90
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/DescriptionFactory.php144
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Exception/FingerprintNotFoundException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Exception/ResultFormatNotFoundException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Excerpts.php102
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ExportPrinter.php58
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ClassDescription.php210
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ConceptDescription.php74
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Conjunction.php182
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Description.php207
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Disjunction.php239
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/NamespaceDescription.php72
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/SomeProperty.php212
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ThingDescription.php47
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ValueDescription.php146
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Parser.php52
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/DescriptionProcessor.php307
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/LegacyParser.php847
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/TermParser.php253
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/Tokenizer.php92
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest.php362
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Deserializer.php160
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Formatter.php87
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Serializer.php164
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequestFactory.php65
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/DefaultParamDefinition.php161
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/ParamListProcessor.php263
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/QueryCreator.php252
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotator.php37
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotatorFactory.php169
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/DescriptionProfileAnnotator.php65
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/DurationProfileAnnotator.php49
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/FormatProfileAnnotator.php47
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/NullProfileAnnotator.php81
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/ParametersProfileAnnotator.php72
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/ProfileAnnotatorDecorator.php97
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/SchemaLinkProfileAnnotator.php57
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/SourceProfileAnnotator.php49
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/StatusCodeProfileAnnotator.php51
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/QueryComparator.php193
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/QueryContext.php57
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/QueryLinker.php120
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/QuerySourceFactory.php111
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/QueryStringifier.php146
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/QueryToken.php166
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/RemoteRequest.php336
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Result/CachedQueryResultPrefetcher.php552
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Result/ResolverJournal.php81
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Result/ResultFieldMatchFinder.php357
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/Result/StringResult.php92
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinter.php113
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/CategoryResultPrinter.php322
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/CsvFileExportPrinter.php180
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/FeedExportPrinter.php453
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/FileExportPrinter.php97
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter.php196
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ListResultBuilder.php266
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ParameterDictionary.php53
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ParameterDictionaryUser.php35
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/RowBuilder.php43
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/SimpleRowBuilder.php118
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/TemplateRendererFactory.php117
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/TemplateRowBuilder.php80
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ValueTextsBuilder.php127
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/NullResultPrinter.php33
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ResultPrinter.php749
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/TableResultPrinter.php380
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/TemplateFileExportPrinter.php252
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Query/ScoreSet.php153
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/QueryEngine.php35
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/QueryFactory.php141
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/RequestOptions.php212
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Rule/Rule.php101
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/BadHttpEndpointResponseException.php86
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/HttpEndpointConnectionException.php26
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/XmlParserException.php26
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/HttpResponseErrorMapper.php87
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/HttpResponseParser.php25
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/Condition.php93
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/FalseCondition.php25
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/FilterCondition.php38
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/SingletonCondition.php55
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/TrueCondition.php26
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/WhereCondition.php45
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php629
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreter.php33
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreterFactory.php69
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php140
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php139
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ConjunctionInterpreter.php248
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/DisjunctionInterpreter.php256
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/DispatchingDescriptionInterpreter.php80
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php81
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php268
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ThingDescriptionInterpreter.php61
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php281
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/EngineOptions.php28
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/QueryEngine.php272
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/QueryResultFactory.php140
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/RepositoryResult.php204
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/XmlResponseParser.php234
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/README.md101
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/ReplicationDataTruncator.php52
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryClient.php122
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnection.php119
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectionProvider.php215
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/FourstoreRepositoryConnector.php169
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/FusekiRepositoryConnector.php59
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/GenericRepositoryConnector.php594
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/VirtuosoRepositoryConnector.php175
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryRedirectLookup.php146
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/SPARQLStore.php486
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/SPARQLStoreFactory.php158
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/TurtleTriplesBuilder.php333
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/ChangeDiff.php255
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/ChangeOp.php371
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/FieldChangeOp.php91
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/TableChangeOp.php159
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangePropagationEntityFinder.php189
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ConceptCache.php270
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityRebuildDispatcher.php512
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/CachingEntityLookup.php437
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/CachingSemanticDataLookup.php272
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIBlobHandler.php252
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIBooleanHandler.php99
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIConceptHandler.php126
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIGeoCoordinateHandler.php128
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DINumberHandler.php149
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DITimeHandler.php136
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIUriHandler.php184
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIWikiPageHandler.php200
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DataItemHandler.php211
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DataItemHandlerDispatcher.php123
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/Exception/DataItemHandlerException.php16
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdCacheManager.php226
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdChanger.php149
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdEntityFinder.php202
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/NativeEntityLookup.php107
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/PropertiesLookup.php122
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/PropertySubjectsLookup.php318
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/SemanticDataLookup.php478
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/StubSemanticData.php384
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/SubobjectListFinder.php149
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/TraversalPropertyLookup.php142
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/UniquenessLookup.php153
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityValueUniquenessConstraintChecker.php218
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Exception/PropertyStatisticsInvalidArgumentException.php15
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Exception/TableMissingIdFieldException.php22
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Installer.php463
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/CachedListLookup.php200
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/ListLookup.php47
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/PropertyLabelSimilarityLookup.php318
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/PropertyUsageListLookup.php150
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/ProximityPropertyValueLookup.php266
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/RedirectTargetLookup.php66
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UndeclaredPropertyListLookup.php176
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UnusedPropertyListLookup.php154
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UsageStatisticsListLookup.php352
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyStatisticsStore.php369
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableDefinition.php175
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableDefinitionBuilder.php247
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableIdReferenceDisposer.php284
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableIdReferenceFinder.php267
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableInfoFetcher.php276
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableRowDiffer.php313
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableRowMapper.php310
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableUpdater.php231
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTypeFinder.php111
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/DependencyLinksTableUpdater.php247
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/DependencyLinksUpdateJournal.php148
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/EntityIdListRelevanceDetectionFilter.php195
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryDependencyLinksStore.php567
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryReferenceBacklinks.php117
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryResultDependencyListResolver.php285
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependencyLinksStoreFactory.php150
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/ConceptQuerySegmentBuilder.php112
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreter.php33
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreterFactory.php76
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php91
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ComparatorMapper.php64
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php197
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/DisjunctionConjunctionInterpreter.php74
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/DispatchingDescriptionInterpreter.php81
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php66
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php319
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ThingDescriptionInterpreter.php57
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php169
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/EngineOptions.php26
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/MySQLValueMatchConditionBuilder.php147
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SQLiteValueMatchConditionBuilder.php100
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTable.php281
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTableRebuilder.php335
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTableUpdater.php218
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/TextChangeUpdater.php312
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/TextSanitizer.php169
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/ValueMatchConditionBuilder.php129
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/FulltextSearchTableFactory.php177
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/HierarchyTempTableBuilder.php204
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/OrderCondition.php252
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QueryEngine.php573
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegment.php169
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListBuildManager.php155
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListBuilder.php276
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php349
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/README.md61
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngineFactory.php175
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/README.md12
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/RedirectStore.php274
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/RequestOptionsProc.php304
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/SQLStoreFactory.php762
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder.php134
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/Examiner/HashField.php87
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/FieldType.php131
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php402
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/PostgresTableBuilder.php423
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/SQLiteTableBuilder.php373
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/Table.php130
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/TableBuilder.php246
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/TemporaryTableBuilder.php117
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableFieldUpdater.php89
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableIntegrityExaminer.php344
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableSchemaManager.php346
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/Content.php275
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/ContentFormatter.php270
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/ContentHandler.php74
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/HtmlBuilder.php217
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/Exception/SchemaConstructionFailedException.php24
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/Exception/SchemaTypeNotFoundException.php39
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/README.md26
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/Schema.php44
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaDefinition.php110
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaFactory.php148
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaValidator.php54
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/SerializerFactory.php127
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Serializers/ExpDataSerializer.php65
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Serializers/FlatSemanticDataSerializer.php32
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Serializers/QueryResultSerializer.php290
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Serializers/SemanticDataSerializer.php108
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/DataValueServiceFactory.php243
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/DataValueServices.php310
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/Exception/ServiceNotFoundException.php24
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/ImporterServiceFactory.php86
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/ImporterServices.php108
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/MediaWikiServices.php165
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/README.md27
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/ServicesContainer.php73
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Services/SharedServicesContainer.php705
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Site.php156
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Store.php582
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/StoreAware.php22
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/StoreFactory.php73
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/StringCondition.php88
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/TypesRegistry.php310
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/BufferedStatsdCollector.php235
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/CharArmor.php43
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/CharExaminer.php90
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/CircularReferenceGuard.php110
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/Csv.php135
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/ErrorCodeFormatter.php77
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/File.php98
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/HmacSerializer.php174
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlColumns.php335
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlDivTable.php151
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlModal.php142
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlTable.php179
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlTabs.php184
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlVTabs.php195
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/Image.php62
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/JsonSchemaValidator.php98
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/Logger.php85
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/Lru.php102
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/Normalizer.php52
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/StatsFormatter.php125
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/TempFile.php69
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/Timer.php72
-rw-r--r--www/wiki/extensions/SemanticMediaWiki/src/Utils/Tokenizer.php52
711 files changed, 128257 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Aliases.php b/www/wiki/extensions/SemanticMediaWiki/src/Aliases.php
new file mode 100644
index 00000000..6cdbce1e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Aliases.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * SemanticMediaWiki compatibility aliases for classes that got moved into the SMW namespace
+ */
+
+// 3.0
+class_alias( \SMW\MediaWiki\Deferred\CallableUpdate::class, 'SMW\DeferredCallableUpdate' );
+class_alias( \SMW\Parser\InTextAnnotationParser::class, 'SMW\InTextAnnotationParser' );
+class_alias( \SMW\Encoder::class, 'SMW\UrlEncoder' );
+class_alias( \SMW\Query\ResultPrinter::class, 'SMW\QueryResultPrinter' );
+class_alias( \SMW\Query\ResultPrinter::class, 'SMWIResultPrinter' );
+class_alias( \SMW\Query\ExportPrinter::class, 'SMW\ExportPrinter' );
+class_alias( \SMW\Query\ResultPrinters\ResultPrinter::class, 'SMW\ResultPrinter' );
+class_alias( \SMW\Query\ResultPrinters\ResultPrinter::class, 'SMWResultPrinter' );
+class_alias( \SMW\Query\ResultPrinters\FileExportPrinter::class, 'SMW\FileExportPrinter' );
+class_alias( \SMW\Query\ResultPrinters\ListResultPrinter::class, 'SMW\ListResultPrinter' );
+class_alias( \SMW\Query\Parser::class, 'SMWQueryParser' );
+class_alias( \SMW\SQLStore\ChangeOp\ChangeOp::class, 'SMW\SQLStore\CompositePropertyTableDiffIterator' );
+class_alias( \SMW\Connection\ConnectionProvider::class, 'SMW\DBConnectionProvider' );
+class_alias( \SMW\DataValues\TypesValue::class, 'SMWTypesValue' );
+class_alias( \SMW\DataValues\PropertyValue::class, 'SMWPropertyValue' );
+class_alias( \SMW\DataValues\StringValue::class, 'SMWStringValue' );
+class_alias( \SMW\MediaWiki\Connection\Database::class, '\SMW\MediaWiki\Database' );
+class_alias( \SMWDIBlob::class, 'SMWDIString' );
+
+// 1.9.
+class_alias( \SMW\Store::class, 'SMWStore' );
+class_alias( \SMW\MediaWiki\Jobs\UpdateJob::class, 'SMWUpdateJob' );
+class_alias( \SMW\MediaWiki\Jobs\RefreshJob::class, 'SMWRefreshJob' );
+class_alias( \SMW\SemanticData::class, 'SMWSemanticData' );
+class_alias( \SMW\DIWikiPage::class, 'SMWDIWikiPage' );
+class_alias( \SMW\DIProperty::class, 'SMWDIProperty' );
+class_alias( \SMW\Serializers\QueryResultSerializer::class, 'SMWDISerializer' );
+class_alias( \SMW\DataValueFactory::class, 'SMWDataValueFactory' );
+class_alias( \SMW\Exception\DataItemException::class, 'SMWDataItemException' );
+class_alias( \SMW\SQLStore\PropertyTableDefinition::class, 'SMWSQLStore3Table' );
+class_alias( \SMW\DIConcept::class, 'SMWDIConcept' );
+class_alias( \SMW\Query\ResultPrinters\TableResultPrinter::class, 'SMWTableResultPrinter' );
+
+// 2.0
+class_alias( \SMW\Query\ResultPrinters\FileExportPrinter::class, 'SMWExportPrinter' );
+class_alias( \SMW\AggregatablePrinter::class, 'SMWAggregatablePrinter' );
+class_alias( \SMW\Query\ResultPrinters\CategoryResultPrinter::class, 'SMWCategoryResultPrinter' );
+class_alias( \SMW\DsvResultPrinter::class, 'SMWDSVResultPrinter' );
+class_alias( \SMW\EmbeddedResultPrinter::class, 'SMWEmbeddedResultPrinter' );
+class_alias( \SMW\RdfResultPrinter::class, 'SMWRDFResultPrinter' );
+class_alias( \SMW\ListResultPrinter::class, 'SMWListResultPrinter' );
+class_alias( \SMW\RawResultPrinter::class, 'SMW\ApiResultPrinter' );
+
+// 2.0
+class_alias( \SMW\SPARQLStore\SPARQLStore::class, 'SMWSparqlStore' );
+class_alias( \SMW\SPARQLStore\RepositoryConnectors\FourstoreRepositoryConnector::class, 'SMWSparqlDatabase4Store' );
+class_alias( \SMW\SPARQLStore\RepositoryConnectors\VirtuosoRepositoryConnector::class, 'SMWSparqlDatabaseVirtuoso' );
+class_alias( \SMW\SPARQLStore\RepositoryConnectors\GenericRepositoryConnector::class, 'SMWSparqlDatabase' );
+
+// 2.1
+class_alias( \SMWSQLStore3::class, 'SMW\SQLStore\SQLStore' );
+class_alias( \SMW\Query\Language\Description::class, 'SMWDescription' );
+class_alias( \SMW\Query\Language\ThingDescription::class, 'SMWThingDescription' );
+class_alias( \SMW\Query\Language\ClassDescription::class, 'SMWClassDescription' );
+class_alias( \SMW\Query\Language\ConceptDescription::class, 'SMWConceptDescription' );
+class_alias( \SMW\Query\Language\NamespaceDescription::class, 'SMWNamespaceDescription' );
+class_alias( \SMW\Query\Language\ValueDescription::class, 'SMWValueDescription' );
+class_alias( \SMW\Query\Language\Conjunction::class, 'SMWConjunction' );
+class_alias( \SMW\Query\Language\Disjunction::class, 'SMWDisjunction' );
+class_alias( \SMW\Query\Language\SomeProperty::class, 'SMWSomeProperty' );
+class_alias( \SMW\Query\PrintRequest::class, 'SMWPrintRequest' );
+class_alias( \SMW\MediaWiki\Search\Search::class, 'SMWSearch' );
+
+// 2.2
+// Some weird SF dependency needs to be removed as quick as possible
+class_alias( \SMW\SQLStore\Lookup\ListLookup::class, 'SMW\SQLStore\PropertiesCollector' );
+class_alias( \SMW\SQLStore\Lookup\ListLookup::class, 'SMW\SQLStore\UnusedPropertiesCollector' );
+
+class_alias( \SMW\Exporter\Element\ExpElement::class, 'SMWExpElement' );
+class_alias( \SMW\Exporter\Element\ExpResource::class, 'SMWExpResource' );
+class_alias( \SMW\Exporter\Element\ExpNsResource::class, 'SMWExpNsResource' );
+class_alias( \SMW\Exporter\Element\ExpLiteral::class, 'SMWExpLiteral' );
+class_alias( \SMW\DataValues\ImportValue::class, 'SMWImportValue' );
+class_alias( \SMW\SQLStore\QueryEngine\QueryEngine::class, 'SMWSQLStore3QueryEngine' );
+
+// 2.3
+class_alias( \SMW\ParserParameterProcessor::class, 'SMW\ParserParameterFormatter' );
+class_alias( \SMW\ParameterProcessorFactory::class, 'SMW\ParameterFormatterFactory' );
+
+// 2.4
+class_alias( \SMW\RequestOptions::class, 'SMWRequestOptions' );
+class_alias( \SMW\StringCondition::class, 'SMWStringCondition' );
+class_alias( \SMW\HashBuilder::class, 'SMW\Hash' );
+class_alias( \SMW\DataValues\BooleanValue::class, 'SMWBoolValue' );
+
+// 2.5
+class_alias( \SMW\QueryPrinterFactory::class, 'SMW\FormatFactory' );
+class_alias( \SMW\ParserFunctions\SubobjectParserFunction::class, 'SMW\SubobjectParserFunction' );
+class_alias( \SMW\ParserFunctions\RecurringEventsParserFunction::class, 'SMW\RecurringEventsParserFunction' );
+class_alias( \SMW\SQLStore\PropertyTableDefinition::class, 'SMW\SQLStore\TableDefinition' );
+class_alias( \SMW\DataModel\ContainerSemanticData::class, 'SMWContainerSemanticData' );
+
+// 3.0 (late alias definition)
+class_alias( \SMW\Elastic\ElasticStore::class, 'SMWElasticStore' );
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ApplicationFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/ApplicationFactory.php
new file mode 100644
index 00000000..17d34942
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ApplicationFactory.php
@@ -0,0 +1,591 @@
+<?php
+
+namespace SMW;
+
+use Closure;
+use Onoi\CallbackContainer\CallbackContainerFactory;
+use Onoi\CallbackContainer\ContainerBuilder;
+use Parser;
+use ParserOutput;
+use SMW\Maintenance\MaintenanceFactory;
+use SMW\MediaWiki\Jobs\JobFactory;
+use SMW\MediaWiki\MwCollaboratorFactory;
+use SMW\MediaWiki\PageCreator;
+use SMW\MediaWiki\TitleFactory;
+use SMW\Query\ProfileAnnotator\QueryProfileAnnotatorFactory;
+use SMW\Services\SharedServicesContainer;
+use SMWQueryParser as QueryParser;
+use Title;
+
+/**
+ * Application instances access for internal and external use
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class ApplicationFactory {
+
+ /**
+ * @var ApplicationFactory
+ */
+ private static $instance = null;
+
+ /**
+ * @var ContainerBuilder
+ */
+ private $containerBuilder;
+
+ /**
+ * @var string
+ */
+ private $servicesFileDir = '';
+
+ /**
+ * @since 2.0
+ *
+ * @param ContainerBuilder|null $containerBuilder
+ * @param string $servicesFileDir
+ */
+ public function __construct( ContainerBuilder $containerBuilder = null, $servicesFileDir = '' ) {
+ $this->containerBuilder = $containerBuilder;
+ $this->servicesFileDir = $servicesFileDir;
+ }
+
+ /**
+ * This method returns the global instance of the application factory.
+ *
+ * Reliance on global state is needed at entry points into SMW such as
+ * hook handlers, special pages and jobs, since there we tend to not
+ * have control over the object lifecycle. Pragmatically we might also
+ * want to use this when refactoring legacy code that already has the
+ * global state dependency. For new code very special justification is
+ * required to rely on global state.
+ *
+ * @since 2.0
+ *
+ * @return self
+ */
+ public static function getInstance() {
+
+ if ( self::$instance !== null ) {
+ return self::$instance;
+ }
+
+ $servicesFileDir = $GLOBALS['smwgServicesFileDir'];
+
+ $containerBuilder = self::newContainerBuilder(
+ new CallbackContainerFactory(),
+ $servicesFileDir
+ );
+
+ return self::$instance = new self( $containerBuilder, $servicesFileDir );
+ }
+
+ /**
+ * @since 2.0
+ */
+ public static function clear() {
+
+ if ( self::$instance !== null ) {
+ self::$instance->getSettings()->clear();
+ }
+
+ self::$instance = null;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param string $objectName
+ * @param callable|array $objectSignature
+ */
+ public function registerObject( $objectName, $objectSignature ) {
+ $this->containerBuilder->registerObject( $objectName, $objectSignature );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $file
+ */
+ public function registerFromFile( $file ) {
+ $this->containerBuilder->registerFromFile( $file );
+ }
+
+ /**
+ * @private
+ *
+ * @note Services called via this function are for internal use only and
+ * not to be relied upon for external access.
+ *
+ *
+ * @param string $service
+ *
+ * @return mixed
+ */
+ public function singleton( ...$service ) {
+ return $this->containerBuilder->singleton( ...$service );
+ }
+
+ /**
+ * @private
+ *
+ * @note Services called via this function are for internal use only and
+ * not to be relied upon for external access.
+ *
+ * @since 2.5
+ *
+ * @param string $service
+ *
+ * @return mixed
+ */
+ public function create( ...$service ) {
+ return $this->containerBuilder->create( ...$service );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return SerializerFactory
+ */
+ public function newSerializerFactory() {
+ return new SerializerFactory();
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return JobFactory
+ */
+ public function newJobFactory() {
+ return $this->containerBuilder->create( 'JobFactory' );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return ParserFunctionFactory
+ */
+ public function newParserFunctionFactory() {
+ return new ParserFunctionFactory();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return MaintenanceFactory
+ */
+ public function newMaintenanceFactory() {
+ return new MaintenanceFactory();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return CacheFactory
+ */
+ public function newCacheFactory() {
+ return $this->containerBuilder->create( 'CacheFactory', $this->getSettings()->get( 'smwgMainCacheType' ) );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return CacheFactory
+ */
+ public function getCacheFactory() {
+ return $this->containerBuilder->singleton( 'CacheFactory', $this->getSettings()->get( 'smwgMainCacheType' ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|null $source
+ *
+ * @return QuerySourceFactory
+ */
+ public function getQuerySourceFactory( $source = null ) {
+ return $this->containerBuilder->singleton( 'QuerySourceFactory' );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return Store
+ */
+ public function getStore( $store = null ) {
+ return $this->containerBuilder->singleton( 'Store', $store );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return Settings
+ */
+ public function getSettings() {
+ return $this->containerBuilder->singleton( 'Settings' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return ConnectionManager
+ */
+ public function getConnectionManager() {
+ return $this->containerBuilder->singleton( 'ConnectionManager' );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return TitleFactory
+ */
+ public function newTitleFactory() {
+ return $this->containerBuilder->create( 'TitleFactory', $this->newPageCreator() );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return PageCreator
+ */
+ public function newPageCreator() {
+ return $this->containerBuilder->create( 'PageCreator' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return PageUpdater
+ */
+ public function newPageUpdater() {
+
+ $pageUpdater = $this->containerBuilder->create(
+ 'PageUpdater',
+ $this->getStore()->getConnection( 'mw.db' ),
+ $this->newDeferredTransactionalCallableUpdate()
+ );
+
+ $pageUpdater->setLogger(
+ $this->getMediaWikiLogger()
+ );
+
+ // https://phabricator.wikimedia.org/T154427
+ // It is unclear what changed in MW 1.29 but it has been observed that
+ // executing a HTMLCacheUpdate from within an transaction can lead to a
+ // "ErrorException ... 1 buffered job ... HTMLCacheUpdateJob never
+ // inserted" hence disable the update functionality
+ $pageUpdater->isHtmlCacheUpdate(
+ false
+ );
+
+ return $pageUpdater;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return IteratorFactory
+ */
+ public function getIteratorFactory() {
+ return $this->containerBuilder->singleton( 'IteratorFactory' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DataValueFactory
+ */
+ public function getDataValueFactory() {
+ return DataValueFactory::getInstance();
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return Cache
+ */
+ public function getCache( $cacheType = null ) {
+ return $this->containerBuilder->singleton( 'Cache', $cacheType );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return InTextAnnotationParser
+ */
+ public function newInTextAnnotationParser( ParserData $parserData ) {
+
+ $mwCollaboratorFactory = $this->newMwCollaboratorFactory();
+
+ $linksProcessor = $this->containerBuilder->create( 'LinksProcessor' );
+ $settings = $this->getSettings();
+
+ $linksProcessor->isStrictMode(
+ $settings->isFlagSet( 'smwgParserFeatures', SMW_PARSER_STRICT )
+ );
+
+ $inTextAnnotationParser = new InTextAnnotationParser(
+ $parserData,
+ $linksProcessor,
+ $mwCollaboratorFactory->newMagicWordsFinder(),
+ $mwCollaboratorFactory->newRedirectTargetFinder()
+ );
+
+ $inTextAnnotationParser->isLinksInValues(
+ $settings->isFlagSet( 'smwgParserFeatures', SMW_PARSER_LINV )
+ );
+
+ $inTextAnnotationParser->showErrors(
+ $settings->isFlagSet( 'smwgParserFeatures', SMW_PARSER_INL_ERROR )
+ );
+
+ return $inTextAnnotationParser;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return ParserData
+ */
+ public function newParserData( Title $title, ParserOutput $parserOutput ) {
+ return $this->containerBuilder->create( 'ParserData', $title, $parserOutput );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return ContentParser
+ */
+ public function newContentParser( Title $title ) {
+ return $this->containerBuilder->create( 'ContentParser', $title );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param SemanticData $semanticData
+ *
+ * @return DataUpdater
+ */
+ public function newDataUpdater( SemanticData $semanticData ) {
+
+ $dataUpdater = new DataUpdater(
+ $this->getStore(),
+ $semanticData
+ );
+
+ $dataUpdater->isCommandLineMode(
+ $GLOBALS['wgCommandLineMode']
+ );
+
+ return $dataUpdater;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return MwCollaboratorFactory
+ */
+ public function newMwCollaboratorFactory() {
+ return new MwCollaboratorFactory( $this );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return NamespaceExaminer
+ */
+ public function getNamespaceExaminer() {
+ return $this->containerBuilder->create( 'NamespaceExaminer' );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return PropertySpecificationLookup
+ */
+ public function getPropertySpecificationLookup() {
+ return $this->containerBuilder->singleton( 'PropertySpecificationLookup' );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return HierarchyLookup
+ */
+ public function newHierarchyLookup() {
+ return $this->containerBuilder->create( 'HierarchyLookup' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return PropertyLabelFinder
+ */
+ public function getPropertyLabelFinder() {
+ return $this->containerBuilder->singleton( 'PropertyLabelFinder' );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return CachedPropertyValuesPrefetcher
+ */
+ public function getCachedPropertyValuesPrefetcher() {
+ return $this->containerBuilder->singleton( 'CachedPropertyValuesPrefetcher' );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return MediaWikiNsContentReader
+ */
+ public function getMediaWikiNsContentReader() {
+ return $this->containerBuilder->singleton( 'MediaWikiNsContentReader' );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return InMemoryPoolCache
+ */
+ public function getInMemoryPoolCache() {
+ return $this->containerBuilder->singleton( 'InMemoryPoolCache' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return \createBalancer
+ */
+ public function getLoadBalancer() {
+ return $this->containerBuilder->singleton( 'DBLoadBalancer' );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param callable $callback
+ *
+ * @return DeferredCallableUpdate
+ */
+ public function newDeferredCallableUpdate( callable $callback = null ) {
+
+ $deferredCallableUpdate = $this->containerBuilder->create(
+ 'DeferredCallableUpdate',
+ $callback
+ );
+
+ $deferredCallableUpdate->isDeferrableUpdate(
+ $this->getSettings()->get( 'smwgEnabledDeferredUpdate' )
+ );
+
+ $deferredCallableUpdate->setLogger(
+ $this->getMediaWikiLogger()
+ );
+
+ $deferredCallableUpdate->isCommandLineMode(
+ $GLOBALS['wgCommandLineMode']
+ );
+
+ return $deferredCallableUpdate;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param callable $callback
+ *
+ * @return DeferredTransactionalUpdate
+ */
+ public function newDeferredTransactionalCallableUpdate( callable $callback = null ) {
+
+ $deferredTransactionalUpdate = $this->containerBuilder->create(
+ 'DeferredTransactionalCallableUpdate',
+ $callback,
+ $this->getStore()->getConnection( 'mw.db' )
+ );
+
+ $deferredTransactionalUpdate->isDeferrableUpdate(
+ $this->getSettings()->get( 'smwgEnabledDeferredUpdate' )
+ );
+
+ $deferredTransactionalUpdate->setLogger(
+ $this->getMediaWikiLogger()
+ );
+
+ $deferredTransactionalUpdate->isCommandLineMode(
+ $GLOBALS['wgCommandLineMode']
+ );
+
+ return $deferredTransactionalUpdate;
+ }
+
+ /**
+ * @deprecated since 2.5, use QueryFactory::newQueryParser
+ * @since 2.1
+ *
+ * @return QueryParser
+ */
+ public function newQueryParser( $queryFeatures = false ) {
+ return $this->getQueryFactory()->newQueryParser( $queryFeatures );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DataItemFactory
+ */
+ public function getDataItemFactory() {
+ return $this->containerBuilder->singleton( 'DataItemFactory' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return QueryFactory
+ */
+ public function getQueryFactory() {
+ return $this->containerBuilder->singleton( 'QueryFactory' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return LoggerInterface
+ */
+ public function getMediaWikiLogger( $channel = 'smw' ) {
+ return $this->containerBuilder->singleton( 'MediaWikiLogger', $channel, $GLOBALS['smwgDefaultLoggerRole'] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return JobQueue
+ */
+ public function getJobQueue() {
+ return $this->containerBuilder->singleton( 'JobQueue' );
+ }
+
+ private static function newContainerBuilder( CallbackContainerFactory $callbackContainerFactory, $servicesFileDir ) {
+
+ $containerBuilder = $callbackContainerFactory->newCallbackContainerBuilder();
+
+ $containerBuilder->registerCallbackContainer( new SharedServicesContainer() );
+ $containerBuilder->registerFromFile( $servicesFileDir . '/' . 'MediaWikiServices.php' );
+ $containerBuilder->registerFromFile( $servicesFileDir . '/' . 'ImporterServices.php' );
+
+ // $containerBuilder = $callbackContainerFactory->newLoggableContainerBuilder(
+ // $containerBuilder,
+ // $callbackContainerFactory->newBacktraceSniffer( 10 ),
+ // $callbackContainerFactory->newCallFuncMemorySniffer()
+ // );
+ // $containerBuilder->setLogger( $containerBuilder->singleton( 'MediaWikiLogger' ) );
+
+ return $containerBuilder;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/CacheFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/CacheFactory.php
new file mode 100644
index 00000000..4725bef7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/CacheFactory.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace SMW;
+
+use ObjectCache;
+use Onoi\Cache\CacheFactory as OnoiCacheFactory;
+use RuntimeException;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class CacheFactory {
+
+ /**
+ * @var string|integer
+ */
+ private $mainCacheType;
+
+ /**
+ * @since 2.2
+ *
+ * @param string|integer|null $mainCacheType
+ */
+ public function __construct( $mainCacheType = null ) {
+ $this->mainCacheType = $mainCacheType;
+
+ if ( $this->mainCacheType === null ) {
+ $this->mainCacheType = $GLOBALS['smwgMainCacheType'];
+ }
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string|integer
+ */
+ public function getMainCacheType() {
+ return $this->mainCacheType;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public static function getCachePrefix() {
+ return $GLOBALS['wgCachePrefix'] === false ? wfWikiID() : $GLOBALS['wgCachePrefix'];
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Title|integer|string $key
+ *
+ * @return string
+ */
+ public static function getPurgeCacheKey( $key ) {
+
+ if ( $key instanceof Title ) {
+ $key = $key->getArticleID();
+ }
+
+ return self::getCachePrefix() . ':smw:arc:' . md5( $key );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param array $cacheOptions
+ *
+ * @return stdClass
+ * @throws RuntimeException
+ */
+ public function newCacheOptions( array $cacheOptions ) {
+
+ if ( !isset( $cacheOptions['useCache'] ) || !isset( $cacheOptions['ttl'] ) ) {
+ throw new RuntimeException( "Cache options is missing a useCache/ttl parameter" );
+ }
+
+ return (object)$cacheOptions;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param integer $cacheSize
+ *
+ * @return Cache
+ */
+ public function newFixedInMemoryCache( $cacheSize = 500 ) {
+ return OnoiCacheFactory::getInstance()->newFixedInMemoryLruCache( $cacheSize );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return Cache
+ */
+ public function newNullCache() {
+ return OnoiCacheFactory::getInstance()->newNullCache();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param integer|string $mediaWikiCacheType
+ *
+ * @return Cache
+ */
+ public function newMediaWikiCompositeCache( $mediaWikiCacheType = null ) {
+
+ $compositeCache = OnoiCacheFactory::getInstance()->newCompositeCache( [
+ $this->newFixedInMemoryCache( 500 ),
+ $this->newMediaWikiCache( $mediaWikiCacheType )
+ ] );
+
+ return $compositeCache;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer|string $mediaWikiCacheType
+ *
+ * @return Cache
+ */
+ public function newMediaWikiCache( $mediaWikiCacheType = null ) {
+
+ $mediaWikiCache = ObjectCache::getInstance(
+ ( $mediaWikiCacheType === null ? $this->getMainCacheType() : $mediaWikiCacheType )
+ );
+
+ return OnoiCacheFactory::getInstance()->newMediaWikiCache( $mediaWikiCache );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer|null $cacheType
+ *
+ * @return Cache
+ */
+ public function newCacheByType( $cacheType = null ) {
+
+ if ( $cacheType === CACHE_NONE || $cacheType === null ) {
+ return $this->newNullCache();
+ }
+
+ return $this->newMediaWikiCache( $cacheType );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $namespace
+ * @param string|integer|null $cacheType
+ * @param integer $cacheLifetime
+ *
+ * @return BlobStore
+ */
+ public function newBlobStore( $namespace, $cacheType = null, $cacheLifetime = 0 ) {
+ return ApplicationFactory::getInstance()->create( 'BlobStore', $namespace, $cacheType, $cacheLifetime );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/CachedPropertyValuesPrefetcher.php b/www/wiki/extensions/SemanticMediaWiki/src/CachedPropertyValuesPrefetcher.php
new file mode 100644
index 00000000..873a887f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/CachedPropertyValuesPrefetcher.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace SMW;
+
+use Onoi\BlobStore\BlobStore;
+use SMWQuery as Query;
+
+/**
+ * This class should be accessed via ApplicationFactory::getCachedPropertyValuesPrefetcher
+ * to ensure a singleton instance.
+ *
+ * The purpose of this class is to give fragmented access to frequent (hence
+ * cacheable) property values to ensure that the store is only used for when a
+ * match can not be found and so freeing up the capacities that can equally be
+ * served from a persistent cache instance.
+ *
+ * It is expected that as soon as the "on.before.semanticdata.update.complete"
+ * event has been emitted that matchable cache entries are purged for the
+ * subject in question.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class CachedPropertyValuesPrefetcher {
+
+ /**
+ * @var string
+ */
+ const VERSION = '0.4.1';
+
+ /**
+ * Namespace occupied by the BlobStore
+ */
+ const CACHE_NAMESPACE = 'smw:pvp:store';
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var BlobStore
+ */
+ private $blobStore;
+
+ /**
+ * @var boolean
+ */
+ private $disableCache = false;
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ * @param BlobStore $blobStore
+ */
+ public function __construct( Store $store, BlobStore $blobStore ) {
+ $this->store = $store;
+ $this->blobStore = $blobStore;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function resetCacheBy( DIWikiPage $subject ) {
+ $this->blobStore->delete( $this->getRootHashFrom( $subject ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $disableCache
+ */
+ public function disableCache( $disableCache ) {
+ $this->disableCache = (bool)$disableCache;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIWikiPage $subject
+ * @param DIProperty $property
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return array
+ */
+ public function getPropertyValues( DIWikiPage $subject, DIProperty $property, RequestOptions $requestOptions = null ) {
+
+ // Items are collected as part of the subject hash so that any request is
+ // stored with that entity identifier allowing it to be evicted entirely
+ // when the subject is changed.
+ //
+ // The key on the other hand represent an individual request identifier
+ // that is stored as part of the overall cache item but making distinct
+ // requests possible, yet is fetched as part of the overall subject to
+ // minimize cache fragmentation and a better eviction strategy.
+ $key = $property->getKey() .
+ ':' . $subject->getSubobjectName() .
+ ':' . ( $requestOptions !== null ? md5( $requestOptions->getHash() ) : null );
+
+ $container = $this->blobStore->read(
+ $this->getRootHashFrom( $subject )
+ );
+
+ if ( $this->disableCache === false && $container->has( $key ) ) {
+ return $container->get( $key );
+ }
+
+ $dataItems = $this->store->getPropertyValues(
+ $subject,
+ $property,
+ $requestOptions
+ );
+
+ $container->set( $key, $dataItems );
+
+ $this->blobStore->save(
+ $container
+ );
+
+ return $dataItems;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Query $query
+ *
+ * @return array
+ */
+ public function queryPropertyValuesFor( Query $query ) {
+ return $this->store->getQueryResult( $query )->getResults();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return BlobStore
+ */
+ public function getBlobStore() {
+ return $this->blobStore;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return Store
+ */
+ public function getStore() {
+ return $this->store;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return string
+ */
+ public function getRootHashFrom( DIWikiPage $subject ) {
+ return md5( $subject->asBase()->getHash() . self::VERSION );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $hash
+ *
+ * @return string
+ */
+ public function createHashFromString( $hash ) {
+ return md5( $hash . self::VERSION );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ChangePropListener.php b/www/wiki/extensions/SemanticMediaWiki/src/ChangePropListener.php
new file mode 100644
index 00000000..b41a0ddc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ChangePropListener.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace SMW;
+
+use Closure;
+use SMW\Exception\PropertyLabelNotResolvedException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ChangePropListener {
+
+ /**
+ * @var []
+ */
+ private static $listenerCallbacks = [];
+
+ /**
+ * @var []
+ */
+ private static $deferrableCallbacks = [];
+
+ /**
+ * @since 3.0
+ */
+ public static function clearListeners() {
+ self::$listenerCallbacks = [];
+ self::$deferrableCallbacks = [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param Closure $callback
+ */
+ public function addListenerCallback( $key, Closure $callback ) {
+
+ if ( $key === '' ) {
+ return;
+ }
+
+ if ( !isset( self::$listenerCallbacks[$key] ) ) {
+ self::$listenerCallbacks[$key] = [];
+ }
+
+ self::$listenerCallbacks[$key][] = $callback;
+ }
+
+ /**
+ * Finalize event inception points by matching the key to a property
+ * equivalent representation.
+ *
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function loadListeners( Store $store ) {
+
+ foreach ( self::$listenerCallbacks as $key => $value ) {
+
+ try {
+ $property = DIProperty::newFromUserLabel( $key );
+ } catch ( PropertyLabelNotResolvedException $e ) {
+ continue;
+ }
+
+ $pid = $store->getObjectIds()->getSMWPropertyID(
+ $property
+ );
+
+ self::$listenerCallbacks[$pid] = $value;
+ unset( self::$listenerCallbacks[$key] );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $pid
+ * @param array $record
+ */
+ public static function record( $pid, array $record ) {
+
+ if ( !isset( self::$listenerCallbacks[$pid] ) ) {
+ return;
+ }
+
+ if ( !isset( self::$deferrableCallbacks[$pid] ) ) {
+ self::$deferrableCallbacks[$pid] = [];
+ }
+
+ // Copy callbacks to the deferred list to isolate the execution
+ // from the event point
+ foreach ( self::$listenerCallbacks[$pid] as $callback ) {
+ self::$deferrableCallbacks[$pid][] = [ $callback, $record ];
+ }
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function callListeners() {
+
+ if ( self::$deferrableCallbacks === [] ) {
+ return;
+ }
+
+ $deferrableCallbacks = self::$deferrableCallbacks;
+
+ $callback = function() use( $deferrableCallbacks ) {
+ foreach ( $deferrableCallbacks as $pid => $records ) {
+ foreach ( $records as $rec ) {
+ call_user_func_array( $rec[0], [ $rec[1] ] );
+ }
+ }
+ };
+
+ $deferredTransactionalUpdate = ApplicationFactory::getInstance()->newDeferredTransactionalCallableUpdate(
+ $callback
+ );
+
+ $deferredTransactionalUpdate->setOrigin(
+ [
+ 'ChangePropListener::callListeners'
+ ]
+ );
+
+ $deferredTransactionalUpdate->commitWithTransactionTicket();
+ $deferredTransactionalUpdate->pushUpdate();
+
+ self::$deferrableCallbacks = [];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/CompatibilityMode.php b/www/wiki/extensions/SemanticMediaWiki/src/CompatibilityMode.php
new file mode 100644
index 00000000..fb214f49
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/CompatibilityMode.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace SMW;
+
+/**
+ * Internal benchmarks (XDebug) have shown that some extensions may affect the
+ * performance to a greater degree than expected and can impose a performance
+ * penalty to the overall system (templates, queries etc.).
+ *
+ * If a user is willing to incur those potential disadvantages by setting the
+ * `CompatibilityMode`, s(he) is to understand the latent possibility of those
+ * disadvantages.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class CompatibilityMode {
+
+ /**
+ * @since 2.4
+ *
+ * @return boolean
+ */
+ public static function extensionNotEnabled() {
+
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ return !$GLOBALS['smwgSemanticsEnabled'];
+ }
+
+ $GLOBALS['smwgSemanticsEnabled'] = true;
+ ApplicationFactory::getInstance()->getSettings()->set( 'smwgSemanticsEnabled', true );
+
+ return false;
+ }
+
+ /**
+ * Allows to run `update.php` with a bare-bone setup in cases where enabledSemantics
+ * has not yet been enabled.
+ *
+ * @since 2.4
+ */
+ public static function enableTemporaryCliUpdateMode() {
+ $GLOBALS['smwgSemanticsEnabled'] = true;
+ ApplicationFactory::getInstance()->getSettings()->set( 'smwgSemanticsEnabled', true );
+ ApplicationFactory::getInstance()->getSettings()->set( 'smwgPageSpecialProperties', [ '_MDAT' ] );
+ }
+
+ /**
+ * @since 2.4
+ */
+ public static function disableSemantics() {
+
+ $disabledSettings = [
+ 'smwgSemanticsEnabled' => false,
+ 'smwgNamespacesWithSemanticLinks' => [],
+ 'smwgQEnabled' => false,
+ 'smwgAutoRefreshOnPurge' => false,
+ 'smwgAutoRefreshOnPageMove' => false,
+ 'smwgFactboxCacheRefreshOnPurge' => false,
+ 'smwgAdminFeatures' => false,
+ 'smwgPageSpecialProperties' => [],
+ 'smwgEnableUpdateJobs' => false,
+ 'smwgEnabledEditPageHelp' => false,
+ 'smwgParserFeatures' => SMW_PARSER_NONE,
+ ];
+
+ foreach ( $disabledSettings as $key => $value) {
+ ApplicationFactory::getInstance()->getSettings()->set( $key, $value );
+ $GLOBALS[$key] = $value;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Connection/CallbackConnectionProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/Connection/CallbackConnectionProvider.php
new file mode 100644
index 00000000..84d6a7fb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Connection/CallbackConnectionProvider.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace SMW\Connection;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class CallbackConnectionProvider implements ConnectionProvider {
+
+ /**
+ * @var callable
+ */
+ private $callback;
+
+ /**
+ * @var mixed
+ */
+ private $connection;
+
+ /**
+ * @since 3.0
+ *
+ * @param callable $callback
+ */
+ public function __construct( callable $callback ) {
+ $this->callback = $callback;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return mixed
+ */
+ public function getConnection() {
+
+ if ( $this->connection === null ) {
+ $this->connection = call_user_func( $this->callback );
+ }
+
+ return $this->connection;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function releaseConnection() {
+ $this->connection = null;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionManager.php b/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionManager.php
new file mode 100644
index 00000000..4e16ffee
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionManager.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace SMW\Connection;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class ConnectionManager {
+
+ /**
+ * By design this variable is static to ensure that ConnectionProvider
+ * instances are only initialized once per request.
+ *
+ * @var array
+ */
+ private static $connectionProviders = [];
+
+ /**
+ * @since 2.1
+ *
+ * @param string|null $id
+ *
+ * @return mixed
+ * @throws RuntimeException
+ */
+ public function getConnection( $id = null ) {
+ return $this->findConnectionProvider( strtolower( $id ) )->getConnection();
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function releaseConnections() {
+ foreach ( self::$connectionProviders as $connectionProvider ) {
+ $connectionProvider->releaseConnection();
+ }
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $id
+ * @param ConnectionProvider $connectionProvider
+ */
+ public function registerConnectionProvider( $id, ConnectionProvider $connectionProvider ) {
+ self::$connectionProviders[strtolower( $id )] = $connectionProvider;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ * @param callable $callback
+ */
+ public function registerCallbackConnection( $id, callable $callback ) {
+ self::$connectionProviders[strtolower( $id )] = new CallbackConnectionProvider( $callback );
+ }
+
+ private function findConnectionProvider( $id ) {
+
+ if ( isset( self::$connectionProviders[$id] ) ) {
+ return self::$connectionProviders[$id];
+ }
+
+ throw new RuntimeException( "{$id} is missing a registered connection provider" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionProvider.php
new file mode 100644
index 00000000..ff305428
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionProvider.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace SMW\Connection;
+
+/**
+ * Interface for database connection providers.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+interface ConnectionProvider {
+
+ /**
+ * Returns the database connection.
+ * Initialization of this connection is done if it was not already initialized.
+ *
+ * @since 1.9
+ */
+ public function getConnection();
+
+ /**
+ * Releases the connection if doing so makes any sense resource wise.
+ *
+ * @since 1.9
+ */
+ public function releaseConnection();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionProviderRef.php b/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionProviderRef.php
new file mode 100644
index 00000000..471f3a35
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Connection/ConnectionProviderRef.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace SMW\Connection;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ConnectionProviderRef {
+
+ /**
+ * @var array
+ */
+ private $connectionProviders = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param array $connectionProviders
+ */
+ public function __construct( array $connectionProviders ) {
+ $this->connectionProviders = $connectionProviders;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function hasConnection( $key ) {
+ return isset( $this->connectionProviders[$key] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return ConnectionProvider
+ * @throws RuntimeException
+ */
+ public function getConnection( $key ) {
+
+ if ( isset( $this->connectionProviders[$key] ) && $this->connectionProviders[$key] instanceof ConnectionProvider ) {
+ return $this->connectionProviders[$key]->getConnection();
+ }
+
+ throw new RuntimeException( "$key is unknown" );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function releaseConnection() {
+ foreach ( $this->connectionProviders as $connectionProvider ) {
+ $connectionProvider->releaseConnection();
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataItemFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/DataItemFactory.php
new file mode 100644
index 00000000..3ca95457
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataItemFactory.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace SMW;
+
+use SMWContainerSemanticData as ContainerSemanticData;
+use SMWDIBlob as DIBlob;
+use SMWDIBoolean as DIBoolean;
+use SMWDIContainer as DIContainer;
+use SMWDIError as DIError;
+use SMWDINumber as DINumber;
+use SMWDIUri as DIUri;
+use SMWDITime as DITime;
+use Title;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class DataItemFactory {
+
+ /**
+ * @since 2.4
+ *
+ * @param string $error
+ *
+ * @return DIError
+ */
+ public function newDIError( $error ) {
+ return new DIError( $error );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $key
+ * @param boolean $inverse
+ *
+ * @return DIProperty
+ */
+ public function newDIProperty( $key, $inverse = false ) {
+ return new DIProperty( str_replace( ' ', '_', $key ), $inverse );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string|Title $title
+ * @param integer $namespace
+ * @param string $interwiki
+ * @param string $subobjectName
+ *
+ * @return DIWikiPage
+ */
+ public function newDIWikiPage( $title, $namespace = NS_MAIN, $interwiki = '', $subobjectName = '' ) {
+
+ if ( $title instanceof Title ) {
+ return DIWikiPage::newFromTitle( $title );
+ }
+
+ return new DIWikiPage( $title, $namespace, $interwiki, $subobjectName );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param ContainerSemanticData $containerSemanticData
+ *
+ * @return DIContainer
+ */
+ public function newDIContainer( ContainerSemanticData $containerSemanticData ) {
+ return new DIContainer( $containerSemanticData );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return ContainerSemanticData
+ */
+ public function newContainerSemanticData( DIWikiPage $subject ) {
+ return new ContainerSemanticData( $subject );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $number
+ *
+ * @return DINumber
+ */
+ public function newDINumber( $number ) {
+ return new DINumber( $number );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $text
+ *
+ * @return DIBlob
+ */
+ public function newDIBlob( $text ) {
+ return new DIBlob( $text );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param boolean $boolean
+ *
+ * @return DIBoolean
+ */
+ public function newDIBoolean( $boolean ) {
+ return new DIBoolean( $boolean );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $concept
+ * @param string $docu
+ * @param integer $queryfeatures
+ * @param integer $size
+ * @param integer $depth
+ *
+ * @return DIConcept
+ */
+ public function newDIConcept( $concept, $docu = '', $queryfeatures = 0, $size = 0, $depth = 0 ) {
+ return new DIConcept( $concept, $docu, $queryfeatures, $size, $depth );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $scheme
+ * @param string $hierpart
+ * @param string $query
+ * @param string $fragment
+ *
+ * @return DIUri
+ */
+ public function newDIUri( $scheme, $hierpart, $query = '', $fragment = '' ) {
+ return new DIUri( $scheme, $hierpart, $query, $fragment );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $calendarmodel
+ * @param integer $year
+ * @param integer|false $month
+ * @param integer|false $day
+ * @param integer|false $hour
+ * @param integer|false $minute
+ * @param integer|false $second
+ * @param integer|false $timezone
+ *
+ * @return DITime
+ */
+ public function newDITime( $calendarmodel, $year, $month = false, $day = false, $hour = false, $minute = false, $second = false, $timezone = false ) {
+ return new DITime( $calendarmodel, $year, $month, $day, $hour, $minute, $second, $timezone );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataModel/ContainerSemanticData.php b/www/wiki/extensions/SemanticMediaWiki/src/DataModel/ContainerSemanticData.php
new file mode 100644
index 00000000..38288940
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataModel/ContainerSemanticData.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace SMW\DataModel;
+
+use SMW\DIWikiPage;
+use SMW\Exception\DataItemException;
+use SMW\SemanticData;
+
+/**
+ * Subclass of SemanticData that is used to store the data in SMWDIContainer
+ * objects. It is special since the subject that the stored property-value pairs
+ * refer may or may not be specified explicitly. This can be tested with
+ * hasAnonymousSubject(). When trying to access the subject in anonymous state,
+ * an Exception will be thrown.
+ *
+ * Anonymous container data items are used when no
+ * page context is available, e.g. when specifying such a value in a search form
+ * where the parent page is not known.
+ *
+ * Besides this change, the subclass mainly is needed to restore the disabled
+ * serialization of SemanticData.
+ *
+ * See also the documentation of SMWDIContainer.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ContainerSemanticData extends SemanticData {
+
+ /**
+ * @var boolean
+ */
+ private $skipAnonymousCheck = false;
+
+ /**
+ * Construct a data container that refers to an anonymous subject. See
+ * the documentation of the class for details.
+ *
+ * @since 1.7
+ *
+ * @param boolean $noDuplicates stating if duplicate data should be avoided
+ */
+ public static function makeAnonymousContainer( $noDuplicates = true, $skipAnonymousCheck = false ) {
+
+ $containerSemanticData = new ContainerSemanticData(
+ new DIWikiPage( 'SMWInternalObject', NS_SPECIAL, '', 'int' ),
+ $noDuplicates
+ );
+
+ if ( $skipAnonymousCheck ) {
+ $containerSemanticData->skipAnonymousCheck();
+ }
+
+ return $containerSemanticData;
+ }
+
+ /**
+ * Restore complete serialization which is disabled in SemanticData.
+ */
+ public function __sleep() {
+ return [
+ 'mSubject',
+ 'mProperties',
+ 'mPropVals',
+ 'mHasVisibleProps',
+ 'mHasVisibleSpecs',
+ 'mNoDuplicates',
+ 'skipAnonymousCheck',
+ 'subSemanticData',
+ 'options',
+ 'extensionData'
+ ];
+ }
+
+ /**
+ * Skip the check as it is required for some "search pattern match" activity
+ * to temporarily to access the container without raising an exception.
+ *
+ * @since 2.4
+ */
+ public function skipAnonymousCheck() {
+ $this->skipAnonymousCheck = true;
+ }
+
+ /**
+ * Check if the subject of this container is an anonymous object.
+ * See the documenation of the class for details.
+ *
+ * @return boolean
+ */
+ public function hasAnonymousSubject() {
+
+ if ( $this->mSubject->getNamespace() == NS_SPECIAL &&
+ $this->mSubject->getDBkey() == 'SMWInternalObject' &&
+ $this->mSubject->getInterwiki() === '' &&
+ $this->mSubject->getSubobjectName() === 'int' ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return subject to which the stored semantic annotation refer to, or
+ * throw an exception if the subject is anonymous (if the data has not
+ * been contextualized with setMasterPage() yet).
+ *
+ * @return DIWikiPage subject
+ * @throws DataItemException
+ */
+ public function getSubject() {
+
+ $error = "This container has been classified as anonymous and by trying to access" .
+ " its subject (that has not been given any) an exception is raised to inform about" .
+ " the incorrect usage. An anonymous container can only be used for a search pattern match.";
+
+ if ( !$this->skipAnonymousCheck && $this->hasAnonymousSubject() ) {
+ throw new DataItemException( $error );
+ }
+
+ return $this->mSubject;
+ }
+
+ /**
+ * Change the object to become an exact copy of the given
+ * SemanticData object. This is used to make other types of
+ * SemanticData into an SMWContainerSemanticData. To copy objects of
+ * the same type, PHP clone() should be used.
+ *
+ * @since 1.7
+ *
+ * @param SemanticData|null $semanticData
+ */
+ public function copyDataFrom( SemanticData $semanticData = null ) {
+
+ if ( $semanticData === null ) {
+ return;
+ }
+
+ $this->mSubject = $semanticData->getSubject();
+ $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();
+ $this->mNoDuplicates = $semanticData->mNoDuplicates;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataModel/SubSemanticData.php b/www/wiki/extensions/SemanticMediaWiki/src/DataModel/SubSemanticData.php
new file mode 100644
index 00000000..89fa42a3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataModel/SubSemanticData.php
@@ -0,0 +1,287 @@
+<?php
+
+namespace SMW\DataModel;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Exception\SubSemanticDataException;
+use SMW\SemanticData;
+
+/**
+ * @private
+ *
+ * Internal handling of the SubSemanticData container and its subsequent
+ * add and remove operations.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class SubSemanticData {
+
+ /**
+ * 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
+ */
+ private $noDuplicates;
+
+ /**
+ * 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
+ */
+ private $subject;
+
+ /**
+ * Semantic data associated to subobjects of the subject of this
+ * SMWSemanticData.
+ * These key-value pairs of subObjectName (string) =>SMWSemanticData.
+ *
+ * @since 2.5
+ * @var SemanticData[]
+ */
+ private $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.
+ *
+ * @var boolean
+ */
+ private $subDataAllowed = true;
+
+ /**
+ * Maximum depth for an recursive sub data assignment
+ *
+ * @var integer
+ */
+ private $subContainerMaxDepth = 3;
+
+ /**
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ * @param boolean $noDuplicates stating if duplicate data should be avoided
+ */
+ public function __construct( DIWikiPage $subject, $noDuplicates = true ) {
+ $this->clear();
+ $this->subject = $subject;
+ $this->noDuplicates = $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 [ 'subject', 'subSemanticData' ];
+ }
+
+ /**
+ * Return subject to which the stored semantic annotations refer to.
+ *
+ * @return DIWikiPage subject
+ */
+ public function getSubject() {
+ return $this->subject;
+ }
+
+ /**
+ * This is used as contingency where the serialized SementicData still
+ * has an array object reference.
+ *
+ * @since 2.5
+ *
+ * @return ContainerSemanticData[]
+ */
+ public function copyDataFrom( array $subSemanticData ) {
+ $this->subSemanticData = $subSemanticData;
+ }
+
+ /**
+ * Return the array of subSemanticData objects in form of
+ * subobjectName => ContainerSemanticData
+ *
+ * @since 2.5
+ *
+ * @return ContainerSemanticData[]
+ */
+ public function getSubSemanticData() {
+ return $this->subSemanticData;
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function clear() {
+ $this->subSemanticData = [];
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $subobjectName|null
+ *
+ * @return boolean
+ */
+ public function hasSubSemanticData( $subobjectName = null ) {
+
+ if ( $this->subSemanticData === [] || $subobjectName === '' ) {
+ return false;
+ }
+
+ return $subobjectName !== null ? isset( $this->subSemanticData[$subobjectName] ) : true;
+ }
+
+ /**
+ * Find a particular subobject container using its name as identifier
+ *
+ * @since 2.5
+ *
+ * @param string $subobjectName
+ *
+ * @return ContainerSemanticData|null
+ */
+ public function findSubSemanticData( $subobjectName ) {
+
+ if ( $this->hasSubSemanticData( $subobjectName ) && isset( $this->subSemanticData[$subobjectName] ) ) {
+ return $this->subSemanticData[$subobjectName];
+ }
+
+ return null;
+ }
+
+ /**
+ * Add data about subobjects
+ *
+ * Will only work if the data that is added is about a subobject of
+ * this SMWSemanticData's subject. Otherwise an exception is thrown.
+ * The SMWSemanticData object that is given will belong to this object
+ * after the operation; it should not be modified further by the caller.
+ *
+ * @since 2.5
+ *
+ * @param SemanticData $semanticData
+ *
+ * @throws SubSemanticDataException if not adding data about a subobject of this data
+ */
+ public function addSubSemanticData( SemanticData $semanticData ) {
+
+ if ( $semanticData->subContainerDepthCounter > $this->subContainerMaxDepth ) {
+ throw new SubSemanticDataException( "Cannot add further subdata with the depth of {$semanticData->subContainerDepthCounter}. You are trying to add data beyond the max depth of {$this->subContainerMaxDepth} to an SemanticData object." );
+ }
+
+ $subobjectName = $semanticData->getSubject()->getSubobjectName();
+
+ if ( $subobjectName == '' ) {
+ throw new SubSemanticDataException( "Cannot add data that is not about a subobject." );
+ }
+
+ if ( $semanticData->getSubject()->getDBkey() !== $this->getSubject()->getDBkey() ) {
+ throw new SubSemanticDataException( "Data for a subobject of {$semanticData->getSubject()->getDBkey()} cannot be added to {$this->getSubject()->getDBkey()}." );
+ }
+
+ $this->appendSubSemanticData( $semanticData, $subobjectName );
+ }
+
+ /**
+ * Remove data about a subobject
+ *
+ * If the removed data is not about a subobject of this object,
+ * it will silently be ignored (nothing to remove). Likewise,
+ * removing data that is not present does not change anything.
+ *
+ * @since 2.5
+ *
+ * @param SemanticData $semanticData
+ */
+ public function removeSubSemanticData( SemanticData $semanticData ) {
+
+ if ( $semanticData->getSubject()->getDBkey() !== $this->getSubject()->getDBkey() ) {
+ return;
+ }
+
+ $subobjectName = $semanticData->getSubject()->getSubobjectName();
+
+ if ( $this->hasSubSemanticData( $subobjectName ) ) {
+ $this->subSemanticData[$subobjectName]->removeDataFrom( $semanticData );
+
+ if ( $this->subSemanticData[$subobjectName]->isEmpty() ) {
+ unset( $this->subSemanticData[$subobjectName] );
+ }
+ }
+ }
+
+ /**
+ * Remove property and all values associated with this property.
+ *
+ * @since 2.5
+ *
+ * @param $property DIProperty
+ */
+ public function removeProperty( DIProperty $property ) {
+
+ // Inverse properties cannot be used for an annotation
+ if ( $property->isInverse() ) {
+ return;
+ }
+
+ foreach ( $this->subSemanticData as $containerSemanticData ) {
+ $containerSemanticData->removeProperty( $property );
+ }
+ }
+
+ private function appendSubSemanticData( $semanticData, $subobjectName ) {
+
+ if ( $this->hasSubSemanticData( $subobjectName ) ) {
+ $this->subSemanticData[$subobjectName]->importDataFrom( $semanticData );
+
+ foreach ( $semanticData->getSubSemanticData() as $containerSemanticData ) {
+ $this->addSubSemanticData( $containerSemanticData );
+ }
+
+ return;
+ }
+
+ $semanticData->subContainerDepthCounter++;
+
+ foreach ( $semanticData->getSubSemanticData() as $containerSemanticData ) {
+
+ // Skip container that are known to be registered (avoids recursive statement extension)
+ if ( $this->hasSubSemanticData( $containerSemanticData->getSubject()->getSubobjectName() ) ) {
+ continue;
+ }
+
+ $this->addSubSemanticData( $containerSemanticData );
+ }
+
+ $semanticData->clearSubSemanticData();
+ $this->subSemanticData[$subobjectName] = $semanticData;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataTypeRegistry.php b/www/wiki/extensions/SemanticMediaWiki/src/DataTypeRegistry.php
new file mode 100644
index 00000000..92e43ea2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataTypeRegistry.php
@@ -0,0 +1,616 @@
+<?php
+
+namespace SMW;
+
+use SMW\DataValues\TypeList;
+use SMW\Lang\Lang;
+use SMWDataItem as DataItem;
+
+/**
+ * DataTypes registry class
+ *
+ * Registry class that manages datatypes, and provides various methods to access
+ * the information
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class DataTypeRegistry {
+
+ /**
+ * @var DataTypeRegistry
+ */
+ protected static $instance = null;
+
+ /**
+ * @var Lang
+ */
+ private $lang;
+
+ /**
+ * Array of type labels indexed by type ids. Used for datatype resolution.
+ *
+ * @var string[]
+ */
+ private $typeLabels = [];
+
+ /**
+ * Array of ids indexed by type aliases. Used for datatype resolution.
+ *
+ * @var string[]
+ */
+ private $typeAliases = [];
+
+ /**
+ * @var string[]
+ */
+ private $canonicalLabels = [];
+
+ /**
+ * Array of class names for creating new SMWDataValue, indexed by type
+ * id.
+ *
+ * @var string[]
+ */
+ private $typeClasses;
+
+ /**
+ * Array of data item classes, indexed by type id.
+ *
+ * @var integer[]
+ */
+ private $typeDataItemIds;
+
+ /**
+ * @var string[]
+ */
+ private $subDataTypes = [];
+
+ /**
+ * @var []
+ */
+ private $browsableTypes = [];
+
+ /**
+ * Lookup map that allows finding a datatype id given a label or alias.
+ * All labels and aliases (ie array keys) are stored lower case.
+ *
+ * @var string[]
+ */
+ private $typeByLabelOrAliasLookup = [];
+
+ /**
+ * Array of default types to use for making datavalues for dataitems.
+ *
+ * @var string[]
+ */
+ private $defaultDataItemTypeMap = [
+ DataItem::TYPE_BLOB => '_txt', // Text type
+ DataItem::TYPE_URI => '_uri', // URL/URI type
+ DataItem::TYPE_WIKIPAGE => '_wpg', // Page type
+ DataItem::TYPE_NUMBER => '_num', // Number type
+ DataItem::TYPE_TIME => '_dat', // Time type
+ DataItem::TYPE_BOOLEAN => '_boo', // Boolean type
+ DataItem::TYPE_CONTAINER => '_rec', // Value list type (replacing former nary properties)
+ DataItem::TYPE_GEO => '_geo', // Geographical coordinates
+ DataItem::TYPE_CONCEPT => '__con', // Special concept page type
+ DataItem::TYPE_PROPERTY => '__pro', // Property type
+
+ // If either of the following two occurs, we want to see a PHP error:
+ //DataItem::TYPE_NOTYPE => '',
+ //DataItem::TYPE_ERROR => '',
+ ];
+
+ /**
+ * @var Closure[]
+ */
+ private $extraneousFunctions = [];
+
+ /**
+ * @var []
+ */
+ private $extenstionData = [];
+
+ /**
+ * @var Options
+ */
+ private $options = null;
+
+ /**
+ * Returns a DataTypeRegistry instance
+ *
+ * @since 1.9
+ *
+ * @return DataTypeRegistry
+ */
+ public static function getInstance() {
+
+ if ( self::$instance !== null ) {
+ return self::$instance;
+ }
+
+ $lang = Localizer::getInstance()->getLang();
+
+ self::$instance = new self(
+ $lang
+ );
+
+ self::$instance->initDatatypes(
+ TypesRegistry::getDataTypeList()
+ );
+
+ self::$instance->setOption(
+ 'smwgDVFeatures',
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgDVFeatures' )
+ );
+
+ return self::$instance;
+ }
+
+ /**
+ * Resets the DataTypeRegistry instance
+ *
+ * @since 1.9
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * @since 1.9.0.2
+ *
+ * @param Lang $lang
+ */
+ public function __construct( Lang $lang ) {
+ $this->lang = $lang;
+ $this->registerLabels();
+ }
+
+ /**
+ * @deprecated since 2.5, use DataTypeRegistry::getDataItemByType
+ */
+ public function getDataItemId( $typeId ) {
+ return $this->getDataItemByType( $typeId );
+ }
+
+ /**
+ * Get the preferred data item ID for a given type. The ID defines the
+ * appropriate data item class for processing data of this type. See
+ * DataItem for possible values.
+ *
+ * @note SMWDIContainer is a pseudo dataitem type that is used only in
+ * data input methods, but not for storing data. Types that work with
+ * SMWDIContainer use SMWDIWikiPage as their DI type. (Since SMW 1.8)
+ *
+ * @param $typeId string id string for the given type
+ * @return integer data item ID
+ */
+ public function getDataItemByType( $typeId ) {
+
+ if ( isset( $this->typeDataItemIds[$typeId] ) ) {
+ return $this->typeDataItemIds[$typeId];
+ }
+
+ return DataItem::TYPE_NOTYPE;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param string
+ *
+ * @return boolean
+ */
+ public function isRegistered( $typeId ) {
+ return isset( $this->typeDataItemIds[$typeId] );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $typeId
+ *
+ * @return boolean
+ */
+ public function isSubDataType( $typeId ) {
+ return isset( $this->subDataTypes[$typeId] ) && $this->subDataTypes[$typeId];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $typeId
+ *
+ * @return boolean
+ */
+ public function isBrowsableType( $typeId ) {
+ return isset( $this->browsableTypes[$typeId] ) && $this->browsableTypes[$typeId];
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $srcType
+ * @param string $tagType
+ *
+ * @return boolean
+ */
+ public function isEqualByType( $srcType, $tagType ) {
+ return $this->getDataItemByType( $srcType ) === $this->getDataItemByType( $tagType );
+ }
+
+ /**
+ * A function for registering/overwriting datatypes for SMW. Should be
+ * called from within the hook 'smwInitDatatypes'.
+ *
+ * @param $id string type ID for which this datatype is registered
+ * @param $className string name of the according subclass of SMWDataValue
+ * @param $dataItemId integer ID of the data item class that this data value uses, see DataItem
+ * @param $label mixed string label or false for types that cannot be accessed by users
+ * @param boolean $isSubDataType
+ * @param boolean $isBrowsableType
+ */
+ public function registerDataType( $id, $className, $dataItemId, $label = false, $isSubDataType = false, $isBrowsableType = false ) {
+ $this->typeClasses[$id] = $className;
+ $this->typeDataItemIds[$id] = $dataItemId;
+ $this->subDataTypes[$id] = $isSubDataType;
+ $this->browsableTypes[$id] = $isBrowsableType;
+
+ if ( $label !== false ) {
+ $this->registerTypeLabel( $id, $label );
+ }
+ }
+
+ private function registerTypeLabel( $typeId, $typeLabel ) {
+ $this->typeLabels[$typeId] = $typeLabel;
+ $this->addTextToIdLookupMap( $typeId, $typeLabel );
+ }
+
+ private function addTextToIdLookupMap( $dataTypeId, $text ) {
+ $this->typeByLabelOrAliasLookup[mb_strtolower($text)] = $dataTypeId;
+ }
+
+ /**
+ * Add a new alias label to an existing datatype id. Note that every ID
+ * should have a primary label, either provided by SMW or registered with
+ * registerDataType(). This function should be called from within the hook
+ * 'smwInitDatatypes'.
+ *
+ * @param string $typeId
+ * @param string $typeAlias
+ */
+ public function registerDataTypeAlias( $typeId, $typeAlias ) {
+ $this->typeAliases[$typeAlias] = $typeId;
+ $this->addTextToIdLookupMap( $typeId, $typeAlias );
+ }
+
+ /**
+ * @deprecated since 3.0, use DataTypeRegistry::findTypeByLabel
+ */
+ public function findTypeId( $label ) {
+ return $this->findTypeByLabel( $label );
+ }
+
+ /**
+ * Look up the ID that identifies the datatype of the given label
+ * internally. This id is used for all internal operations. If the
+ * label does not belong to a known type, the empty string is returned.
+ *
+ * @since 3.0
+ *
+ * @param string $label
+ *
+ * @return string
+ */
+ public function findTypeByLabel( $label ) {
+
+ $label = mb_strtolower( $label );
+
+ if ( isset( $this->typeByLabelOrAliasLookup[$label] ) ) {
+ return $this->typeByLabelOrAliasLookup[$label];
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $label
+ * @param string|false $languageCode
+ *
+ * @return string
+ */
+ public function findTypeByLabelAndLanguage( $label, $languageCode = false ) {
+
+ if ( !$languageCode ) {
+ return $this->findTypeByLabel( $label );
+ }
+
+ $lang = $this->lang->fetch(
+ $languageCode
+ );
+
+ return $lang->findDatatypeByLabel( $label );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public function getFieldType( $type ) {
+
+ if ( isset( $this->typeDataItemIds[$type] ) ) {
+ return $this->defaultDataItemTypeMap[ $this->typeDataItemIds[$type]];
+ }
+
+ return '_wpg';
+ }
+
+ /**
+ * Get the translated user label for a given internal ID. If the ID does
+ * not have a label associated with it in the current language, the
+ * empty string is returned. This is the case both for internal type ids
+ * and for invalid (unknown) type ids, so this method cannot be used to
+ * distinguish the two.
+ *
+ * @param string $id
+ *
+ * @return string
+ */
+ public function findTypeLabel( $id ) {
+
+ if ( isset( $this->typeLabels[$id] ) ) {
+ return $this->typeLabels[$id];
+ }
+
+ // internal type without translation to user space;
+ // might also happen for historic types after an upgrade --
+ // alas, we have no idea what the former label would have been
+ return '';
+ }
+
+ /**
+ * Returns a label for a typeId that is independent from the user/content
+ * language
+ *
+ * @since 2.3
+ *
+ * @return string
+ */
+ public function findCanonicalLabelById( $id ) {
+
+ if ( isset( $this->canonicalLabels[$id] ) ) {
+ return $this->canonicalLabels[$id];
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getCanonicalDatatypeLabels() {
+ return $this->canonicalLabels;
+ }
+
+ /**
+ * Return an array of all labels that a user might specify as the type of
+ * a property, and that are internal (i.e. not user defined). No labels are
+ * returned for internal types without user labels (e.g. the special types
+ * for some special properties), and for user defined types.
+ *
+ * @return array
+ */
+ public function getKnownTypeLabels() {
+ return $this->typeLabels;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return array
+ */
+ public function getKnownTypeAliases() {
+ return $this->typeAliases;
+ }
+
+ /**
+ * @deprecated since 2.5, use DataTypeRegistry::getDefaultDataItemByType
+ */
+ public function getDefaultDataItemTypeId( $diType ) {
+ return $this->getDefaultDataItemByType( $diType );
+ }
+
+ /**
+ * Returns a default DataItem for a matchable type ID
+ *
+ * @since 2.5
+ *
+ * @param string $diType
+ *
+ * @return string|null
+ */
+ public function getDefaultDataItemByType( $typeId ) {
+
+ if ( isset( $this->defaultDataItemTypeMap[$typeId] ) ) {
+ return $this->defaultDataItemTypeMap[$typeId];
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a class based on a typeId
+ *
+ * @since 1.9
+ *
+ * @param string $typeId
+ *
+ * @return string|null
+ */
+ public function getDataTypeClassById( $typeId ) {
+
+ if ( $this->hasDataTypeClassById( $typeId ) ) {
+ return $this->typeClasses[$typeId];
+ }
+
+ return null;
+ }
+
+ /**
+ * Whether a datatype class is registered for a particular typeId
+ *
+ * @since 1.9
+ *
+ * @param string $typeId
+ *
+ * @return boolean
+ */
+ public function hasDataTypeClassById( $typeId ) {
+ return isset( $this->typeClasses[$typeId] ) && class_exists( $this->typeClasses[$typeId] );
+ }
+
+ /**
+ * Gather all available datatypes and label<=>id<=>datatype
+ * associations. This method is called before most methods of this
+ * factory.
+ */
+ protected function initDatatypes( array $typeList ) {
+
+ foreach ( $typeList as $id => $definition ) {
+
+ if ( isset( $definition[0] ) ) {
+ $this->typeClasses[$id] = $definition[0];
+ }
+
+ $this->typeDataItemIds[$id] = $definition[1];
+ $this->subDataTypes[$id] = $definition[2];
+ $this->browsableTypes[$id] = $definition[3];
+ }
+
+ // Deprecated since 1.9
+ \Hooks::run( 'smwInitDatatypes' );
+
+ // Since 1.9
+ \Hooks::run( 'SMW::DataType::initTypes', [ $this ] );
+ }
+
+ /**
+ * @deprecated since 3.0, use DataTypeRegistry::setExtensionData
+ * Inject services and objects that are planned to be used during the invocation of
+ * a DataValue
+ *
+ * @since 2.3
+ *
+ * @param string $name
+ * @param \Closure $callback
+ */
+ public function registerExtraneousFunction( $name, \Closure $callback ) {
+ $this->extraneousFunctions[$name] = $callback;
+ }
+
+ /**
+ * @deprecated since 3.0, use DataTypeRegistry::getExtensionData
+ * @since 2.3
+ *
+ * @return Closure[]
+ */
+ public function getExtraneousFunctions() {
+ return $this->extraneousFunctions;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return Options
+ */
+ public function getOptions() {
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ return $this->options;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function setOption( $key, $value ) {
+ $this->getOptions()->set( $key, $value );
+ }
+
+ /**
+ * This function allows for registered types to add additional data or functions
+ * required by an individual DataValue of that type.
+ *
+ * Register the data:
+ * $dataTypeRegistry = DataTypeRegistry::getInstance();
+ *
+ * $dataTypeRegistry->registerDataType( '__foo', ... );
+ * $dataTypeRegistry->setExtensionData( '__foo', [ 'ext.function' => ... ] );
+ * ...
+ *
+ * Access the data:
+ * $dataValueFactory = DataValueFactory::getInstance();
+ *
+ * $dataValue = $dataValueFactory->newDataValueByType( '__foo' );
+ * $dataValue->getExtensionData( 'ext.function' )
+ * ...
+ *
+ * @since 3.0
+ *
+ * @param string $id
+ * @param array $data
+ */
+ public function setExtensionData( $id, array $data = [] ) {
+ if ( $this->isRegistered( $id ) ) {
+ $this->extenstionData[$id] = $data;
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ *
+ * @return []
+ */
+ public function getExtensionData( $id ) {
+
+ if ( isset( $this->extenstionData[$id] ) ) {
+ return $this->extenstionData[$id];
+ }
+
+ return [];
+ }
+
+ private function registerLabels() {
+
+ foreach ( $this->lang->getDatatypeLabels() as $typeId => $typeLabel ) {
+ $this->registerTypeLabel( $typeId, $typeLabel );
+ }
+
+ foreach ( $this->lang->getDatatypeAliases() as $typeAlias => $typeId ) {
+ $this->registerDataTypeAlias( $typeId, $typeAlias );
+ }
+
+ foreach ( $this->lang->getCanonicalDatatypeLabels() as $label => $id ) {
+ $this->canonicalLabels[$id] = $label;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataUpdater.php b/www/wiki/extensions/SemanticMediaWiki/src/DataUpdater.php
new file mode 100644
index 00000000..b9aef739
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataUpdater.php
@@ -0,0 +1,430 @@
+<?php
+
+namespace SMW;
+
+use Title;
+use User;
+use WikiPage;
+
+/**
+ * This function takes care of storing the collected semantic data and
+ * clearing out any outdated entries for the processed page. It assumes
+ * that parsing has happened and that all relevant information are
+ * contained and provided for.
+ *
+ * Optionally, this function also takes care of triggering indirect updates
+ * that might be needed for an overall database consistency. If the saved page
+ * describes a property or data type, the method checks whether the property
+ * type, the data type, the allowed values, or the conversion factors have
+ * changed.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class DataUpdater {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var SemanticData
+ */
+ private $semanticData;
+
+ /**
+ * @var TransactionalCallableUpdate
+ */
+ private $transactionalCallableUpdate;
+
+ /**
+ * @var boolean|null
+ */
+ private $canCreateUpdateJob = null;
+
+ /**
+ * @var boolean
+ */
+ private $processSemantics = false;
+
+ /**
+ * @var boolean
+ */
+ private $isCommandLineMode = false;
+
+ /**
+ * @var boolean|string
+ */
+ private $isChangeProp = false;
+
+ /**
+ * @var boolean
+ */
+ private $isDeferrableUpdate = false;
+
+ /**
+ * @var string
+ */
+ private $origin = '';
+
+ /**
+ * @since 1.9
+ *
+ * @param Store $store
+ * @param SemanticData $semanticData
+ */
+ public function __construct( Store $store, SemanticData $semanticData ) {
+ $this->store = $store;
+ $this->semanticData = $semanticData;
+ $this->transactionalCallableUpdate = ApplicationFactory::getInstance()->newDeferredTransactionalCallableUpdate();
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:$wgCommandLineMode
+ * Indicates whether MW is running in command-line mode.
+ *
+ * @since 3.0
+ *
+ * @param boolean $isCommandLineMode
+ */
+ public function isCommandLineMode( $isCommandLineMode ) {
+ $this->isCommandLineMode = $isCommandLineMode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isChangeProp
+ */
+ public function isChangeProp( $isChangeProp ) {
+ $this->isChangeProp = (bool)$isChangeProp;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isChangeProp
+ */
+ public function isDeferrableUpdate( $isDeferrableUpdate ) {
+ $this->isDeferrableUpdate = (bool)$isDeferrableUpdate;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $origin
+ */
+ public function setOrigin( $origin ) {
+ $this->origin = $origin;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return DIWikiPage
+ */
+ public function getSubject() {
+ return $this->semanticData->getSubject();
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param boolean $canCreateUpdateJob
+ */
+ public function canCreateUpdateJob( $canCreateUpdateJob ) {
+ $this->canCreateUpdateJob = (bool)$canCreateUpdateJob;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function doUpdate() {
+
+ if ( !$this->canPerformUpdate() ) {
+ return false;
+ }
+
+ DeferredCallableUpdate::releasePendingUpdates();
+
+ if ( $this->isDeferrableUpdate === false || $this->isCommandLineMode ) {
+ return $this->performUpdate();
+ }
+
+ $this->transactionalCallableUpdate->setCallback( function() {
+ $this->performUpdate();
+ } );
+
+ $this->transactionalCallableUpdate->setOrigin(
+ [
+ __METHOD__,
+ $this->origin,
+ $this->getSubject()->getHash()
+ ]
+ );
+
+ $this->transactionalCallableUpdate->isDeferrableUpdate(
+ $this->isDeferrableUpdate
+ );
+
+ $this->transactionalCallableUpdate->commitWithTransactionTicket();
+ $this->transactionalCallableUpdate->pushUpdate();
+
+ return true;
+ }
+
+ private function canPerformUpdate() {
+
+ $title = $this->getSubject()->getTitle();
+
+ // Protect against null and namespace -1 see Bug 50153
+ if ( $title === null || $title->isSpecialPage() ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @note Make sure to have a valid revision (null means delete etc.) and
+ * check if semantic data should be processed and displayed for a page in
+ * the given namespace
+ */
+ private function performUpdate() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ if ( $this->canCreateUpdateJob === null ) {
+ $this->canCreateUpdateJob( $applicationFactory->getSettings()->get( 'smwgEnableUpdateJobs' ) );
+ }
+
+ $title = $this->getSubject()->getTitle();
+ $wikiPage = $applicationFactory->newPageCreator()->createPage( $title );
+
+ $revision = $wikiPage->getRevision();
+ $user = $revision !== null ? User::newFromId( $revision->getUser() ) : null;
+
+ $this->addAnnotations( $title, $wikiPage, $revision, $user );
+
+ // In case of a restricted update, only the protection update is required
+ // hence the process bails-out early to avoid unnecessary DB connections
+ // or updates
+ if ( $this->checkUpdateEditProtection( $wikiPage, $user ) === true ) {
+ return true;
+ }
+
+ $this->checkChangePropagation();
+ $this->updateData();
+
+ if ( $this->semanticData->getOption( Enum::PURGE_ASSOC_PARSERCACHE ) === true ) {
+ $jobQueue = $applicationFactory->getJobQueue();
+ $jobQueue->runFromQueue( [ 'SMW\ParserCachePurgeJob' => 2 ] );
+ }
+
+ return true;
+ }
+
+ private function addAnnotations( Title $title, WikiPage $wikiPage, $revision, $user ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ if ( $revision !== null ) {
+ $this->processSemantics = $applicationFactory->getNamespaceExaminer()->isSemanticEnabled( $title->getNamespace() );
+ }
+
+ if ( !$this->processSemantics ) {
+ return $this->semanticData = new SemanticData( $this->getSubject() );
+ }
+
+ $pageInfoProvider = $applicationFactory->newMwCollaboratorFactory()->newPageInfoProvider(
+ $wikiPage,
+ $revision,
+ $user
+ );
+
+ $this->semanticData->setExtensionData( 'revision_id', $revision->getId() );
+
+ $propertyAnnotatorFactory = $applicationFactory->singleton( 'PropertyAnnotatorFactory' );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newNullPropertyAnnotator(
+ $this->semanticData
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newPredefinedPropertyAnnotator(
+ $propertyAnnotator,
+ $pageInfoProvider
+ );
+
+ // Standard text hooks are not run through a JSON content object therefore
+ // we attach possible annotations at this point
+ if ( $title->getNamespace() === SMW_NS_SCHEMA ) {
+
+ $schemaFactory = $applicationFactory->singleton( 'SchemaFactory' );
+
+ try {
+ $schema = $schemaFactory->newSchema(
+ $title->getDBKey(),
+ $pageInfoProvider->getNativeData()
+ );
+ } catch ( \Exception $e ) {
+ $schema = null;
+ }
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newSchemaPropertyAnnotator(
+ $propertyAnnotator,
+ $schema
+ );
+ }
+
+ $propertyAnnotator->addAnnotation();
+
+ \Hooks::run(
+ 'SMW::DataUpdater::ContentProcessor',
+ [
+ $this->semanticData,
+ $wikiPage->getContent()
+ ]
+ );
+ }
+
+ private function checkUpdateEditProtection( $wikiPage, $user ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $editProtectionUpdater = $applicationFactory->create( 'EditProtectionUpdater',
+ $wikiPage,
+ $user
+ );
+
+ $editProtectionUpdater->doUpdateFrom( $this->semanticData );
+
+ return $editProtectionUpdater->isRestrictedUpdate();
+ }
+
+ /**
+ * @note Comparison must happen *before* the storage update;
+ * even finding uses of a property fails after its type changed.
+ */
+ private function checkChangePropagation() {
+
+ // canCreateUpdateJob: if it is not enabled there's not much to do here
+ // isChangeProp: means the update is part of the ChangePropagationDispatchJob
+ // therefore skip
+ if ( !$this->canCreateUpdateJob || $this->isChangeProp ) {
+ return;
+ }
+
+ $namespace = $this->semanticData->getSubject()->getNamespace();
+
+ if ( $namespace !== SMW_NS_PROPERTY && $namespace !== NS_CATEGORY ) {
+ return;
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $propertyChangePropagationNotifier = new PropertyChangePropagationNotifier(
+ $this->store,
+ $applicationFactory->newSerializerFactory()
+ );
+
+ $propertyChangePropagationNotifier->setPropertyList(
+ $applicationFactory->getSettings()->get( 'smwgChangePropagationWatchlist' )
+ );
+
+ $propertyChangePropagationNotifier->isCommandLineMode(
+ $this->isCommandLineMode
+ );
+
+ $propertyChangePropagationNotifier->checkAndNotify(
+ $this->semanticData
+ );
+ }
+
+ private function updateData() {
+
+ $this->store->setOption(
+ Store::OPT_CREATE_UPDATE_JOB,
+ $this->canCreateUpdateJob
+ );
+
+ $semanticData = $this->checkOnRequiredRedirectUpdate(
+ $this->semanticData
+ );
+
+ $subject = $semanticData->getSubject();
+
+ if ( $this->processSemantics ) {
+ $this->store->updateData( $semanticData );
+ } elseif ( $this->store->getObjectIds()->exists( $subject ) ) {
+ // Only clear the data where it is know that "exists" is true otherwise
+ // an empty entity is created and later being removed by the
+ // "PropertyTableOutdatedReferenceDisposer" since it is an entity that is
+ // empty == has no reference
+ $this->store->clearData( $subject );
+ }
+
+ return true;
+ }
+
+ private function checkOnRequiredRedirectUpdate( SemanticData $semanticData ) {
+
+ // Check only during online-mode so that when a user operates Special:MovePage
+ // or #redirect the same process is applied
+ if ( !$this->canCreateUpdateJob ) {
+ return $semanticData;
+ }
+
+ $redirects = $semanticData->getPropertyValues(
+ new DIProperty( '_REDI' )
+ );
+
+ if ( $redirects !== [] && !$semanticData->getSubject()->equals( end( $redirects ) ) ) {
+ return $this->doUpdateUnknownRedirectTarget( $semanticData, end( $redirects ) );
+ }
+
+ return $semanticData;
+ }
+
+ private function doUpdateUnknownRedirectTarget( SemanticData $semanticData, DIWikiPage $target ) {
+
+ // Only keep the reference to safeguard that even in case of a text keeping
+ // its annotations there are removed from the Store. A redirect is not
+ // expected to contain any other annotation other than that of the redirect
+ // target
+ $subject = $semanticData->getSubject();
+ $semanticData = new SemanticData( $subject );
+
+ $semanticData->addPropertyObjectValue(
+ new DIProperty( '_REDI' ),
+ $target
+ );
+
+ // Force a manual changeTitle before the general update otherwise
+ // #redirect can cause an inconsistent data container as observed in #895
+ $source = $subject->getTitle();
+ $target = $target->getTitle();
+
+ $this->store->changeTitle(
+ $source,
+ $target,
+ $source->getArticleID(),
+ $target->getArticleID()
+ );
+
+ $dispatchContext = EventHandler::getInstance()->newDispatchContext();
+ $dispatchContext->set( 'title', $subject->getTitle() );
+
+ EventHandler::getInstance()->getEventDispatcher()->dispatch(
+ 'factbox.cache.delete',
+ $dispatchContext
+ );
+
+ return $semanticData;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValueFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValueFactory.php
new file mode 100644
index 00000000..b8ed09e7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValueFactory.php
@@ -0,0 +1,403 @@
+<?php
+
+namespace SMW;
+
+use SMW\DataValues\PropertyValue;
+use SMW\Services\DataValueServiceFactory;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+use SMWDIError;
+use SMWErrorValue as ErrorValue;
+
+/**
+ * Factory class for creating SMWDataValue objects for supplied types or
+ * properties and data values.
+ *
+ * The class has the main entry point newTypeIdValue(), which creates a new
+ * datavalue object, possibly with preset user values, captions and
+ * property names. To create suitable datavalues for a given property, the
+ * method newDataValueByProperty() can be used.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class DataValueFactory {
+
+ /**
+ * @var DataTypeRegistry
+ */
+ private static $instance = null;
+
+ /**
+ * @var DataTypeRegistry
+ */
+ private $dataTypeRegistry = null;
+
+ /**
+ * @var DataValueServiceFactory
+ */
+ private $dataValueServiceFactory;
+
+ /**
+ * @var array
+ */
+ private $defaultOutputFormatters;
+
+ /**
+ * @since 1.9
+ *
+ * @param DataTypeRegistry $dataTypeRegistry
+ * @param DataValueServiceFactory $dataValueServiceFactory
+ */
+ protected function __construct( DataTypeRegistry $dataTypeRegistry, DataValueServiceFactory $dataValueServiceFactory ) {
+ $this->dataTypeRegistry = $dataTypeRegistry;
+ $this->dataValueServiceFactory = $dataValueServiceFactory;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return DataValueFactory
+ */
+ public static function getInstance() {
+
+ if ( self::$instance !== null ) {
+ return self::$instance;
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $dataValueServiceFactory = $applicationFactory->create( 'DataValueServiceFactory' );
+ $dataTypeRegistry = DataTypeRegistry::getInstance();
+
+ $dataValueServiceFactory->importExtraneousFunctions(
+ $dataTypeRegistry->getExtraneousFunctions()
+ );
+
+ self::$instance = new self(
+ $dataTypeRegistry,
+ $dataValueServiceFactory
+ );
+
+ self::$instance->setDefaultOutputFormatters(
+ $applicationFactory->getSettings()->get( 'smwgDefaultOutputFormatters' )
+ );
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function clear() {
+ $this->dataTypeRegistry->clear();
+ self::$instance = null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $defaultOutputFormatters
+ */
+ public function setDefaultOutputFormatters( array $defaultOutputFormatters ) {
+
+ $this->defaultOutputFormatters = [];
+
+ foreach ( $defaultOutputFormatters as $type => $formatter ) {
+
+ $type = str_replace( ' ' , '_', $type );
+
+ if ( $type{0} !== '_' && ( $dType = $this->dataTypeRegistry->findTypeByLabel( $type ) ) !== '' ) {
+ $type = $dType;
+ }
+
+ $this->defaultOutputFormatters[$type] = $formatter;
+ }
+ }
+
+ /**
+ * Create a value from a type id. If no $value is given, an empty
+ * container is created, the value of which can be set later on.
+ *
+ * @param $typeId string id string for the given type
+ * @param $valueString mixed user value string, or false if unknown
+ * @param $caption mixed user-defined caption, or false if none given
+ * @param $property SMWDIProperty property object for which this value is made, or null
+ * @param $contextPage SMWDIWikiPage that provides a context for parsing the value string, or null
+ *
+ * @return DataValue
+ */
+ public function newDataValueByType( $typeId, $valueString = false, $caption = false, DIProperty $property = null, $contextPage = null ) {
+
+ if ( !$this->dataTypeRegistry->hasDataTypeClassById( $typeId ) ) {
+ return new ErrorValue(
+ $typeId,
+ [ 'smw_unknowntype', $typeId ],
+ $valueString,
+ $caption
+ );
+ }
+
+ $dataValue = $this->dataValueServiceFactory->newDataValueByType(
+ $typeId,
+ $this->dataTypeRegistry->getDataTypeClassById( $typeId )
+ );
+
+ $dataValue->setDataValueServiceFactory(
+ $this->dataValueServiceFactory
+ );
+
+ $dataValue->copyOptions(
+ $this->dataTypeRegistry->getOptions()
+ );
+
+ foreach ( $this->dataTypeRegistry->getExtensionData( $typeId ) as $key => $value ) {
+
+ if ( !is_string( $key ) ) {
+ continue;
+ }
+
+ $dataValue->setExtensionData( $key, $value );
+ }
+
+ $localizer = Localizer::getInstance();
+
+ $dataValue->setOption(
+ DataValue::OPT_USER_LANGUAGE,
+ $localizer->getUserLanguage()->getCode()
+ );
+
+ $dataValue->setOption(
+ DataValue::OPT_CONTENT_LANGUAGE,
+ $localizer->getContentLanguage()->getCode()
+ );
+
+ $dataValue->setOption(
+ DataValue::OPT_COMPACT_INFOLINKS,
+ $GLOBALS['smwgCompactLinkSupport']
+ );
+
+ if ( isset( $this->defaultOutputFormatters[$typeId] ) ) {
+ $dataValue->setOutputFormat( $this->defaultOutputFormatters[$typeId] );
+ }
+
+ if ( $property !== null ) {
+ $dataValue->setProperty( $property );
+
+ if ( isset( $this->defaultOutputFormatters[$property->getKey()] ) ) {
+ $dataValue->setOutputFormat( $this->defaultOutputFormatters[$property->getKey()] );
+ }
+ }
+
+ if ( !is_null( $contextPage ) ) {
+ $dataValue->setContextPage( $contextPage );
+ }
+
+ if ( $valueString !== false ) {
+ $dataValue->setUserValue( $valueString, $caption );
+ }
+
+ return $dataValue;
+ }
+
+ /**
+ * Create a value for a data item.
+ *
+ * @param $dataItem DataItem
+ * @param $property mixed null or SMWDIProperty property object for which this value is made
+ * @param $caption mixed user-defined caption, or false if none given
+ *
+ * @return DataValue
+ */
+ public function newDataValueByItem( DataItem $dataItem, DIProperty $property = null, $caption = false ) {
+
+ if ( $property !== null ) {
+ $typeId = $property->findPropertyTypeID();
+ } else {
+ $typeId = $this->dataTypeRegistry->getDefaultDataItemByType( $dataItem->getDiType() );
+ }
+
+ $dataValue = $this->newDataValueByType( $typeId, false, $caption, $property );
+ $dataValue->setDataItem( $dataItem );
+
+ if ( $caption !== false ) {
+ $dataValue->setCaption( $caption );
+ }
+
+ return $dataValue;
+ }
+
+ /**
+ * Create a value for the given property, provided as an SMWDIProperty
+ * object. If no value is given, an empty container is created, the
+ * value of which can be set later on.
+ *
+ * @param $property SMWDIProperty property object for which this value is made
+ * @param $valueString mixed user value string, or false if unknown
+ * @param $caption mixed user-defined caption, or false if none given
+ * @param $contextPage SMWDIWikiPage that provides a context for parsing the value string, or null
+ *
+ * @return DataValue
+ */
+ public function newDataValueByProperty( DIProperty $property, $valueString = false, $caption = false, $contextPage = null ) {
+
+ $typeId = $property->isInverse() ? '_wpg' : $property->findPropertyTypeID();
+
+ return $this->newDataValueByType( $typeId, $valueString, $caption, $property, $contextPage );
+ }
+
+ /**
+ * This factory method returns a data value object from a given property,
+ * value string. It is intended to be used on user input to allow to
+ * turn a property and value string into a data value object.
+ *
+ * @since 1.9
+ *
+ * @param string $propertyName property string
+ * @param string $valueString user value string
+ * @param mixed $caption user-defined caption
+ * @param SMWDIWikiPage|null $contextPage context for parsing the value string
+ *
+ * @return DataValue
+ */
+ public function newDataValueByText( $propertyName, $valueString, $caption = false, DIWikiPage $contextPage = null ) {
+
+ $propertyDV = $this->newPropertyValueByLabel( $propertyName, $caption, $contextPage );
+
+ if ( !$propertyDV->isValid() ) {
+ return $propertyDV;
+ }
+
+ if ( $propertyDV->isRestricted() ) {
+ $dataValue = new ErrorValue(
+ $propertyDV->getPropertyTypeID(),
+ $propertyDV->getRestrictionError(),
+ $valueString,
+ $caption
+ );
+
+ if ( $propertyDV->getDataItem() instanceof DIProperty ) {
+ $dataValue->setProperty( $propertyDV->getDataItem() );
+ }
+
+ return $dataValue;
+ }
+
+ $propertyDI = $propertyDV->getDataItem();
+
+ if ( $propertyDI instanceof SMWDIError ) {
+ return $propertyDV;
+ }
+
+ if ( $propertyDI instanceof DIProperty && !$propertyDI->isInverse() ) {
+ $dataValue = $this->newDataValueByProperty(
+ $propertyDI,
+ $valueString,
+ $caption,
+ $contextPage
+ );
+
+ $dataValue->setProperty( $propertyDV->getDataItem() );
+
+ } elseif ( $propertyDI instanceof DIProperty && $propertyDI->isInverse() ) {
+ $dataValue = new ErrorValue( $propertyDV->getPropertyTypeID(),
+ [ 'smw_noinvannot' ],
+ $valueString,
+ $caption
+ );
+
+ $dataValue->setProperty( $propertyDV->getDataItem() );
+ } else {
+ $dataValue = new ErrorValue(
+ $propertyDV->getPropertyTypeID(),
+ [ 'smw-property-name-invalid', $propertyName ],
+ $valueString,
+ $caption
+ );
+
+ $dataValue->setProperty( $propertyDV->getDataItem() );
+ }
+
+ if ( $dataValue->isValid() && !$dataValue->canUse() ) {
+ $dataValue = new ErrorValue(
+ $propertyDV->getPropertyTypeID(),
+ [ 'smw-datavalue-restricted-use', implode( ',', $dataValue->getErrors() ) ],
+ $valueString,
+ $caption
+ );
+
+ $dataValue->setProperty( $propertyDV->getDataItem() );
+ }
+
+ return $dataValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $propertyLabel
+ * @param string|false $caption
+ * @param DIWikiPage|null $contextPage
+ *
+ * @return DataValue
+ */
+ public function newPropertyValueByLabel( $propertyLabel, $caption = false, DIWikiPage $contextPage = null ) {
+ return $this->newDataValueByType( PropertyValue::TYPE_ID, $propertyLabel, $caption, null, $contextPage );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $typeid
+ * @param string|array $errormsg
+ * @param string $uservalue
+ * @param string $caption
+ *
+ * @return ErrorValue
+ */
+ public function newErrorValue( $typeid, $errormsg = '', $uservalue = '', $caption = false ) {
+ return new ErrorValue( $typeid, $errormsg, $uservalue, $caption );
+ }
+
+/// Deprecated methods
+
+ /**
+ * @deprecated since 2.4, use DataValueFactory::newDataValueByItem
+ *
+ * @return DataValue
+ */
+ public static function newDataItemValue( DataItem $dataItem, DIProperty $property = null, $caption = false ) {
+ return self::getInstance()->newDataValueByItem( $dataItem, $property, $caption );
+ }
+
+ /**
+ * @deprecated since 2.4, use DataValueFactory::newDataValueByProperty
+ *
+ * @return DataValue
+ */
+ public static function newPropertyObjectValue( DIProperty $property, $valueString = false, $caption = false, $contextPage = null ) {
+ return self::getInstance()->newDataValueByProperty( $property, $valueString, $caption, $contextPage );
+ }
+
+ /**
+ * @deprecated since 2.4, use DataValueFactory::newDataValueByType
+ *
+ * @return DataValue
+ */
+ public static function newTypeIdValue( $typeId, $valueString = false, $caption = false, DIProperty $property = null, $contextPage = null ) {
+ return self::getInstance()->newDataValueByType( $typeId, $valueString, $caption, $property, $contextPage );
+ }
+
+ /**
+ * @deprecated since 2.4, use DataTypeRegistry::newDataValueByText
+ *
+ * @return DataValue
+ */
+ public function newPropertyValue( $propertyName, $valueString, $caption = false, DIWikiPage $contextPage = null ) {
+ return $this->newDataValueByText( $propertyName, $valueString, $caption, $contextPage );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AbstractMultiValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AbstractMultiValue.php
new file mode 100644
index 00000000..007aedb7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AbstractMultiValue.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMWDataValue as DataValue;
+use SMWPropertyListValue as PropertyListValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+abstract class AbstractMultiValue extends DataValue {
+
+ /**
+ * @since 2.5
+ *
+ * @param string $userValue
+ *
+ * @return array
+ */
+ abstract public function getValuesFromString( $userValue );
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty[] $properties
+ *
+ * @return DIProperty[]|null
+ */
+ abstract public function setFieldProperties( array $properties );
+
+ /**
+ * @since 2.5
+ *
+ * @return DIProperty[]|null
+ */
+ abstract public function getProperties();
+
+ /**
+ * Return the array (list) of properties that the individual entries of
+ * this datatype consist of.
+ *
+ * @since 2.5
+ *
+ * @return DIProperty[]|null
+ */
+ abstract public function getPropertyDataItems();
+
+ /**
+ * Create a list (array with numeric keys) containing the datavalue
+ * objects that this SMWRecordValue object holds. Values that are not
+ * present are set to null. Note that the first index in the array is
+ * 0, not 1.
+ *
+ * @since 2.5
+ *
+ * @return DataItem[]|null
+ */
+ public function getDataItems() {
+
+ if ( !$this->isValid() ) {
+ return [];
+ }
+
+ $dataItems = [];
+ $index = 0;
+
+ foreach ( $this->getPropertyDataItems() as $diProperty ) {
+ $values = $this->getDataItem()->getSemanticData()->getPropertyValues( $diProperty );
+ $dataItems[$index] = count( $values ) > 0 ? reset( $values ) : null;
+ $index++;
+ }
+
+ return $dataItems;
+ }
+
+ /**
+ * @note called by SMWResultArray::loadContent for matching an index as denoted
+ * in |?Foo=Bar|+index=1 OR |?Foo=Bar|+index=Bar
+ *
+ * @see https://www.semantic-mediawiki.org/wiki/Help:Type_Record#Semantic_search
+ *
+ * @since 2.5
+ *
+ * @param string|integer $index
+ *
+ * @return DataItem[]|null
+ */
+ public function getDataItemByIndex( $index ) {
+
+ if ( is_numeric( $index ) ) {
+ $pos = $index - 1;
+ $dataItems = $this->getDataItems();
+ return isset( $dataItems[$pos] ) ? $dataItems[$pos] : null;
+ }
+
+ if ( ( $property = $this->getPropertyDataItemByIndex( $index ) ) !== null ) {
+ $values = $this->getDataItem()->getSemanticData()->getPropertyValues( $property );
+ return reset( $values );
+ }
+
+ return null;
+ }
+
+ /**
+ * @note called by SMWResultArray::getNextDataValue to match an index
+ * that has been denoted using |?Foo=Bar|+index=1 OR |?Foo=Bar|+index=Bar
+ *
+ * @since 2.5
+ *
+ * @param string|integer $index
+ *
+ * @return DIProperty|null
+ */
+ public function getPropertyDataItemByIndex( $index ) {
+
+ $properties = $this->getPropertyDataItems();
+
+ if ( is_numeric( $index ) ) {
+ $pos = $index - 1;
+ return isset( $properties[$pos] ) ? $properties[$pos] : null;
+ }
+
+ foreach ( $properties as $property ) {
+ if ( $property->getLabel() === $index ) {
+ return $property;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the array (list) of properties that the individual entries of
+ * this datatype consist of.
+ *
+ * @since 2.5
+ *
+ * @param DIProperty|null $property
+ *
+ * @return DIProperty[]|[]
+ */
+ protected function getFieldProperties( DIProperty $property = null ) {
+
+ if ( $property === null || $property->getDiWikiPage() === null ) {
+ return [];
+ }
+
+ $dataItem = ApplicationFactory::getInstance()->getPropertySpecificationLookup()->getFieldListBy( $property );
+
+ if ( !$dataItem ) {
+ return [];
+ }
+
+ $propertyListValue = new PropertyListValue( '__pls' );
+ $propertyListValue->setDataItem( $dataItem );
+
+ if ( !$propertyListValue->isValid() ) {
+ return [];
+ }
+
+ return $propertyListValue->getPropertyDataItems();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsListValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsListValue.php
new file mode 100644
index 00000000..9388dc7e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsListValue.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\Localizer;
+
+/**
+ * To support value list via the NS_MEDIAWIKI namespace
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class AllowsListValue extends StringValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '__pvali';
+
+ /**
+ * Fixed Mediawiki NS prefix
+ */
+ const LIST_PREFIX = 'Smw_allows_list_';
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $value ) {
+
+ if ( $value === '' ) {
+ $this->addErrorMsg( 'smw_emptystring' );
+ }
+
+ $allowsListValueParser = $this->dataValueServiceFactory->getValueParser( $this );
+
+ $allowsListValueParser->parse( $value );
+
+ if ( $allowsListValueParser->getErrors() !== [] ) {
+ foreach ( $allowsListValueParser->getErrors() as $error ) {
+ $this->addErrorMsg( $error );
+ }
+ } else {
+ parent::parseUserValue( $value );
+ }
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ *
+ * @param string $value
+ */
+ public function getShortWikiText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ $id = $this->getDataItem()->getString();
+
+ return '[['. Localizer::getInstance()->getNamespaceTextById( NS_MEDIAWIKI ) . ':' . self::LIST_PREFIX . $id . '|' . $id .']]';
+ }
+
+ /**
+ * @see DataValue::getLongHtmlText
+ *
+ * @param string $value
+ */
+ public function getLongHtmlText( $linker = null ) {
+ return $this->getShortHtmlText( $linker );
+ }
+
+ /**
+ * @see DataValue::getShortHtmlText
+ *
+ * @param string $value
+ */
+ public function getShortHtmlText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ $id = $this->getDataItem()->getString();
+ $title = \Title::newFromText( self::LIST_PREFIX . $id, NS_MEDIAWIKI );
+
+ return \Html::rawElement(
+ 'a',
+ [
+ 'href' => $title->getLocalUrl(),
+ 'target' => '_blank'
+ ],
+ $id
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsPatternValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsPatternValue.php
new file mode 100644
index 00000000..e1fb0054
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsPatternValue.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\Localizer;
+use SMW\Message;
+
+/**
+ * To support regular expressions in connection with the `Allows pattern`
+ * property.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class AllowsPatternValue extends StringValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '__pvap';
+
+ /**
+ * Fixed Mediawiki page
+ */
+ const REFERENCE_PAGE_ID = 'Smw_allows_pattern';
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $value ) {
+
+ if ( $value === '' ) {
+ $this->addErrorMsg( 'smw_emptystring' );
+ }
+
+ if ( ( $this->getOption( 'smwgDVFeatures' ) & SMW_DV_PVAP ) == 0 && $value !== '' ) {
+ $this->addErrorMsg( [ 'smw-datavalue-feature-not-supported', 'Allows pattern (SMW_DV_PVAP)' ] );
+ }
+
+ $allowsPatternValueParser = $this->dataValueServiceFactory->getValueParser( $this );
+
+ $content = $allowsPatternValueParser->parse(
+ $value
+ );
+
+ if ( !$content ) {
+ $this->addErrorMsg( [ 'smw-datavalue-allows-pattern-reference-unknown', $value ], Message::ESCAPED );
+ }
+
+ parent::parseUserValue( $value );
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ *
+ * @param string $value
+ */
+ public function getShortWikiText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ $id = $this->getDataItem()->getString();
+
+ return '[['. Localizer::getInstance()->getNamespaceTextById( NS_MEDIAWIKI ) . ':' . self::REFERENCE_PAGE_ID . '|' . $id .']]';
+ }
+
+ /**
+ * @see DataValue::getLongHtmlText
+ *
+ * @param string $value
+ */
+ public function getLongHtmlText( $linker = null ) {
+ return $this->getShortHtmlText( $linker );
+ }
+
+ /**
+ * @see DataValue::getShortHtmlText
+ *
+ * @param string $value
+ */
+ public function getShortHtmlText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ $id = $this->getDataItem()->getString();
+ $title = \Title::newFromText( self::REFERENCE_PAGE_ID, NS_MEDIAWIKI );
+
+ return \Html::rawElement(
+ 'a',
+ [
+ 'href' => $title->getLocalUrl(),
+ 'target' => '_blank'
+ ],
+ $id
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsValue.php
new file mode 100644
index 00000000..db4e184e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/AllowsValue.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace SMW\DataValues;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class AllowsValue extends StringValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '__pval';
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/BooleanValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/BooleanValue.php
new file mode 100644
index 00000000..96813b07
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/BooleanValue.php
@@ -0,0 +1,256 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\Localizer;
+use SMW\Message;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+use SMWDIBoolean as DIBoolean;
+
+/**
+ * This datavalue implements the handling of Boolean datavalues.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class BooleanValue extends DataValue {
+
+ /**
+ * The text to write for "true" if a custom output format was set.
+ * @var string
+ */
+ protected $trueCaption;
+
+ /**
+ * The text to write for "false" if a custom output format was set.
+ * @var string
+ */
+ protected $falseCaption;
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( $typeid );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ */
+ protected function parseUserValue( $value ) {
+
+ $value = trim( $value );
+
+ if ( $this->m_caption === false ) {
+ $this->m_caption = $value;
+ }
+
+ $this->m_dataitem = new DIBoolean(
+ $this->doParseBoolValue( $value )
+ );
+ }
+
+ /**
+ * @see DataValue::loadDataItem
+ *
+ * @param DataItem $dataItem
+ *
+ * @return boolean
+ */
+ protected function loadDataItem( DataItem $dataItem ) {
+
+ if ( $dataItem->getDIType() !== DataItem::TYPE_BOOLEAN ) {
+ return false;
+ }
+
+ $this->m_dataitem = $dataItem;
+ $this->m_caption = $this->getStandardCaption( true );
+
+ return true;
+ }
+
+ /**
+ * @see DataValue::setOutputFormat
+ */
+ public function setOutputFormat( $formatstring ) {
+
+ if ( $formatstring == $this->m_outformat ) {
+ return;
+ }
+
+ unset( $this->trueCaption );
+ unset( $this->falseCaption );
+
+ if ( $formatstring === '' ) { // no format
+ // (unsetting the captions is exactly the right thing here)
+ } elseif ( strtolower( $formatstring ) == '-' ) { // "plain" format
+ $this->trueCaption = 'true';
+ $this->falseCaption = 'false';
+ } elseif ( strtolower( $formatstring ) == 'num' ) { // "numeric" format
+ $this->trueCaption = 1;
+ $this->falseCaption = 0;
+ } elseif ( strtolower( $formatstring ) == 'tick' ) { // "tick" format
+ $this->trueCaption = '✓';
+ $this->falseCaption = '✕';
+ } elseif ( strtolower( $formatstring ) == 'x' ) { // X format
+ $this->trueCaption = '<span style="font-family: sans-serif; ">X</span>';
+ $this->falseCaption = '&nbsp;';
+ } else { // format "truelabel, falselabel" (hopefully)
+ $captions = explode( ',', $formatstring, 2 );
+ if ( count( $captions ) == 2 ) { // note: escaping needed to be safe; MW-sanitising would be an alternative
+ $this->trueCaption = \Sanitizer::removeHTMLtags( trim( $captions[0] ) );
+ $this->falseCaption = \Sanitizer::removeHTMLtags( trim( $captions[1] ) );
+ } // else: no format that is recognised, ignore
+ }
+
+ // Localized version
+ if ( strpos( $formatstring, 'LOCL' ) !== false ) {
+ $this->setLocalizedCaptions( $formatstring );
+ }
+
+ $this->m_caption = $this->getStandardCaption( true );
+ $this->m_outformat = $formatstring;
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ */
+ public function getShortWikiText( $linker = null ) {
+ return $this->m_caption;
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ */
+ public function getShortHTMLText( $linker = null ) {
+ return $this->m_caption;
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ */
+ public function getLongWikiText( $linker = null ) {
+ return $this->isValid() ? $this->getStandardCaption( true ) : $this->getErrorText();
+ }
+
+ /**
+ * @see DataValue::getLongHTMLText
+ */
+ public function getLongHTMLText( $linker = null ) {
+ return $this->isValid() ? $this->getStandardCaption( true ) : $this->getErrorText();
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ */
+ public function getWikiValue() {
+ return $this->getFirstBooleanCaptionFrom(
+ $this->isValid() && $this->m_dataitem->getBoolean() ? 'smw_true_words' : 'smw_false_words',
+ Message::CONTENT_LANGUAGE
+ );
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @return boolean
+ */
+ public function getBoolean() {
+ return !$this->isValid() ? false : $this->m_dataitem->getBoolean();
+ }
+
+ /**
+ * Get text for displaying the value of this property, or false if not
+ * valid.
+ * @param $useformat bool, true if the output format should be used, false if the returned text should be parsable
+ * @return string
+ */
+ protected function getStandardCaption( $useformat ) {
+
+ if ( !$this->isValid() ) {
+ return false;
+ }
+
+ if ( $useformat && ( isset( $this->trueCaption ) ) ) {
+ return $this->m_dataitem->getBoolean() ? $this->trueCaption : $this->falseCaption;
+ }
+
+ return $this->getFirstBooleanCaptionFrom(
+ $this->m_dataitem->getBoolean() ? 'smw_true_words' : 'smw_false_words',
+ $this->getOption( 'content.language' )
+ );
+ }
+
+ private function doParseBoolValue( $value ) {
+
+ // Use either the global or page related content language
+ $contentLanguage = $this->getOption( 'content.language' );
+
+ $lcv = mb_strtolower( $value );
+ $boolvalue = false;
+
+ if ( $lcv === '1' ) {
+ $boolvalue = true;
+ } elseif ( $lcv === '0' ) {
+ $boolvalue = false;
+ } elseif ( in_array( $lcv, $this->getBooleanWordsFrom( 'smw_true_words', $contentLanguage, 'true' ), true ) ) {
+ $boolvalue = true;
+ } elseif ( in_array( $lcv, $this->getBooleanWordsFrom( 'smw_false_words', $contentLanguage, 'false' ), true ) ) {
+ $boolvalue = false;
+ } else {
+ $this->addErrorMsg(
+ [ 'smw_noboolean', $value ],
+ Message::TEXT,
+ Message::USER_LANGUAGE
+ );
+ }
+
+ return $boolvalue;
+ }
+
+ private function setLocalizedCaptions( &$formatstring ) {
+
+ if ( !( $languageCode = Localizer::getLanguageCodeFrom( $formatstring ) ) ) {
+ $languageCode = $this->getOption( 'user.language' );
+ }
+
+ $this->trueCaption = $this->getFirstBooleanCaptionFrom(
+ 'smw_true_words',
+ $languageCode
+ );
+
+ $this->falseCaption = $this->getFirstBooleanCaptionFrom(
+ 'smw_false_words',
+ $languageCode
+ );
+ }
+
+ private function getFirstBooleanCaptionFrom( $msgKey, $languageCode = null ) {
+
+ $vals = $this->getBooleanWordsFrom(
+ $msgKey,
+ $languageCode
+ );
+
+ return reset( $vals );
+ }
+
+ private function getBooleanWordsFrom( $msgKey, $languageCode = null, $canonicalForm = null ) {
+
+ $vals = explode(
+ ',',
+ Message::get( $msgKey, Message::TEXT, $languageCode )
+ );
+
+ if ( $canonicalForm !== null ) {
+ $vals[] = $canonicalForm;
+ }
+
+ return $vals;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ErrorMsgTextValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ErrorMsgTextValue.php
new file mode 100644
index 00000000..4372f144
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ErrorMsgTextValue.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\Message;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+use SMWDIBlob as DIBlob;
+
+/**
+ * Handling of a language dependent error message encoded by Message::encode.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ErrorMsgTextValue extends DataValue {
+
+ /**
+ * @see DataValue::__construct
+ */
+ public function __construct( $typeId = '' ) {
+ parent::__construct( '__errt' );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $value ) {
+
+ if ( $value === '' ) {
+ $this->addErrorMsg( 'smw_emptystring' );
+ }
+
+ $this->m_dataitem = new DIBlob( $value );
+ }
+
+ /**
+ * @see DataValue::loadDataItem
+ *
+ * @param SMWDataItem $dataitem
+ *
+ * @return boolean
+ */
+ protected function loadDataItem( DataItem $dataItem ) {
+
+ if ( !$dataItem instanceof DIBlob ) {
+ return false;
+ }
+
+ $this->m_caption = false;
+ $this->m_dataitem = $dataItem;
+
+ return true;
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ */
+ public function getShortWikiText( $linker = null ) {
+ return $this->constructErrorText( null );
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ */
+ public function getShortHTMLText( $linker = null ) {
+ return $this->constructErrorText( $linker );
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ */
+ public function getLongWikiText( $linker = null ) {
+ return $this->constructErrorText( $linker );
+ }
+
+ /**
+ * @see DataValue::getLongHTMLText
+ */
+ public function getLongHTMLText( $linker = null ) {
+ return $this->constructErrorText( $linker );
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ */
+ public function getWikiValue() {
+ return $this->constructErrorText();
+ }
+
+ private function constructErrorText( $linker = null ) {
+
+ if ( !$this->isValid() || $this->getDataItem() === [] ) {
+ return '';
+ }
+
+ $string = $this->getDataItem()->getString();
+ $type = $linker !== null ? Message::PARSE : Message::TEXT;
+
+ if ( ( $message = Message::decode( $string, $type, $this->getOption( self::OPT_USER_LANGUAGE ) ) ) !== false ) {
+ return $message;
+ }
+
+ return $string;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ExternalFormatterUriValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ExternalFormatterUriValue.php
new file mode 100644
index 00000000..6d401256
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ExternalFormatterUriValue.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMWURIValue as UriValue;
+
+/**
+ * https://www.ietf.org/rfc/rfc3986.txt describes:
+ *
+ * " ... Uniform Resource Identifier (URI) is a compact sequence of characters
+ * that identifies an abstract or physical resource." with "... Uniform Resource
+ * Locator" (URL) refers to the subset of URIs that provide a means of locating
+ * the resource by describing its primary access mechanism ..."
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ExternalFormatterUriValue extends UriValue {
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( '__peru' );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $value ) {
+
+ if ( $value === '' ) {
+ $this->addErrorMsg( 'smw_emptystring' );
+ return;
+ }
+
+ if ( filter_var( $value, FILTER_VALIDATE_URL ) === false && preg_match( '/((mailto\:|(news|urn|tel|(ht|f)tp(s?))\:\/\/){1}\S+)/u', $value ) === false ) {
+ $this->addErrorMsg( [ 'smw-datavalue-external-formatter-invalid-uri', $value ] );
+ return;
+ }
+
+ if ( strpos( $value, '$1' ) === false ) {
+ $this->addErrorMsg( 'smw-datavalue-external-formatter-uri-missing-placeholder' );
+ return;
+ }
+
+ parent::parseUserValue( $value );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function getUriWithPlaceholderSubstitution( $value ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ // Avoid already encoded values like `W%D6LLEKLA01` to be
+ // encoded twice
+ $value = $this->encode( rawurldecode( $value ) );
+
+ // %241 == encoded $1
+ return str_replace( [ '%241', '$1' ], [ '$1', $value ], $this->getDataItem()->getUri() );
+ }
+
+ // http://php.net/manual/en/function.urlencode.php#97969
+ private function encode( $string ) {
+ return str_replace(
+ [ '%21', '%2A', '%27', '%28', '%29', '%3B', '%3A', '%40', '%26', '%3D', '%2B', '%24', '%2C', '%2F', '%3F', '%25', '%23', '%5B', '%5D' ],
+ [ '!', '*', "'", "(", ")", ";", ":", "@", "&", "=", "+", "$", ",", "/", "?", "%", "#", "[", "]" ],
+ urlencode( $string )
+ );
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ExternalIdentifierValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ExternalIdentifierValue.php
new file mode 100644
index 00000000..fab1a42f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ExternalIdentifierValue.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\DIProperty;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ExternalIdentifierValue extends StringValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '_eid';
+
+ /**
+ * @var string|null
+ */
+ private $uri = null;
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $value ) {
+ parent::parseUserValue( $value );
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ *
+ * @param string $value
+ */
+ public function getShortWikiText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ if ( !$this->m_caption ) {
+ $this->m_caption = $this->m_dataitem->getString();
+ }
+
+ if ( $linker === null ) {
+ return $this->m_caption;
+ }
+
+ $uri = $this->makeUri(
+ $this->m_dataitem->getString()
+ );
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ if ( $this->getOutputFormat() == 'nowiki' ) {
+ $url = $this->makeNonlinkedWikiText( $uri );
+ } else {
+ $url = '['. $uri . ' '. $this->m_caption . ']';
+ }
+
+ return \Html::rawElement(
+ 'span',
+ [
+ 'class' => 'plainlinks smw-eid'
+ ],
+ $url
+ );
+ }
+
+ /**
+ * @see StringValue::getShortHTMLText
+ */
+ public function getShortHTMLText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ if ( !$this->m_caption ) {
+ $this->m_caption = $this->m_dataitem->getString();
+ }
+
+ if ( $linker === null ) {
+ return $this->m_caption;
+ }
+
+ $uri = $this->makeUri(
+ $this->m_dataitem->getString()
+ );
+
+ if ( !$this->isValid() ) {
+ return $this->m_caption;
+ }
+
+ return \Html::rawElement(
+ 'a',
+ [
+ 'href' => $uri,
+ 'target' => '_blank'
+ ],
+ $this->m_caption
+ );
+ }
+
+ /**
+ * @see StringValue::getLongWikiText
+ */
+ public function getLongWikiText( $linked = null ) {
+ return $this->getShortWikiText( $linked );
+ }
+
+ /**
+ * @see StringValue::getLongHTMLText
+ */
+ public function getLongHTMLText( $linker = null ) {
+ return $this->getShortHTMLText( $linker );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DataItem
+ */
+ public function getUri() {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ $dataValue = $this->dataValueServiceFactory->getDataValueFactory()->newDataValueByType(
+ '_uri',
+ $this->makeUri( $this->m_dataitem->getString() )
+ );
+
+ return $dataValue->getDataItem();
+ }
+
+ private function makeUri( $value ) {
+
+ if ( $this->uri !== null ) {
+ return $this->uri;
+ }
+
+ $dataItem = $this->dataValueServiceFactory->getPropertySpecificationLookup()->getExternalFormatterUri(
+ $this->getProperty()
+ );
+
+ if ( $dataItem === null ) {
+ $this->addErrorMsg( 'smw-datavalue-external-identifier-formatter-missing' );
+ return;
+ }
+
+ $dataValue = $this->dataValueServiceFactory->getDataValueFactory()->newDataValueByItem(
+ $dataItem,
+ new DIProperty( '_PEFU' )
+ );
+
+ if ( $dataValue->getErrors() !== [] ) {
+ $this->addError( $dataValue->getErrors() );
+ return;
+ }
+
+ return $this->uri = $dataValue->getUriWithPlaceholderSubstitution( $value );
+ }
+
+ private function makeNonlinkedWikiText( $url ) {
+ return str_replace( ':', '&#58;', $url );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ImportValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ImportValue.php
new file mode 100644
index 00000000..9a9e1bb4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ImportValue.php
@@ -0,0 +1,231 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\Message;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+use SMWDIBlob as DIBlob;
+
+/**
+ * This datavalue implements datavalues used by special property '_IMPO' used
+ * for assigning imported vocabulary to some page of the wiki. It looks up a
+ * MediaWiki message to find out whether a user-supplied vocabulary name can be
+ * imported in the wiki, and whether its declaration is correct (to the extent
+ * that this can be checked).
+ *
+ * @author Fabian Howahl
+ * @author Markus Krötzsch
+ */
+class ImportValue extends DataValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '__imp';
+
+ /**
+ * Fixed Mediawiki import prefix
+ */
+ const IMPORT_PREFIX = 'Smw_import_';
+
+ /**
+ * Type string assigned by the import declaration
+ *
+ * @var string
+ */
+ private $termType = '';
+
+ /**
+ * String provided by user which is used to look up data on Mediawiki:*-Page
+ *
+ * @var string
+ */
+ private $qname = '';
+
+ /**
+ * URI of namespace (without local name)
+ *
+ * @var string
+ */
+ private $uri = '';
+
+ /**
+ * Namespace id (e.g. "foaf")
+ *
+ * @var string
+ */
+ private $namespace = '';
+
+ /**
+ * Local name (e.g. "knows")
+ *
+ * @var string
+ */
+ private $term = '';
+
+ /**
+ * Wiki name of the vocab (e.g. "Friend of a Friend"), might contain wiki markup
+ *
+ * @var string
+ */
+ private $declarativeName = '';
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = self::TYPE_ID ) {
+ parent::__construct( $typeid );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $value ) {
+ $this->qname = $value;
+
+ $importValueParser = $this->dataValueServiceFactory->getValueParser(
+ $this
+ );
+
+ list( $this->namespace, $this->term, $this->uri, $this->declarativeName, $this->termType ) = $importValueParser->parse(
+ $value
+ );
+
+ if ( $importValueParser->getErrors() !== [] ) {
+
+ foreach ( $importValueParser->getErrors() as $message ) {
+ $this->addErrorMsg( $message );
+ }
+
+ $this->m_dataitem = new DIBlob( 'ERROR' );
+ return;
+ }
+
+ // Encoded string for DB storage
+ $this->m_dataitem = new DIBlob(
+ $this->namespace . ' ' .
+ $this->term . ' ' .
+ $this->uri . ' ' .
+ $this->termType
+ );
+
+ // check whether caption is set, otherwise assign link statement to caption
+ if ( $this->m_caption === false ) {
+ $this->m_caption = $this->createCaption( $this->namespace, $this->qname, $this->uri, $this->declarativeName );
+ }
+ }
+
+ /**
+ * @see SMWDataValue::loadDataItem
+ *
+ * @param DataItem $dataitem
+ *
+ * @return boolean
+ */
+ protected function loadDataItem( DataItem $dataItem ) {
+
+ if ( !$dataItem instanceof DIBlob ) {
+ return false;
+ }
+
+ $this->m_dataitem = $dataItem;
+ $parts = explode( ' ', $dataItem->getString(), 4 );
+
+ if ( count( $parts ) != 4 ) {
+ $this->addErrorMsg( [ 'smw-datavalue-import-invalid-format', $dataItem->getString() ] );
+ } else {
+ $this->namespace = $parts[0];
+ $this->term = $parts[1];
+ $this->uri = $parts[2];
+ $this->termType = $parts[3];
+ $this->qname = $this->namespace . ':' . $this->term;
+ $this->declarativeName = '';
+ $this->m_caption = $this->createCaption( $this->namespace, $this->qname, $this->uri, $this->declarativeName );
+ }
+
+ return true;
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ */
+ public function getShortWikiText( $linked = null ) {
+ return $this->m_caption;
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ */
+ public function getShortHTMLText( $linker = null ) {
+ return htmlspecialchars( $this->qname );
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ */
+ public function getLongWikiText( $linked = null ) {
+
+ if ( !$this->isValid() ) {
+ return $this->getErrorText();
+ }
+
+ return "[[MediaWiki:" . self::IMPORT_PREFIX . $this->namespace . "|" . $this->qname . "]]";
+ }
+
+ /**
+ * @see DataValue::getLongHTMLText
+ */
+ public function getLongHTMLText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return $this->getErrorText();
+ }
+
+ return htmlspecialchars( $this->qname );
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ */
+ public function getWikiValue() {
+ return $this->qname;
+ }
+
+ public function getNS() {
+ return $this->uri;
+ }
+
+ public function getNSID() {
+ return $this->namespace;
+ }
+
+ public function getLocalName() {
+ return $this->term;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getTermType() {
+ return $this->termType;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getImportReference() {
+ return $this->namespace . ':' . $this->term . '|' . $this->uri;
+ }
+
+ private function createCaption( $namespace, $qname, $uri, $declarativeName ) {
+ return "[[MediaWiki:" . self::IMPORT_PREFIX . $namespace . "|" . $qname . "]] " . Message::get( [ 'parentheses', "[$uri $namespace] | " . $declarativeName ], Message::PARSE );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/InfoLinksProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/InfoLinksProvider.php
new file mode 100644
index 00000000..a03f6b2c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/InfoLinksProvider.php
@@ -0,0 +1,284 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\Message;
+use SMW\Parser\InTextAnnotationParser;
+use SMW\PropertySpecificationLookup;
+use SMWDataValue as DataValue;
+use SMWDIBlob as DIBlob;
+use SMWInfolink as Infolink;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class InfoLinksProvider {
+
+ /**
+ * @var DataValue
+ */
+ private $dataValue;
+
+ /**
+ * @var PropertySpecificationLookup
+ */
+ private $propertySpecificationLookup;
+
+ /**
+ * @var Infolink[]
+ */
+ protected $infoLinks = [];
+
+ /**
+ * Used to control the addition of the standard search link.
+ * @var boolean
+ */
+ private $hasSearchLink;
+
+ /**
+ * Used to control service link creation.
+ * @var boolean
+ */
+ private $hasServiceLinks;
+
+ /**
+ * @var boolean
+ */
+ private $enabledServiceLinks = true;
+
+ /**
+ * @var boolean
+ */
+ private $compactLink = false;
+
+ /**
+ * @var boolean|array
+ */
+ private $serviceLinkParameters = false;
+
+ /**
+ * @var []
+ */
+ private $browseLinks = [ '__sob' ];
+
+ /**
+ * @since 2.4
+ *
+ * @param DataValue $dataValue
+ * @param PropertySpecificationLookup $propertySpecificationLookup
+ */
+ public function __construct( DataValue $dataValue, PropertySpecificationLookup $propertySpecificationLookup ) {
+ $this->dataValue = $dataValue;
+ $this->propertySpecificationLookup = $propertySpecificationLookup;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function init() {
+ $this->infoLinks = [];
+ $this->hasSearchLink = false;
+ $this->hasServiceLinks = false;
+ $this->enabledServiceLinks = true;
+ $this->serviceLinkParameters = false;
+ $this->compactLink = false;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function disableServiceLinks() {
+ $this->enabledServiceLinks = false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $compactLink
+ */
+ public function setCompactLink( $compactLink ) {
+ $this->compactLink = (bool)$compactLink;
+ }
+
+ /**
+ * Adds a single SMWInfolink object to the infoLinks array.
+ *
+ * @since 2.4
+ *
+ * @param Infolink $link
+ */
+ public function addInfolink( Infolink $infoLink ) {
+ $this->infoLinks[] = $infoLink;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array|false $serviceLinkParameters
+ */
+ public function setServiceLinkParameters( $serviceLinkParameters ) {
+ $this->serviceLinkParameters = $serviceLinkParameters;
+ }
+
+ /**
+ * 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.
+ *
+ * @since 2.4
+ */
+ public function createInfoLinks() {
+
+ if ( $this->infoLinks !== [] ) {
+ return $this->infoLinks;
+ }
+
+ if ( !$this->dataValue->isValid() ) {
+ return [];
+ }
+
+ // Avoid any localization when generating the value
+ $this->dataValue->setOutputFormat( '' );
+
+ $value = $this->dataValue->getWikiValue();
+ $property = $this->dataValue->getProperty();
+
+ // InTextAnnotationParser will detect :: therefore avoid link
+ // breakage by encoding the string
+ if ( strpos( $value, '::' ) !== false && !InTextAnnotationParser::hasMarker( $value ) ) {
+ $value = str_replace( ':', '-3A', $value );
+ }
+
+ if ( in_array( $this->dataValue->getTypeID(), $this->browseLinks ) ) {
+ $infoLink = Infolink::newBrowsingLink( '+', $this->dataValue->getLongWikiText() );
+ $infoLink->setCompactLink( $this->compactLink );
+ } elseif ( $property !== null ) {
+ $infoLink = Infolink::newPropertySearchLink( '+', $property->getLabel(), $value );
+ $infoLink->setCompactLink( $this->compactLink );
+ }
+
+ $this->infoLinks[] = $infoLink;
+ $this->hasSearchLink = $this->infoLinks !== [];
+
+ // add further service links
+ if ( !$this->hasServiceLinks && $this->enabledServiceLinks ) {
+ $this->addServiceLinks();
+ }
+
+ return $this->infoLinks;
+ }
+
+ /**
+ * 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|null $linker
+ *
+ * @return string
+ */
+ public function getInfolinkText( $outputformat, $linker = null ) {
+
+ $result = '';
+ $first = true;
+ $extralinks = [];
+
+ foreach ( $this->dataValue->getInfolinks() as $link ) {
+
+ if ( $outputformat === SMW_OUTPUT_WIKI ) {
+ $text = $link->getWikiText();
+ } else {
+ $text = $link->getHTML( $linker );
+ }
+
+ // the comment is needed to prevent MediaWiki from linking
+ // URL-strings together with the nbsps!
+ if ( $first ) {
+ $result .= ( $outputformat === SMW_OUTPUT_WIKI ? '<!-- --> ' : '&#160;&#160;' ) . $text;
+ $first = false;
+ } else {
+ $extralinks[] = $text;
+ }
+ }
+
+ if ( $extralinks !== [] ) {
+ $result .= smwfEncodeMessages( $extralinks, 'service', '', false );
+ }
+
+ // #1453 SMW::on/off will break any potential link therefore just don't even try
+ return !InTextAnnotationParser::hasMarker( $result ) ? $result : '';
+ }
+
+ /**
+ * Servicelinks are special kinds of infolinks that are created from
+ * current parameters and in-wiki specification of URL templates. This
+ * method adds the current property's servicelinks found in the
+ * messages. The number and content of the parameters is depending on
+ * the datatype, and the service link message is usually crafted with a
+ * particular datatype in mind.
+ */
+ public function addServiceLinks() {
+
+ if ( $this->hasServiceLinks ) {
+ return;
+ }
+
+ $dataItem = null;
+
+ if ( $this->dataValue->getProperty() !== null ) {
+ $dataItem = $this->dataValue->getProperty()->getDiWikiPage();
+ }
+
+ // No property known, or not associated with a page!
+ if ( $dataItem === null ) {
+ return;
+ }
+
+ $args = $this->serviceLinkParameters;
+
+ if ( $args === false ) {
+ return; // no services supported
+ }
+
+ // add a 0 element as placeholder
+ array_unshift( $args, '' );
+
+ $servicelinks = $this->propertySpecificationLookup->getSpecification(
+ $dataItem,
+ new DIProperty( '_SERV' )
+ );
+
+ foreach ( $servicelinks as $servicelink ) {
+ $this->makeLink( $servicelink, $args );
+ }
+
+ $this->hasServiceLinks = true;
+ }
+
+ private function makeLink( $dataItem, $args ) {
+
+ if ( !( $dataItem instanceof DIBlob ) ) {
+ return;
+ }
+
+ // messages distinguish ' ' from '_'
+ $args[0] = 'smw_service_' . str_replace( ' ', '_', $dataItem->getString() );
+ $text = Message::get( $args, Message::TEXT, Message::CONTENT_LANGUAGE );
+ $links = preg_split( "/[\n][\s]?/u", $text );
+
+ foreach ( $links as $link ) {
+ $linkdat = explode( '|', $link, 2 );
+
+ if ( count( $linkdat ) == 2 ) {
+ $this->addInfolink( Infolink::newExternalLink( $linkdat[0], trim( $linkdat[1] ) ) );
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/KeywordValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/KeywordValue.php
new file mode 100644
index 00000000..b14e7d7a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/KeywordValue.php
@@ -0,0 +1,287 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\DIProperty;
+use SMW\Localizer;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWInfolink as Infolink;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class KeywordValue extends StringValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '_keyw';
+
+ /**
+ * @var string|null
+ */
+ private $uri = null;
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $value ) {
+
+ // For the normal blob field setup multi-byte requires more space and
+ // since we use the o_hash field to store the normalized content and
+ // as match field, ensure to have enough space to actually store
+ // a mb keyword
+ $maxLength = mb_detect_encoding( $value, 'ASCII', true ) ? 150 : 85;
+
+ if ( mb_strlen( $value, 'utf8' ) > $maxLength ) {
+ $this->addErrorMsg( [ 'smw-datavalue-keyword-maximum-length', $maxLength ] );
+ return;
+ }
+
+ if ( $this->getOption( self::OPT_QUERY_COMP_CONTEXT ) || $this->getOption( self::OPT_QUERY_CONTEXT ) ) {
+ $value = DIBlob::normalize( $value );
+ }
+
+ if ( $this->m_caption === false ) {
+ $this->m_caption = $value;
+ }
+
+ parent::parseUserValue( $value );
+
+ $this->m_dataitem->setOption( 'is.keyword', true );
+ }
+
+ /**
+ * @see DataValue::getDataItem
+ */
+ public function getDataItem() {
+
+ if ( $this->isValid() && $this->getOption( 'is.search' ) ) {
+ return new DIBlob( DIBlob::normalize( $this->m_dataitem->getString() ) );
+ }
+
+ return parent::getDataItem();
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ *
+ * @param string $value
+ */
+ public function getShortWikiText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ if ( !$this->m_caption ) {
+ $this->m_caption = $this->m_dataitem->getString();
+ }
+
+ if ( $linker === null ) {
+ return $this->m_caption;
+ }
+
+ $uri = $this->makeUri(
+ $this->m_dataitem->getString(),
+ SMW_OUTPUT_WIKI,
+ $linker
+ );
+
+ if ( $uri === '' ) {
+ return $this->m_caption;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * @see StringValue::getShortHTMLText
+ */
+ public function getShortHTMLText( $linker = null ) {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ if ( !$this->m_caption ) {
+ $this->m_caption = $this->m_dataitem->getString();
+ }
+
+ if ( $linker === null ) {
+ return $this->m_caption;
+ }
+
+ $uri = $this->makeUri(
+ $this->m_dataitem->getString(),
+ SMW_OUTPUT_HTML,
+ $linker
+ );
+
+ if ( $uri === '' ) {
+ return $this->m_caption;
+ }
+
+ return $uri;
+ }
+
+ /**
+ * @see StringValue::getLongWikiText
+ */
+ public function getLongWikiText( $linked = null ) {
+ return $this->getShortWikiText( $linked );
+ }
+
+ /**
+ * @see StringValue::getLongHTMLText
+ */
+ public function getLongHTMLText( $linker = null ) {
+ return $this->getShortHTMLText( $linker );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DataItem
+ */
+ public function getUri() {
+
+ if ( !$this->isValid() ) {
+ return '';
+ }
+
+ $uri = $this->makeUri(
+ $this->m_dataitem->getString(),
+ SMW_OUTPUT_RAW
+ );
+
+ if ( $uri === '' ) {
+ return '';
+ }
+
+ $dataValue = $this->dataValueServiceFactory->getDataValueFactory()->newDataValueByType(
+ '_uri',
+ $uri
+ );
+
+ return $dataValue->getDataItem();
+ }
+
+ private function makeUri( $value, $outputformat, $linker = null ) {
+
+ if ( $this->uri !== null ) {
+ return $this->uri;
+ }
+
+ $propertySpecificationLookup = $this->dataValueServiceFactory->getPropertySpecificationLookup();
+
+ // Formatter schema?
+ $dataItems = $propertySpecificationLookup->getSpecification(
+ $this->getProperty(),
+ new DIProperty( '_FORMAT_SCHEMA' )
+ );
+
+ if ( $dataItems === [] ) {
+ return '';
+ }
+
+ $dataItem = end( $dataItems );
+
+ $dataItems = $propertySpecificationLookup->getSpecification(
+ $dataItem,
+ new DIProperty( '_SCHEMA_DEF' )
+ );
+
+ if ( $dataItems === [] ) {
+ return '';
+ }
+
+ $dataItem = end( $dataItems );
+
+ $link = $this->getFormatLink( $dataItem, $value );
+
+ if ( $link === '' ) {
+ return '';
+ }
+
+ $this->uri = $link->getText( $outputformat );
+
+ $this->uri = Localizer::getInstance()->getCanonicalizedUrlByNamespace(
+ NS_SPECIAL,
+ $this->uri
+ );
+
+ return $this->uri;
+ }
+
+ private function getFormatLink( $dataItem, $value ) {
+
+ $infolink = '';
+
+ $data = json_decode(
+ $dataItem->getString(),
+ true
+ );
+
+ // Schema enforced
+ if ( $data['type'] !== 'LINK_FORMAT_SCHEMA' ) {
+ return '';
+ }
+
+ if ( !isset( $data['rule']['link_to'] ) ) {
+ return '';
+ }
+
+ $link_to = $data['rule']['link_to'];
+ $label = $this->getProperty()->getLabel();
+
+ if ( $link_to === 'SPECIAL_ASK' ) {
+ $infolink = Infolink::newInternalLink( $this->m_caption, ':Special:Ask', false, [] );
+ $infolink->setParameter( "[[$label::$value]]", false );
+ $infolink->setCompactLink( $this->getOption( KeywordValue::OPT_COMPACT_INFOLINKS, false ) );
+
+ foreach ( $data['rule']['parameters'] as $key => $value ) {
+
+ if ( $key === 'title' || $key === 'msg' ) {
+ $key = "b$key";
+ }
+
+ if ( $key === 'printouts' ) {
+ foreach ( $value as $v ) {
+ $infolink->setParameter( "?$v" );
+ }
+ } else {
+ $infolink->setParameter( $value, $key );
+ }
+ }
+
+ } elseif ( $link_to === 'SPECIAL_SEARCH_BY_PROPERTY' ) {
+ $infolink = Infolink::newInternalLink( $this->m_caption, ':Special:SearchByProperty', false, [] );
+ $infolink->setCompactLink( $this->getOption( KeywordValue::OPT_COMPACT_INFOLINKS, false ) );
+ $infolink->setParameter( ":$label" );
+ $infolink->setParameter( $value );
+ }
+
+ return $infolink;
+ }
+
+ private function makeNonlinkedWikiText( $url ) {
+ return str_replace( ':', '&#58;', $url );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/LanguageCodeValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/LanguageCodeValue.php
new file mode 100644
index 00000000..f6bef8d8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/LanguageCodeValue.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\Localizer;
+use SMWDIBlob as DIBlob;
+
+/**
+ * Handles a string value to adhere the BCP47 normative content declaration for
+ * a language code tag
+ *
+ * @see https://en.wikipedia.org/wiki/IETF_language_tag
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class LanguageCodeValue extends StringValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '__lcode';
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $userValue ) {
+
+ $languageCode = Localizer::asBCP47FormattedLanguageCode( $userValue );
+
+ if ( $languageCode === '' ) {
+ $this->addErrorMsg( [
+ 'smw-datavalue-languagecode-missing',
+ $this->m_property !== null ? $this->m_property->getLabel() : 'UNKNOWN'
+ ] );
+ return;
+ }
+
+ // Checks whether the language tag is valid in MediaWiki for when
+ // it is not executed in a query context
+ if ( !$this->getOption( self::OPT_QUERY_CONTEXT ) && !Localizer::isKnownLanguageTag( $languageCode ) ) {
+ $this->addErrorMsg( [
+ 'smw-datavalue-languagecode-invalid',
+ $languageCode
+ ] );
+ return;
+ }
+
+ $this->m_dataitem = new DIBlob( $languageCode );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/MonolingualTextValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/MonolingualTextValue.php
new file mode 100644
index 00000000..2233ef7d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/MonolingualTextValue.php
@@ -0,0 +1,363 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DataValues\ValueFormatters\DataValueFormatter;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Localizer;
+use SMWContainerSemanticData as ContainerSemanticData;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+use SMWDIBlob as DIBlob;
+use SMWDIContainer as DIContainer;
+
+/**
+ * MonolingualTextValue requires two components, a language code and a
+ * text.
+ *
+ * A text `foo@en` is expected to be invoked with a BCP47 language
+ * code tag and a language dependent text component.
+ *
+ * Internally, the value is stored as container object that represents
+ * the language code and text as separate entities in order to be queried
+ * individually.
+ *
+ * External output representation depends on the context (wiki, html)
+ * whether the language code is omitted or not.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class MonolingualTextValue extends AbstractMultiValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '_mlt_rec';
+
+ /**
+ * @var DIProperty[]|null
+ */
+ private static $properties = null;
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @see AbstractMultiValue::setFieldProperties
+ *
+ * @param DIProperty[] $properties
+ */
+ public function setFieldProperties( array $properties ) {
+ // Keep the interface while the properties for this type
+ // are fixed.
+ }
+
+ /**
+ * @see AbstractMultiValue::getProperties
+ *
+ * @param DIProperty[] $properties
+ */
+ public function getProperties() {
+ self::$properties;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param $userValue
+ * @param string $languageCode
+ *
+ * @return string
+ */
+ public function getTextWithLanguageTag( $text, $languageCode ) {
+ return $text . '@' . Localizer::asBCP47FormattedLanguageCode( $languageCode );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ * @note called by DataValue::setUserValue
+ *
+ * @param string $userValue
+ */
+ protected function parseUserValue( $userValue ) {
+
+ list( $text, $languageCode ) = $this->getValuesFromString( $userValue );
+
+ $languageCodeValue = $this->newLanguageCodeValue( $languageCode );
+
+ if (
+ ( $languageCode !== '' && $languageCodeValue->getErrors() !== [] ) ||
+ ( $languageCode === '' && $this->isEnabledFeature( SMW_DV_MLTV_LCODE ) ) ) {
+ $this->addError( $languageCodeValue->getErrors() );
+ return;
+ }
+
+ $dataValues = [];
+
+ foreach ( $this->getPropertyDataItems() as $property ) {
+
+ if (
+ ( $languageCode === '' && $property->getKey() === '_LCODE' ) ||
+ ( $text === '' && $property->getKey() === '_TEXT' ) ) {
+ continue;
+ }
+
+ $value = $text;
+
+ if ( $property->getKey() === '_LCODE' ) {
+ $value = $languageCode;
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByProperty(
+ $property,
+ $value,
+ false,
+ $this->m_contextPage
+ );
+
+ $this->addError( $dataValue->getErrors() );
+
+ $dataValues[] = $dataValue;
+ }
+
+ // Generate a hash from the normalized representation so that foo@en being
+ // the same as foo@EN independent of a user input
+ $containerSemanticData = $this->newContainerSemanticData( $text . '@' . $languageCode );
+
+ foreach ( $dataValues as $dataValue ) {
+ $containerSemanticData->addDataValue( $dataValue );
+ }
+
+ // Remember the data to extend the sortkey
+ $containerSemanticData->setExtensionData( 'sort.data', implode( ';', [ $text, $languageCode ] ) );
+
+ $this->m_dataitem = new DIContainer( $containerSemanticData );
+ }
+
+ /**
+ * @note called by MonolingualTextValueDescriptionDeserializer::deserialize
+ * and MonolingualTextValue::parseUserValue
+ *
+ * No explicit check is made on the validity of a language code and is
+ * expected to be done before calling this method.
+ *
+ * @since 2.4
+ *
+ * @param string $userValue
+ *
+ * @return array
+ */
+ public function getValuesFromString( $userValue ) {
+ return $this->dataValueServiceFactory->getValueParser( $this )->parse( $userValue );
+ }
+
+ /**
+ * @see DataValue::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;
+ }
+
+ return false;
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ */
+ public function getShortWikiText( $linker = null ) {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::WIKI_SHORT, $linker );
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ */
+ public function getShortHTMLText( $linker = null ) {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::HTML_SHORT, $linker );
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ */
+ public function getLongWikiText( $linker = null ) {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::WIKI_LONG, $linker );
+ }
+
+ /**
+ * @see DataValue::getLongHTMLText
+ */
+ public function getLongHTMLText( $linker = null ) {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::HTML_LONG, $linker );
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ */
+ public function getWikiValue() {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::VALUE );
+ }
+
+ /**
+ * @since 2.4
+ * @note called by AbstractRecordValue::getPropertyDataItems
+ *
+ * @return DIProperty[]
+ */
+ public function getPropertyDataItems() {
+
+ if ( self::$properties !== null && self::$properties !== [] ) {
+ return self::$properties;
+ }
+
+ foreach ( [ '_TEXT', '_LCODE' ] as $id ) {
+ self::$properties[] = new DIProperty( $id );
+ }
+
+ return self::$properties;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function getDataItems() {
+ return parent::getDataItems();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $languageCode
+ *
+ * @return DataValue|null
+ */
+ public function getTextValueByLanguage( $languageCode ) {
+
+ if ( ( $list = $this->toArray() ) === [] ) {
+ return null;
+ }
+
+ if ( $list['_LCODE'] !== Localizer::asBCP47FormattedLanguageCode( $languageCode ) ) {
+ return null;
+ }
+
+ if ( $list['_TEXT'] === '' ) {
+ return null;
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ new DIBlob( $list['_TEXT'] ),
+ new DIProperty( '_TEXT' )
+ );
+
+ return $dataValue;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function toArray() {
+
+ if ( !$this->isValid() || $this->getDataItem() === [] ) {
+ return [];
+ }
+
+ $semanticData = $this->getDataItem()->getSemanticData();
+
+ $list = [
+ '_TEXT' => '',
+ '_LCODE' => ''
+ ];
+
+ $dataItems = $semanticData->getPropertyValues( new DIProperty( '_TEXT' ) );
+ $dataItem = reset( $dataItems );
+
+ if ( $dataItem !== false ) {
+ $list['_TEXT'] = $dataItem->getString();
+ }
+
+ $dataItems = $semanticData->getPropertyValues( new DIProperty( '_LCODE' ) );
+ $dataItem = reset( $dataItems );
+
+ if ( $dataItem !== false ) {
+ $list['_LCODE'] = $dataItem->getString();
+ }
+
+ return $list;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function toString() {
+
+ if ( !$this->isValid() || $this->getDataItem() === [] ) {
+ return '';
+ }
+
+ $list = $this->toArray();
+
+ return $list['_TEXT'] . '@' . $list['_LCODE'];
+ }
+
+ private function newContainerSemanticData( $value ) {
+
+ if ( $this->m_contextPage === null ) {
+ $containerSemanticData = ContainerSemanticData::makeAnonymousContainer();
+ $containerSemanticData->skipAnonymousCheck();
+ } else {
+ $subobjectName = '_ML' . md5( $value );
+
+ $subject = new DIWikiPage(
+ $this->m_contextPage->getDBkey(),
+ $this->m_contextPage->getNamespace(),
+ $this->m_contextPage->getInterwiki(),
+ $subobjectName
+ );
+
+ $containerSemanticData = new ContainerSemanticData( $subject );
+ }
+
+ return $containerSemanticData;
+ }
+
+ private function newLanguageCodeValue( $languageCode ) {
+
+ $languageCodeValue = new LanguageCodeValue();
+
+ if ( $this->m_property !== null ) {
+ $languageCodeValue->setProperty( $this->m_property );
+ }
+
+ $languageCodeValue->setUserValue( $languageCode );
+
+ return $languageCodeValue;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php
new file mode 100644
index 00000000..a3bb90e3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php
@@ -0,0 +1,384 @@
+<?php
+
+namespace SMW\DataValues\Number;
+
+use InvalidArgumentException;
+use SMW\Message;
+use SMW\Options;
+use SMWNumberValue as NumberValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class IntlNumberFormatter {
+
+ /**
+ * Localization related constants
+ */
+ const CONTENT_LANGUAGE = Message::CONTENT_LANGUAGE;
+ const USER_LANGUAGE = Message::USER_LANGUAGE;
+ const PREFERRED_LANGUAGE = 'preferred.language';
+
+ /**
+ * Separator related constants
+ */
+ const DECIMAL_SEPARATOR = NumberValue::DECIMAL_SEPARATOR;
+ const THOUSANDS_SEPARATOR = NumberValue::THOUSANDS_SEPARATOR;
+
+ /**
+ * Format related constants
+ */
+ const DEFAULT_FORMAT = 'default.format';
+ const VALUE_FORMAT = 'value.format';
+
+ /**
+ * @var IntlNumberFormatter
+ */
+ private static $instance = null;
+
+ /**
+ * @var Options
+ */
+ private $options = null;
+
+ /**
+ * @var integer
+ */
+ private $maxNonExpNumber = null;
+
+ /**
+ * @var integer
+ */
+ private $defaultPrecision = 3;
+
+ /**
+ * @since 2.1
+ *
+ * @param integer $maxNonExpNumber
+ */
+ public function __construct( $maxNonExpNumber ) {
+ $this->maxNonExpNumber = $maxNonExpNumber;
+ $this->options = new Options();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return IntlNumberFormatter
+ */
+ public static function getInstance() {
+
+ if ( self::$instance === null ) {
+ self::$instance = new self(
+ $GLOBALS['smwgMaxNonExpNumber']
+ );
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function reset() {
+ $this->options->set( self::DECIMAL_SEPARATOR, false );
+ $this->options->set( self::THOUSANDS_SEPARATOR, false );
+ $this->options->set( self::USER_LANGUAGE, false );
+ $this->options->set( self::CONTENT_LANGUAGE, false );
+ $this->options->set( self::PREFERRED_LANGUAGE, false );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string $key
+ * @param mixed $value
+ */
+ public function setOption( $key, $value ) {
+ $this->options->set( $key, $value );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $type
+ * @param string|integer $locale
+ *
+ * @return string
+ */
+ public function getSeparatorByLanguage( $type, $locale = '' ) {
+
+ $language = $locale === self::USER_LANGUAGE ? $this->getUserLanguage() : $this->getContentLanguage();
+
+ if ( $type === self::DECIMAL_SEPARATOR ) {
+ return $this->getPreferredLocalizedSeparator( self::DECIMAL_SEPARATOR, 'smw_decseparator', $language );
+ }
+
+ if ( $type === self::THOUSANDS_SEPARATOR ) {
+ return $this->getPreferredLocalizedSeparator( self::THOUSANDS_SEPARATOR, 'smw_kiloseparator', $language );
+ }
+
+ throw new InvalidArgumentException( $type . " is unknown" );
+ }
+
+ /**
+ * This method formats a float number value according to the given language and
+ * precision settings, with some intelligence to produce readable output. Used
+ * to format a number that was not hand-formatted by a user.
+ *
+ * @param mixed $value input number
+ * @param integer|false $precision optional positive integer, controls how many digits after
+ * the decimal point are shown
+ * @param string|integer $format
+ *
+ * @since 2.1
+ *
+ * @return string
+ */
+ public function format( $value, $precision = false, $format = '' ) {
+
+ if ( $format === self::VALUE_FORMAT ) {
+ return $this->getValueFormattedNumberWithPrecision( $value, $precision );
+ }
+
+ if ( $precision !== false || $format === self::DEFAULT_FORMAT ) {
+ return $this->getDefaultFormattedNumberWithPrecision( $value, $precision );
+ }
+
+ return $this->doFormatByHeuristicRuleWith( $value, $precision );
+ }
+
+ /**
+ * This method formats a float number value according to the given language and
+ * precision settings, with some intelligence to produce readable output. Used
+ * to format a number that was not hand-formatted by a user.
+ *
+ * @param mixed $value input number
+ * @param integer|false $precision optional positive integer, controls how many digits after
+ * the decimal point are shown
+ *
+ * @since 2.1
+ *
+ * @return string
+ */
+ private function doFormatByHeuristicRuleWith( $value, $precision = false ) {
+
+ // BC configuration to keep default behaviour
+ $precision = $this->defaultPrecision;
+
+ $decseparator = $this->getSeparatorByLanguage(
+ self::DECIMAL_SEPARATOR,
+ self::USER_LANGUAGE
+ );
+
+ // If number is a trillion or more, then switch to scientific
+ // notation. If number is less than 0.0000001 (i.e. twice precision),
+ // then switch to scientific notation. Otherwise print number
+ // using number_format. This may lead to 1.200, so then use trim to
+ // remove trailing zeroes.
+ $doScientific = false;
+
+ // @todo: Don't do all this magic for integers, since the formatting does not fit there
+ // correctly. E.g. one would have integers formatted as 1234e6, not as 1.234e9, right?
+ // The "$value!=0" is relevant: we want to scientify numbers that are close to 0, but never 0!
+ if ( ( $precision > 0 ) && ( $value != 0 ) ) {
+ $absValue = abs( $value );
+ if ( $absValue >= $this->maxNonExpNumber ) {
+ $doScientific = true;
+ } elseif ( $absValue < pow( 10, - $precision ) ) {
+ $doScientific = true;
+ } elseif ( $absValue < 1 ) {
+ if ( $absValue < pow( 10, - $precision ) ) {
+ $doScientific = true;
+ } else {
+ // Increase decimal places for small numbers, e.g. .00123 should be 5 places.
+ for ( $i = 0.1; $absValue <= $i; $i *= 0.1 ) {
+ $precision++;
+ }
+ }
+ }
+ }
+
+ if ( $doScientific ) {
+ // Should we use decimal places here?
+ $value = sprintf( "%1.6e", $value );
+ // Make it more readable by removing trailing zeroes from n.n00e7.
+ $value = preg_replace( '/(\\.\\d+?)0*e/u', '${1}e', $value, 1 );
+ // NOTE: do not use the optional $count parameter with preg_replace. We need to
+ // remain compatible with PHP 4.something.
+ if ( $decseparator !== '.' ) {
+ $value = str_replace( '.', $decseparator, $value );
+ }
+ } else {
+ $value = $this->doFormatWithPrecision(
+ $value,
+ $precision,
+ $decseparator,
+ $this->getSeparatorByLanguage( self::THOUSANDS_SEPARATOR, self::USER_LANGUAGE )
+ );
+
+ // Make it more readable by removing ending .000 from nnn.000
+ // Assumes substr is faster than a regular expression replacement.
+ $end = $decseparator . str_repeat( '0', $precision );
+ $lenEnd = strlen( $end );
+
+ if ( substr( $value, - $lenEnd ) === $end ) {
+ $value = substr( $value, 0, - $lenEnd );
+ } else {
+ $decseparator = preg_quote( $decseparator, '/' );
+ // If above replacement occurred, no need to do the next one.
+ // Make it more readable by removing trailing zeroes from nn.n00.
+ $value = preg_replace( "/($decseparator\\d+?)0*$/u", '$1', $value, 1 );
+ }
+ }
+
+ return $value;
+ }
+
+ private function getValueFormattedNumberWithPrecision( $value, $precision = false ) {
+
+ // The decimal are in ISO format (.), the separator as plain representation
+ // may collide with the content language (FR) therefore use the content language
+ // to match the decimal separator
+ if ( $this->isScientific( $value ) ) {
+ return $this->doFormatExponentialNotation( $value );
+ }
+
+ if ( $precision === false || $precision === null ) {
+ $precision = $this->getPrecisionFrom( $value );
+ }
+
+ return $this->doFormatWithPrecision(
+ $value,
+ $precision,
+ $this->getSeparatorByLanguage( self::DECIMAL_SEPARATOR, self::CONTENT_LANGUAGE ),
+ ''
+ );
+ }
+
+ private function getDefaultFormattedNumberWithPrecision( $value, $precision = false ) {
+
+ if ( $precision === false ) {
+ return $this->isDecimal( $value ) ? $this->applyDefaultPrecision( $value ) : floatval( $value );
+ }
+
+ return $this->doFormatWithPrecision(
+ $value,
+ $precision,
+ $this->getSeparatorByLanguage( self::DECIMAL_SEPARATOR, self::USER_LANGUAGE ),
+ $this->getSeparatorByLanguage( self::THOUSANDS_SEPARATOR, self::USER_LANGUAGE )
+ );
+ }
+
+ private function isDecimal( $value ) {
+ return floor( $value ) !== $value;
+ }
+
+ private function isScientific( $value ) {
+ return strpos( $value, 'E' ) !== false || strpos( $value, 'e' ) !== false;
+ }
+
+ private function applyDefaultPrecision( $value ) {
+ return round( $value, $this->defaultPrecision );
+ }
+
+ private function getPrecisionFrom( $value ) {
+ return strlen( strrchr( $value, "." ) ) - 1;
+ }
+
+ private function doFormatExponentialNotation( $value ) {
+ return str_replace(
+ [ '.', 'E' ],
+ [ $this->getSeparatorByLanguage( self::DECIMAL_SEPARATOR, self::CONTENT_LANGUAGE ), 'e' ],
+ $value
+ );
+ }
+
+ private function doFormatWithPrecision( $value, $precision = false, $decimal, $thousand ) {
+
+ $replacement = 0;
+
+ // Don't try to be more precise than the actual value (e.g avoid turning
+ // 72.769482308 into 72.76948230799999350892904)
+ if ( ( $actualPrecision = $this->getPrecisionFrom( $value ) ) < $precision && $actualPrecision > 0 && !$this->isScientific( $value ) ) {
+ $replacement = $precision - $actualPrecision;
+ $precision = $actualPrecision;
+ }
+
+ $value = (float)$value;
+ $isNegative = $value < 0;
+
+ // Format to some level of precision; number_format does rounding and
+ // locale formatting, x and y are used temporarily since number_format
+ // supports only single characters for either
+ $value = number_format( $value, $precision, 'x', 'y' );
+
+ // Due to https://bugs.php.net/bug.php?id=76824
+ if ( $isNegative && $value >= 0 ) {
+ $value = "-$value";
+ }
+
+ $value = str_replace(
+ [ 'x', 'y' ],
+ [
+ $decimal,
+ $thousand
+ ],
+ $value
+ );
+
+ if ( $replacement > 0 ) {
+ $value .= str_repeat( '0', $replacement );
+ }
+
+ return $value;
+ }
+
+ private function getUserLanguage() {
+
+ $language = Message::USER_LANGUAGE;
+
+ // The preferred language is set when the output formatter contained
+ // something like LOCL@es
+
+ if ( $this->options->has( self::PREFERRED_LANGUAGE ) && $this->options->get( self::PREFERRED_LANGUAGE ) ) {
+ $language = $this->options->get( self::PREFERRED_LANGUAGE );
+ } elseif ( $this->options->has( self::USER_LANGUAGE ) && $this->options->get( self::USER_LANGUAGE ) ) {
+ $language = $this->options->get( self::USER_LANGUAGE );
+ }
+
+ return $language;
+ }
+
+ private function getContentLanguage() {
+
+ $language = Message::CONTENT_LANGUAGE;
+
+ if ( $this->options->has( self::CONTENT_LANGUAGE ) && $this->options->get( self::CONTENT_LANGUAGE ) ) {
+ $language = $this->options->get( self::CONTENT_LANGUAGE );
+ }
+
+ return $language;
+ }
+
+ private function getPreferredLocalizedSeparator( $custom, $standard, $language ) {
+
+ if ( $this->options->has( $custom ) && ( $separator = $this->options->get( $custom ) ) !== false ) {
+ return $separator;
+ }
+
+ return Message::get( $standard, Message::TEXT, $language );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/UnitConverter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/UnitConverter.php
new file mode 100644
index 00000000..066c8937
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/UnitConverter.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace SMW\DataValues\Number;
+
+use SMW\ApplicationFactory;
+use SMW\CachedPropertyValuesPrefetcher;
+use SMW\DIProperty;
+use SMWDIBlob as DIBlob;
+use SMWNumberValue as NumberValue;
+
+/**
+ * Returns conversion data from a cache instance to enable a responsive query
+ * feedback and eliminate possible repeated DB requests.
+ *
+ * The cache is evicted as soon as the property that contains "Corresponds to"
+ * is altered.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class UnitConverter {
+
+ /**
+ * @var NumberValue
+ */
+ private $numberValue;
+
+ /**
+ * @var CachedPropertyValuesPrefetcher
+ */
+ private $cachedPropertyValuesPrefetcher;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var array
+ */
+ private $unitIds = [];
+
+ /**
+ * @var array
+ */
+ private $unitFactors = [];
+
+ /**
+ * @var false|string
+ */
+ private $mainUnit = false;
+
+ /**
+ * @var array
+ */
+ protected $prefixalUnitPreference = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param NumberValue $numberValue
+ * @param CachedPropertyValuesPrefetcher|null $cachedPropertyValuesPrefetcher
+ */
+ public function __construct( NumberValue $numberValue, CachedPropertyValuesPrefetcher $cachedPropertyValuesPrefetcher = null ) {
+ $this->numberValue = $numberValue;
+ $this->cachedPropertyValuesPrefetcher = $cachedPropertyValuesPrefetcher;
+
+ if ( $this->cachedPropertyValuesPrefetcher === null ) {
+ $this->cachedPropertyValuesPrefetcher = ApplicationFactory::getInstance()->getCachedPropertyValuesPrefetcher();
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getUnitIds() {
+ return $this->unitIds;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getUnitFactors() {
+ return $this->unitFactors;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ public function getMainUnit() {
+ return $this->mainUnit;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getPrefixalUnitPreference() {
+ return $this->prefixalUnitPreference;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ */
+ public function fetchConversionData( DIProperty $property ) {
+
+ $this->unitIds = [];
+ $this->unitFactors = [];
+ $this->mainUnit = false;
+ $this->prefixalUnitPreference = [];
+ $this->errors = [];
+
+ $factors = $this->cachedPropertyValuesPrefetcher->getPropertyValues(
+ $property->getDiWikiPage(),
+ new DIProperty( '_CONV' )
+ );
+
+ $this->numberValue->setContextPage( $property->getDiWikiPage() );
+
+ if ( $factors === null || $factors === [] ) { // no custom type
+ return $this->errors[] = 'smw_nounitsdeclared';
+ }
+
+ $number = '';
+ $unit = '';
+ $asPrefix = false;
+
+ foreach ( $factors as $di ) {
+
+ // ignore corrupted data and bogus inputs
+ if ( !( $di instanceof DIBlob ) ||
+ ( $this->numberValue->parseNumberValue( $di->getString(), $number, $unit, $asPrefix ) != 0 ) ||
+ ( $number == 0 ) ) {
+ continue;
+ }
+
+ $this->matchUnitAliases(
+ $number,
+ $asPrefix,
+ preg_split( '/\s*,\s*/u', $unit )
+ );
+ }
+
+ // No unit with factor 1? Make empty string the main unit.
+ if ( $this->mainUnit === false ) {
+ $this->mainUnit = '';
+ }
+
+ // Always add an extra empty unit; not as a synonym for the main unit
+ // but as a new unit with ID '' so if users do not give any unit, the
+ // conversion tooltip will still display the main unit for clarity
+ // (the empty unit is never displayed; we filter it when making
+ // conversion values)
+ $this->unitFactors = [ '' => 1 ] + $this->unitFactors;
+ $this->unitIds[''] = '';
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty|null $property
+ */
+ public function initConversionData( DIProperty $property = null ) {
+
+ if ( $property === null || ( $propertyDiWikiPage = $property->getDiWikiPage() ) === null ) {
+ return;
+ }
+
+ $blobStore = $this->cachedPropertyValuesPrefetcher->getBlobStore();
+
+ // Ensure that when the property page is altered the cache gets
+ // evicted
+ $hash = $this->cachedPropertyValuesPrefetcher->getRootHashFrom(
+ $propertyDiWikiPage
+ );
+
+ $container = $blobStore->read(
+ $hash
+ );
+
+ $key = '--conv';
+
+ if ( $container->has( $key ) ) {
+ $data = $container->get( $key );
+ $this->unitIds = $data['ids'];
+ $this->unitFactors = $data['factors'];
+ $this->mainUnit = $data['main'];
+ $this->prefixalUnitPreference = $data['prefix'];
+ return;
+ }
+
+ $this->fetchConversionData( $property );
+
+ if ( $this->errors !== [] ) {
+ return;
+ }
+
+ $data = [
+ 'ids' => $this->unitIds,
+ 'factors' => $this->unitFactors,
+ 'main' => $this->mainUnit,
+ 'prefix' => $this->prefixalUnitPreference
+ ];
+
+ $container->set( $key, $data );
+
+ $blobStore->save(
+ $container
+ );
+ }
+
+ private function matchUnitAliases( $number, $asPrefix, array $unitAliases ) {
+ $first = true;
+
+ foreach ( $unitAliases as $unit ) {
+ $unit = $this->numberValue->normalizeUnit( $unit );
+
+ // Legacy match the preserve some behaviour where spaces where normalized
+ // no matter what
+ $normalizedUnit = $this->numberValue->normalizeUnit(
+ str_replace( [ '&nbsp;', '&#160;', '&thinsp;', ' ' ], '', $unit )
+ );
+
+ if ( $first ) {
+ $unitid = $unit;
+ if ( $number == 1 ) { // add main unit to front of array (displayed first)
+ $this->mainUnit = $unit;
+ $this->unitFactors = [ $unit => 1 ] + $this->unitFactors;
+ } else { // non-main units are not ordered (can be modified via display units)
+ $this->unitFactors[$unit] = $number;
+ }
+ $first = false;
+ }
+ // add all known units to m_unitids to simplify checking for them
+ $this->unitIds[$unit] = $unitid;
+ $this->unitIds[$normalizedUnit] = $unitid;
+ $this->prefixalUnitPreference[$unit] = $asPrefix;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/PropertyChainValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/PropertyChainValue.php
new file mode 100644
index 00000000..7c6054b8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/PropertyChainValue.php
@@ -0,0 +1,220 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\DataValueFactory;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyChainValue extends StringValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '__pchn';
+
+ /**
+ * @var PropertyValue[]
+ */
+ private $propertyValues = [];
+
+ /**
+ * @var PropertyValue
+ */
+ private $lastPropertyChainValue;
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $value
+ *
+ * @return boolean
+ */
+ public static function isChained( $value ) {
+ return strpos( $value, '.' ) !== false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return PropertyValue
+ */
+ public function getLastPropertyChainValue() {
+ return $this->lastPropertyChainValue;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return PropertyValue[]
+ */
+ public function getPropertyChainValues() {
+ return $this->propertyValues;
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ */
+ public function setCaption( $caption ) {
+ $this->m_caption = $caption;
+
+ if ( $this->lastPropertyChainValue !== null ) {
+ $this->lastPropertyChainValue->setCaption( $caption );
+ }
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ */
+ public function getShortWikiText( $linker = null ) {
+
+ if ( $this->lastPropertyChainValue !== null ) {
+ return $this->lastPropertyChainValue->getShortWikiText( $linker ) . $this->doHintPropertyChainMembers();
+ }
+
+ return '';
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ */
+ public function getLongWikiText( $linker = null ) {
+
+ if ( $this->lastPropertyChainValue !== null ) {
+ return $this->lastPropertyChainValue->getLongWikiText( $linker ) . $this->doHintPropertyChainMembers();
+ }
+
+ return '';
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ */
+ public function getShortHTMLText( $linker = null ) {
+
+ if ( $this->lastPropertyChainValue !== null ) {
+ return $this->lastPropertyChainValue->getShortHTMLText( $linker ) . $this->doHintPropertyChainMembers();
+ }
+
+ return '';
+ }
+
+ /**
+ * @see DataValue::getLongHTMLText
+ */
+ public function getLongHTMLText( $linker = null ) {
+
+ if ( $this->lastPropertyChainValue !== null ) {
+ return $this->lastPropertyChainValue->getLongHTMLText( $linker ) . $this->doHintPropertyChainMembers();
+ }
+
+ return '';
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ */
+ public function getWikiValue() {
+ return $this->lastPropertyChainValue !== null ? $this->lastPropertyChainValue->getWikiValue() : '';
+ }
+
+ /**
+ * @see PropertyValue::isVisible
+ */
+ public function isVisible() {
+ return $this->isValid() && ( $this->lastPropertyChainValue->getDataItem()->isUserDefined() || $this->lastPropertyChainValue->getDataItem()->getLabel() !== '' );
+ }
+
+ /**
+ * @see SMWDataValue::loadDataItem()
+ *
+ * @param $dataitem SMWDataItem
+ *
+ * @return boolean
+ */
+ protected function loadDataItem( DataItem $dataItem ) {
+
+ if ( !$dataItem instanceof DIBlob ) {
+ return false;
+ }
+
+ $this->m_caption = false;
+ $this->m_dataitem = $dataItem;
+
+ $this->initPropertyChain( $dataItem->getString() );
+
+ return true;
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ * @note called by DataValue::setUserValue
+ *
+ * @param string $userValue
+ */
+ protected function parseUserValue( $value ) {
+
+ if ( $value === '' ) {
+ $this->addErrorMsg( 'smw_emptystring' );
+ }
+
+ if ( !$this->isChained( $value ) ) {
+ $this->addErrorMsg( 'smw-datavalue-propertychain-missing-chain-indicator' );
+ }
+
+ $this->initPropertyChain( $value );
+
+ $this->m_dataitem = new DIBlob( $value );
+ }
+
+ private function initPropertyChain( $value ) {
+
+ $chain = explode( '.', $value );
+
+ // Get the last which represents the final output
+ // Foo.Bar.Foobar.Baz
+ $last = array_pop( $chain );
+
+ $this->lastPropertyChainValue = DataValueFactory::getInstance()->newPropertyValueByLabel( $last );
+
+ if ( !$this->lastPropertyChainValue->isValid() ) {
+ return $this->addError( $this->lastPropertyChainValue->getErrors() );
+ }
+
+ $this->lastPropertyChainValue->copyOptions( $this->getOptions() );
+
+ // Generate a forward list from the remaining property labels
+ // Foo.Bar.Foobar
+ foreach ( $chain as $value ) {
+ $propertyValue = DataValueFactory::getInstance()->newPropertyValueByLabel( $value );
+
+ if ( !$propertyValue->isValid() ) {
+ continue;
+ }
+
+ $propertyValue->copyOptions( $this->getOptions() );
+
+ $this->propertyValues[] = $propertyValue;
+ }
+ }
+
+ private function doHintPropertyChainMembers() {
+ return '&nbsp;' . \Html::rawElement( 'span', [ 'title' => $this->m_dataitem ], '⠉' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/PropertyValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/PropertyValue.php
new file mode 100644
index 00000000..d88286d9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/PropertyValue.php
@@ -0,0 +1,501 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\DataValueFactory;
+use SMW\DataValues\ValueFormatters\DataValueFormatter;
+use SMW\DIProperty;
+use SMW\Exception\DataItemException;
+use SMW\Message;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+
+/**
+ * Objects of this class represent properties in SMW.
+ *
+ * This class represents both normal (user-defined) properties and predefined
+ * ("special") properties. Predefined properties may still have a standard label
+ * (and associated wiki article) and they will behave just like user-defined
+ * properties in most cases (e.g. when asking for a printout text, a link to the
+ * according page is produced).
+ *
+ * It is possible that predefined properties have no visible label at all, if they
+ * are used only internally and never specified by or shown to the user. Those
+ * will use their internal ID as DB key, and empty texts for most printouts. All
+ * other properties use their canonical DB key (even if they are predefined and
+ * have an id).
+ *
+ * Functions are provided to check whether a property is visible or
+ * user-defined, and to get the internal ID, if any.
+ *
+ * @note This datavalue is used only for representing properties and, possibly
+ * objects/values, but never for subjects (pages as such). Hence it does not
+ * provide a complete Title-like interface, or support for things like sortkey.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class PropertyValue extends DataValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '__pro';
+
+ /**
+ * Avoid the display of a tooltip
+ */
+ const OPT_NO_HIGHLIGHT = 'no.highlight';
+
+ /**
+ * Use linker with the highlighter
+ */
+ const OPT_HIGHLIGHT_LINKER = 'highlight.linker';
+
+ /**
+ * Avoid the display of a preferred label marker
+ */
+ const OPT_NO_PREF_LHNT = 'no.preflabel.marker';
+
+ /**
+ * Special formatting of the label/preferred label
+ */
+ const FORMAT_LABEL = 'format.label';
+
+ /**
+ * Label to be used for matching a DB search
+ */
+ const SEARCH_LABEL = 'search.label';
+
+ /**
+ * Cache for wiki page value object associated to this property, or
+ * null if no such page exists. Use getWikiPageValue() to get the data.
+ * @var SMWWikiPageValue
+ */
+ protected $m_wikipage = null;
+
+ /**
+ * @var array
+ */
+ protected $linkAttributes = [];
+
+ /**
+ * @var string
+ */
+ private $preferredLabel = '';
+
+ /**
+ * @var DIProperty
+ */
+ private $inceptiveProperty;
+
+ /**
+ * @var ValueFormatter
+ */
+ private $valueFormatter;
+
+ /**
+ * @since 2.4
+ *
+ * @param string $typeid
+ */
+ public function __construct( $typeid = self::TYPE_ID ) {
+ parent::__construct( $typeid );
+ }
+
+ /**
+ * @deprecated since 3.0
+ */
+ static private function makeUserProperty( $propertyLabel ) {
+ return DataValueFactory::getInstance()->newPropertyValueByLabel( $propertyLabel );
+ }
+
+ /**
+ * @removed since 3.0
+ */
+ static private function makeProperty( $propertyid ) {
+ $diProperty = new DIProperty( $propertyid );
+ $dvProperty = new SMWPropertyValue( self::TYPE_ID );
+ $dvProperty->setDataItem( $diProperty );
+ return $dvProperty;
+ }
+
+ /**
+ * We use the internal wikipage object to store some of this objects data.
+ * Clone it to make sure that data can be modified independently from the
+ * original object's content.
+ */
+ public function __clone() {
+ if ( !is_null( $this->m_wikipage ) ) {
+ $this->m_wikipage = clone $this->m_wikipage;
+ }
+ }
+
+ /**
+ * @note If the inceptive property and the property referenced in dataItem
+ * are not equal then the dataItem represents the end target to which the
+ * inceptive property has been redirected.
+ *
+ * @since 2.4
+ *
+ * @return DIProperty
+ */
+ public function getInceptiveProperty() {
+ return $this->inceptiveProperty;
+ }
+
+ /**
+ * Extended parsing function to first check whether value refers to pre-defined
+ * property, resolve aliases, and set internal property id accordingly.
+ * @todo Accept/enforce property namespace.
+ */
+ protected function parseUserValue( $value ) {
+ $this->m_wikipage = null;
+
+ $propertyValueParser = $this->dataValueServiceFactory->getValueParser(
+ $this
+ );
+
+ $propertyValueParser->isQueryContext(
+ $this->getOption( self::OPT_QUERY_CONTEXT )
+ );
+
+ $reqCapitalizedFirstChar = $this->getContextPage() !== null && $this->getContextPage()->getNamespace() === SMW_NS_PROPERTY;
+
+ $propertyValueParser->reqCapitalizedFirstChar(
+ $reqCapitalizedFirstChar
+ );
+
+ list( $propertyName, $capitalizedName, $inverse ) = $propertyValueParser->parse( $value );
+
+ foreach ( $propertyValueParser->getErrors() as $error ) {
+ return $this->addErrorMsg( $error, Message::PARSE );
+ }
+
+ try {
+ $this->m_dataitem = $this->createDataItemFrom(
+ $reqCapitalizedFirstChar,
+ $propertyName,
+ $capitalizedName,
+ $inverse
+ );
+ } catch ( DataItemException $e ) { // happens, e.g., when trying to sort queries by property "-"
+ $this->addErrorMsg( [ 'smw_noproperty', $value ] );
+ $this->m_dataitem = new DIProperty( 'ERROR', false ); // just to have something
+ }
+
+ // @see the SMW_DV_PROV_DTITLE explanation
+ if ( $this->isEnabledFeature( SMW_DV_PROV_DTITLE ) ) {
+ $dataItem = $this->dataValueServiceFactory->getPropertySpecificationLookup()->getPropertyFromDisplayTitle(
+ $value
+ );
+
+ $this->m_dataitem = $dataItem ? $dataItem : $this->m_dataitem;
+ }
+
+ // Copy the original DI to ensure we can compare it against a possible redirect
+ $this->inceptiveProperty = $this->m_dataitem;
+
+ if ( $this->isEnabledFeature( SMW_DV_PROV_REDI ) ) {
+ $this->m_dataitem = $this->m_dataitem->getRedirectTarget();
+ }
+
+ // If no external caption has been invoked then fetch a preferred label
+ if ( $this->m_caption === false || $this->m_caption === '' ) {
+ $this->preferredLabel = $this->m_dataitem->getPreferredLabel( $this->getOption( self::OPT_USER_LANGUAGE ) );
+ }
+
+ // Use the preferred label as first choice for a caption, if available
+ if ( $this->preferredLabel !== '' ) {
+ $this->m_caption = $this->preferredLabel;
+ } elseif ( $this->m_caption === false ) {
+ $this->m_caption = $value;
+ }
+ }
+
+ /**
+ * @see SMWDataValue::loadDataItem()
+ * @param $dataitem DataItem
+ * @return boolean
+ */
+ protected function loadDataItem( DataItem $dataItem ) {
+
+ if ( $dataItem->getDIType() !== DataItem::TYPE_PROPERTY ) {
+ return false;
+ }
+
+ $this->inceptiveProperty = $dataItem;
+ $this->m_dataitem = $dataItem;
+ $this->preferredLabel = $this->m_dataitem->getPreferredLabel();
+
+ unset( $this->m_wikipage );
+ $this->m_caption = false;
+ $this->linkAttributes = [];
+
+ if ( $this->preferredLabel !== '' ) {
+ $this->m_caption = $this->preferredLabel;
+ }
+
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getPreferredLabel() {
+ return $this->preferredLabel;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array $linkAttributes
+ */
+ public function setLinkAttributes( array $linkAttributes ) {
+ $this->linkAttributes = $linkAttributes;
+
+ if ( $this->getWikiPageValue() instanceof SMWDataValue ) {
+ $this->m_wikipage->setLinkAttributes( $linkAttributes );
+ }
+ }
+
+ public function setCaption( $caption ) {
+ parent::setCaption( $caption );
+ if ( $this->getWikiPageValue() instanceof SMWDataValue ) { // pass caption to embedded datavalue (used for printout)
+ $this->m_wikipage->setCaption( $caption );
+ }
+ }
+
+ public function setOutputFormat( $formatstring ) {
+
+ if ( $formatstring === false || $formatstring === '' ) {
+ return;
+ }
+
+ $this->m_outformat = $formatstring;
+
+ if ( $this->getWikiPageValue() instanceof SMWDataValue ) {
+ $this->m_wikipage->setOutputFormat( $formatstring );
+ }
+ }
+
+ public function setInverse( $isinverse ) {
+ return $this->m_dataitem = new DIProperty( $this->m_dataitem->getKey(), ( $isinverse == true ) );
+ }
+
+ /**
+ * Return a wiki page value that can be used for displaying this
+ * property, or null if no such wiki page exists (for predefined
+ * properties without any label).
+ * @return SMWWikiPageValue or null
+ */
+ public function getWikiPageValue() {
+
+ if ( isset( $this->m_wikipage ) ) {
+ return $this->m_wikipage;
+ }
+
+ $diWikiPage = $this->m_dataitem->getCanonicalDiWikiPage();
+
+ if ( $diWikiPage !== null ) {
+ $this->m_wikipage = DataValueFactory::getInstance()->newDataValueByItem( $diWikiPage, null, $this->m_caption );
+ $this->m_wikipage->setOutputFormat( $this->m_outformat );
+ $this->m_wikipage->setLinkAttributes( $this->linkAttributes );
+ $this->m_wikipage->copyOptions( $this->getOptions() );
+ $this->addError( $this->m_wikipage->getErrors() );
+ } else { // should rarely happen ($value is only changed if the input $value really was a label for a predefined prop)
+ $this->m_wikipage = null;
+ }
+
+ return $this->m_wikipage;
+ }
+
+ /**
+ * Return TRUE if this is a property that can be displayed, and not a pre-defined
+ * property that is used only internally and does not even have a user-readable name.
+ * @note Every user defined property is necessarily visible.
+ */
+ public function isVisible() {
+ return $this->isValid() && ( $this->m_dataitem->isUserDefined() || $this->m_dataitem->getCanonicalLabel() !== '' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function isRestricted() {
+
+ if ( !$this->isValid() ) {
+ return true;
+ }
+
+ $propertyRestrictionExaminer = $this->dataValueServiceFactory->getPropertyRestrictionExaminer();
+
+ $propertyRestrictionExaminer->isQueryContext(
+ $this->getOption( self::OPT_QUERY_CONTEXT )
+ );
+
+ $propertyRestrictionExaminer->checkRestriction(
+ $this->getDataItem(),
+ $this->getContextPage()
+ );
+
+ if ( !$propertyRestrictionExaminer->hasRestriction() ) {
+ return false;
+ }
+
+ $this->restrictionError = $propertyRestrictionExaminer->getError();
+
+ return true;
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ *
+ * @return string
+ */
+ public function getShortWikiText( $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::WIKI_SHORT, $linker ] );
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ *
+ * @return string
+ */
+ public function getShortHTMLText( $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::HTML_SHORT, $linker ] );
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ *
+ * @return string
+ */
+ public function getLongWikiText( $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::WIKI_LONG, $linker ] );
+ }
+
+ /**
+ * @see DataValue::getLongHTMLText
+ *
+ * @return string
+ */
+ public function getLongHTMLText( $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::HTML_LONG, $linker ] );
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ *
+ * @return string
+ */
+ public function getWikiValue() {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::VALUE ] );
+ }
+
+ /**
+ * Outputs a formatted property label that takes into account preferred/
+ * canonical label characteristics
+ *
+ * @param integer|string $format
+ * @param Linker|null $linker
+ *
+ * @return string
+ */
+ public function getFormattedLabel( $format = DataValueFormatter::VALUE, $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ $this->setOption(
+ self::FORMAT_LABEL,
+ $format
+ );
+
+ return $this->valueFormatter->format( $this, [ self::FORMAT_LABEL, $linker ] );
+ }
+
+ /**
+ * Outputs a label that corresponds to the display and sort characteristics (
+ * e.g. display title etc.) and can be used to initiate a match and search
+ * process.
+ *
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getSearchLabel() {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ self::SEARCH_LABEL ] );
+ }
+
+ /**
+ * Convenience method to find the type id of this property. Most callers
+ * should rather use DIProperty::findPropertyTypeId() directly. Note
+ * that this is not the same as getTypeID(), which returns the id of
+ * this property datavalue.
+ *
+ * @return string
+ */
+ public function getPropertyTypeID() {
+
+ if ( !$this->isValid() ) {
+ return '__err';
+ }
+
+ return $this->m_dataitem->findPropertyTypeId();
+ }
+
+ private function createDataItemFrom( $reqCapitalizedFirstChar, $propertyName, $capitalizedName, $inverse ) {
+
+ $contentLanguage = $this->getOption( self::OPT_CONTENT_LANGUAGE );
+
+ // Probe on capitalizedFirstChar because we only want predefined
+ // properties (e.g. Has type vs. has type etc.) to adhere the rule while
+ // custom (user) defined properties can appear in any form
+ if ( $reqCapitalizedFirstChar ) {
+ $dataItem = DIProperty::newFromUserLabel( $capitalizedName, $inverse, $contentLanguage );
+ $propertyName = $dataItem->isUserDefined() ? $propertyName : $capitalizedName;
+ }
+
+ return DIProperty::newFromUserLabel( $propertyName, $inverse, $contentLanguage );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ReferenceValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ReferenceValue.php
new file mode 100644
index 00000000..cbcb4208
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ReferenceValue.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\ApplicationFactory;
+use SMW\DataModel\ContainerSemanticData;
+use SMW\DataValueFactory;
+use SMW\DataValues\ValueFormatters\DataValueFormatter;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Message;
+use SMWDataItem as DataItem;
+use SMWDIContainer as DIContainer;
+use SMWDITime as DITime;
+
+/**
+ * ReferenceValue allows to define additional DV to describe the state of a
+ * SourceValue in terms of provenance or referential evidence. ReferenceValue is
+ * stored as separate entity to the original subject in order to encapsulate the
+ * SourceValue from the remaining annotations with reference to a subject.
+ *
+ * Defining which fields are required can vary and therefore is left to the user
+ * to specify such requirements using the `'Has fields' property.
+ *
+ * For example, declaring `[[Has fields::SomeValue;Date;SomeUrl;...]]` on a
+ * `SomeProperty` property page is to define:
+ *
+ * - a property called `SomeValue` with its own specification
+ * - a date property with the Date type
+ * - a property called `SomeUrl` with its own specification
+ * - ... any other property the users expects to require when making a value
+ * annotation of this type
+ *
+ * An annotation like `[[SomeProperty::Foo;12-12-1212;http://example.org]]` is
+ * expected to be a concatenated string and to be separated by ';' to indicate
+ * the next value string and will corespondent to the index of the `Has fields`
+ * declaration.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ReferenceValue extends AbstractMultiValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '_ref_rec';
+
+ /**
+ * @var DIProperty[]|null
+ */
+ private $properties = null;
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function setFieldProperties( array $properties ) {
+ foreach ( $properties as $property ) {
+ if ( $property instanceof DIProperty ) {
+ $this->properties[] = $property;
+ }
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getProperties() {
+ return $this->properties;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ 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 );
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getShortWikiText( $linker = null ) {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::WIKI_SHORT, $linker );
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getShortHTMLText( $linker = null ) {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::HTML_SHORT, $linker );
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getLongWikiText( $linker = null ) {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::WIKI_LONG, $linker );
+ }
+
+ /**
+ * @see DataValue::getLongHTMLText
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getLongHTMLText( $linker = null ) {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::HTML_LONG, $linker );
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getWikiValue() {
+ return $this->dataValueServiceFactory->getValueFormatter( $this )->format( DataValueFormatter::VALUE );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getPropertyDataItems() {
+
+ if ( $this->properties === null ) {
+ $this->properties = $this->getFieldProperties( $this->getProperty() );
+
+ if ( count( $this->properties ) == 0 ) {
+ $this->addErrorMsg( [ 'smw-datavalue-reference-invalid-fields-definition' ], Message::PARSE );
+ }
+ }
+
+ return $this->properties;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getDataItems() {
+ return parent::getDataItems();
+ }
+
+ /**
+ * @note called by DataValue::setUserValue
+ * @see DataValue::parseUserValue
+ *
+ * {@inheritDoc}
+ */
+ protected function parseUserValue( $value ) {
+
+ if ( $value === '' ) {
+ $this->addErrorMsg( [ 'smw_novalues' ] );
+ return;
+ }
+
+ $containerSemanticData = $this->newContainerSemanticData( $value );
+ $sortKeys = [];
+
+ $values = $this->getValuesFromString( $value );
+ $index = 0; // index in value array
+
+ $propertyIndex = 0; // index in property list
+ $empty = true;
+
+ foreach ( $this->getPropertyDataItems() as $property ) {
+
+ if ( !array_key_exists( $index, $values ) || $this->getErrors() !== [] ) {
+ break; // stop if there are no values left
+ }
+
+ // generating the DVs:
+ if ( ( $values[$index] === '' ) || ( $values[$index] == '?' ) ) { // explicit omission
+ $index++;
+ } else {
+ $dataValue = DataValueFactory::getInstance()->newDataValueByProperty(
+ $property,
+ $values[$index],
+ false,
+ $containerSemanticData->getSubject()
+ );
+
+ if ( $dataValue->isValid() ) { // valid DV: keep
+ $dataItem = $dataValue->getDataItem();
+
+ $containerSemanticData->addPropertyObjectValue(
+ $property,
+ $dataItem
+ );
+
+ // Chronological order determined first
+ if ( $dataItem instanceof DITime ) {
+ array_unshift( $sortKeys, $dataItem->getSortKey() );
+ } else {
+ $sortKeys[] = $dataItem->getSortKey();
+ }
+
+ $index++;
+ $empty = false;
+ } elseif ( $index == 0 || ( count( $values ) - $index ) == ( count( $this->properties ) - $propertyIndex ) ) {
+ $containerSemanticData->addPropertyObjectValue( $property, $dataValue->getDataItem() );
+ $this->addError( $dataValue->getErrors() );
+ ++$index;
+ }
+ }
+
+ ++$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 DataValue::loadDataItem
+ */
+ 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;
+ }
+
+ return false;
+ }
+
+ private function newContainerSemanticData( $value ) {
+
+ if ( $this->m_contextPage === null ) {
+ $containerSemanticData = ContainerSemanticData::makeAnonymousContainer();
+ $containerSemanticData->skipAnonymousCheck();
+ } else {
+ $subobjectName = '_REF' . md5( $value );
+
+ $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/src/DataValues/StringValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/StringValue.php
new file mode 100644
index 00000000..cb68f84c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/StringValue.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\DataValues\ValueFormatters\DataValueFormatter;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+use SMWDIBlob as DIBlob;
+
+/**
+ * Implements a string/text based datavalue suitable for defining text properties.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Nikolas Iwan
+ * @author Markus Krötzsch
+ */
+class StringValue extends DataValue {
+
+ /**
+ * DV text identifier
+ */
+ const TYPE_ID = '_txt';
+
+ /**
+ * DV identifier
+ */
+ const TYPE_LEGACY_ID = '_str';
+
+ /**
+ * DV code identifier
+ */
+ const TYPE_COD_ID = '_cod';
+
+ /**
+ * @var ValueFormatter
+ */
+ private $valueFormatter;
+
+ /**
+ * @see DataValue::getShortWikiText
+ *
+ * {@inheritDoc}
+ */
+ public function getShortWikiText( $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::WIKI_SHORT, $linker ] );
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ *
+ * {@inheritDoc}
+ */
+ public function getShortHTMLText( $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::HTML_SHORT, $linker ] );
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ *
+ * {@inheritDoc}
+ */
+ public function getLongWikiText( $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::WIKI_LONG, $linker ] );
+ }
+
+ /**
+ * @todo Rather parse input to obtain properly formatted HTML.
+ * @see DataValue::getLongHTMLText
+ *
+ * {@inheritDoc}
+ */
+ public function getLongHTMLText( $linker = null ) {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::HTML_LONG, $linker ] );
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ *
+ * {@inheritDoc}
+ */
+ public function getWikiValue() {
+
+ if ( $this->valueFormatter === null ) {
+ $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this );
+ }
+
+ return $this->valueFormatter->format( $this, [ DataValueFormatter::VALUE, null ] );
+ }
+
+ /**
+ * @see DataValue::getInfolinks
+ *
+ * {@inheritDoc}
+ */
+ public function getInfolinks() {
+
+ if ( $this->m_typeid != '_cod' ) {
+ return parent::getInfolinks();
+ }
+
+ return [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return integer
+ */
+ public function getLength() {
+
+ if ( !$this->isValid() ) {
+ return 0;
+ }
+
+ return mb_strlen( $this->m_dataitem->getString() );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * {@inheritDoc}
+ */
+ protected function parseUserValue( $value ) {
+
+ if ( $value === '' ) {
+ $this->addErrorMsg( 'smw_emptystring' );
+ }
+
+ $this->m_dataitem = new DIBlob( $value );
+ }
+
+ /**
+ * @see DataValue::loadDataItem
+ *
+ * {@inheritDoc}
+ */
+ protected function loadDataItem( DataItem $dataItem ) {
+
+ if ( !$dataItem instanceof DIBlob ) {
+ return false;
+ }
+
+ $this->m_caption = false;
+ $this->m_dataitem = $dataItem;
+
+ return true;
+ }
+
+ /**
+ * @see DataValue::getServiceLinkParams
+ *
+ * {@inheritDoc}
+ */
+ protected function getServiceLinkParams() {
+
+ if ( !$this->isValid() ) {
+ return false;
+ }
+
+ // Create links to mapping services based on a wiki-editable message. The parameters
+ // available to the message are:
+ // $1: urlencoded string
+ return [ rawurlencode( $this->m_dataitem->getString() ) ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TelephoneUriValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TelephoneUriValue.php
new file mode 100644
index 00000000..24be076d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TelephoneUriValue.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMWURIValue as UriValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class TelephoneUriValue extends UriValue {
+
+ /**
+ * @since 2.4
+ *
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( '_tel' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TemperatureValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TemperatureValue.php
new file mode 100644
index 00000000..37048df8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TemperatureValue.php
@@ -0,0 +1,221 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMWDINumber as DINumber;
+use SMWNumberValue as NumberValue;
+
+/**
+ * This datavalue implements unit support for measuring temperatures. This is
+ * mostly an example implementation of how to realise custom unit types easily.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class TemperatureValue extends NumberValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '_tem';
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * NumberValue::convertToMainUnit
+ */
+ protected function convertToMainUnit( $number, $unit ) {
+
+ $this->m_unitin = $this->getUnitID( $unit );
+
+ if ( ( $value = $this->convertToKelvin( $number, $this->m_unitin ) ) === false ) {
+ return false;
+ }
+
+ $this->m_dataitem = new DINumber( $value );
+
+ return true;
+ }
+
+ /**
+ * NumberValue::makeConversionValues
+ */
+ protected function makeConversionValues() {
+
+ if ( $this->m_unitvalues !== false ) {
+ return; // do this only once
+ }
+
+ $this->m_unitvalues = [];
+
+ if ( !$this->isValid() ) {
+ return $this->m_unitvalues;
+ }
+
+ $displayUnit = $this->getPreferredDisplayUnit();
+ $number = $this->m_dataitem->getNumber();
+
+ $unitvalues = [
+ 'K' => $number,
+ '°C' => $number - 273.15,
+ '°F' => ( $number - 273.15 ) * 1.8 + 32,
+ '°R' => ( $number ) * 1.8
+ ];
+
+ if ( isset( $unitvalues[$displayUnit] ) ) {
+ $this->m_unitvalues[$displayUnit] = $unitvalues[$displayUnit];
+ }
+
+ $this->m_unitvalues += $unitvalues;
+ }
+
+ /**
+ * NumberValue::makeUserValue
+ */
+ protected function makeUserValue() {
+
+ if ( ( $this->m_outformat ) && ( $this->m_outformat != '-' ) &&
+ ( $this->m_outformat != '-n' ) && ( $this->m_outformat != '-u' ) ) { // first try given output unit
+ $printUnit = $this->normalizeUnit( $this->m_outformat );
+ $this->m_unitin = $this->getUnitID( $printUnit );
+ } else {
+ $this->m_unitin = $this->getPreferredDisplayUnit();
+ $printUnit = $this->m_unitin;
+ }
+
+ $value =$this->convertToUnit(
+ $this->m_dataitem->getNumber(),
+ $this->m_unitin
+ );
+
+ // -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( $value );
+ $this->m_caption .= '&#160;';
+ } else {
+ $this->m_caption = $this->getNormalizedFormattedNumber( $value );
+ $this->m_caption .= ' ';
+ }
+
+ // -n is the format for displaying the number only
+ if ( $this->m_outformat == '-n' ) {
+ $printUnit = '';
+ }
+
+ $this->m_caption .= $printUnit;
+ }
+
+ /**
+ * Helper method to find the main representation of a certain unit.
+ */
+ protected function getUnitID( $unit ) {
+ /// TODO possibly localise some of those strings
+ switch ( $unit ) {
+ case '':
+ case 'K':
+ case 'Kelvin':
+ case 'kelvin':
+ case 'kelvins':
+ return 'K';
+ // There's a dedicated Unicode character (℃, U+2103) for degrees C.
+ // Your font may or may not display it; do not be alarmed.
+ case '°C':
+ case '℃':
+ case 'Celsius':
+ case 'centigrade':
+ return '°C';
+ case '°F':
+ case 'Fahrenheit':
+ return '°F';
+ case '°R':
+ case 'Rankine':
+ return '°R';
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * NumberValue::getUnitList
+ */
+ public function getUnitList() {
+ return [ 'K', '°C', '°F', '°R' ];
+ }
+
+ /**
+ * NumberValue::getUnit
+ */
+ public function getUnit() {
+ return 'K';
+ }
+
+ private function getPreferredDisplayUnit() {
+
+ $unit = $this->getUnit();
+
+ if ( $this->getProperty() === null ) {
+ return $unit;
+ }
+
+ $units = $this->dataValueServiceFactory->getPropertySpecificationLookup()->getDisplayUnits(
+ $this->getProperty()
+ );
+
+ if ( $units !== null && $units !== [] ) {
+ $unit = $this->getUnitID( end( $units ) );
+ }
+
+ return $this->getUnitID( $unit );
+ }
+
+ private function convertToKelvin( $number, $unit ) {
+
+ switch ( $unit ) {
+ case 'K':
+ return $number;
+ break;
+ case '°C':
+ return $number + 273.15;
+ break;
+ case '°F':
+ return ( $number - 32 ) / 1.8 + 273.15;
+ break;
+ case '°R':
+ return ( $number ) / 1.8;
+ }
+
+ return false; // unsupported unit
+ }
+
+ private function convertToUnit( $number, $unit ) {
+
+ switch ( $unit ) {
+ case 'K':
+ return $number;
+ break;
+ case '°C':
+ return $number - 273.15;
+ break;
+ case '°F':
+ return ( $number - 273.15 ) * 1.8 + 32;
+ break;
+ case '°R':
+ return ( $number ) * 1.8;
+ break;
+ // default: unit not supported
+ }
+
+ return 0;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/CalendarModel.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/CalendarModel.php
new file mode 100644
index 00000000..8ac24544
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/CalendarModel.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace SMW\DataValues\Time;
+
+/**
+ * It is assumed that the changeover from the Julian calendar to the Gregorian
+ * calendar occurred in October of 1582.
+ *
+ * For dates on or before 4 October 1582, the Julian calendar is used; for dates
+ * on or after 15 October 1582, the Gregorian calendar is used.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+interface CalendarModel {
+
+ /**
+ * Gregorian calendar
+ */
+ const CM_GREGORIAN = 1;
+
+ /**
+ * Julian calendar
+ */
+ const CM_JULIAN = 2;
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/Components.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/Components.php
new file mode 100644
index 00000000..f1aea129
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/Components.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace SMW\DataValues\Time;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Components {
+
+ /**
+ * @var array
+ */
+ public static $months = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+ ];
+
+ /**
+ * @var array
+ */
+ public static $monthsShort = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec'
+ ];
+
+ /**
+ * @var []
+ */
+ private $components = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param array $components
+ */
+ public function __construct( array $components = [] ) {
+ $this->components = $components;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function get( $key ) {
+
+ if ( isset( $this->components[$key] ) ) {
+ return $this->components[$key];
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/IntlTimeFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/IntlTimeFormatter.php
new file mode 100644
index 00000000..888d6711
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/IntlTimeFormatter.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace SMW\DataValues\Time;
+
+use Language;
+use SMW\Localizer;
+use SMWDITime as DITime;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class IntlTimeFormatter {
+
+ const LOCL_DEFAULT = 0;
+ const LOCL_TIMEZONE = 0x2;
+ const LOCL_TIMEOFFSET = 0x4;
+
+ /**
+ * @var DITime
+ */
+ private $dataItem;
+
+ /**
+ * @var Language
+ */
+ private $language;
+
+ /**
+ * @var boolean
+ */
+ private $hasLocalTimeCorrection = false;
+
+ /**
+ * @since 2.4
+ *
+ * @param DITime $dataItem
+ * @param Language|null $language
+ */
+ public function __construct( DITime $dataItem, Language $language = null ) {
+ $this->dataItem = $dataItem;
+ $this->language = $language;
+
+ if ( $this->language === null ) {
+ $this->language = Localizer::getInstance()->getContentLanguage();
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasLocalTimeCorrection() {
+ return $this->hasLocalTimeCorrection;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $formatFlag
+ *
+ * @return string|boolean
+ */
+ public function getLocalizedFormat( $formatFlag = self::LOCL_DEFAULT ) {
+
+ $dateTime = $this->dataItem->asDateTime();
+ $timezone = '';
+
+ $this->hasLocalTimeCorrection = false;
+
+ if ( !$dateTime ) {
+ return false;
+ }
+
+ $localizer = Localizer::getInstance();
+
+ if ( ( self::LOCL_TIMEOFFSET & $formatFlag ) != 0 ) {
+ $dateTime = $localizer->getLocalTime( $dateTime );
+ $this->hasLocalTimeCorrection = isset( $dateTime->hasLocalTimeCorrection ) ? $dateTime->hasLocalTimeCorrection : false;
+ }
+
+ if ( ( self::LOCL_TIMEZONE & $formatFlag ) != 0 ) {
+ $timezone = $this->dataItem->getTimezone();
+ $dateTime = Timezone::getModifiedTime( $dateTime, $timezone );
+ }
+
+ $lang = $localizer->getLang(
+ $this->language
+ );
+
+ $precision = $this->dataItem->getPrecision();
+
+ // Look for the Z precision which indicates the position of the TZ
+ if ( $precision === SMW_PREC_YMDT && $timezone !== '' ) {
+ $precision = SMW_PREC_YMDTZ;
+ }
+
+ $preferredDateFormatByPrecision = $lang->getPreferredDateFormatByPrecision(
+ $precision
+ );
+
+ // Mark the position since we cannot use DateTime::setTimezone in case
+ // it is a military zone
+ $preferredDateFormatByPrecision = str_replace( 'T', '**', $preferredDateFormatByPrecision );
+
+ $dateString = $this->formatWithLocalizedTextReplacement(
+ $dateTime,
+ $preferredDateFormatByPrecision
+ );
+
+ return str_replace( '**', $timezone, $dateString );
+ }
+
+ /**
+ * Permitted formatting options are specified by http://php.net/manual/en/function.date.php
+ *
+ * @since 2.4
+ *
+ * @param string $format
+ *
+ * @return string|boolean
+ */
+ public function format( $format ) {
+
+ $dateTime = $this->dataItem->asDateTime();
+
+ if ( !$dateTime ) {
+ return false;
+ }
+
+ $output = $this->formatWithLocalizedTextReplacement(
+ $dateTime,
+ $format
+ );
+
+ return $output;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $format
+ *
+ * @return boolean
+ */
+ public function containsValidDateFormatRule( $format ) {
+
+ foreach ( str_split( $format ) as $value ) {
+ if ( in_array( $value, [ 'd', 'D', 'j', 'l', 'N', 'w', 'W', 'F', 'M', 'm', 'n', 't', 'L', 'o', 'Y', 'y', "c", 'r' ] ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * DateTime generally outputs English textual representation
+ *
+ * - D A textual representation of a day, three letters
+ * - l (lowercase 'L'), A full textual representation of the day of the week
+ * - F A full textual representation of a month, such as January or March
+ * - M A short textual representation of a month, three letters
+ * - a Lowercase Ante meridiem and Post meridiem am or pm
+ * - A Uppercase Ante meridiem and Post meridiem
+ */
+ private function formatWithLocalizedTextReplacement( $dateTime, $format ) {
+
+ $output = $dateTime->format( $format );
+
+ // (n) DateTime => 1 through 12
+ $monthNumber = $dateTime->format( 'n' );
+
+ // (N) DateTime => 1 (for Monday) through 7 (for Sunday)
+ // (w) DateTime => 0 (for Sunday) through 6 (for Saturday)
+ // MW => 1 (for Sunday) through 7 (for Saturday)
+ $dayNumber = $dateTime->format( 'w' ) + 1;
+
+ if ( strpos( $format, 'F' ) !== false ) {
+ $output = str_replace(
+ $dateTime->format( 'F' ),
+ $this->language->getMonthName( $monthNumber ),
+ $output
+ );
+ }
+
+ if ( strpos( $format, 'M' ) !== false ) {
+ $output = str_replace(
+ $dateTime->format( 'M' ),
+ $this->language->getMonthAbbreviation( $monthNumber ),
+ $output
+ );
+ }
+
+ if ( strpos( $format, 'l' ) !== false ) {
+ $output = str_replace(
+ $dateTime->format( 'l' ),
+ $this->language->getWeekdayName( $dayNumber ),
+ $output
+ );
+ }
+
+ if ( strpos( $format, 'D' ) !== false ) {
+ $output = str_replace(
+ $dateTime->format( 'D' ),
+ $this->language->getWeekdayAbbreviation( $dayNumber ),
+ $output
+ );
+ }
+
+ return $output;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/JulianDay.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/JulianDay.php
new file mode 100644
index 00000000..150b206d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/JulianDay.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace SMW\DataValues\Time;
+
+use RuntimeException;
+
+/**
+ * Julian dates (abbreviated JD) are a continuous count of days and fractions
+ * since noon Universal Time on January 1, 4713 BCE (on the Julian calendar).
+ *
+ * It is assumed that the changeover from the Julian calendar to the Gregorian
+ * calendar occurred in October of 1582.
+ *
+ * For dates on or before 4 October 1582, the Julian calendar is used; for dates
+ * on or after 15 October 1582, the Gregorian calendar is used.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author Markus Krötzsch
+ */
+class JulianDay implements CalendarModel {
+
+ /**
+ * Moment of switchover to Gregorian calendar.
+ */
+ const J1582 = 2299160.5;
+
+ /**
+ * Offset of Julian Days for Modified JD inputs.
+ */
+ const MJD = 2400000.5;
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $calendarmodel
+ * @param integer $year
+ * @param integer $month
+ * @param integer $day
+ * @param integer $hour
+ * @param integer $minute
+ * @param integer $second
+ *
+ * @return float
+ */
+ public static function getJD( $calendarModel = self::CM_GREGORIAN, $year, $month, $day, $hour, $minute, $second ) {
+ return self::format( self::date2JD( $calendarModel, $year, $month, $day ) + self::time2JDoffset( $hour, $minute, $second ) );
+ }
+
+ /**
+ * Return a formatted value
+ *
+ * @note April 25, 2017 20:00-4:00 is expected to be 2457869.5 and not
+ * 2457869.4999999665 hence apply the same formatting on all values to avoid
+ * some unexpected behaviour as observed in #2454
+ *
+ * @since 3.0
+ *
+ * @param $value
+ *
+ * @return float
+ */
+ public static function format( $value ) {
+ // Keep microseconds to a certain degree distinguishable
+ return floatval( number_format( $value, 7, '.', '' ) );
+ }
+
+ /**
+ * The MJD has a starting point of midnight on November 17, 1858 and is
+ * computed by MJD = JD - 2400000.5
+ *
+ * @since 2.4
+ *
+ * @param float jdValue
+ *
+ * @return float
+ */
+ public static function getModifiedJulianDate( $jdValue ) {
+ return $jdValue - self::MJD;
+ }
+
+ /**
+ * Compute the Julian Day number from a given date in the specified
+ * calendar model. This calculation assumes that neither calendar
+ * has a year 0.
+ *
+ * @param $year integer representing the year
+ * @param $month integer representing the month
+ * @param $day integer representing the day
+ * @param $calendarmodel integer either CM_GREGORIAN or CM_JULIAN
+ *
+ * @return float Julian Day number
+ * @throws RuntimeException
+ */
+ protected static function date2JD( $calendarmodel, $year, $month, $day ) {
+ $astroyear = ( $year < 1 ) ? ( $year + 1 ) : $year;
+
+ if ( $calendarmodel === self::CM_GREGORIAN ) {
+ $a = intval( ( 14 - $month ) / 12 );
+ $y = $astroyear + 4800 - $a;
+ $m = $month + 12 * $a - 3;
+ return $day + floor( ( 153 * $m + 2 ) / 5 ) + 365 * $y + floor( $y / 4 ) - floor( $y / 100 ) + floor( $y / 400 ) - 32045.5;
+ } elseif ( $calendarmodel === self::CM_JULIAN ) {
+ $y2 = ( $month <= 2 ) ? ( $astroyear - 1 ) : $astroyear;
+ $m2 = ( $month <= 2 ) ? ( $month + 12 ) : $month;
+ return floor( ( 365.25 * ( $y2 + 4716 ) ) ) + floor( ( 30.6001 * ( $m2 + 1 ) ) ) + $day - 1524.5;
+ }
+
+ throw new RuntimeException( "Unsupported calendar model ($calendarmodel)" );
+ }
+
+ /**
+ * Compute the offset for the Julian Day number from a given time.
+ * This computation is the same for all calendar models.
+ *
+ * @param $hours integer representing the hour
+ * @param $minutes integer representing the minutes
+ * @param $seconds integer representing the seconds
+ *
+ * @return float offset for a Julian Day number to get this time
+ */
+ protected static function time2JDoffset( $hours, $minutes, $seconds ) {
+ return ( $hours / 24 ) + ( $minutes / ( 60 * 24 ) ) + ( $seconds / ( 3600 * 24 ) );
+ }
+
+ /**
+ * Convert a Julian Day number to a date in the given calendar model.
+ * This calculation assumes that neither calendar has a year 0.
+ * @note The algorithm may fail for some cases, in particular since the
+ * conversion to Gregorian needs positive JD. If this happens, wrong
+ * values will be returned. Avoid date conversions before 10000 BCE.
+ *
+ * @param $jdValue float number of Julian Days
+ * @param $calendarModel integer either CM_GREGORIAN or CM_JULIAN
+ *
+ * @return array( calendarModel, yearnumber, monthnumber, daynumber )
+ * @throws RuntimeException
+ */
+ public static function JD2Date( $jdValue, $calendarModel = null ) {
+
+ if ( $calendarModel === null ) { // 1582/10/15
+ $calendarModel = $jdValue < self::J1582 ? self::CM_JULIAN : self::CM_GREGORIAN;
+ }
+
+ if ( $calendarModel === self::CM_GREGORIAN ) {
+ $jdValue += 2921940; // add the days of 8000 years (this algorithm only works for positive JD)
+ $j = floor( $jdValue + 0.5 ) + 32044;
+ $g = floor( $j / 146097 );
+ $dg = $j % 146097;
+ $c = floor( ( ( floor( $dg / 36524 ) + 1 ) * 3 ) / 4 );
+ $dc = $dg - $c * 36524;
+ $b = floor( $dc / 1461 );
+ $db = $dc % 1461;
+ $a = floor( ( ( floor( $db / 365 ) + 1 ) * 3 ) / 4 );
+ $da = $db - ( $a * 365 );
+ $y = $g * 400 + $c * 100 + $b * 4 + $a;
+ $m = floor( ( $da * 5 + 308 ) / 153 ) - 2;
+ $d = $da - floor( ( ( $m + 4 ) * 153 ) / 5 ) + 122;
+
+ $year = $y - 4800 + floor( ( $m + 2 ) / 12 ) - 8000;
+ $month = ( ( $m + 2 ) % 12 + 1 );
+ $day = $d + 1;
+ } elseif ( $calendarModel === self::CM_JULIAN ) {
+ $b = floor( $jdValue + 0.5 ) + 1524;
+ $c = floor( ( $b - 122.1 ) / 365.25 );
+ $d = floor( 365.25 * $c );
+ $e = floor( ( $b - $d ) / 30.6001 );
+
+ $month = floor( ( $e < 14 ) ? ( $e - 1 ) : ( $e - 13 ) );
+ $year = floor( ( $month > 2 ) ? ( $c - 4716 ) : ( $c - 4715 ) );
+ $day = ( $b - $d - floor( 30.6001 * $e ) );
+ } else {
+ throw new RuntimeException( "Unsupported calendar model ($calendarModel)" );
+ }
+
+ $year = ( $year < 1 ) ? ( $year - 1 ) : $year; // correct "year 0" to -1 (= 1 BC(E))
+
+ return [ $calendarModel, $year, $month, $day ];
+ }
+
+ /**
+ * Extract the time from a Julian Day number and return it as a string.
+ * This conversion is the same for all calendar models.
+ *
+ * @param $jdvalue float number of Julian Days
+ * @return array( hours, minutes, seconds )
+ */
+ public static function JD2Time( $jdvalue ) {
+ $wjd = $jdvalue + 0.5;
+ $fraction = $wjd - floor( $wjd );
+ $time = round( $fraction * 3600 * 24 );
+ $hours = floor( $time / 3600 );
+ $time = $time - $hours * 3600;
+ $minutes = floor( $time / 60 );
+ $seconds = floor( $time - $minutes * 60 );
+ return [ $hours, $minutes, $seconds ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/Timezone.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/Timezone.php
new file mode 100644
index 00000000..a9e4dcd4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Time/Timezone.php
@@ -0,0 +1,388 @@
+<?php
+
+namespace SMW\DataValues\Time;
+
+use DateInterval;
+use DateTime;
+use DateTimeZone;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class Timezone {
+
+ /**
+ * A new TZ is expected to be added at the end of the list without changing
+ * existing ID's (those are used as internal serialization identifier).
+ *
+ * The associated offsets are in hours or fractions of hours.
+ *
+ * 'FOO' => array( ID, OffsetInSeconds, isMilitary )
+ *
+ * @var array
+ */
+ private static $shortList = [
+ "UTC" => [ 0, 0, false ],
+ "Z" => [ 1, 0, true ],
+ "A" => [ 2, 3600, true ],
+ "ACDT" => [ 3, 37800, false ],
+ "ACST" => [ 4, 34200, false ],
+ "ADT" => [ 5, -10800, false ],
+ "AEDT" => [ 6, 39600, false ],
+ "AEST" => [ 7, 36000, false ],
+ "AKDT" => [ 8, -28800, false ],
+ "AKST" => [ 9, -32400, false ],
+ "AST" => [ 10, -14400, false ],
+ "AWDT" => [ 11, 32400, false ],
+ "AWST" => [ 12, 28800, false ],
+ "B" => [ 13, 7200, true ],
+ "BST" => [ 14, 3600, false ],
+ "C" => [ 15, 10800, true ],
+ "CDT" => [ 16, -18000, false ],
+ "CEDT" => [ 17, 7200, false ],
+ "CEST" => [ 18, 7200, false ],
+ "CET" => [ 19, 3600, false ],
+ "CST" => [ 20, -21600, false ],
+ "CXT" => [ 21, 25200, false ],
+ "D" => [ 22, 14400, true ],
+ "E" => [ 23, 18000, true ],
+ "EDT" => [ 24, -14400, false ],
+ "EEDT" => [ 25, 10800, false ],
+ "EEST" => [ 26, 10800, false ],
+ "EET" => [ 27, 7200, false ],
+ "EST" => [ 28, -18000, false ],
+ "F" => [ 29, 21600, true ],
+ "G" => [ 30, 25200, true ],
+ "GMT" => [ 31, 0, false ],
+ "H" => [ 32, 28800, true ],
+ "HAA" => [ 33, -10800, false ],
+ "HAC" => [ 34, -18000, false ],
+ "HADT" => [ 35, -32400, false ],
+ "HAE" => [ 36, -14400, false ],
+ "HAP" => [ 37, -25200, false ],
+ "HAR" => [ 38, -21600, false ],
+ "HAST" => [ 39, -36000, false ],
+ "HAT" => [ 40, -9000, false ],
+ "HAY" => [ 41, -28800, false ],
+ "HNA" => [ 42, -14400, false ],
+ "HNC" => [ 43, -21600, false ],
+ "HNE" => [ 44, -18000, false ],
+ "HNP" => [ 45, -28800, false ],
+ "HNR" => [ 46, -25200, false ],
+ "HNT" => [ 47, -12600, false ],
+ "HNY" => [ 48, -32400, false ],
+ "I" => [ 49, 32400, true ],
+ "IST" => [ 50, 3600, false ],
+ "K" => [ 51, 36000, true ],
+ "L" => [ 52, 39600, true ],
+ "M" => [ 53, 43200, true ],
+ "MDT" => [ 54, -21600, false ],
+ "MESZ" => [ 55, 7200, false ],
+ "MEZ" => [ 56, 3600, false ],
+ "MSD" => [ 57, 14400, false ],
+ "MSK" => [ 58, 10800, false ],
+ "MST" => [ 59, -25200, false ],
+ "N" => [ 60, -3600, true ],
+ "NDT" => [ 61, -9000, false ],
+ "NFT" => [ 62, 41400, false ],
+ "NST" => [ 63, -12600, false ],
+ "O" => [ 64, -7200, true ],
+ "P" => [ 65, -10800, true ],
+ "PDT" => [ 66, -25200, false ],
+ "PST" => [ 67, -28800, false ],
+ "Q" => [ 68, -14400, true ],
+ "R" => [ 69, -18000, true ],
+ "S" => [ 70, -21600, true ],
+ "T" => [ 71, -25200, true ],
+ "U" => [ 72, -28800, true ],
+ "V" => [ 73, -32400, true ],
+ "W" => [ 74, -36000, true ],
+ "WDT" => [ 75, 32400, false ],
+ "WEDT" => [ 76, 3600, false ],
+ "WEST" => [ 77, 3600, false ],
+ "WET" => [ 78, 0, false ],
+ "WST" => [ 79, 28800, false ],
+ "X" => [ 80, -39600, true ],
+ "Y" => [ 81, -43200, true ],
+ ];
+
+ /**
+ * Generated from the DateTimeZone::listAbbreviations and contains "Area/Location",
+ * e.g. "America/New_York".
+ *
+ * Citing https://en.wikipedia.org/wiki/Tz_database which describes that " ...
+ * The underscore character is used in place of spaces. Hyphens are used
+ * where they appear in the name of a location ... names have a maximum
+ * length of 14 characters ..."
+ *
+ * @var array
+ */
+ private static $dateTimeZoneList = [];
+
+ /**
+ * @var array
+ */
+ private static $offsetCache = [];
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public static function listShortAbbreviations() {
+ return array_keys( self::$shortList );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $identifer
+ *
+ * @return boolean
+ */
+ public static function isValid( $identifer ) {
+
+ $identifer = str_replace( ' ', '_', $identifer );
+
+ if ( isset( self::$shortList[strtoupper( $identifer )] ) ) {
+ return true;
+ }
+
+ $dateTimeZoneList = self::getDateTimeZoneList();
+
+ if ( isset( $dateTimeZoneList[$identifer] ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $abbreviation
+ *
+ * @return boolean
+ */
+ public static function isMilitary( $abbreviation ) {
+
+ $abbreviation = strtoupper( $abbreviation );
+
+ if ( isset( self::$shortList[$abbreviation] ) ) {
+ return self::$shortList[$abbreviation][2];
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $identifer
+ *
+ * @return false|integer
+ */
+ public static function getIdByAbbreviation( $identifer ) {
+
+ if ( isset( self::$shortList[strtoupper( $identifer )] ) ) {
+ return self::$shortList[strtoupper( $identifer )][0];
+ }
+
+ $identifer = str_replace( ' ', '_', $identifer );
+ $dateTimeZoneList = self::getDateTimeZoneList();
+
+ if ( isset( $dateTimeZoneList[$identifer] ) ) {
+ return $dateTimeZoneList[$identifer];
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $identifer
+ *
+ * @return false|string
+ */
+ public static function getTimezoneLiteralById( $identifer ) {
+
+ foreach ( self::$shortList as $abbreviation => $value ) {
+ if ( is_numeric( $identifer ) && $value[0] == $identifer ) {
+ return $abbreviation;
+ }
+ }
+
+ $dateTimeZoneList = self::getDateTimeZoneList();
+
+ if ( ( $abbreviation = array_search( $identifer, $dateTimeZoneList ) ) !== false ) {
+ return $abbreviation;
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $abbreviation
+ *
+ * @return false|string
+ */
+ public static function getOffsetByAbbreviation( $abbreviation ) {
+
+ if ( isset( self::$shortList[strtoupper( $abbreviation )] ) ) {
+ return self::$shortList[strtoupper( $abbreviation )][1];
+ }
+
+ $abbreviation = str_replace( ' ', '_', $abbreviation );
+
+ if ( isset( self::$offsetCache[$abbreviation] ) ) {
+ return self::$offsetCache[$abbreviation];
+ }
+
+ $offset = false;
+
+ try {
+ $dateTimeZone = new DateTimeZone( $abbreviation );
+ $offset = $dateTimeZone->getOffset( new DateTime() );
+ } catch( \Exception $e ) {
+ //
+ }
+
+ return self::$offsetCache[$abbreviation] = $offset;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $abbreviation
+ *
+ * @return string
+ */
+ public static function getNameByAbbreviation( $abbreviation ) {
+
+ $abbreviation = strtoupper( $abbreviation );
+
+ if ( isset( self::$shortList[$abbreviation] ) ) {
+ $name = timezone_name_from_abbr( $abbreviation );
+ }
+
+ // If the abbrevation couldn't be matched use the offset instead
+ if ( !$name ) {
+ $name = timezone_name_from_abbr(
+ "",
+ self::getOffsetByAbbreviation( $abbreviation ) * 3600,
+ 0
+ );
+ }
+
+ return $name;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $abbreviation
+ *
+ * @return DateInterval
+ */
+ public static function newDateIntervalWithOffsetFrom( $abbreviation ) {
+
+ $minutes = 0;
+ $hour = 0;
+
+ // Here we don't care for +/-, the caller of the function
+ // has to care for it
+ $offsetInSeconds = abs( self::getOffsetByAbbreviation( $abbreviation ) );
+
+ return new DateInterval( "PT{$offsetInSeconds}S" );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $abbreviation
+ *
+ * @return false|DateTimeZone
+ */
+ public static function newDateTimeZone( $abbreviation ) {
+
+ try {
+ $dateTimeZone = new DateTimeZone( $abbreviation );
+ } catch( \Exception $e ) {
+ if ( ( $name = self::getNameByAbbreviation( $abbreviation ) ) !== false ) {
+ return new DateTimeZone( $name );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Generated from the DateTimeZone::listAbbreviations
+ *
+ * @since 2.5
+ *
+ * @return array
+ */
+ public static function getDateTimeZoneList() {
+
+ if ( self::$dateTimeZoneList !== [] ) {
+ return self::$dateTimeZoneList;
+ }
+
+ $list = DateTimeZone::listIdentifiers();
+
+ foreach ( $list as $identifier ) {
+ self::$dateTimeZoneList[$identifier] = $identifier;
+ }
+
+ return self::$dateTimeZoneList;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DateTime $dateTime
+ * @param string|integer &$tz
+ *
+ * @return DateTime
+ */
+ public static function getModifiedTime( DateTime $dateTime, &$tz = 0 ) {
+
+ if ( ( $timezoneLiteral = self::getTimezoneLiteralById( $tz ) ) === false ) {
+ $tz = $timezoneLiteral;
+ return $dateTime;
+ }
+
+ $dateTimeZone = null;
+
+ if ( !self::isMilitary( $timezoneLiteral ) && self::getOffsetByAbbreviation( $timezoneLiteral ) != 0 ) {
+ $dateTimeZone = self::newDateTimeZone( $timezoneLiteral );
+ }
+
+ // DI is stored in UTC time therefore find and add the offset
+ if ( !$dateTimeZone instanceof DateTimeZone ) {
+ $dateInterval = self::newDateIntervalWithOffsetFrom( $timezoneLiteral );
+
+ if ( self::getOffsetByAbbreviation( $timezoneLiteral ) > 0 ) {
+ $dateTime->add( $dateInterval );
+ } else {
+ $dateTime->sub( $dateInterval );
+ }
+ } else {
+ $dateTime->setTimezone( $dateTimeZone );
+ }
+
+ $tz = $timezoneLiteral;
+
+ return $dateTime;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TypesValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TypesValue.php
new file mode 100644
index 00000000..97763490
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/TypesValue.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace SMW\DataValues;
+
+use SMW\DataTypeRegistry;
+use SMW\Localizer;
+use SMWDataItem as DataItem;
+use SMWDataItemException as DataItemException;
+use SMWDataValue as DataValue;
+use SMWDIUri as DIUri;
+use SpecialPageFactory;
+use Title;
+
+/**
+ * This datavalue implements special processing suitable for defining types of
+ * properties. Types behave largely like values of type SMWWikiPageValue
+ * with three main differences. First, they actively check if a value is an
+ * alias for another type, modifying the internal representation accordingly.
+ * Second, they have a modified display for emphasizing if some type is defined
+ * in SMW (built-in). Third, they use type ids for storing data (DB keys)
+ * instead of using page titles.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class TypesValue extends DataValue {
+
+ /**
+ * DV identifier
+ */
+ const TYPE_ID = '__typ';
+
+ /**
+ * @var string
+ */
+ private $typeLabel;
+
+ /**
+ * @var string
+ */
+ private $givenLabel;
+
+ /**
+ * @var string
+ */
+ private $m_typeId;
+
+ /**
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( self::TYPE_ID );
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @param string $typeId
+ *
+ * @return TypesValue
+ */
+ public static function newFromTypeId( $typeId ) {
+ $result = new TypesValue( self::TYPE_ID );
+
+ try {
+ $dataItem = self::getTypeUriFromTypeId( $typeId );
+ } catch ( DataItemException $e ) {
+ $dataItem = self::getTypeUriFromTypeId( 'notype' );
+ }
+
+ $result->setDataItem( $dataItem );
+
+ return $result;
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @param string $typeId
+ *
+ * @return DIUri
+ */
+ public static function getTypeUriFromTypeId( $typeId ) {
+ return new DIUri( 'http', 'semantic-mediawiki.org/swivt/1.0', '', $typeId );
+ }
+
+ /**
+ * @see DataValue::getShortWikiText
+ *
+ * {@inheritDoc}
+ */
+ public function getShortWikiText( $linker = null ) {
+
+ if ( !$linker || $this->m_outformat === '-' || $this->m_caption === '' ) {
+ return $this->m_caption;
+ }
+
+ $titleText = $this->makeSpecialPageTitleText();
+
+ $contentLanguage = Localizer::getInstance()->getLanguage(
+ $this->getOption( self::OPT_CONTENT_LANGUAGE )
+ );
+
+ $namespace = $contentLanguage->getNsText(
+ NS_SPECIAL
+ );
+
+ return "[[$namespace:$titleText|{$this->m_caption}]]";
+ }
+
+ /**
+ * @see DataValue::getShortHTMLText
+ *
+ * {@inheritDoc}
+ */
+ public function getShortHTMLText( $linker = null ) {
+
+ if ( !$linker || $this->m_outformat === '-' || $this->m_caption === '' ) {
+ return htmlspecialchars( $this->m_caption );
+ }
+
+ $title = Title::makeTitle(
+ NS_SPECIAL,
+ $this->makeSpecialPageTitleText()
+ );
+
+ return $linker->link( $title, htmlspecialchars( $this->m_caption ) );
+ }
+
+ /**
+ * @see DataValue::getLongWikiText
+ *
+ * {@inheritDoc}
+ */
+ public function getLongWikiText( $linker = null ) {
+
+ if ( !$linker || $this->typeLabel === '' ) {
+ return $this->typeLabel;
+ }
+
+ $titleText = $this->makeSpecialPageTitleText();
+
+ $contentLanguage = Localizer::getInstance()->getLanguage(
+ $this->getOption( self::OPT_CONTENT_LANGUAGE )
+ );
+
+ $namespace = $contentLanguage->getNsText(
+ NS_SPECIAL
+ );
+
+ return "[[$namespace:$titleText|{$this->typeLabel}]]";
+ }
+
+ /**
+ * @see DataValue::getLongHTMLText
+ *
+ * {@inheritDoc}
+ */
+ public function getLongHTMLText( $linker = null ) {
+
+ if ( !$linker || $this->typeLabel === '' ) {
+ return htmlspecialchars( $this->typeLabel );
+ }
+
+ $title = Title::makeTitle(
+ NS_SPECIAL,
+ $this->makeSpecialPageTitleText()
+ );
+
+ return $linker->link( $title, htmlspecialchars( $this->typeLabel ) );
+ }
+
+ /**
+ * @see DataValue::getWikiValue
+ *
+ * {@inheritDoc}
+ */
+ public function getWikiValue() {
+ return $this->typeLabel;
+ }
+
+ /**
+ * @see DataValue::loadDataItem
+ *
+ * {@inheritDoc}
+ */
+ protected function parseUserValue( $value ) {
+
+ if ( $this->m_caption === false ) {
+ $this->m_caption = $value;
+ }
+
+ $valueParts = explode( ':', $value, 2 );
+ $contentLanguage = $this->getOption( self::OPT_CONTENT_LANGUAGE );
+
+ if ( $value !== '' && $value{0} === '_' ) {
+ $this->m_typeId = $value;
+ } else {
+ $this->givenLabel = smwfNormalTitleText( $value );
+ $this->m_typeId = DataTypeRegistry::getInstance()->findTypeByLabelAndLanguage( $this->givenLabel, $contentLanguage );
+ }
+
+ if ( $this->m_typeId === '' ) {
+ $this->addErrorMsg( [ 'smw_unknowntype', $this->givenLabel ] );
+ $this->typeLabel = $this->givenLabel;
+ } else {
+ $this->typeLabel = DataTypeRegistry::getInstance()->findTypeLabel( $this->m_typeId );
+ }
+
+ try {
+ $this->m_dataitem = self::getTypeUriFromTypeId( $this->m_typeId );
+ } catch ( DataItemException $e ) {
+ $this->m_dataitem = self::getTypeUriFromTypeId( 'notype' );
+ $this->addErrorMsg( [ 'smw-datavalue-type-invalid-typeuri', $this->m_typeId ] );
+ }
+ }
+
+ /**
+ * @see DataValue::loadDataItem
+ *
+ * {@inheritDoc}
+ */
+ protected function loadDataItem( DataItem $dataItem ) {
+
+ if ( ( $dataItem instanceof DIUri ) && ( $dataItem->getScheme() == 'http' ) &&
+ ( $dataItem->getHierpart() == 'semantic-mediawiki.org/swivt/1.0' ) &&
+ ( $dataItem->getQuery() === '' ) ) {
+
+ $this->m_typeId = $dataItem->getFragment();
+ $this->typeLabel = DataTypeRegistry::getInstance()->findTypeLabel( $this->m_typeId );
+ $this->m_caption = $this->givenLabel = $this->typeLabel;
+ $this->m_dataitem = $dataItem;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function makeSpecialPageTitleText() {
+ return SpecialPageFactory::getLocalNameFor( 'Types', $this->typeLabel );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/UniquenessConstraintValue.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/UniquenessConstraintValue.php
new file mode 100644
index 00000000..5a6545e5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/UniquenessConstraintValue.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace SMW\DataValues;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class UniquenessConstraintValue extends BooleanValue {
+
+ /**
+ * @since 2.4
+ *
+ * @param string $typeid
+ */
+ public function __construct( $typeid = '' ) {
+ parent::__construct( '__pvuc' );
+ }
+
+ /**
+ * @see DataValue::parseUserValue
+ *
+ * @param string $value
+ */
+ protected function parseUserValue( $userValue ) {
+
+ if ( !$this->isEnabledFeature( SMW_DV_PVUC ) ) {
+ $this->addErrorMsg(
+ [
+ 'smw-datavalue-feature-not-supported',
+ 'SMW_DV_PVUC'
+ ]
+ );
+ }
+
+ parent::parseUserValue( $userValue );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/CodeStringValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/CodeStringValueFormatter.php
new file mode 100644
index 00000000..d6f3a069
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/CodeStringValueFormatter.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use SMWDataValue as DataValue;
+use SMWOutputs as Outputs;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class CodeStringValueFormatter extends StringValueFormatter {
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function isFormatterFor( DataValue $dataValue ) {
+ return $dataValue->getTypeID() === '_cod';
+ }
+
+ /**
+ * @see StringValueFormatter::doFormat
+ */
+ protected function doFormat( $dataValue, $type, $linker ) {
+
+ $abbreviate = $type === self::WIKI_LONG || $type === self::HTML_LONG;
+ $text = $dataValue->getDataItem()->getString();
+
+ // Escape and wrap values of type Code. The result is escaped to be
+ // HTML-safe (it will also work in wiki context). The result will
+ // contain mark-up that must not be escaped again.
+
+ Outputs::requireResource( 'ext.smw.style' );
+
+ if ( $this->isJson( $text ) ) {
+ $result = self::asJson( $text );
+ } else {
+ // This disables all active wiki and HTML markup:
+ $result = str_replace(
+ [ '<code>', '</code>', '<nowiki>', '</nowiki>', '<', '>', ' ', '[', '{', '=', "'", ':', "\n", '&#x005B;' ],
+ [ '', '', '', '', '&lt;', '&gt;', '&#160;', '&#91;', '&#x007B;', '&#x003D;', '&#x0027;', '&#58;', "<br />", '&#91;' ],
+ $text
+ );
+ }
+
+ if ( $abbreviate ) {
+ $result = "<div style=\"min-height:5em; overflow:auto;\">$result</div>";
+ }
+
+ return "<div class=\"smwpre\">$result</div>";
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ public static function asJson( $string, $flag = 0 ) {
+
+ if ( $flag > 0 ) {
+ return json_encode( json_decode( $string ), $flag );
+ }
+
+ return json_encode( json_decode( $string ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+ }
+
+ private function isJson( $string ) {
+
+ // Don't bother
+ if ( substr( $string, 0, 1 ) !== '{' ) {
+ return false;
+ }
+
+ json_decode( $string );
+
+ return ( json_last_error() == JSON_ERROR_NONE );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/DataValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/DataValueFormatter.php
new file mode 100644
index 00000000..8ffdf555
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/DataValueFormatter.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use SMW\Options;
+use SMWDataValue as DataValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+abstract class DataValueFormatter implements ValueFormatter {
+
+ /**
+ * 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 DataValue::setUserValue.
+ */
+ const VALUE = 0;
+
+ /**
+ * 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.
+ */
+ const WIKI_SHORT = 1;
+
+ /**
+ * 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.
+ */
+ const HTML_SHORT = 2;
+
+ /**
+ * Return the long textual description of the value, as printed for example
+ * in the factbox. If errors occurred, return the error message. The result
+ * is always a wiki-source string.
+ */
+ const WIKI_LONG = 3;
+
+ /**
+ * 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.
+ */
+ const HTML_LONG = 4;
+
+ /**
+ * @var DataValue
+ */
+ protected $dataValue;
+
+ /**
+ * @var Options
+ */
+ private $options = null;
+
+ /**
+ * @since 2.4
+ *
+ * @param DataValue|null $dataValue
+ */
+ public function __construct( DataValue $dataValue = null ) {
+ $this->dataValue = $dataValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DataValue $dataValue
+ *
+ * @return boolean
+ */
+ abstract public function isFormatterFor( DataValue $dataValue );
+
+ /**
+ * @since 2.4
+ *
+ * @param DataValue $dataValue
+ */
+ public function setDataValue( DataValue $dataValue ) {
+ $this->dataValue = $dataValue;
+ $this->options = null;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->dataValue->getErrors();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $key
+ * @param mixed $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 ) {
+
+ if ( $this->options !== null && $this->options->has( $key ) ) {
+ return $this->options->get( $key );
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/DispatchingDataValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/DispatchingDataValueFormatter.php
new file mode 100644
index 00000000..c8056a98
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/DispatchingDataValueFormatter.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use RuntimeException;
+use SMWDataValue as DataValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class DispatchingDataValueFormatter {
+
+ /**
+ * @var DataValueFormatter[]
+ */
+ private $dataValueFormatters = [];
+
+ /**
+ * @var DataValueFormatter[]
+ */
+ private $defaultDataValueFormatters = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param DataValueFormatter $dataValueFormatter
+ */
+ public function addDataValueFormatter( DataValueFormatter $dataValueFormatter ) {
+ $this->dataValueFormatters[] = $dataValueFormatter;
+ }
+
+ /**
+ * DataValueFormatters registered with this method are validated after
+ * DispatchingDataValueFormatter::getDataValueFormatterFor was not able to
+ * match any Formatter. This to ensure that a distinct FooStringValueFormatter
+ * is tried before the default StringValueFormatter.
+ *
+ * @since 2.4
+ *
+ * @param DataValueFormatter $dataValueFormatter
+ */
+ public function addDefaultDataValueFormatter( DataValueFormatter $dataValueFormatter ) {
+ $this->defaultDataValueFormatters[] = $dataValueFormatter;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DataValue $dataValue
+ *
+ * @return DataValueFormatter
+ * @throws RuntimeException
+ */
+ public function getDataValueFormatterFor( DataValue $dataValue ) {
+
+ foreach ( $this->dataValueFormatters as $dataValueFormatter ) {
+ if ( $dataValueFormatter->isFormatterFor( $dataValue ) ) {
+ $dataValueFormatter->setDataValue( $dataValue );
+ return $dataValueFormatter;
+ }
+ }
+
+ foreach ( $this->defaultDataValueFormatters as $dataValueFormatter ) {
+ if ( $dataValueFormatter->isFormatterFor( $dataValue ) ) {
+ $dataValueFormatter->setDataValue( $dataValue );
+ return $dataValueFormatter;
+ }
+ }
+
+ throw new RuntimeException( "The dispatcher could not match a DataValueFormatter for " . get_class( $dataValue ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/MonolingualTextValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/MonolingualTextValueFormatter.php
new file mode 100644
index 00000000..71702889
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/MonolingualTextValueFormatter.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use RuntimeException;
+use SMW\DataValueFactory;
+use SMW\DataValues\MonolingualTextValue;
+use SMW\DIProperty;
+use SMW\Message;
+use SMWDataValue as DataValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class MonolingualTextValueFormatter extends DataValueFormatter {
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function isFormatterFor( DataValue $dataValue ) {
+ return $dataValue instanceof MonolingualTextValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function format( $type, $linker = null ) {
+
+ if ( !$this->dataValue instanceof MonolingualTextValue ) {
+ throw new RuntimeException( "The formatter is missing a valid MonolingualTextValue object" );
+ }
+
+ if (
+ $this->dataValue->getCaption() !== false &&
+ ( $type === self::WIKI_SHORT || $type === self::HTML_SHORT ) ) {
+ return $this->dataValue->getCaption();
+ }
+
+ return $this->getOutputText( $type, $linker );
+ }
+
+ protected function getOutputText( $type, $linker = null ) {
+
+ if ( !$this->dataValue->isValid() ) {
+ return ( ( $type == self::WIKI_SHORT ) || ( $type == self::HTML_SHORT ) ) ? '' : $this->dataValue->getErrorText();
+ }
+
+ // For the inverse case, return the subject that contains the reference
+ // for Foo annotated with [[Bar::abc@en]] -> [[-Bar::Foo]]
+ if ( $this->dataValue->getProperty() !== null && $this->dataValue->getProperty()->isInverse() ) {
+
+ $dataItems = $this->dataValue->getDataItem()->getSemanticData()->getPropertyValues(
+ new DIProperty( $this->dataValue->getProperty()->getKey() )
+ );
+
+ $dataItem = reset( $dataItems );
+
+ if ( !$dataItem ) {
+ return '';
+ }
+
+ return $dataItem->getDBKey();
+ }
+
+ return $this->doFormatFinalOutputFor( $type, $linker );
+ }
+
+ private function doFormatFinalOutputFor( $type, $linker ) {
+
+ $text = '';
+ $languagecode = '';
+
+ foreach ( $this->dataValue->getPropertyDataItems() as $property ) {
+
+ // If we wanted to omit the language code display for some outputs then
+ // this is the point to make it happen
+ if ( ( $type == self::HTML_LONG || $type == self::WIKI_SHORT ) && $property->getKey() === '_LCODE' ) {
+ //continue;
+ }
+
+ $dataItems = $this->dataValue->getDataItem()->getSemanticData()->getPropertyValues(
+ $property
+ );
+
+ // Should not happen but just in case
+ if ( !$dataItems === [] ) {
+ $this->dataValue->addErrorMsg( 'smw-datavalue-monolingual-dataitem-missing' );
+ continue;
+ }
+
+ $dataItem = reset( $dataItems );
+
+ if ( $dataItem === false ) {
+ continue;
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ $result = $this->findValueOutputFor(
+ $type,
+ $dataValue,
+ $linker
+ );
+
+ if ( $property->getKey() === '_LCODE' && $type !== self::VALUE ) {
+ $languagecode = ' ' . Message::get( [ 'smw-datavalue-monolingual-lcode-parenthesis', $result ] );
+ } elseif ( $property->getKey() === '_LCODE' && $type === self::VALUE ) {
+ $languagecode = '@' . $result;
+ } else {
+ $text = $result;
+ }
+ }
+
+ return $text . $languagecode;
+ }
+
+ private function findValueOutputFor( $type, $dataValue, $linker ) {
+ switch ( $type ) {
+ case self::VALUE:
+ return $dataValue->getWikiValue();
+ case self::WIKI_SHORT:
+ return $dataValue->getShortWikiText( $linker );
+ case self::HTML_SHORT:
+ return $dataValue->getShortHTMLText( $linker );
+ case self::WIKI_LONG:
+ return $dataValue->getShortWikiText( $linker );
+ case self::HTML_LONG:
+ return $dataValue->getShortHTMLText( $linker );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/NoValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/NoValueFormatter.php
new file mode 100644
index 00000000..f154a4c0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/NoValueFormatter.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use RuntimeException;
+use SMWDataValue as DataValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class NoValueFormatter extends DataValueFormatter {
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function isFormatterFor( DataValue $dataValue ) {
+ return $dataValue instanceof DataValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function format( $type, $linker = null ) {
+
+ if ( !$this->dataValue instanceof DataValue ) {
+ throw new RuntimeException( "The formatter is missing a valid DataValue object" );
+ }
+
+ return $this->dataValue->getDataItem()->getSerialization();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/NumberValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/NumberValueFormatter.php
new file mode 100644
index 00000000..a9a50f2b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/NumberValueFormatter.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use RuntimeException;
+use SMW\Highlighter;
+use SMWDataValue as DataValue;
+use SMWNumberValue as NumberValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class NumberValueFormatter extends DataValueFormatter {
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function isFormatterFor( DataValue $dataValue ) {
+ return $dataValue instanceof NumberValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function format( $type, $linker = null ) {
+
+ if ( !$this->dataValue instanceof NumberValue ) {
+ throw new RuntimeException( "The formatter is missing a valid NumberValue object" );
+ }
+
+ if ( $type === self::VALUE ) {
+ return $this->valueFormat();
+ }
+
+ if ( $type === self::WIKI_SHORT || $type === self::HTML_SHORT ) {
+ return $this->shortFormat( $linker );
+ }
+
+ if ( $type === self::WIKI_LONG || $type === self::HTML_LONG ) {
+ return $this->longFormat( $linker );
+ }
+
+ return 'UNKNOWN';
+ }
+
+ private function valueFormat() {
+
+ if ( !$this->dataValue->isValid() ) {
+ return 'error';
+ }
+
+ $unit = $this->dataValue->getUnit();
+
+ $number = $this->dataValue->getNormalizedFormattedNumber(
+ $this->dataValue->getNumber()
+ );
+
+ if ( $unit === '' ) {
+ return $number;
+ }
+
+ return $this->dataValue->hasPrefixalUnitPreference( $unit ) ? $unit . ' ' . $number : $number . ' ' . $unit;
+ }
+
+ private function shortFormat( $linker = null ) {
+
+ $outformat = $this->dataValue->getOutputFormat();
+
+ if ( $linker === null || ( $linker === false ) || ( $outformat == '-' ) || ( $outformat == '-u' ) || ( $outformat == '-n' ) || !$this->dataValue->isValid() ) {
+ return $this->dataValue->getCaption();
+ }
+
+ $convertedUnitValues = $this->dataValue->getConvertedUnitValues();
+ $tooltip = '';
+
+ $i = 0;
+
+ foreach ( $convertedUnitValues as $unit => $value ) {
+ if ( $unit != $this->dataValue->getCanonicalMainUnit() ) {
+ $number = $this->dataValue->getLocalizedFormattedNumber( $value );
+ if ( $unit !== '' ) {
+ $tooltip .= $this->dataValue->hasPrefixalUnitPreference( $unit ) ? $unit . '&#160;' . $number : $number . '&#160;' . $unit;
+ } else{
+ $tooltip .= $number;
+ }
+ $tooltip .= ' <br />';
+ $i++;
+ if ( $i >= 5 ) { // limit number of printouts in tooltip
+ break;
+ }
+ }
+ }
+
+ if ( $tooltip === '' ) {
+ return $this->dataValue->getCaption();
+ }
+
+ $highlighter = Highlighter::factory(
+ Highlighter::TYPE_QUANTITY,
+ $this->dataValue->getOption( DataValue::OPT_USER_LANGUAGE )
+ );
+
+ $highlighter->setContent(
+ [
+ 'caption' => $this->dataValue->getCaption(),
+ 'content' => $tooltip
+ ]
+ );
+
+ return $highlighter->getHtml();
+ }
+
+ private function longFormat( $linker = null ) {
+
+ if ( !$this->dataValue->isValid() ) {
+ return $this->dataValue->getErrorText();
+ }
+
+ $outformat = $this->dataValue->getOutputFormat();
+ $convertedUnitValues = $this->dataValue->getConvertedUnitValues();
+
+ $result = '';
+ $i = 0;
+
+ foreach ( $convertedUnitValues as $unit => $value ) {
+
+ if ( $i == 1 ) {
+ $result .= ' (';
+ } elseif ( $i > 1 ) {
+ $result .= ', ';
+ }
+
+ $number = ( $outformat != '-' ? $this->dataValue->getLocalizedFormattedNumber( $value ) : $value );
+
+ if ( $unit !== '' ) {
+ $result .= $this->dataValue->hasPrefixalUnitPreference( $unit ) ? $unit . '&#160;' . $number : $number . '&#160;' . $unit;
+ } else {
+ $result .= $number;
+ }
+
+ $i++;
+
+ if ( $outformat == '-' ) { // no further conversions for plain output format
+ break;
+ }
+ }
+
+ if ( $i > 1 ) {
+ $result .= ')';
+ }
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/PropertyValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/PropertyValueFormatter.php
new file mode 100644
index 00000000..81407fb6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/PropertyValueFormatter.php
@@ -0,0 +1,337 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use RuntimeException;
+use SMW\ApplicationFactory;
+use SMW\Highlighter;
+use SMW\Localizer;
+use SMW\Message;
+use SMWDataValue as DataValue;
+use SMW\DataValues\PropertyValue;
+use SMW\PropertySpecificationLookup;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyValueFormatter extends DataValueFormatter {
+
+ /**
+ * @var PropertySpecificationLookup
+ */
+ private $propertySpecificationLookup;
+
+ /**
+ * @since 3.0
+ *
+ * @param PropertySpecificationLookup $propertySpecificationLookup
+ */
+ public function __construct( PropertySpecificationLookup $propertySpecificationLookup ) {
+ $this->propertySpecificationLookup = $propertySpecificationLookup;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isFormatterFor( DataValue $dataValue ) {
+ return $dataValue instanceof PropertyValue;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function format( $dataValue, $options = null ) {
+
+ if ( !is_array( $options ) ) {
+ throw new RuntimeException( "Option is not an array!" );
+ }
+
+ if ( !$dataValue instanceof PropertyValue ) {
+ throw new RuntimeException( "The formatter is missing a valid PropertyValue object" );
+ }
+
+ $this->dataValue = $dataValue;
+
+ $type = $options[0];
+ $linker = isset( $options[1] ) ? $options[1] : null;
+
+ if ( !$this->dataValue->isVisible() ) {
+ return '';
+ }
+
+ if ( $type === self::VALUE ) {
+ return $this->getWikiValue();
+ }
+
+ if ( $type === PropertyValue::FORMAT_LABEL ) {
+ return $this->getFormattedLabel( $linker );
+ }
+
+ if ( $type === PropertyValue::SEARCH_LABEL ) {
+ return $this->getSearchLabel();
+ }
+
+ $wikiPageValue = $this->prepareWikiPageValue( $linker );
+ $text = '';
+
+ if ( $wikiPageValue === null ) {
+ return '';
+ }
+
+ if ( $type === self::WIKI_SHORT ) {
+ $text = $this->doHighlightText(
+ $wikiPageValue->getShortWikiText( $linker ),
+ $this->dataValue->getOption( PropertyValue::OPT_HIGHLIGHT_LINKER ) ? $linker : null
+ );
+ }
+
+ if ( $type === self::HTML_SHORT ) {
+ $text = $this->doHighlightText( $wikiPageValue->getShortHTMLText( $linker ), $linker );
+ }
+
+ if ( $type === self::WIKI_LONG ) {
+ $text = $this->doHighlightText( $wikiPageValue->getLongWikiText( $linker ) );
+ }
+
+ if ( $type === self::HTML_LONG ) {
+ $text = $this->doHighlightText( $wikiPageValue->getLongHTMLText( $linker ), $linker );
+ }
+
+ return $text . $this->hintPreferredLabelUse();
+ }
+
+ /**
+ * Formatting rule set:
+ * - preferred goes before translation
+ * - displayTitle goes before translation
+ * - translation goes before "normal" label
+ */
+ private function getFormattedLabel( $linker = null ) {
+
+ $property = $this->dataValue->getDataItem();
+ $output = '';
+ $displayTitle = '';
+
+ $preferredLabel = $property->getPreferredLabel(
+ $this->dataValue->getOption( PropertyValue::OPT_USER_LANGUAGE )
+ );
+
+ $label = $preferredLabel;
+
+ if ( $preferredLabel === '' && ( $label = $this->findTranslatedPropertyLabel( $property ) ) === '' ) {
+ $label = $property->getLabel();
+ }
+
+ if ( $this->dataValue->getWikiPageValue() !== null ) {
+ $displayTitle = $this->dataValue->getWikiPageValue()->getDisplayTitle();
+ }
+
+ $canonicalLabel = $property->getCanonicalLabel();
+
+ // Display title goes before a translated label (but not preferred)
+ if ( $preferredLabel === '' && $displayTitle !== '' ) {
+ $label = $displayTitle;
+ // $canonicalLabel = $displayTitle;
+ }
+
+ // Internal format only used by PropertyValue
+ $format = $this->dataValue->getOption( PropertyValue::FORMAT_LABEL );
+ $this->dataValue->setCaption( $label );
+
+ if ( $format === self::VALUE ) {
+ $output = $this->dataValue->getWikiValue();
+ }
+
+ if ( $format === self::WIKI_LONG && $linker !== null ) {
+ $output = $this->dataValue->getLongWikiText( $linker );
+ } elseif ( $format === self::WIKI_LONG && $preferredLabel === '' && $displayTitle !== '' ) {
+ $output = $displayTitle;
+ } elseif ( $format === self::WIKI_LONG ) {
+ // Avoid Title::getPrefixedText as it transforms the text to have a
+ // leading capital letter in some configurations
+ $output = Localizer::getInstance()->createTextWithNamespacePrefix( SMW_NS_PROPERTY, $label );
+ }
+
+ if ( $format === self::HTML_SHORT && $linker !== null ) {
+ $output = $this->dataValue->getShortHTMLText( $linker );
+ }
+
+ // Output both according to the formatting rule set forth by
+ if ( $canonicalLabel !== $label ) {
+ $canonicalLabel = \Html::rawElement(
+ 'span', [ 'style' => 'font-size:small;' ], '(' . $canonicalLabel . ')' );
+ $output = $output . '&nbsp;'. $canonicalLabel;
+ }
+
+ return $output;
+ }
+
+ private function getWikiValue() {
+
+ $property = $this->dataValue->getDataItem();
+ $languageCode = $this->dataValue->getOption( PropertyValue::OPT_USER_LANGUAGE );
+
+ if ( ( $preferredLabel = $property->getPreferredLabel( $languageCode ) ) !== '' ) {
+ return $preferredLabel;
+ }
+
+ if ( $this->dataValue->getWikiPageValue() !== null && $this->dataValue->getWikiPageValue()->getDisplayTitle() !== '' ) {
+ return $this->dataValue->getWikiPageValue()->getDisplayTitle();
+ }
+
+ if ( ( $translatedPropertyLabel = $this->findTranslatedPropertyLabel( $property ) ) !== '' ) {
+ return $translatedPropertyLabel;
+ }
+
+ return $this->dataValue->getDataItem()->getLabel();
+ }
+
+ /**
+ * The display title modifies the search/sort characteristics (#1534),
+ * (foo:Bar vs. Foo:Bar vs. FOO:bar) therefore select a possible DisplayTitle
+ * before any other label preference.
+ */
+ private function getSearchLabel() {
+
+ $wikiPageValue = $this->dataValue->getWikiPageValue();
+
+ if ( $wikiPageValue !== null && ( $displayTitle = $wikiPageValue->getDisplayTitle() ) !== '' ) {
+ return $displayTitle;
+ }
+
+ return $this->dataValue->getDataItem()->getLabel();
+ }
+
+ private function prepareWikiPageValue( $linker = null ) {
+
+ $wikiPageValue = $this->dataValue->getWikiPageValue();
+
+ if ( $wikiPageValue === null ) {
+ return null;
+ }
+
+ $property = $this->dataValue->getDataItem();
+ $caption = $this->dataValue->getCaption();
+
+ if ( $caption !== false && $caption !== '' ) {
+ $wikiPageValue->setCaption( $caption );
+ } elseif ( ( $preferredLabel = $this->dataValue->getPreferredLabel() ) !== '' ) {
+ $wikiPageValue->setCaption( $preferredLabel );
+ } elseif ( ( $translatedPropertyLabel = $this->findTranslatedPropertyLabel( $property ) ) !== '' ) {
+ $wikiPageValue->setCaption( $translatedPropertyLabel );
+ } else {
+ $wikiPageValue->setCaption( $property->getLabel() );
+ }
+
+ return $wikiPageValue;
+ }
+
+ private function doHighlightText( $text, $linker = null ) {
+
+ $content = '';
+
+ if ( !$this->canHighlight( $content, $linker ) ) {
+ return $text;
+ }
+
+ $highlighter = Highlighter::factory(
+ Highlighter::TYPE_PROPERTY,
+ $this->dataValue->getOption( PropertyValue::OPT_USER_LANGUAGE )
+ );
+
+ $highlighter->setContent(
+ [
+ 'userDefined' => $this->dataValue->getDataItem()->isUserDefined(),
+ 'caption' => $text,
+ 'content' => $content !== '' ? $content : Message::get( 'smw_isspecprop' )
+ ]
+ );
+
+ return $highlighter->getHtml();
+ }
+
+ private function canHighlight( &$propertyDescription, $linker ) {
+
+ if ( $this->dataValue->getOption( PropertyValue::OPT_NO_HIGHLIGHT ) === true ) {
+ return false;
+ }
+
+ $dataItem = $this->dataValue->getDataItem();
+
+ $propertyDescription = $this->propertySpecificationLookup->getPropertyDescriptionByLanguageCode(
+ $dataItem,
+ $this->dataValue->getOption( PropertyValue::OPT_USER_LANGUAGE ),
+ $linker
+ );
+
+ return !$dataItem->isUserDefined() || $propertyDescription !== '';
+ }
+
+ private function hintPreferredLabelUse() {
+
+ if ( !$this->dataValue->isEnabledFeature( SMW_DV_PROV_LHNT ) ||
+ $this->dataValue->getOption( PropertyValue::OPT_NO_PREF_LHNT ) ) {
+ return '';
+ }
+
+ $property = $this->dataValue->getDataItem();
+
+ $preferredLabel = $property->getPreferredLabel(
+ $this->dataValue->getOption( PropertyValue::OPT_USER_LANGUAGE )
+ );
+
+ // When comparing with a caption set from the "outside", normalize
+ // the string to avoid a false negative in case of a non-breaking space
+ $caption = str_replace(
+ [ "&#160;", "&nbsp;", html_entity_decode( '&#160;', ENT_NOQUOTES, 'UTF-8' ) ],
+ " ",
+ $this->dataValue->getCaption()
+ );
+
+ if ( $preferredLabel === '' || $caption !== $preferredLabel ) {
+ return '';
+ }
+
+ $label = $property->getLabel();
+
+ if ( $preferredLabel === $label ) {
+ return '';
+ }
+
+ return '&nbsp;' . \Html::rawElement(
+ 'span',
+ [
+ 'title' => $property->getCanonicalLabel()
+ ],
+ '<sup>ᵖ</sup>'
+ );
+ }
+
+ private function findTranslatedPropertyLabel( $property ) {
+
+ // User-defined properties don't have any translatable label (this is
+ // what the preferred label is for)
+ if ( $property->isUserDefined() ) {
+ return '';
+ }
+
+ $prefix = '';
+
+ if ( $property->isInverse() ) {
+ $prefix = '-';
+ }
+
+ return $prefix . ApplicationFactory::getInstance()->getPropertyLabelFinder()->findPropertyLabelFromIdByLanguageCode(
+ $property->getKey(),
+ $this->dataValue->getOption( PropertyValue::OPT_USER_LANGUAGE )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/ReferenceValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/ReferenceValueFormatter.php
new file mode 100644
index 00000000..927f5579
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/ReferenceValueFormatter.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use RuntimeException;
+use SMW\DataValueFactory;
+use SMW\DataValues\ExternalIdentifierValue;
+use SMW\DataValues\ReferenceValue;
+use SMW\DIWikiPage;
+use SMW\Message;
+use SMWDataValue as DataValue;
+use SMWDITime as DITime;
+use SMWDIUri as DIUri;
+use SMWPropertyValue as PropertyValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ReferenceValueFormatter extends DataValueFormatter {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isFormatterFor( DataValue $dataValue ) {
+ return $dataValue instanceof ReferenceValue;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function format( $type, $linker = null ) {
+
+ if ( !$this->dataValue instanceof ReferenceValue ) {
+ throw new RuntimeException( "The formatter is missing a valid ReferenceValue object" );
+ }
+
+ if ( $this->dataValue->getCaption() !== false &&
+ ( $type === self::WIKI_SHORT || $type === self::HTML_SHORT ) ) {
+ return $this->dataValue->getCaption();
+ }
+
+ return $this->getOutputText( $type, $linker );
+ }
+
+ protected function getOutputText( $type, $linker = null ) {
+
+ if ( !$this->dataValue->isValid() ) {
+ return ( ( $type == self::WIKI_SHORT ) || ( $type == self::HTML_SHORT ) ) ? '' : $this->dataValue->getErrorText();
+ }
+
+ return $this->createOutput( $type, $linker );
+ }
+
+ private function createOutput( $type, $linker ) {
+
+ $results = $this->getListOfFormattedPropertyDataItems(
+ $type,
+ $linker,
+ $this->dataValue->getPropertyDataItems()
+ );
+
+ if ( $type == self::VALUE || $linker === null ) {
+ return implode( ';', $results );
+ }
+
+ $result = array_shift( $results );
+ $class = 'smw-reference-otiose';
+
+ // "smw-highlighter smwttinline" signals to invoke the tooltip
+ if ( count( $results ) > 0 ) {
+ $class = 'smw-reference smw-reference-indicator smw-highlighter smwttinline';
+ }
+
+ // Add an extra "title" attribute to support nojs environments by allowing
+ // it to display references even without JS, it will be removed when JS is available
+ // to show the "normal" tooltip
+ $result .= \Html::rawElement(
+ 'span',
+ [
+ 'class' => $class,
+ 'data-title' => Message::get( 'smw-ui-tooltip-title-reference', Message::TEXT, Message::USER_LANGUAGE ),
+ 'data-content' => '<ul><li>' . implode( '</li><li>', $results ) . '</li></ul>',
+ 'title' => strip_tags( implode( ', ', $results ) )
+ ]
+ );
+
+ return $result;
+ }
+
+ private function getListOfFormattedPropertyDataItems( $type, $linker, $propertyDataItems ) {
+
+ $results = [];
+
+ foreach ( $propertyDataItems as $propertyDataItem ) {
+
+ $propertyValues = $this->dataValue->getDataItem()->getSemanticData()->getPropertyValues( $propertyDataItem );
+ $dataItem = reset( $propertyValues );
+
+ // By definition the first element in the list is the VALUE other
+ // members are referencing to
+ $isValue = $results === [];
+ $dataValue = null;
+
+ if ( $dataItem !== false ) {
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem( $dataItem, $propertyDataItem );
+ $output = $this->findValueOutputFor( $isValue, $type, $dataValue, $linker );
+ } else {
+ $output = '?';
+ }
+
+ // Return a plain value in case no linker object is available
+ if ( $dataValue !== null && $linker === null ) {
+ return [ $dataValue->getWikiValue() ];
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $propertyDataItem
+ );
+
+ // Tooltip in tooltip isn't expected to work therefore avoid them
+ // when generating property labels in a reference output
+ $dataValue->setOption( PropertyValue::OPT_NO_HIGHLIGHT, true );
+
+ if ( !$isValue && $type !== self::VALUE ) {
+ $output = Message::get(
+ [
+ 'smw-datavalue-reference-outputformat',
+ $dataValue->getShortHTMLText( smwfGetLinker() ),
+ $output
+ ],
+ Message::TEXT
+ );
+ }
+
+ $results[] = $output;
+ }
+
+ return $results;
+ }
+
+ private function findValueOutputFor( $isValue, $type, $dataValue, $linker ) {
+
+ $dataItem = $dataValue->getDataItem();
+
+ // Turn Uri/Page links into a href representation when not used as value
+ if ( !$isValue &&
+ ( $dataItem instanceof DIUri || $dataItem instanceof DIWikiPage ) &&
+ $type !== self::VALUE || $dataValue->getTypeID() === ExternalIdentifierValue::TYPE_ID ) {
+ return $dataValue->getShortHTMLText( smwfGetLinker() );
+ }
+
+ // Dates and times are to be displayed in a localized format
+ if ( !$isValue && $dataItem instanceof DITime && $type !== self::VALUE ) {
+ $dataValue->setOutputFormat( 'LOCL' );
+ }
+
+ switch ( $type ) {
+ case self::VALUE:
+ return $dataValue->getWikiValue();
+ case self::WIKI_SHORT:
+ return $dataValue->getShortWikiText( $linker );
+ case self::HTML_SHORT:
+ return $dataValue->getShortHTMLText( $linker );
+ case self::WIKI_LONG:
+ return $dataValue->getLongWikiText( $linker );
+ case self::HTML_LONG:
+ return $dataValue->getLongHTMLText( $linker );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/StringValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/StringValueFormatter.php
new file mode 100644
index 00000000..d55ad620
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/StringValueFormatter.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use RuntimeException;
+use SMW\DataValues\StringValue;
+use SMW\Highlighter;
+use SMW\Utils\Normalizer;
+use SMWDataValue as DataValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class StringValueFormatter extends DataValueFormatter {
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function isFormatterFor( DataValue $dataValue ) {
+ return $dataValue instanceof StringValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function format( $dataValue, $options = null ) {
+
+ if ( !is_array( $options ) ) {
+ throw new RuntimeException( "Option is not an array!" );
+ }
+
+ // Normally we would do `list( $type, $linker ) = $options;` BUT due to
+ // PHP 7.0 ... "The order that the assignment operations are performed in has changed."
+
+ $type = $options[0];
+ $linker = isset( $options[1] ) ? $options[1] : null;
+
+ if ( !$dataValue instanceof StringValue ) {
+ throw new RuntimeException( "The formatter is missing a valid StringValue object" );
+ }
+
+ if ( $type === self::VALUE ) {
+ return $dataValue->isValid() ? $dataValue->getDataItem()->getString() : 'error';
+ }
+
+ if ( $dataValue->getCaption() !== false && $type === self::WIKI_SHORT ) {
+ return $dataValue->getCaption();
+ }
+
+ if ( $dataValue->getCaption() !== false && $type === self::HTML_SHORT ) {
+ return smwfXMLContentEncode( $dataValue->getCaption() );
+ }
+
+ if ( !$dataValue->isValid() ) {
+ return $dataValue->getDataItem()->getUserValue();
+ }
+
+ return $this->doFormat( $dataValue, $type, $linker );
+ }
+
+ protected function doFormat( $dataValue, $type, $linker ) {
+
+ $text = $dataValue->getDataItem()->getString();
+ $length = mb_strlen( $text );
+
+ // Make a possibly shortened printout string for displaying the value.
+ // The result is only escaped to be HTML-safe if this is requested
+ // explicitly. The result will contain mark-up that must not be escaped
+ // again.
+ $abbreviate = $type === self::WIKI_LONG || $type === self::HTML_LONG;
+ $requestedLength = intval( $dataValue->getOutputFormat() );
+
+ // Appease the MW parser to correctly apply formatting on the
+ // first indent
+ if ( $text !== '' && ( $text{0} === '*' || $text{0} === '#' || $text{0} === ':' ) ) {
+ $text = "\n" . $text . "\n";
+ }
+
+ if ( $requestedLength > 0 && $requestedLength < $length ) {
+ // Reduces the length and finish it with a whole word
+ return Normalizer::reduceLengthTo( $text, $requestedLength ) . ' …';
+ }
+
+ if ( $type === self::HTML_SHORT || $type === self::HTML_LONG ) {
+ $text = smwfXMLContentEncode( $text );
+ }
+
+ if ( $abbreviate && $length > 255 ) {
+ $text = $this->getAbbreviatedText( $text, $length, $linker );
+ }
+
+ return $text;
+ }
+
+ private function getAbbreviatedText( $text, $length, $linker ) {
+
+ if ( $linker === false || $linker === null ) {
+ $ellipsis = ' <span class="smwwarning">…</span> ';
+ } else {
+ $highlighter = Highlighter::factory( Highlighter::TYPE_TEXT );
+ $highlighter->setContent( [
+ 'caption' => ' … ',
+ 'content' => $text
+ ] );
+
+ $ellipsis = $highlighter->getHtml();
+ }
+
+ $startOff = 42;
+ $endOff = 42;
+
+ // Avoid breaking a link (i.e. [[ ... ]])
+ if ( ( $pos = stripos ( $text, '[[' ) ) && $pos < 42 ) {
+ $startOff = stripos ( $text, ']]' ) + 2;
+ }
+
+ if ( ( $pos = strrpos ( $text, ']]' ) ) && $pos > $length - $endOff ) {
+ $endOff = $length - strrpos( $text, '[[' );
+ }
+
+ return mb_substr( $text, 0, $startOff ) . $ellipsis . mb_substr( $text, $length - $endOff );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/TimeValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/TimeValueFormatter.php
new file mode 100644
index 00000000..40440e91
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/TimeValueFormatter.php
@@ -0,0 +1,402 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+use RuntimeException;
+use SMW\DataValues\Time\IntlTimeFormatter;
+use SMW\Localizer;
+use SMWDataValue as DataValue;
+use SMWDITime as DITime;
+use SMWTimeValue as TimeValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ * @author Fabian Howahl
+ * @author Terry A. Hurlbut
+ */
+class TimeValueFormatter extends DataValueFormatter {
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function isFormatterFor( DataValue $dataValue ) {
+ return $dataValue instanceof TimeValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function format( $type, $linker = null ) {
+
+ if ( !$this->dataValue instanceof TimeValue ) {
+ throw new RuntimeException( "The formatter is missing a valid TimeValue object" );
+ }
+
+ if (
+ $this->dataValue->isValid() &&
+ ( $type === self::WIKI_SHORT || $type === self::HTML_SHORT ) ) {
+ return ( $this->dataValue->getCaption() !== false ) ? $this->dataValue->getCaption() : $this->getPreferredCaption();
+ }
+
+ if (
+ $this->dataValue->isValid() &&
+ ( $type === self::WIKI_LONG || $type === self::HTML_LONG ) ) {
+ return $this->getPreferredCaption();
+ }
+
+ // #1074
+ return $this->dataValue->getCaption() !== false ? $this->dataValue->getCaption() : '';
+ }
+
+ /**
+ * @private
+ *
+ * Compute a string representation that largely follows the ISO8601 standard
+ * of representing dates. Large year numbers may have more than 4 digits,
+ * which is not strictly conforming to the standard. The date includes year,
+ * month, and day regardless of the input precision, but will only include
+ * time when specified.
+ *
+ * Conforming to the 2000 version of ISO8601, year 1 BC(E) is represented
+ * as "0000", year 2 BC(E) as "-0001" and so on.
+ *
+ * @since 2.4
+ *
+ * @param DITime $dataItem
+ * @param boolean $mindefault 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 ) {
+
+ $dataItem = $this->dataValue->getDataItemForCalendarModel( DITime::CM_GREGORIAN );
+ $precision = $dataItem->getPrecision();
+
+ $result = $dataItem->getYear() > 0 ? '' : '-';
+ $result .= str_pad( $dataItem->getYear(), 4, "0", STR_PAD_LEFT );
+
+ $monthnum = $precision >= DITime::PREC_YM ? $dataItem->getMonth() : ( $mindefault ? 1 : 12 );
+ $result .= '-' . str_pad( $monthnum, 2, "0", STR_PAD_LEFT );
+
+ $day = $dataItem->getDay();
+
+ if ( !$mindefault && $precision < DITime::PREC_YMD ) {
+ $day = DITime::getDayNumberForMonth( $monthnum, $dataItem->getYear(), DITime::CM_GREGORIAN );
+ }
+
+ $result .= '-' . str_pad( $day, 2, "0", STR_PAD_LEFT );
+
+ if ( $precision === DITime::PREC_YMDT ) {
+ $result .= 'T' . $this->getTimeString( ( $mindefault ? '00:00:00' : '23:59:59' ) );
+ }
+
+ return $result;
+ }
+
+ /**
+ * @private
+ *
+ * Use MediaWiki's date and time formatting. It can't handle all inputs
+ * properly, but has superior i18n support.
+ *
+ * @since 2.4
+ *
+ * @param DITime $dataItem
+ *
+ * @return string
+ */
+ public function getMediaWikiDate() {
+
+ $dataItem = $this->dataValue->getDataItemForCalendarModel( DITime::CM_GREGORIAN );
+ $precision = $dataItem->getPrecision();
+
+ $language = Localizer::getInstance()->getLanguage(
+ $this->dataValue->getOption( DataValue::OPT_USER_LANGUAGE )
+ );
+
+ $year = $dataItem->getYear();
+
+ if ( $year < 0 || $year > 9999 ) {
+ $year = '0000';
+ }
+
+ $year = str_pad( $year, 4, "0", STR_PAD_LEFT );
+
+ if ( $precision <= DITime::PREC_Y ) {
+ return $language->formatNum( $year, true );
+ }
+
+ $month = str_pad( $dataItem->getMonth(), 2, "0", STR_PAD_LEFT );
+ $day = str_pad( $dataItem->getDay(), 2, "0", STR_PAD_LEFT );
+
+ // date/timeanddate options:
+ // 1. Whether to adjust the time output according to the user
+ // configured offset ($timecorrection)
+ // 2. format, if it's false output the default one (default true)
+ // 3. timecorrection, the time offset as returned from
+ // Special:Preferences
+
+ if ( $precision <= DITime::PREC_YMD ) {
+ return $language->date( "$year$month$day" . '000000', false, true, false );
+ }
+
+ $time = str_replace( ':', '', $this->getTimeString() );
+
+ return $language->timeanddate( "$year$month$day$time", false, true, false );
+ }
+
+ /**
+ * @private
+ *
+ * @todo Internationalize the CE and BCE strings.
+ *
+ * Compute a suitable string to display the given date item.
+ *
+ * @note MediaWiki's date functions are not applicable for the range of
+ * historic dates we support.
+ *
+ * @since 2.4
+ *
+ * @param DITime $dataitem
+ *
+ * @return string
+ */
+ public function getCaptionFromDataItem( DITime $dataItem ) {
+
+ // If the language code is empty then the content language code is used
+ $lang = Localizer::getInstance()->getLang(
+ Localizer::getInstance()->getContentLanguage()
+ );
+
+ // https://en.wikipedia.org/wiki/Anno_Domini
+ // "...placing the "AD" abbreviation before the year number ... BC is
+ // placed after the year number (for example: AD 2016, but 68 BC)..."
+ // Chicago Manual of Style 2010, pp. 476–7; Goldstein 2007, p. 6.
+
+ if ( $dataItem->getYear() > 0 ) {
+ $cestring = $dataItem->getEra() > 0 ? 'AD' : '';
+ $result = ( $cestring ? ( $cestring . ' ' ) : '' ) . number_format( $dataItem->getYear(), 0, '.', '' );
+ } else {
+ $bcestring = 'BC';
+ $result = number_format( -( $dataItem->getYear() ), 0, '.', '' ) . ( $bcestring ? ( ' ' . $bcestring ) : '' );
+ }
+
+ if ( $dataItem->getPrecision() >= DITime::PREC_YM ) {
+ $result = $lang->getMonthLabel( $dataItem->getMonth() ) . " " . $result;
+ }
+
+ if ( $dataItem->getPrecision() >= DITime::PREC_YMD ) {
+ $result = $dataItem->getDay() . " " . $result;
+ }
+
+ if ( $dataItem->getPrecision() >= DITime::PREC_YMDT ) {
+ $result .= " " . $this->getTimeString();
+ }
+
+ $result .= $this->hintCalendarModel( $dataItem );
+
+ return $result;
+ }
+
+ /**
+ * @private
+ *
+ * Return the time as a string. The time string has the format HH:MM:SS,
+ * without any timezone information (see class documentation for details
+ * on current timezone handling).
+ * The parameter $default optionally specifies the value returned
+ * if the date is valid but has no explicitly specified time. It can
+ * also be set to false to detect this situation.
+ *
+ * @since 2.4
+ *
+ * @param string $default
+ *
+ * @return string
+ */
+ public function getTimeString( $default = '00:00:00' ) {
+
+ $dataItem = $this->dataValue->getDataItemForCalendarModel( DITime::CM_GREGORIAN );
+
+ if ( $dataItem->getPrecision() < DITime::PREC_YMDT ) {
+ return $default;
+ }
+
+ return sprintf( "%02d", $dataItem->getHour() ) . ':' .
+ sprintf( "%02d", $dataItem->getMinute() ) . ':' .
+ sprintf( "%02d", $dataItem->getSecond() );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DITime|null $dataItem
+ *
+ * @return string
+ */
+ public function getCaptionFromFreeFormat( DITime $dataItem = null ) {
+
+ $language = Localizer::getInstance()->getLanguage(
+ $this->dataValue->getOption( DataValue::OPT_USER_LANGUAGE )
+ );
+
+ // Prehistory dates are not supported when using this output format
+ // Only match options encapsulated by [ ... ]
+ if (
+ $dataItem !== null &&
+ $dataItem->getYear() > DITime::PREHISTORY &&
+ preg_match("/\[([^\]]*)\]/", $this->dataValue->getOutputFormat(), $matches ) ) {
+ $intlTimeFormatter = new IntlTimeFormatter( $dataItem, $language );
+
+ if ( ( $caption = $intlTimeFormatter->format( $matches[1] ) ) !== false ) {
+
+ if ( $intlTimeFormatter->containsValidDateFormatRule( $matches[1] ) ) {
+ $caption .= $this->hintCalendarModel( $dataItem );
+ }
+
+ return $caption;
+ }
+ }
+
+ return $this->getISO8601Date();
+ }
+
+ /**
+ * @private
+ *
+ * @since 2.4
+ *
+ * @param DITime|null $dataItem
+ *
+ * @return string
+ */
+ public function getLocalizedFormat( DITime $dataItem = null ) {
+
+ if ( $dataItem === null ) {
+ return '';
+ }
+
+ if ( $dataItem->getYear() < DITime::PREHISTORY ) {
+ return $this->getISO8601Date();
+ }
+
+ $outputFormat = $this->dataValue->getOutputFormat();
+ $formatFlag = IntlTimeFormatter::LOCL_DEFAULT;
+ $hasTimeCorrection = false;
+
+ if ( strpos( $outputFormat, 'TO' ) !== false ) {
+ $formatFlag = IntlTimeFormatter::LOCL_TIMEOFFSET;
+ $outputFormat = str_replace( '#TO', '', $outputFormat );
+ }
+
+ if ( strpos( $outputFormat, 'TZ' ) !== false ) {
+ $formatFlag = $formatFlag | IntlTimeFormatter::LOCL_TIMEZONE;
+ $outputFormat = str_replace( '#TZ', '', $outputFormat );
+ }
+
+ if ( ( $language = Localizer::getInstance()->getLanguageCodeFrom( $outputFormat ) ) === false ) {
+ $language = $this->dataValue->getOption( DataValue::OPT_USER_LANGUAGE );
+ }
+
+ $language = Localizer::getInstance()->getLanguage( $language );
+
+ $intlTimeFormatter = new IntlTimeFormatter(
+ $dataItem,
+ $language
+ );
+
+ // Avoid an exception on "DateTime::__construct(): Failed to parse time
+ // string (2147483647-01-01 00:00:0.0000000) at position 17 (0): Double
+ // time specification" for an annotation like [[Date::Jan 10000000000]]
+ try {
+ $localizedFormat = $intlTimeFormatter->getLocalizedFormat( $formatFlag ) .
+ $this->hintTimeCorrection( $intlTimeFormatter->hasLocalTimeCorrection() ) .
+ $this->hintCalendarModel( $dataItem );
+ } catch ( \Exception $e ) {
+ $localizedFormat = $this->getISO8601Date();
+ }
+
+ return $localizedFormat;
+ }
+
+ /**
+ * Compute a suitable string to display this date, taking into account the
+ * output format and the preferable calendar models for the data.
+ *
+ * @note MediaWiki's date functions are not applicable for the range
+ * of historic dates we support.
+ *
+ * @return string
+ */
+ protected function getPreferredCaption() {
+
+ $dataItem = $this->dataValue->getDataItem();
+ $format = strtoupper( $this->dataValue->getOutputFormat() );
+
+ if ( $format == 'ISO' || $this->dataValue->getOutputFormat() == '-' ) {
+ return $this->getISO8601Date();
+ } elseif ( $format == 'MEDIAWIKI' ) {
+ return $this->getMediaWikiDate();
+ } elseif ( $format == 'SORTKEY' ) {
+ return $dataItem->getSortKey();
+ } elseif ( $format == 'JD' ) {
+ return $dataItem->getJD();
+ }
+
+ // Does the formatting require calendar conversion?
+ $model = $dataItem->getCalendarModel();
+
+ if (
+ ( strpos( $format, 'JL' ) !== false ) ||
+ ( $dataItem->getJD() < TimeValue::J1582 && strpos( $format, 'GR' ) === false ) ) {
+ $model = DITime::CM_JULIAN;
+ } elseif ( strpos( $format, 'GR' ) !== false ) {
+ $model = DITime::CM_GREGORIAN;
+ }
+
+ if ( strpos( $format, '-F[' ) !== false ) {
+ return $this->getCaptionFromFreeFormat( $this->dataValue->getDataItemForCalendarModel( $model ) );
+ } elseif ( strpos( $format, 'LOCL' ) !== false ) {
+ return $this->getLocalizedFormat( $this->dataValue->getDataItemForCalendarModel( $model ) );
+ } elseif ( $dataItem->getYear() > TimeValue::PREHISTORY && $dataItem->getPrecision() >= DITime::PREC_YM ) {
+ // Do not convert between Gregorian and Julian if only
+ // year is given (years largely overlap in history, but
+ // assuming 1 Jan as the default date, the year number
+ // would change in conversion).
+ // Also do not convert calendars in prehistory: not
+ // meaningful (getDataItemForCalendarModel may return null).
+ return $this->getCaptionFromDataItem( $this->dataValue->getDataItemForCalendarModel( $model ) );
+ }
+
+ return $this->getCaptionFromDataItem( $dataItem );
+ }
+
+ private function hintTimeCorrection( $hasTimeCorrection ) {
+
+ if ( $hasTimeCorrection ) {
+ return '&nbsp;' . \Html::rawElement( 'sup', [ 'title' => 'ISO: ' . $this->getISO8601Date() ], 'ᴸ' );
+ }
+
+ return '';
+ }
+
+ private function hintCalendarModel( $dataItem ) {
+
+ if ( $this->dataValue->isEnabledFeature( SMW_DV_TIMEV_CM ) && $dataItem->getCalendarModel() !== DITime::CM_GREGORIAN ) {
+ return ' ' . \Html::rawElement( 'sup', [], $dataItem->getCalendarModelLiteral() );
+ }
+
+ return '';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/ValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/ValueFormatter.php
new file mode 100644
index 00000000..921570be
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueFormatters/ValueFormatter.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace SMW\DataValues\ValueFormatters;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+interface ValueFormatter {
+
+ /**
+ * @since 2.4
+ *
+ * @param mixed $type
+ * @param mixed|null $linker
+ *
+ * @return mixed
+ * @throws RuntimeException
+ */
+ public function format( $type, $linker = null );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/AllowsListValueParser.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/AllowsListValueParser.php
new file mode 100644
index 00000000..b476a7b2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/AllowsListValueParser.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace SMW\DataValues\ValueParsers;
+
+use SMW\DataValues\AllowsListValue;
+use SMW\MediaWiki\MediaWikiNsContentReader;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class AllowsListValueParser implements ValueParser {
+
+ /**
+ * @var MediaWikiNsContentReader
+ */
+ private $mediaWikiNsContentReader;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var array
+ */
+ private static $contents = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param MediaWikiNsContentReader $mediaWikiNsContentReader
+ */
+ public function __construct( MediaWikiNsContentReader $mediaWikiNsContentReader ) {
+ $this->mediaWikiNsContentReader = $mediaWikiNsContentReader;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function clear() {
+ self::$contents = [];
+ $this->errors = [];
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $userValue
+ *
+ * @return string|false
+ */
+ public function parse( $userValue ) {
+
+ $this->errors = [];
+
+ if ( isset( self::$contents[$userValue] ) ) {
+ return self::$contents[$userValue];
+ }
+
+ self::$contents[$userValue] = $this->parse_contents(
+ $userValue,
+ $this->mediaWikiNsContentReader->read( AllowsListValue::LIST_PREFIX . $userValue )
+ );
+
+ return self::$contents[$userValue];
+ }
+
+ private function parse_contents( $userValue, $contents ) {
+
+ if ( $contents === '' ) {
+ return $this->errors[] = [ 'smw-datavalue-allows-value-list-unknown', $userValue ];
+ }
+
+ if ( $contents{0} === '{' && ( $list = json_decode( $contents, true ) ) && is_array( $list ) ) {
+ return $list;
+ }
+
+ return $this->parse_string( $userValue, $contents );
+ }
+
+ private function parse_string( $userValue, $contents ) {
+
+ $parts = array_map( 'trim', preg_split( "([\n][\s]?)", $contents ) );
+ $list = [];
+
+ foreach ( $parts as $part ) {
+
+ // Only recognize those with a * Foo
+ if ( strpos( $part, '*' ) === false ) {
+ continue;
+ }
+
+ // Remove * from the content, other processes may use the hierarchy
+ // indicator something else
+ $part = trim( str_replace( '*', '', $part ) );
+
+ // Allow something like * Foo|Bar
+ if ( strpos( $part, '|' ) !== false ) {
+ list( $reference, $val ) = explode( '|', $part, 2 );
+ $list[$reference] = $val;
+ } else {
+ $list[$part] = $part;
+ }
+ }
+
+ if ( $list === [] ) {
+ $this->errors[] = [ 'smw-datavalue-allows-value-list-missing-marker', $userValue ];
+ }
+
+ return $list;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/AllowsPatternValueParser.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/AllowsPatternValueParser.php
new file mode 100644
index 00000000..f8a29c6e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/AllowsPatternValueParser.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace SMW\DataValues\ValueParsers;
+
+use SMW\DataValues\AllowsPatternValue;
+use SMW\MediaWiki\MediaWikiNsContentReader;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class AllowsPatternValueParser implements ValueParser {
+
+ /**
+ * @var MediaWikiNsContentReader
+ */
+ private $mediaWikiNsContentReader;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param MediaWikiNsContentReader $mediaWikiNsContentReader
+ */
+ public function __construct( MediaWikiNsContentReader $mediaWikiNsContentReader ) {
+ $this->mediaWikiNsContentReader = $mediaWikiNsContentReader;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $userValue
+ *
+ * @return string|false
+ */
+ public function parse( $userValue ) {
+
+ $this->errors = [];
+
+ $contentList = $this->doParseContent(
+ $this->mediaWikiNsContentReader->read( AllowsPatternValue::REFERENCE_PAGE_ID )
+ );
+
+ if ( !isset( $contentList[$userValue] ) ) {
+ return false;
+ }
+
+ return $contentList[$userValue];
+ }
+
+ private function doParseContent( $contents ) {
+
+ $list = [];
+
+ if ( $contents === '' ) {
+ return null;
+ }
+
+ $parts = array_map( 'trim', preg_split( "([\n][\s]?)", $contents ) );
+
+ // Get definition from first line
+ array_shift( $parts );
+
+ foreach ( $parts as $part ) {
+
+ if ( strpos( $part, '|' ) === false ) {
+ continue;
+ }
+
+ list( $reference, $regex ) = explode( '|', $part, 2 );
+ $list[trim( $reference )] = $regex;
+ }
+
+ return $list;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/ImportValueParser.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/ImportValueParser.php
new file mode 100644
index 00000000..4674433f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/ImportValueParser.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace SMW\DataValues\ValueParsers;
+
+use SMW\DataValues\ImportValue;
+use SMW\MediaWiki\MediaWikiNsContentReader;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class ImportValueParser implements ValueParser {
+
+ /**
+ * @var MediaWikiNsContentReader
+ */
+ private $mediaWikiNsContentReader;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @since 2.2
+ *
+ * @param MediaWikiNsContentReader $mediaWikiNsContentReader
+ */
+ public function __construct( MediaWikiNsContentReader $mediaWikiNsContentReader ) {
+ $this->mediaWikiNsContentReader = $mediaWikiNsContentReader;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array|null
+ */
+ public function parse( $value ) {
+
+ list( $namespace, $section, $controlledVocabulary ) = $this->splitByNamespaceSection(
+ $value
+ );
+
+ if ( $this->errors !== [] ) {
+ return null;
+ }
+
+ list( $uri, $name, $typelist ) = $this->doParse(
+ $controlledVocabulary
+ );
+
+ $type = $this->checkForValidType(
+ $namespace,
+ $section,
+ $uri,
+ $typelist
+ );
+
+ if ( $this->errors !== [] ) {
+ return null;
+ }
+
+ return [
+ $namespace,
+ $section,
+ $uri,
+ $name,
+ $type
+ ];
+ }
+
+ /**
+ * @return array|null
+ */
+ private function splitByNamespaceSection( $value ) {
+
+ if ( strpos( $value, ':' ) === false ) {
+
+ $this->errors[] = [
+ 'smw-datavalue-import-invalid-value',
+ $value
+ ];
+
+ return null;
+ }
+
+ list( $namespace, $section ) = explode( ':', $value, 2 );
+
+ /*
+ * A controlled vocabulary is a list of terms, with terms being unambiguous,
+ * and non-redundant. Vocabulary definitions adhere only a limited set of
+ * rules/constraints (e.g. Type/Label)
+ */
+ $controlledVocabulary = $this->mediaWikiNsContentReader->read(
+ ImportValue::IMPORT_PREFIX . $namespace
+ );
+
+ // Check that elements exists for the namespace
+ if ( $controlledVocabulary === '' ) {
+
+ $this->errors[] = [
+ 'smw-datavalue-import-unknown-namespace',
+ $namespace
+ ];
+
+ return null;
+ }
+
+ return [ $namespace, $section, $controlledVocabulary ];
+ }
+
+ /**
+ * @return array|null
+ */
+ private function checkForValidType( $namespace, $section, $uri, $typelist ) {
+
+ if ( $uri === '' ) {
+
+ $this->errors[] = [
+ 'smw-datavalue-import-missing-namespace-uri',
+ $namespace
+ ];
+
+ return null;
+ }
+
+ if ( !isset( $typelist[$section] ) ) {
+
+ $this->errors[] = [
+ 'smw-datavalue-import-missing-type',
+ $section,
+ $namespace
+ ];
+
+ return null;
+ }
+
+ return $typelist[$section];
+ }
+
+ /**
+ * @return array|null
+ */
+ private function doParse( $controlledVocabulary ) {
+
+ $list = [];
+ $importDefintions = array_map( 'trim', preg_split( "([\n][\s]?)", $controlledVocabulary ) );
+
+ // Get definition from first line
+ $fristLine = array_shift( $importDefintions );
+
+ if ( strpos( $fristLine, '|' ) === false ) {
+ return;
+ }
+
+ list( $uri, $name ) = explode( '|', $fristLine, 2 );
+
+ foreach ( $importDefintions as $importDefintion ) {
+
+ if ( strpos( $importDefintion, '|' ) === false ) {
+ continue;
+ }
+
+ list( $secname, $typestring ) = explode( '|', $importDefintion, 2 );
+ $list[trim( $secname )] = $typestring;
+ }
+
+ return [ $uri, $name, $list ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/MonolingualTextValueParser.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/MonolingualTextValueParser.php
new file mode 100644
index 00000000..fdf1e5ec
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/MonolingualTextValueParser.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace SMW\DataValues\ValueParsers;
+
+use SMW\Localizer;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class MonolingualTextValueParser implements ValueParser {
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string|array $userValue
+ *
+ * @return array
+ */
+ public function parse( $userValue ) {
+
+ // Allow things like [ "en" => "Foo ..." ] when retrieved from a JSON string
+ if ( is_array( $userValue ) ) {
+ foreach ( $userValue as $key => $value ) {
+ $languageCode = is_string( $key ) ? $key : '';
+ $text = is_string( $value ) ? $value : '';
+ }
+ } else {
+ $text = $userValue;
+ $languageCode = mb_substr( strrchr( $userValue, "@" ), 1 );
+
+ // Remove the language code and marker from the text
+ if ( $languageCode !== '' ) {
+ $text = substr_replace( $userValue, '', ( mb_strlen( $languageCode ) + 1 ) * -1 );
+ }
+ }
+
+ return [ $text, Localizer::asBCP47FormattedLanguageCode( $languageCode ) ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/PropertyValueParser.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/PropertyValueParser.php
new file mode 100644
index 00000000..884aac62
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/PropertyValueParser.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace SMW\DataValues\ValueParsers;
+
+use SMW\Localizer;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyValueParser implements ValueParser {
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var array
+ */
+ private $invalidCharacterList = [];
+
+ /**
+ * @var boolean
+ */
+ private $isCapitalLinks = true;
+
+ /**
+ * @var boolean
+ */
+ private $reqCapitalizedFirstChar = false;
+
+ /**
+ * @var boolean
+ */
+ private $isQueryContext = false;
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $invalidCharacterList
+ */
+ public function setInvalidCharacterList( array $invalidCharacterList ) {
+ $this->invalidCharacterList = $invalidCharacterList;
+ }
+
+ /**
+ * Corresponds to the $wgCapitalLinks setting
+ *
+ * @since 3.0
+ *
+ * @param boolean $isCapitalLinks
+ */
+ public function isCapitalLinks( $isCapitalLinks ) {
+ $this->isCapitalLinks = (bool)$isCapitalLinks;
+ }
+
+ /**
+ * Whether upper case for the first character is required or not in case of
+ * $wgCapitalLinks = false.
+ *
+ * @since 2.5
+ *
+ * @param boolean $reqCapitalizedFirstChar
+ */
+ public function reqCapitalizedFirstChar( $reqCapitalizedFirstChar ) {
+ $this->reqCapitalizedFirstChar = (bool)$reqCapitalizedFirstChar;
+ }
+
+ /**
+ * Whether or not the parsing is executed within a query context which may
+ * allow exceptions on the validation of invalid characters.
+ *
+ * @since 2.5
+ *
+ * @param boolean $isQueryContext
+ */
+ public function isQueryContext( $isQueryContext ) {
+ $this->isQueryContext = (bool)$isQueryContext;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $userValue
+ *
+ * @return array
+ */
+ public function parse( $userValue ) {
+
+ $this->errors = [];
+
+ // #1727 <Foo> or <Foo-<Bar> are not permitted but
+ // Foo-<Bar will be converted to Foo-
+ $userValue = strip_tags(
+ htmlspecialchars_decode( $userValue )
+ );
+
+ if ( !$this->hasValidCharacters( $userValue ) ) {
+ return [ null, null, null ];
+ }
+
+ return $this->getNormalizedValueFrom( $userValue );
+ }
+
+ private function hasValidCharacters( $value ) {
+
+ if ( trim( $value ) === '' ) {
+ $this->errors[] = [ 'smw_emptystring' ];
+ return false;
+ }
+
+ $invalidCharacter = '';
+
+ foreach ( $this->invalidCharacterList as $character ) {
+ if ( strpos( $value, $character ) !== false ) {
+ $invalidCharacter = $character;
+ break;
+ }
+ }
+
+ // #1567, Only allowed in connection with a query context (e.g sort=#)
+ if ( $invalidCharacter === '' && strpos( $value, '#' ) !== false && !$this->isQueryContext ) {
+ $invalidCharacter = '#';
+ }
+
+ if ( $invalidCharacter !== '' ) {
+
+ // Replace selected control chars otherwise the error display becomes
+ // unreadable
+ $invalidCharacter = str_replace(
+ [ "\r", "\n", ],
+ [ "CR", "LF" ],
+ $invalidCharacter
+ );
+
+ $this->errors[] = [ 'smw-datavalue-property-invalid-character', $value, $invalidCharacter ];
+ return false;
+ }
+
+ // #676, only on a query context allow Foo.Bar
+ if ( $invalidCharacter === '' && !$this->isQueryContext && strpos( $value, '.' ) !== false ) {
+ $this->errors[] = [ 'smw-datavalue-property-invalid-chain', $value ];
+ return false;
+ }
+
+ return true;
+ }
+
+ private function getNormalizedValueFrom( $value ) {
+
+ $inverse = false;
+ $capitalizedName = '';
+
+ // slightly normalise label
+ $propertyName = $this->doNormalize(
+ ltrim( rtrim( $value, ' ]' ), ' [' ),
+ $this->isCapitalLinks
+ );
+
+ if ( $this->reqCapitalizedFirstChar ) {
+ $capitalizedName = $this->doNormalize( $propertyName, true );
+ }
+
+ // property refers to an inverse
+ if ( ( $propertyName !== '' ) && ( $propertyName { 0 } == '-' ) ) {
+ $propertyName = $this->doNormalize( (string)substr( $value, 1 ), $this->isCapitalLinks );
+ /// NOTE The cast is necessary at least in PHP 5.3.3 to get string '' instead of boolean false.
+ /// NOTE It is necessary to normalize again here, since normalization may uppercase the first letter.
+ $inverse = true;
+ }
+
+ return [ $propertyName, $capitalizedName, $inverse ];
+ }
+
+ private function doNormalize( $text, $isCapitalLinks ) {
+
+ $text = trim( $text );
+
+ if ( $isCapitalLinks ) {
+ $text = Localizer::getInstance()->getContentLanguage()->ucfirst( $text );
+ }
+
+ return str_replace( '_', ' ', $text );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/TimeValueParser.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/TimeValueParser.php
new file mode 100644
index 00000000..91ce12cc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/TimeValueParser.php
@@ -0,0 +1,369 @@
+<?php
+
+namespace SMW\DataValues\ValueParsers;
+
+use SMW\DataValues\Time\Components;
+use SMW\DataValues\Time\Timezone;
+use SMW\Localizer;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Markus Krötzsch
+ * @author Fabian Howahl
+ * @author Terry A. Hurlbut
+ * @author mwjames
+ */
+class TimeValueParser implements ValueParser {
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var string
+ */
+ private $userValue = '';
+
+ /**
+ * @var array
+ */
+ private $languageCode = 'en';
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $languageCode
+ */
+ public function setLanguageCode( $languageCode ) {
+ $this->languageCode = $languageCode;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function clearErrors() {
+ $this->errors = [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $userValue
+ *
+ * @return string|false
+ */
+ public function parse( $userValue ) {
+
+ $this->errors = [];
+ $this->userValue = $userValue;
+
+ $datecomponents = [];
+ $calendarmodel = $era = $hours = $minutes = $seconds = $microseconds = $timeoffset = $timezone = false;
+
+ $status = $this->parseDateString(
+ $userValue,
+ $datecomponents,
+ $calendarmodel,
+ $era,
+ $hours,
+ $minutes,
+ $seconds,
+ $microseconds,
+ $timeoffset,
+ $timezone
+ );
+
+ // Default to JD input if a single number was given as the date
+ if ( ( $calendarmodel === false ) && ( $era === false ) && ( count( $datecomponents ) == 1 || count( $datecomponents ) == 2 ) && ( intval( end( $datecomponents ) ) >= 100000 ) ) {
+ $calendarmodel = 'JD';
+ }
+
+ $components = new Components(
+ [
+ 'value' => $userValue,
+ 'datecomponents' => $datecomponents,
+ 'calendarmodel' => $calendarmodel,
+ 'era' => $era,
+ 'hours' => $hours,
+ 'minutes' => $minutes,
+ 'seconds' => $seconds,
+ 'microseconds' => $microseconds,
+ 'timeoffset' => $timeoffset,
+ 'timezone' => $timezone
+ ]
+ );
+
+ return $status ? $components : false;
+ }
+
+ /**
+ * Parse the given string to check if it a date/time value.
+ * The function sets the provided call-by-ref values to the respective
+ * values. If errors are encountered, they are added to the objects
+ * error list and false is returned. Otherwise, true is returned.
+ *
+ * @todo This method in principle allows date parsing to be internationalized
+ * further.
+ *
+ * @param $string string input time representation, e.g. "12 May 2007 13:45:23-3:30"
+ * @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 set to a value between 0 and 24
+ * @param $minutes integer set to a value between 0 and 59
+ * @param $seconds integer set to a value between 0 and 59, or false if not given
+ * @param $timeoffset double set to a value for time offset (e.g. 3.5), or false if not given
+ *
+ * @return boolean stating if the parsing succeeded
+ */
+ private function parseDateString( $string, &$datecomponents, &$calendarmodel, &$era, &$hours, &$minutes, &$seconds, &$microseconds, &$timeoffset, &$timezone ) {
+
+ $calendarmodel = $timezoneoffset = $era = $ampm = false;
+ $hours = $minutes = $seconds = $microseconds = $timeoffset = $timezone = false;
+
+ // Fetch possible "America/Argentina/Mendoza"
+ $timzoneIdentifier = substr( $string, strrpos( $string, ' ' ) + 1 );
+
+ if ( Timezone::isValid( $timzoneIdentifier ) ) {
+ $string = str_replace( $timzoneIdentifier, '', $string );
+ $timezoneoffset = Timezone::getOffsetByAbbreviation( $timzoneIdentifier ) / 3600;
+ $timezone = Timezone::getIdByAbbreviation( $timzoneIdentifier );
+ }
+
+ // Preprocessing for supporting different date separation characters;
+ // * this does not allow localized time notations such as "10.34 pm"
+ // * this creates problems with keywords that contain "." such as "p.m."
+ // * yet "." is an essential date separation character in languages such as German
+ $parsevalue = str_replace( [ '/', '.', '&nbsp;', ',', '年', '月', '日', '時', '分' ], [ '-', ' ', ' ', ' ', ' ', ' ', ' ', ':', ' ' ], $string );
+
+ $matches = preg_split( "/([T]?[0-2]?[0-9]:[\:0-9]+[+\-]?[0-2]?[0-9\:]+|[\p{L}]+|[0-9]+|[ ])/u", $parsevalue, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
+ $datecomponents = [];
+ $unclearparts = [];
+
+ // Used for looking back; numbers are days/months/years by default but
+ // may be re-interpreted if certain further symbols are found
+ $matchisnumber = false;
+
+ // Used for ensuring that date parts are in one block
+ $matchisdate = false;
+
+ foreach ( $matches as $match ) {
+ $prevmatchwasnumber = $matchisnumber;
+ $prevmatchwasdate = $matchisdate;
+ $matchisnumber = $matchisdate = false;
+
+ if ( $match == ' ' ) {
+ $matchisdate = $prevmatchwasdate; // spaces in dates do not end the date
+ } elseif ( $match == '-' ) { // can only occur separately between date components
+ $datecomponents[] = $match; // we check later if this makes sense
+ $matchisdate = true;
+ } elseif ( is_numeric( $match ) &&
+ ( $prevmatchwasdate || count( $datecomponents ) == 0 ) ) {
+ $datecomponents[] = $match;
+ $matchisnumber = true;
+ $matchisdate = true;
+ } elseif ( $era === false && in_array( $match, [ 'AD', 'CE' ] ) ) {
+ $era = '+';
+ } elseif ( $era === false && in_array( $match, [ 'BC', 'BCE' ] ) ) {
+ $era = '-';
+ } elseif ( $calendarmodel === false && in_array( $match, [ 'Gr', 'GR' , 'He', 'Jl', 'JL', 'MJD', 'JD', 'OS' ] ) ) {
+ $calendarmodel = $match;
+ } elseif ( $ampm === false && ( strtolower( $match ) === 'am' || strtolower( $match ) === 'pm' ) ) {
+ $ampm = strtolower( $match );
+ } elseif ( $hours === false && self::parseTimeString( $match, $hours, $minutes, $seconds, $timeoffset ) ) {
+ // nothing to do
+ } elseif ( $hours !== false && $timezoneoffset === false && Timezone::isValid( $match ) ) {
+ // only accept timezone if time has already been set
+ $timezoneoffset = Timezone::getOffsetByAbbreviation( $match ) / 3600;
+ $timezone = Timezone::getIdByAbbreviation( $match );
+ } elseif ( $prevmatchwasnumber && $hours === false && $timezoneoffset === false &&
+ Timezone::isMilitary( $match ) &&
+ self::parseMilTimeString( end( $datecomponents ), $hours, $minutes, $seconds ) ) {
+ // military timezone notation is found after a number -> re-interpret the number as military time
+ array_pop( $datecomponents );
+ $timezoneoffset = Timezone::getOffsetByAbbreviation( $match ) / 3600;
+ $timezone = Timezone::getIdByAbbreviation( $match );
+ } elseif ( ( $prevmatchwasdate || count( $datecomponents ) == 0 ) &&
+ $this->parseMonthString( $match, $monthname ) ) {
+ $datecomponents[] = $monthname;
+ $matchisdate = true;
+ } elseif ( $prevmatchwasnumber && $prevmatchwasdate && in_array( $match, [ 'st', 'nd', 'rd', 'th' ] ) ) {
+ $datecomponents[] = 'd' . strval( array_pop( $datecomponents ) ); // must be a day; add standard marker
+ $matchisdate = true;
+ } elseif ( is_string( $match ) ) {
+ $microseconds = $match;
+ } else {
+ $unclearparts[] = $match;
+ }
+ }
+
+ // $this->debug( $datecomponents, $calendarmodel, $era, $hours, $minutes, $seconds, $microseconds, $timeoffset, $timezone );
+
+ // Abort if we found unclear or over-specific information:
+ if ( count( $unclearparts ) != 0 ) {
+ $this->errors[] = [ 'smw-datavalue-time-invalid-values', $this->userValue, implode( ', ', $unclearparts ) ];
+ return false;
+ }
+
+ if ( ( $timezoneoffset !== false && $timeoffset !== false ) ) {
+ $this->errors[] = [ 'smw-datavalue-time-invalid-offset-zone-usage', $this->userValue ];
+ return false;
+ }
+
+ if ( ( $timezoneoffset !== false && $timeoffset !== false ) ) {
+ $this->errors[] = [ 'smw-datavalue-time-invalid-offset-zone-usage', $this->userValue ];
+ return false;
+ }
+
+ $timeoffset = $timeoffset + $timezoneoffset;
+
+ // Check if the a.m. and p.m. information is meaningful
+ // Note: the == 0 check subsumes $hours===false
+ if ( $ampm !== false && ( $hours > 12 || $hours == 0 ) ) {
+ $this->errors[] = [ 'smw-datavalue-time-invalid-ampm', $this->userValue, $hours ];
+ return false;
+ } elseif ( $ampm == 'am' && $hours == 12 ) {
+ $hours = 0;
+ } elseif ( $ampm == 'pm' && $hours < 12 ) {
+ $hours += 12;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse the given string to check if it encodes an international time.
+ * If successful, the function sets the provided call-by-ref values to
+ * the respective numbers and returns true. Otherwise, it returns
+ * false and does not set any values.
+ *
+ * @param $string string input time representation, e.g. "13:45:23-3:30"
+ * @param $hours integer between 0 and 24
+ * @param $minutes integer between 0 and 59
+ * @param $seconds integer between 0 and 59, or false if not given
+ * @param $timeoffset double for time offset (e.g. 3.5), or false if not given
+ *
+ * @return boolean stating if the parsing succeeded
+ */
+ private static function parseTimeString( $string, &$hours, &$minutes, &$seconds, &$timeoffset ) {
+
+ if ( !preg_match( "/^[T]?([0-2]?[0-9]):([0-5][0-9])(:[0-5][0-9])?(([+\-][0-2]?[0-9])(:(30|00))?)?$/u", $string, $match ) ) {
+ return false;
+ } else {
+ $nhours = intval( $match[1] );
+ $nminutes = $match[2] ? intval( $match[2] ) : false;
+
+ if ( ( count( $match ) > 3 ) && ( $match[3] !== '' ) ) {
+ $nseconds = intval( substr( $match[3], 1 ) );
+ } else {
+ $nseconds = false;
+ }
+
+ if ( ( $nhours < 25 ) && ( ( $nhours < 24 ) || ( $nminutes + $nseconds == 0 ) ) ) {
+ $hours = $nhours;
+ $minutes = $nminutes;
+ $seconds = $nseconds;
+ if ( ( count( $match ) > 5 ) && ( $match[5] !== '' ) ) {
+ $timeoffset = intval( $match[5] );
+ if ( ( count( $match ) > 7 ) && ( $match[7] == '30' ) ) {
+ $timeoffset += 0.5;
+ }
+ } else {
+ $timeoffset = false;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse the given string to check if it encodes a "military time".
+ * If successful, the function sets the provided call-by-ref values to
+ * the respective numbers and returns true. Otherwise, it returns
+ * false and does not set any values.
+ *
+ * @param $string string input time representation, e.g. "134523"
+ * @param $hours integer between 0 and 24
+ * @param $minutes integer between 0 and 59
+ * @param $seconds integer between 0 and 59, or false if not given
+ *
+ * @return boolean stating if the parsing succeeded
+ */
+ private static function parseMilTimeString( $string, &$hours, &$minutes, &$seconds ) {
+
+ if ( !preg_match( "/^([0-2][0-9])([0-5][0-9])([0-5][0-9])?$/u", $string, $match ) ) {
+ return false;
+ } else {
+ $nhours = intval( $match[1] );
+ $nminutes = $match[2] ? intval( $match[2] ) : false;
+ $nseconds = ( ( count( $match ) > 3 ) && $match[3] ) ? intval( $match[3] ) : false;
+
+ if ( ( $nhours < 25 ) && ( ( $nhours < 24 ) || ( $nminutes + $nseconds == 0 ) ) ) {
+ $hours = $nhours;
+ $minutes = $nminutes;
+ $seconds = $nseconds;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse the given string to check if it refers to the string name ot
+ * abbreviation of a month name. If yes, it is replaced by a normalized
+ * month name (placed in the call-by-ref parameter) and true is
+ * returned. Otherwise, false is returned and $monthname is not changed.
+ *
+ * @param $string string month name or abbreviation to parse
+ * @param $monthname string with standard 3-letter English month abbreviation
+ *
+ * @return boolean stating whether a month was found
+ */
+ private function parseMonthString( $string, &$monthname ) {
+
+ // takes precedence over English month names!
+ $monthnum = Localizer::getInstance()->getLang( $this->languageCode )->findMonthNumberByLabel( $string );
+
+ if ( $monthnum !== false ) {
+ $monthnum -= 1;
+ } else {
+ $monthnum = array_search( $string, Components::$months ); // check English names
+ }
+
+ if ( $monthnum !== false ) {
+ $monthname = Components::$monthsShort[$monthnum];
+ return true;
+ } elseif ( array_search( $string, Components::$monthsShort ) !== false ) {
+ $monthname = $string;
+ return true;
+ }
+
+ return false;
+ }
+
+ private function debug( $datecomponents, $calendarmodel, $era, $hours, $minutes, $seconds, $microseconds, $timeoffset, $timezone ) {
+ //print "\n\n Results \n\n";
+ //debug_zval_dump( $datecomponents );
+ //print "\ncalendarmodel: $calendarmodel \ntimezoneoffset: $timezoneoffset \nera: $era \nampm: $ampm \nh: $hours \nm: $minutes \ns:$seconds \ntimeoffset: $timeoffset \n";
+ //debug_zval_dump( $unclearparts );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/ValueParser.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/ValueParser.php
new file mode 100644
index 00000000..439b7ddf
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueParsers/ValueParser.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace SMW\DataValues\ValueParsers;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+interface ValueParser {
+
+ /**
+ * @since 2.2
+ *
+ * @param mixed $value
+ *
+ * @return array|null
+ */
+ public function parse( $value );
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getErrors();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/AllowsListConstraintValueValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/AllowsListConstraintValueValidator.php
new file mode 100644
index 00000000..80bde017
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/AllowsListConstraintValueValidator.php
@@ -0,0 +1,269 @@
+<?php
+
+namespace SMW\DataValues\ValueValidators;
+
+use SMW\ApplicationFactory;
+use SMW\DataValues\ValueParsers\AllowsListValueParser;
+use SMW\PropertySpecificationLookup;
+use SMW\Message;
+use SMWDataValue as DataValue;
+use SMWNumberValue as NumberValue;
+use SMWDIBlob as DIBlob;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class AllowsListConstraintValueValidator implements ConstraintValueValidator {
+
+ /**
+ * @var AllowsListValueParser
+ */
+ private $allowsListValueParser;
+
+ /**
+ * @var PropertySpecificationLookup
+ */
+ private $propertySpecificationLookup;
+
+ /**
+ * @var boolean
+ */
+ private $hasConstraintViolation = false;
+
+ /**
+ * @var string
+ */
+ private $errorMsg = '';
+
+ /**
+ * @since 2.4
+ *
+ * @param AllowsListValueParser $allowsListValueParser
+ * @param PropertySpecificationLookup $propertySpecificationLookup
+ */
+ public function __construct( AllowsListValueParser $allowsListValueParser, PropertySpecificationLookup $propertySpecificationLookup ) {
+ $this->allowsListValueParser = $allowsListValueParser;
+ $this->propertySpecificationLookup = $propertySpecificationLookup;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function hasConstraintViolation() {
+ return $this->hasConstraintViolation;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function validate( $dataValue ) {
+
+ $this->hasConstraintViolation = false;
+ $this->errorMsg = 'smw-datavalue-constraint-error-allows-value-list';
+
+ if ( !$dataValue instanceof DataValue || $dataValue->getProperty() === null ) {
+ return $this->hasConstraintViolation;
+ }
+
+ $property = $dataValue->getProperty();
+
+ $allowedValues = $this->propertySpecificationLookup->getAllowedValues(
+ $property
+ );
+
+ $allowedListValues = $this->propertySpecificationLookup->getAllowedListValues(
+ $property
+ );
+
+ if ( $allowedValues === [] && $allowedListValues === [] ) {
+ return $this->hasConstraintViolation;
+ }
+
+ $allowedValueList = [];
+
+ $isAllowed = $this->checkConstraintViolation(
+ $dataValue,
+ $allowedValues,
+ $allowedValueList
+ );
+
+
+ if ( !$isAllowed ) {
+ foreach ( $allowedListValues as $dataItem ) {
+ $list = $this->allowsListValueParser->parse( $dataItem->getString() );
+
+ // Combine different lists into one
+ if ( is_array( $list ) ) {
+ $allowedValues = array_merge( $allowedValues, $list );
+ }
+ }
+
+ // On assignments like [Foo => Foo] (* Foo) or [Foo => Bar] (* Foo|Bar)
+ // use the key as comparison entity
+ $allowedValues = array_keys( $allowedValues );
+ } else {
+ return;
+ }
+
+ $isAllowed = $this->checkConstraintViolation(
+ $dataValue,
+ $allowedValues,
+ $allowedValueList
+ );
+
+ if ( $isAllowed === true ) {
+ return;
+ }
+
+ $count = count( $allowedValueList );
+
+ // Only the first 10 values otherwise the list may become too long
+ $allowedValueList = implode( ', ', array_slice(
+ array_keys( $allowedValueList ), 0 , 10 )
+ );
+
+ $allowedValueList = str_replace( [ '>', '<' ], [ '%3C', '%3E' ], $allowedValueList );
+
+ $dataValue->addErrorMsg(
+ [
+ $this->errorMsg,
+ $dataValue->getWikiValue(),
+ $allowedValueList . ( $count > 10 ? ', ...' : '' ),
+ $property->getLabel()
+ ],
+ Message::PARSE
+ );
+
+ $this->hasConstraintViolation = true;
+ }
+
+ private function checkConstraintViolation( $dataValue, $allowedValues, &$allowedValueList ) {
+
+ if ( !is_array( $allowedValues ) ) {
+ return true;
+ }
+
+ $hash = $dataValue->getDataItem()->getHash();
+ $value = $dataValue->getWikiValue();
+
+ $testDataValue = ApplicationFactory::getInstance()->getDataValueFactory()->newTypeIDValue(
+ $dataValue->getTypeID()
+ );
+
+ $isAllowed = false;
+
+ // Track a range related constraint which can be used as single
+ // `[[Allows value::>0]]` assignment or as conjunctive compound as in
+ // `[[Allows value::>0]] [[Allows value::<100]]` and can appear in any
+ // order
+ $range = null;
+
+ foreach ( $allowedValues as $allowedValue ) {
+
+ if ( is_string( $allowedValue ) ) {
+ $allowedValue = new DIBlob( $allowedValue );
+ }
+
+ if ( !$allowedValue instanceof DIBlob ) {
+ continue;
+ }
+
+ if ( $testDataValue instanceof NumberValue ) {
+
+ // Check [[Allows value::>0]]
+ if ( $this->check_range( '>', $value, $allowedValue, $range, $isAllowed, $allowedValueList ) ) {
+ continue;
+ }
+
+ // Check [[Allows value::<100]]
+ if ( $this->check_range( '<', $value, $allowedValue, $range, $isAllowed, $allowedValueList ) ) {
+ continue;
+ }
+
+ // Check [[Allows value::1...100]]
+ if ( $this->check_bounds( $value, $allowedValue, $isAllowed, $allowedValueList ) ) {
+ break;
+ }
+ }
+
+ // For a time based range one could use the JD date and simply apply
+ // a >, < comparison on something like `[[Allows value::>1970]]
+ // [[Allows value::<31.12.2100]]`
+
+ // String range based constraints seems to make not much sense for
+ // something like `[[Allows value::>abc]] [[Allows value::<def]]`
+
+ $testDataValue->setUserValue( $allowedValue->getString() );
+
+ if ( $hash === $testDataValue->getDataItem()->getHash() ) {
+ $isAllowed = true;
+ break;
+ } else {
+ // Filter dups
+ $allowedValueList[$allowedValue->getString()] = true;
+ }
+ }
+
+ return $isAllowed;
+ }
+
+ private function check_range( $exp, $value, $allowedValue, &$range, &$isAllowed, &$allowedValueList ) {
+
+ $v = $allowedValue->getString();
+
+ // If a previous range comparison failed then bail-out!
+ if ( $v{0} === $exp && ( $range === null || $range ) ) {
+ $v = intval( trim( substr( $v, 1 ) ) );
+
+ if ( $exp === '>' && $value > $v ) {
+ $isAllowed = true;
+ } elseif ( $exp === '<' && $value < $v ) {
+ $isAllowed = true;
+ } else {
+ $isAllowed = false;
+ $range = false;
+ }
+
+ if ( $range === false ) {
+ $allowedValueList[$allowedValue->getString()] = true;
+ }
+
+ return true;
+ }
+
+ $this->errorMsg = 'smw-datavalue-constraint-error-allows-value-range';
+
+ return false;
+ }
+
+ private function check_bounds( $value, $allowedValue, &$isAllowed, &$allowedValueList ) {
+
+ $v = $allowedValue->getString();
+
+ if ( strpos( $v, '...' ) === false ) {
+ return false;
+ }
+
+ list( $lower, $upper ) = explode( '...', $v );
+
+ if ( $value >= intval( $lower ) && $value <= intval( $upper ) ) {
+ return $isAllowed = true;
+ } else {
+ $allowedValueList[$allowedValue->getString()] = true;
+ }
+
+ $this->errorMsg = 'smw-datavalue-constraint-error-allows-value-range';
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/CompoundConstraintValueValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/CompoundConstraintValueValidator.php
new file mode 100644
index 00000000..ec412a02
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/CompoundConstraintValueValidator.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace SMW\DataValues\ValueValidators;
+
+use RuntimeException;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class CompoundConstraintValueValidator implements ConstraintValueValidator {
+
+ /**
+ * @var boolean
+ */
+ private $hasConstraintViolation = false;
+
+ /**
+ * @var array
+ */
+ private $constraintValueValidators = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param ConstraintValueValidator $constraintValueValidator
+ */
+ public function registerConstraintValueValidator( ConstraintValueValidator $constraintValueValidator ) {
+ $this->constraintValueValidators[] = $constraintValueValidator;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function hasConstraintViolation() {
+ return $this->hasConstraintViolation;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function validate( $dataValue ) {
+
+ $this->hasConstraintViolation = false;
+
+ if ( $this->constraintValueValidators === [] ) {
+ throw new RuntimeException( "Missing a registered ConstraintValueValidator" );
+ }
+
+ // Any constraint violation by a ConstraintValueValidator registered will
+ // force an immediate halt without checking any other possible constraint
+ foreach ( $this->constraintValueValidators as $constraintValueValidator ) {
+ $constraintValueValidator->validate( $dataValue );
+
+ if ( $constraintValueValidator->hasConstraintViolation() ) {
+ $this->hasConstraintViolation = true;
+ break;
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/ConstraintValueValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/ConstraintValueValidator.php
new file mode 100644
index 00000000..bddb7901
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/ConstraintValueValidator.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace SMW\DataValues\ValueValidators;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+interface ConstraintValueValidator {
+
+ /**
+ * @since 2.4
+ *
+ * @param DataValue $dataValue
+ */
+ public function validate( $dataValue );
+
+ /**
+ * @since 2.4
+ *
+ * @return boolean
+ */
+ public function hasConstraintViolation();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/PatternConstraintValueValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/PatternConstraintValueValidator.php
new file mode 100644
index 00000000..d96e2656
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/PatternConstraintValueValidator.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace SMW\DataValues\ValueValidators;
+
+use SMW\ApplicationFactory;
+use SMW\DataValues\ValueParsers\AllowsPatternValueParser;
+use SMWDataValue as DataValue;
+
+/**
+ * To support regular expressions in connection with the `Allows pattern`
+ * property.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class PatternConstraintValueValidator implements ConstraintValueValidator {
+
+ /**
+ * @var AllowsPatternContentParser
+ */
+ private $allowsPatternValueParser;
+
+ /**
+ * @var boolean
+ */
+ private $hasConstraintViolation = false;
+
+ /**
+ * @since 2.4
+ *
+ * @param AllowsPatternValueParser $allowsPatternValueParser
+ */
+ public function __construct( AllowsPatternValueParser $allowsPatternValueParser ) {
+ $this->allowsPatternValueParser = $allowsPatternValueParser;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function hasConstraintViolation() {
+ return $this->hasConstraintViolation;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function validate( $dataValue ) {
+
+ $this->hasConstraintViolation = false;
+
+ if (
+ !$dataValue instanceof DataValue ||
+ $dataValue->getProperty() === null ||
+ !$dataValue->isEnabledFeature( SMW_DV_PVAP ) ) {
+ return $this->hasConstraintViolation;
+ }
+
+ if ( ( $reference = ApplicationFactory::getInstance()->getPropertySpecificationLookup()->getAllowedPatternBy( $dataValue->getProperty() ) ) === '' ) {
+ return $this->hasConstraintViolation;
+ }
+
+ $content = $this->allowsPatternValueParser->parse(
+ $reference
+ );
+
+ if ( !$content ) {
+ return $this->hasConstraintViolation;
+ }
+
+ // Prevent a possible remote code execution vulnerability in connection
+ // with PCRE
+ $pattern = str_replace( [ '/e' ], [ '' ], trim( $content ) );
+
+ $this->doPregMatch(
+ $pattern,
+ $dataValue,
+ $reference
+ );
+ }
+
+ private function doPregMatch( $pattern, $dataValue, $reference ) {
+
+ // Convert escaping as in /\d{4}
+ $pattern = str_replace( "/\\", "\\", $pattern );
+
+ // Add a mandatory backslash
+ if ( $pattern !== '' && $pattern{0} !== '/' ) {
+ $pattern = '/' . $pattern;
+ }
+
+ if ( substr( $pattern, -1 ) !== '/' ) {
+ $pattern = $pattern . '/';
+ }
+
+ // @to suppress any errors caused by an invalid regex, the user should
+ // test the expression before making it available
+ if ( !@preg_match( $pattern, $dataValue->getDataItem()->getSortKey() ) ) {
+ $dataValue->addErrorMsg(
+ [
+ 'smw-datavalue-allows-pattern-mismatch',
+ $dataValue->getWikiValue(),
+ $reference
+ ]
+ );
+
+ $this->hasConstraintViolation = true;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/PropertySpecificationConstraintValueValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/PropertySpecificationConstraintValueValidator.php
new file mode 100644
index 00000000..b58903d9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/PropertySpecificationConstraintValueValidator.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace SMW\DataValues\ValueValidators;
+
+use SMWDataValue as DataValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertySpecificationConstraintValueValidator implements ConstraintValueValidator {
+
+ /**
+ * @var boolean
+ */
+ private $hasConstraintViolation = false;
+
+ /**
+ * @var array
+ */
+ private static $inMemoryLabelToLanguageTracer = [];
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function hasConstraintViolation() {
+ return $this->hasConstraintViolation;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function validate( $dataValue ) {
+
+ $this->hasConstraintViolation = false;
+
+ if (
+ !$dataValue instanceof DataValue ||
+ $dataValue->getProperty() === null ||
+ $dataValue->getContextPage() === null ||
+ $dataValue->getContextPage()->getNamespace() !== SMW_NS_PROPERTY ) {
+ return $this->hasConstraintViolation;
+ }
+
+ if ( $dataValue->getProperty()->getKey() === '_PPLB' ) {
+ return $this->doValidateCodifiedPreferredPropertyLabelConstraints( $dataValue );
+ }
+ }
+
+ private function doValidateCodifiedPreferredPropertyLabelConstraints( $dataValue ) {
+
+ // Annotated but not enabled
+ if ( !$dataValue->isEnabledFeature( SMW_DV_PPLB ) ) {
+ return $dataValue->addErrorMsg(
+ [
+ 'smw-datavalue-feature-not-supported',
+ 'SMW_DV_PPLB'
+ ]
+ );
+ }
+
+ $value = $dataValue->toArray();
+ $dbKey = $dataValue->getContextPage()->getDBKey();
+
+ // Language has been already assigned!
+ if ( ( $isKnownBy = $this->isKnownByLabelAndLanguage( $value, $dbKey ) ) !== false ) {
+ $dataValue->addErrorMsg(
+ [
+ 'smw-property-preferred-label-language-combination-exists',
+ $value['_TEXT'],
+ $value['_LCODE'],
+ $isKnownBy
+ ]
+ );
+ }
+ }
+
+ private function isKnownByLabelAndLanguage( $value, $dbkey ) {
+
+ $lang = isset( $value['_LCODE'] ) ? $value['_LCODE'] : false;
+
+ if ( !isset( self::$inMemoryLabelToLanguageTracer[$dbkey] ) ) {
+ self::$inMemoryLabelToLanguageTracer[$dbkey] = [];
+ }
+
+ if ( $lang && !isset( self::$inMemoryLabelToLanguageTracer[$dbkey][$lang] ) ) {
+ self::$inMemoryLabelToLanguageTracer[$dbkey][$lang] = $value['_TEXT'];
+ }
+
+ if ( $lang && self::$inMemoryLabelToLanguageTracer[$dbkey][$lang] !== $value['_TEXT'] ) {
+ return self::$inMemoryLabelToLanguageTracer[$dbkey][$lang];
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/UniquenessConstraintValueValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/UniquenessConstraintValueValidator.php
new file mode 100644
index 00000000..65ace8e3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/ValueValidators/UniquenessConstraintValueValidator.php
@@ -0,0 +1,205 @@
+<?php
+
+namespace SMW\DataValues\ValueValidators;
+
+use SMW\PropertySpecificationLookup;
+use SMW\DIWikiPage;
+use SMWDataValue as DataValue;
+use SMW\RequestOptions;
+use SMW\Store;
+
+/**
+ * @private
+ *
+ * Only allow values that are unique where uniqueness is establised for the first (
+ * in terms of time which also entails that after a full rebuild the first value
+ * found is being categorised as established value) value assigned to a property
+ * (that requires this trait) and any value that compares to an establised
+ * value with the same literal representation is being identified as violating the
+ * uniqueness constraint.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class UniquenessConstraintValueValidator implements ConstraintValueValidator {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertySpecificationLookup
+ */
+ private $propertySpecificationLookup;
+
+ /**
+ * @var boolean
+ */
+ private $hasConstraintViolation = false;
+
+ /**
+ * Tracks annotations for the current context to verify that a subject only
+ * contains unique assignments.
+ *
+ * @var array
+ */
+ private static $annotations = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ * @param PropertySpecificationLookup $propertySpecificationLookup
+ */
+ public function __construct( Store $store, PropertySpecificationLookup $propertySpecificationLookup ) {
+ $this->store = $store;
+ $this->propertySpecificationLookup = $propertySpecificationLookup;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function hasConstraintViolation() {
+ return $this->hasConstraintViolation;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function clear() {
+ self::$annotations = [];
+ }
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function validate( $dataValue ) {
+
+ $this->hasConstraintViolation = false;
+
+ if ( !$this->canValidate( $dataValue ) ) {
+ return $this->hasConstraintViolation;
+ }
+
+ $property = $dataValue->getProperty();
+
+ if ( !$this->propertySpecificationLookup->hasUniquenessConstraint( $property ) ) {
+ return $this->hasConstraintViolation;
+ }
+
+ $contextPage = $dataValue->getContextPage();
+
+ // Exclude the current page from the result match to check whether another
+ // page matches the condition and if so then the value can no longer be
+ // assigned and is not unique
+ $requestOptions = new RequestOptions();
+
+ $requestOptions->addExtraCondition( function( $store, $query, $alias ) use( $contextPage ) {
+ return $query->neq( "$alias.s_id", $store->getObjectIds()->getId( $contextPage ) );
+ }
+ );
+
+ $requestOptions->setLimit( 2 );
+ $count = 0;
+
+ if ( !$this->hasAnnotation( $dataValue ) ) {
+ $entityValueUniquenessConstraintChecker = $this->store->service( 'EntityValueUniquenessConstraintChecker' );
+
+ $res = $entityValueUniquenessConstraintChecker->checkConstraint(
+ $property,
+ $dataValue->getDataItem(),
+ $requestOptions
+ );
+
+ $count = count( $res );
+ }
+
+ // Check whether the current page has any other annotation for the
+ // same property
+ if ( $count < 1 && $this->isRegistered( $dataValue ) ) {
+ $dataValue->addErrorMsg(
+ [
+ 'smw-datavalue-uniqueness-constraint-isknown',
+ $property->getLabel(),
+ $contextPage->getTitle()->getPrefixedText(),
+ $dataValue->getWikiValue()
+ ]
+ );
+
+ $this->hasConstraintViolation = true;
+ }
+
+ // Has the page different values for the same property?
+ if ( $count < 1 ) {
+ return $this->hasConstraintViolation;
+ }
+
+ $this->hasConstraintViolation = true;
+
+ foreach ( $res as $dataItem ) {
+ $val = $dataValue->isValid() ? $dataValue->getWikiValue() : '...';
+ $text = '';
+
+ if ( $dataItem !== null && ( $title = $dataItem->getTitle() ) !== null ) {
+ $text = $title->getPrefixedText();
+ }
+
+ $dataValue->addErrorMsg(
+ [
+ 'smw-datavalue-uniqueness-constraint-error',
+ $property->getLabel(),
+ $val,
+ $text
+ ]
+ );
+ }
+
+ return $this->hasConstraintViolation;
+ }
+
+ private function canValidate( $dataValue ) {
+
+ if ( !$dataValue->isEnabledFeature( SMW_DV_PVUC ) || !$dataValue instanceof DataValue ) {
+ return false;
+ }
+
+ return $dataValue->getContextPage() !== null && $dataValue->getProperty() !== null;
+ }
+
+ private function isRegistered( $dataValue ) {
+
+ $contextPage = $dataValue->getContextPage();
+ $dataItem = $dataValue->getDataItem();
+ $property = $dataValue->getProperty();
+
+ $valueHash = md5( $property->getKey() . $dataItem->getHash() );
+ $key = $property->getKey();
+ $hash = $contextPage->getHash();
+
+ if ( isset( self::$annotations[$hash][$key] ) && self::$annotations[$hash][$key] !== $valueHash ) {
+ return true;
+ } else {
+ self::$annotations[$hash][$key] = $valueHash;
+ }
+
+ return false;
+ }
+
+ private function hasAnnotation( $dataValue ) {
+
+ $key = $dataValue->getProperty()->getKey();
+ $hash = $dataValue->getContextPage()->getHash();
+
+ return isset( self::$annotations[$hash][$key] );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Defines.php b/www/wiki/extensions/SemanticMediaWiki/src/Defines.php
new file mode 100644
index 00000000..7a93dcab
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Defines.php
@@ -0,0 +1,300 @@
+<?php
+/**
+ * Constants relevant to Semantic MediaWiki
+ *
+ */
+
+/**
+ * @ingroup Constants
+ * @ingroup SMW
+ */
+
+/**@{
+ * SMW\ResultPrinter related constants that define
+ * how/if headers should be displayed
+ */
+define( 'SMW_HEADERS_SHOW', 2 );
+define( 'SMW_HEADERS_PLAIN', 1 );
+define( 'SMW_HEADERS_HIDE', 0 ); // Used to be "false" hence use "0" to support extensions that still assume this.
+/**@}*/
+
+/**@{
+ * Constants for denoting output modes in many functions: HTML or Wiki?
+ * "File" is for printing results into stand-alone files (e.g. building RSS)
+ * and should be treated like HTML when building single strings. Only query
+ * printers tend to have special handling for that.
+ */
+define( 'SMW_OUTPUT_HTML', 1 );
+define( 'SMW_OUTPUT_WIKI', 2 );
+define( 'SMW_OUTPUT_FILE', 3 );
+define( 'SMW_OUTPUT_RAW', 4 );
+/**@}*/
+
+/**@{
+ * Constants for displaying the factbox
+ */
+define( 'SMW_FACTBOX_HIDDEN', 1 );
+define( 'SMW_FACTBOX_SPECIAL', 2 );
+define( 'SMW_FACTBOX_NONEMPTY', 3 );
+define( 'SMW_FACTBOX_SHOWN', 5 );
+
+define( 'SMW_FACTBOX_CACHE', 16 );
+define( 'SMW_FACTBOX_PURGE_REFRESH', 32 );
+define( 'SMW_FACTBOX_DISPLAY_SUBOBJECT', 64 );
+
+/**@}*/
+
+/**@{
+ * Constants for regulating equality reasoning
+ */
+define( 'SMW_EQ_NONE', 0 );
+define( 'SMW_EQ_SOME', 1 );
+define( 'SMW_EQ_FULL', 2 );
+/**@}*/
+
+/**@{
+ * Flags to classify available query descriptions,
+ * used to enable/disable certain features
+ */
+define( 'SMW_PROPERTY_QUERY', 1 ); // [[some property::...]]
+define( 'SMW_CATEGORY_QUERY', 2 ); // [[Category:...]]
+define( 'SMW_CONCEPT_QUERY', 4 ); // [[Concept:...]]
+define( 'SMW_NAMESPACE_QUERY', 8 ); // [[User:+]] etc.
+define( 'SMW_CONJUNCTION_QUERY', 16 ); // any conjunctions
+define( 'SMW_DISJUNCTION_QUERY', 32 ); // any disjunctions (OR, ||)
+define( 'SMW_ANY_QUERY', 0xFFFFFFFF ); // subsumes all other options
+/**@}*/
+
+/**@{
+ * Constants for defining which concepts to show only if cached
+ */
+define( 'CONCEPT_CACHE_ALL', 4 ); // show concept elements anywhere only if cached
+define( 'CONCEPT_CACHE_HARD', 1 ); // show without cache if concept is not harder than permitted inline queries
+define( 'CONCEPT_CACHE_NONE', 0 ); // show all concepts even without any cache
+/**@}*/
+
+/**@{
+ * Constants for identifying javascripts as used in SMWOutputs
+ */
+/// @deprecated Use module 'ext.smw.tooltips', see SMW_Ouptuts.php. Vanishes in SMW 1.7 at the latest.
+define( 'SMW_HEADER_TOOLTIP', 2 );
+/// @deprecated Module removed. Vanishes in SMW 1.7 at the latest.
+define( 'SMW_HEADER_SORTTABLE', 3 );
+/// @deprecated Use module 'ext.smw.style', see SMW_Ouptuts.php. Vanishes in SMW 1.7 at the latest.
+define( 'SMW_HEADER_STYLE', 4 );
+/**@}*/
+
+/**@{
+ * Comparators for datavalues
+ */
+define( 'SMW_CMP_EQ', 1 ); // Matches only datavalues that are equal to the given value.
+define( 'SMW_CMP_LEQ', 2 ); // Matches only datavalues that are less or equal than the given value.
+define( 'SMW_CMP_GEQ', 3 ); // Matches only datavalues that are greater or equal to the given value.
+define( 'SMW_CMP_NEQ', 4 ); // Matches only datavalues that are unequal to the given value.
+define( 'SMW_CMP_LIKE', 5 ); // Matches only datavalues that are LIKE the given value.
+define( 'SMW_CMP_NLKE', 6 ); // Matches only datavalues that are not LIKE the given value.
+define( 'SMW_CMP_LESS', 7 ); // Matches only datavalues that are less than the given value.
+define( 'SMW_CMP_GRTR', 8 ); // Matches only datavalues that are greater than the given value.
+define( 'SMW_CMP_PRIM_LIKE', 20 ); // Native LIKE matches (in disregards of an existing full-text index)
+define( 'SMW_CMP_PRIM_NLKE', 21 ); // Native NLIKE matches (in disregards of an existing full-text index)
+define( 'SMW_CMP_IN', 22 ); // Short-cut for ~* ... *
+define( 'SMW_CMP_PHRASE', 23 ); // Short-cut for a phrase match ~" ... " mostly for a full-text context
+define( 'SMW_CMP_NOT', 24 ); // Short-cut for ~! ... * ostly for a full-text context
+/**@}*/
+
+/**@{
+ * Constants for date formats (using binary encoding of nine bits:
+ * 3 positions x 3 interpretations)
+ */
+define( 'SMW_MDY', 785 ); // Month-Day-Year
+define( 'SMW_DMY', 673 ); // Day-Month-Year
+define( 'SMW_YMD', 610 ); // Year-Month-Day
+define( 'SMW_YDM', 596 ); // Year-Day-Month
+define( 'SMW_MY', 97 ); // Month-Year
+define( 'SMW_YM', 76 ); // Year-Month
+define( 'SMW_Y', 9 ); // Year
+define( 'SMW_YEAR', 1 ); // an entered digit can be a year
+define( 'SMW_DAY', 2 ); // an entered digit can be a year
+define( 'SMW_MONTH', 4 ); // an entered digit can be a month
+define( 'SMW_DAY_MONTH_YEAR', 7 ); // an entered digit can be a day, month or year
+define( 'SMW_DAY_YEAR', 3 ); // an entered digit can be either a month or a year
+/**@}*/
+
+/**@{
+ * Constants for date/time precision
+ */
+define( 'SMW_PREC_Y', 0 );
+define( 'SMW_PREC_YM', 1 );
+define( 'SMW_PREC_YMD', 2 );
+define( 'SMW_PREC_YMDT', 3 );
+define( 'SMW_PREC_YMDTZ', 4 ); // with time zone
+/**@}*/
+
+/**@{
+ * Constants for SPARQL supported features (mostly SPARQL 1.1) because we are unable
+ * to verify against the REST API whether a feature is supported or not
+ */
+define( 'SMW_SPARQL_QF_NONE', 0 ); // does not support any features
+define( 'SMW_SPARQL_QF_REDI', 2 ); // support for inverse property paths to find redirects
+define( 'SMW_SPARQL_QF_SUBP', 4 ); // support for rdfs:subPropertyOf*
+define( 'SMW_SPARQL_QF_SUBC', 8 ); // support for rdfs:subClassOf*
+define( 'SMW_SPARQL_QF_COLLATION', 16 ); // support for use of $smwgEntityCollation
+define( 'SMW_SPARQL_QF_NOCASE', 32 ); // support case insensitive pattern matches
+/**@}*/
+
+/**@{
+ * Constants for ValueLookupStore
+ */
+define( 'SMW_VL_SD', 1 ); // enables ValueLookupStore::getSemanticData
+define( 'SMW_VL_PL', 2 ); // enables ValueLookupStore::getProperties
+define( 'SMW_VL_PV', 4 ); // enables ValueLookupStore::getPropertyValues
+define( 'SMW_VL_PS', 8 ); // enables ValueLookupStore::getPropertySubject
+/**@}*/
+
+/**@{
+ * Deprecated since 3.0, remove options after complete removal in 3.1
+ */
+define( 'SMW_HTTP_DEFERRED_ASYNC', true );
+define( 'SMW_HTTP_DEFERRED_SYNC_JOB', 4 );
+define( 'SMW_HTTP_DEFERRED_LAZY_JOB', 8 );
+/**@}*/
+
+/**@{
+ * Constants DV features
+ */
+define( 'SMW_DV_NONE', 0 );
+define( 'SMW_DV_PROV_REDI', 2 ); // PropertyValue to follow a property redirect target
+define( 'SMW_DV_MLTV_LCODE', 4 ); // MonolingualTextValue requires language code
+define( 'SMW_DV_NUMV_USPACE', 8 ); // Preserve spaces in unit labels
+define( 'SMW_DV_PVAP', 16 ); // Allows pattern
+define( 'SMW_DV_WPV_DTITLE', 32 ); // WikiPageValue to use an explicit display title
+define( 'SMW_DV_PROV_DTITLE', 64 ); // PropertyValue allow to find a property using the display title
+define( 'SMW_DV_PVUC', 128 ); // Declares a uniqueness constraint
+define( 'SMW_DV_TIMEV_CM', 256 ); // TimeValue to indicate calendar model
+define( 'SMW_DV_PPLB', 512 ); // Preferred property label
+define( 'SMW_DV_PROV_LHNT', 1024 ); // PropertyValue to output a hint in case of a preferred label usage
+/**@}*/
+
+/**@{
+ * Constants for Fulltext types
+ */
+define( 'SMW_FT_NONE', 0 );
+define( 'SMW_FT_BLOB', 2 ); // DataItem::TYPE_BLOB
+define( 'SMW_FT_URI', 4 ); // DataItem::TYPE_URI
+define( 'SMW_FT_WIKIPAGE', 8 ); // DataItem::TYPE_WIKIPAGE
+/**@}*/
+
+/**@{
+ * Constants for admin features
+ */
+define( 'SMW_ADM_NONE', 0 );
+define( 'SMW_ADM_REFRESH', 2 ); // RefreshStore
+define( 'SMW_ADM_DISPOSAL', 4 ); // IDDisposal
+define( 'SMW_ADM_SETUP', 8 ); // SetupStore
+define( 'SMW_ADM_PSTATS', 16 ); // Property statistics update
+define( 'SMW_ADM_FULLT', 32 ); // Fulltext update
+/**@}*/
+
+/**@{
+ * Constants for ResultPrinter
+ */
+define( 'SMW_RF_NONE', 0 );
+define( 'SMW_RF_TEMPLATE_OUTSEP', 2 ); // #2022 Enable 2.5 behaviour for template handling
+/**@}*/
+
+/**@{
+ * Constants for $smwgExperimentalFeatures
+ */
+/**@}*/
+
+/**@{
+ * Constants for $smwgFieldTypeFeatures
+ */
+define( 'SMW_FIELDT_NONE', 0 );
+define( 'SMW_FIELDT_CHAR_NOCASE', 2 ); // Using FieldType::TYPE_CHAR_NOCASE
+define( 'SMW_FIELDT_CHAR_LONG', 4 ); // Using FieldType::TYPE_CHAR_LONG
+/**@}*/
+
+/**@{
+ * Constants for $smwgQueryProfiler
+ */
+define( 'SMW_QPRFL_NONE', 0 );
+define( 'SMW_QPRFL_PARAMS', 2 ); // Support for Query parameters
+define( 'SMW_QPRFL_DUR', 4 ); // Support for Query duration
+/**@}*/
+
+/**@{
+ * Constants for $smwgBrowseFeatures
+ */
+define( 'SMW_BROWSE_NONE', 0 );
+define( 'SMW_BROWSE_TLINK', 2 ); // Support for the toolbox link
+define( 'SMW_BROWSE_SHOW_INVERSE', 4 ); // Support inverse direction
+define( 'SMW_BROWSE_SHOW_INCOMING', 8 ); // Support for incoming links
+define( 'SMW_BROWSE_SHOW_GROUP', 16 ); // Support for grouping properties
+define( 'SMW_BROWSE_SHOW_SORTKEY', 32 ); // Support for the sortkey display
+define( 'SMW_BROWSE_USE_API', 64 ); // Support for using the API as request backend
+/**@}*/
+
+/**@{
+ * Constants for $smwgParserFeatures
+ */
+define( 'SMW_PARSER_NONE', 0 );
+define( 'SMW_PARSER_STRICT', 2 ); // Support for strict mode
+define( 'SMW_PARSER_UNSTRIP', 4 ); // Support for using the StripMarkerDecoder
+define( 'SMW_PARSER_INL_ERROR', 8 ); // Support for display of inline errors
+define( 'SMW_PARSER_HID_CATS', 16 ); // Support for parsing hidden categories
+define( 'SMW_PARSER_LINV', 32 ); // Support for links in value
+define( 'SMW_PARSER_LINKS_IN_VALUES', 32 ); // Support for links in value
+/**@}*/
+
+/**@{
+ * Constants for LinksInValue features
+ */
+define( 'SMW_LINV_PCRE', 2 ); // Using the PCRE approach
+define( 'SMW_LINV_OBFU', 4 ); // Using the Obfuscator approach
+/**@}*/
+
+/**@{
+ * Constants for $smwgCategoryFeatures
+ */
+define( 'SMW_CAT_NONE', 0 );
+define( 'SMW_CAT_REDIRECT', 2 ); // Support resolving category redirects
+define( 'SMW_CAT_INSTANCE', 4 ); // Support using a category as instantiatable object
+define( 'SMW_CAT_HIERARCHY', 8 ); // Support for category hierarchies
+/**@}*/
+
+/**@{
+ * Constants for $smwgQSortFeatures
+ */
+define( 'SMW_QSORT_NONE', 0 );
+define( 'SMW_QSORT', 2 ); // General sort support
+define( 'SMW_QSORT_RANDOM', 4 ); // Random sort support
+define( 'SMW_QSORT_UNCONDITIONAL', 8 ); // Unconditional sort support
+/**@}*/
+
+/**@{
+ * Constants for $smwgRemoteReqFeatures
+ */
+define( 'SMW_REMOTE_REQ_SEND_RESPONSE', 2 ); // Remote responses are enabled
+define( 'SMW_REMOTE_REQ_SHOW_NOTE', 4 ); // Shows a note
+/**@}*/
+
+/**@{
+ * Constants for Schema groups
+ */
+define( 'SMW_SCHEMA_GROUP_FORMAT', 'schema.group.format' );
+define( 'SMW_SCHEMA_GROUP_SEARCH_FORM', 'schema.group.search.form' );
+
+/**@{
+ * Constants for Special:Ask submit method
+ */
+define( 'SMW_SASK_SUBMIT_GET', 'get' );
+define( 'SMW_SASK_SUBMIT_GET_REDIRECT', 'get.redirect' );
+define( 'SMW_SASK_SUBMIT_POST', 'post' );
+/**@}*/
+
+/**@{
+ * Constants for content types
+ */
+define( 'CONTENT_MODEL_SMW_SCHEMA', 'smw/schema' );
+/**@}*/
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/DescriptionDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/DescriptionDeserializer.php
new file mode 100644
index 00000000..4230784c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/DescriptionDeserializer.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace SMW\Deserializers\DVDescriptionDeserializer;
+
+use Deserializers\DispatchableDeserializer;
+use SMW\ApplicationFactory;
+use SMW\DataItemFactory;
+use SMW\Query\DescriptionFactory;
+use SMW\Query\QueryComparator;
+use SMWDataValue as DataValue;
+
+/**
+ * @private
+ *
+ * Create an Description object based on a value string that was entered
+ * in a query. Turning inputs that a user enters in place of a value within
+ * a query string into query conditions is often a standard procedure. The
+ * processing must take comparators like "<" into account, but otherwise
+ * the normal parsing function can be used. However, there can be datatypes
+ * where processing is more complicated, e.g. if the input string contains
+ * more than one value, each of which may have comparators, as in
+ * SMWRecordValue. In this case, it makes sense to overwrite this method.
+ * Another reason to do this is to add new forms of comparators or new ways
+ * of entering query conditions.
+ *
+ * The resulting Description may or may not make use of the datavalue
+ * object that this function was called on, so it must be ensured that this
+ * value is not used elsewhere when calling this method. The function can
+ * return ThingDescription to not impose any condition, e.g. if parsing
+ * failed. Error messages of this DataValue object are propagated.
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+abstract class DescriptionDeserializer implements DispatchableDeserializer {
+
+ /**
+ * @var DescriptionFactory
+ */
+ protected $descriptionFactory;
+
+ /**
+ * @var DataItemFactory
+ */
+ protected $dataItemFactory;
+
+ /**
+ * @var array
+ */
+ protected $errors = [];
+
+ /**
+ * @var DataValue
+ */
+ protected $dataValue;
+
+ /**
+ * @since 2.5
+ *
+ * @param DescriptionFactory|null $descriptionFactory
+ * @param DataItemFactory|null $dataItemFactory
+ */
+ public function __construct( DescriptionFactory $descriptionFactory = null, DescriptionFactory $dataItemFactory = null ) {
+ $this->descriptionFactory = $descriptionFactory;
+ $this->dataItemFactory = $dataItemFactory;
+
+ if ( $this->descriptionFactory === null ) {
+ $this->descriptionFactory = ApplicationFactory::getInstance()->getQueryFactory()->newDescriptionFactory();
+ }
+
+ if ( $this->dataItemFactory === null ) {
+ $this->dataItemFactory = ApplicationFactory::getInstance()->getDataItemFactory();
+ }
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DataValue $dataValue
+ */
+ public function setDataValue( DataValue $dataValue ) {
+ $this->dataValue = $dataValue;
+ $this->errors = [];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $error
+ */
+ public function addError( $error ) {
+
+ if ( is_array( $error ) ) {
+ return $this->errors = array_merge( $this->errors, $error );
+ }
+
+ $this->errors[] = $error;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * Helper function for DescriptionDeserializer::deserialize that prepares a
+ * single value string, possibly extracting comparators. $value is changed
+ * to consist only of the remaining effective value string (without the
+ * comparator).
+ *
+ * @param string $value
+ * @param string|integer $comparator
+ */
+ protected function prepareValue( &$value, &$comparator ) {
+ $comparator = QueryComparator::getInstance()->extractComparatorFromString( $value );
+
+ // [[in:lorem ipsum]] / [[Has text::in:lorem ipsum]] to be turned into a
+ // proximity match where lorem AND ipsum needs to be present in the
+ // indexed match field.
+ //
+ // For those query engines that support those search patterns!
+ if ( $comparator === SMW_CMP_IN ) {
+ $comparator = SMW_CMP_LIKE;
+
+ // Looking for something like [[in:phrase:foo]]
+ if ( strpos( $value, 'phrase:' ) !== false ) {
+ $value = str_replace( 'phrase:', '', $value );
+ $value = '"' . $value . '"';
+ }
+
+ // `in:...` is for the "busy" user to avoid adding wildcards now and
+ // then to the value string
+ $value = "*$value*";
+
+ // No property and the assumption is [[in:...]] with the expected use
+ // of the wide proximity as indicated by an additional `~`
+ if ( $this->dataValue->getProperty() === null ) {
+ $value = "~$value";
+ }
+ }
+
+ // [[not:foo bar]]
+ // For those query engines that support those text search patterns!
+ if ( $comparator === SMW_CMP_NOT ) {
+ $comparator = SMW_CMP_NLKE;
+
+ $value = str_replace( '!', '', $value );
+
+ // Opposed to `in:` which includes *, `not:` is intended to match
+ // only the exact entered term. It can be extended using *
+ // if necessary (e.g. [[Has text::not:foo*]]).
+
+ // Use as phrase to signal an exact term match for a wide proximity
+ // search
+ if ( $this->dataValue->getProperty() === null ) {
+ $value = "~\"$value\"";
+ }
+ }
+
+ // [[phrase:lorem ipsum]] to be turned into a promixity phrase_match
+ // where the entire string (incl. its order) are to be matched.
+ //
+ // For those query engines that support those search patterns!
+ if ( $comparator === SMW_CMP_PHRASE ) {
+ $comparator = SMW_CMP_LIKE;
+ $value = '"' . $value . '"';
+
+ if ( $this->dataValue->getProperty() === null ) {
+ $value = "~$value";
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/DispatchingDescriptionDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/DispatchingDescriptionDeserializer.php
new file mode 100644
index 00000000..5d803064
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/DispatchingDescriptionDeserializer.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace SMW\Deserializers\DVDescriptionDeserializer;
+
+use RuntimeException;
+use SMWDataValue as DataValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class DispatchingDescriptionDeserializer {
+
+ /**
+ * @var DescriptionDeserializer[]
+ */
+ private $descriptionDeserializers = [];
+
+ /**
+ * @var DescriptionDeserializer
+ */
+ private $defaultDescriptionDeserializer = null;
+
+ /**
+ * @since 2.3
+ *
+ * @param DescriptionDeserializer $descriptionDeserializer
+ */
+ public function addDescriptionDeserializer( DescriptionDeserializer $descriptionDeserializer ) {
+ $this->descriptionDeserializers[] = $descriptionDeserializer;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DescriptionDeserializer $defaultDescriptionDeserializer
+ */
+ public function addDefaultDescriptionDeserializer( DescriptionDeserializer $defaultDescriptionDeserializer ) {
+ $this->defaultDescriptionDeserializer = $defaultDescriptionDeserializer;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DataValue $dataValue
+ *
+ * @return DescriptionDeserializer
+ * @throws RuntimeException
+ */
+ public function getDescriptionDeserializerBy( DataValue $dataValue ) {
+
+ foreach ( $this->descriptionDeserializers as $descriptionDeserializer ) {
+ if ( $descriptionDeserializer->isDeserializerFor( $dataValue ) ) {
+ $descriptionDeserializer->setDataValue( $dataValue );
+ return $descriptionDeserializer;
+ }
+ }
+
+ if ( $this->defaultDescriptionDeserializer !== null && $this->defaultDescriptionDeserializer->isDeserializerFor( $dataValue ) ) {
+ $this->defaultDescriptionDeserializer->setDataValue( $dataValue );
+ return $this->defaultDescriptionDeserializer;
+ }
+
+ throw new RuntimeException( "Missing registered DescriptionDeserializer." );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/MonolingualTextValueDescriptionDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/MonolingualTextValueDescriptionDeserializer.php
new file mode 100644
index 00000000..066953fd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/MonolingualTextValueDescriptionDeserializer.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace SMW\Deserializers\DVDescriptionDeserializer;
+
+use InvalidArgumentException;
+use SMW\DataValueFactory;
+use SMW\DataValues\MonolingualTextValue;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ThingDescription;
+use SMW\Query\Language\ValueDescription;
+use SMWDIBlob as DIBlob;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class MonolingualTextValueDescriptionDeserializer extends DescriptionDeserializer {
+
+ /**
+ * @since 2.4
+ *
+ * {@inheritDoc}
+ */
+ public function isDeserializerFor( $serialization ) {
+ return $serialization instanceof MonolingualTextValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $value
+ *
+ * @return Description
+ * @throws InvalidArgumentException
+ */
+ public function deserialize( $value ) {
+
+ if ( !is_string( $value ) ) {
+ throw new InvalidArgumentException( 'Value needs to be a string' );
+ }
+
+ if ( $value === '' ) {
+ $this->addError( wfMessage( 'smw_novalues' )->text() );
+ return new ThingDescription();
+ }
+
+ $subdescriptions = [];
+ list( $text, $languageCode ) = $this->dataValue->getValuesFromString( $value );
+
+ foreach ( $this->dataValue->getPropertyDataItems() as $property ) {
+
+ // If the DVFeature doesn't require a language code to be present then
+ // allow to skip it as conjunctive condition when it is empty
+ if (
+ ( $languageCode === '' ) &&
+ ( $property->getKey() === '_LCODE' ) &&
+ ( !$this->dataValue->isEnabledFeature( SMW_DV_MLTV_LCODE ) ) ) {
+ continue;
+ }
+
+ $value = $property->getKey() === '_LCODE' ? $languageCode : $text;
+ $comparator = SMW_CMP_EQ;
+
+ $this->prepareValue( $value, $comparator );
+
+ // Directly use the DI instead of going through the DVFactory to
+ // avoid having ~zh-* being validated when building a DV
+ // If one of the values is empty use, ? so queries can be arbitrary
+ // in respect of the query condition
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ new DIBlob( $value === '' ? '?' : $value ),
+ $property,
+ false,
+ $this->dataValue->getContextPage()
+ );
+
+ if ( !$dataValue->isValid() ) {
+ $this->addError( $dataValue->getErrors() );
+ continue;
+ }
+
+ $subdescriptions[] = $this->newSubdescription( $dataValue, $comparator );
+ }
+
+ return $this->getFinalDescriptionFor( $subdescriptions );
+ }
+
+ private function getFinalDescriptionFor( $subdescriptions ) {
+
+ $count = count( $subdescriptions );
+
+ if ( $count == 0 ) {
+ return new ThingDescription();
+ }
+
+ if ( $count == 1 ) {
+ return reset( $subdescriptions );
+ }
+
+ return new Conjunction( $subdescriptions );
+ }
+
+ private function newSubdescription( $dataValue, $comparator ) {
+
+ $description = new ValueDescription(
+ $dataValue->getDataItem(),
+ $dataValue->getProperty(),
+ $comparator
+ );
+
+ if ( $dataValue->getWikiValue() === '+' || $dataValue->getWikiValue() === '?' ) {
+ $description = new ThingDescription();
+ }
+
+ return new SomeProperty(
+ $dataValue->getProperty(),
+ $description
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/NumberValueDescriptionDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/NumberValueDescriptionDeserializer.php
new file mode 100644
index 00000000..b3e34810
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/NumberValueDescriptionDeserializer.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace SMW\Deserializers\DVDescriptionDeserializer;
+
+use SMWDINumber as DINumber;
+use SMWNumberValue as NumberValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class NumberValueDescriptionDeserializer extends DescriptionDeserializer {
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isDeserializerFor( $serialization ) {
+ return $serialization instanceof NumberValue;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $value
+ *
+ * @return Description
+ */
+ public function deserialize( $value ) {
+
+ $comparator = SMW_CMP_EQ;
+ $this->prepareValue( $value, $comparator );
+
+ if( $comparator !== SMW_CMP_LIKE && $comparator !== SMW_CMP_PRIM_LIKE ) {
+
+ $this->dataValue->setUserValue( $value );
+
+ if ( $this->dataValue->isValid() ) {
+ return $this->descriptionFactory->newValueDescription(
+ $this->dataValue->getDataItem(),
+ $this->dataValue->getProperty(),
+ $comparator
+ );
+ } else {
+ return $this->descriptionFactory->newThingDescription();
+ }
+ }
+
+ // Remove things that belong to SMW_CMP_LIKE
+ $value = str_replace( [ '~', '*', '!' ], '', $value );
+
+ $this->dataValue->setUserValue( $value );
+
+ if ( !$this->dataValue->isValid() ) {
+ return $this->descriptionFactory->newThingDescription();
+ }
+
+ $dataItem = $this->dataValue->getDataItem();
+ $property = $this->dataValue->getProperty();
+
+ if ( $this->getErrors() !== [] ) {
+ return $this->descriptionFactory->newThingDescription();
+ }
+
+ // in:/~ signals a range request for a number context
+ if ( $dataItem->getNumber() >= 0 ) {
+ // `[[Has number::in:99]]` -> `[[Has number:: [[≥0]] [[≤99]] ]]`)
+ $description = $this->descriptionFactory->newConjunction(
+ [
+ $this->descriptionFactory->newValueDescription( new DINumber( 0 ), $property, SMW_CMP_GEQ ),
+ $this->descriptionFactory->newValueDescription( $dataItem, $property, SMW_CMP_LEQ )
+ ]
+ );
+ } else {
+ // `[[Has number::in:-100]]` -> `[[Has number:: [[≥-100]] [[≤0]] ]]`
+ $description = $this->descriptionFactory->newConjunction(
+ [
+ $this->descriptionFactory->newValueDescription( $dataItem, $property, SMW_CMP_GEQ ),
+ $this->descriptionFactory->newValueDescription( new DINumber( 0 ), $property,SMW_CMP_LEQ )
+ ]
+ );
+ }
+
+ return $description;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/RecordValueDescriptionDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/RecordValueDescriptionDeserializer.php
new file mode 100644
index 00000000..3d08be24
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/RecordValueDescriptionDeserializer.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace SMW\Deserializers\DVDescriptionDeserializer;
+
+use InvalidArgumentException;
+use SMW\DataValueFactory;
+use SMW\DataValues\ReferenceValue;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ThingDescription;
+use SMWRecordValue as RecordValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class RecordValueDescriptionDeserializer extends DescriptionDeserializer {
+
+ /**
+ * @since 2.3
+ *
+ * {@inheritDoc}
+ */
+ public function isDeserializerFor( $serialization ) {
+ return $serialization instanceof RecordValue || $serialization instanceof ReferenceValue;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $value
+ *
+ * @return Description
+ * @throws InvalidArgumentException
+ */
+ public function deserialize( $value ) {
+
+ if ( !is_string( $value ) ) {
+ throw new InvalidArgumentException( 'value needs to be a string' );
+ }
+
+ if ( $value === '' ) {
+ $this->addError( wfMessage( 'smw_novalues' )->text() );
+ return new ThingDescription();
+ }
+
+ $subdescriptions = [];
+ $values = $this->dataValue->getValuesFromString( $value );
+
+ $valueIndex = 0; // index in value array
+ $propertyIndex = 0; // index in property list
+
+ foreach ( $this->dataValue->getPropertyDataItems() as $diProperty ) {
+
+ // stop if there are no values left
+ if ( !is_array( $values ) || !array_key_exists( $valueIndex, $values ) ) {
+ break;
+ }
+
+ $description = $this->getDescriptionForProperty(
+ $diProperty,
+ $values,
+ $valueIndex,
+ $propertyIndex
+ );
+
+ if ( $description !== null ) {
+ $subdescriptions[] = $description;
+ }
+
+ ++$propertyIndex;
+ }
+
+ if ( $subdescriptions === [] ) {
+ $this->addError( wfMessage( 'smw_novalues' )->text() );
+ }
+
+ return $this->getDescriptionFor( $subdescriptions );
+ }
+
+ private function getDescriptionFor( $subdescriptions ) {
+ switch ( count( $subdescriptions ) ) {
+ case 0:
+ return new ThingDescription();
+ case 1:
+ return reset( $subdescriptions );
+ default:
+ return new Conjunction( $subdescriptions );
+ }
+ }
+
+ private function getDescriptionForProperty( $diProperty, $values, &$valueIndex, $propertyIndex ) {
+
+ $values[$valueIndex] = str_replace( "-3B", ";", $values[$valueIndex] );
+ $beforePrepareValue = $values[$valueIndex];
+
+ $description = null;
+ $comparator = SMW_CMP_EQ;
+
+ $this->prepareValue( $values[$valueIndex], $comparator );
+
+ // generating the DVs:
+ if ( ( $values[$valueIndex] === '' ) || ( $values[$valueIndex] == '?' ) ) { // explicit omission
+ $valueIndex++;
+ return $description;
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByProperty(
+ $diProperty,
+ $values[$valueIndex],
+ false,
+ $this->dataValue->getContextPage()
+ );
+
+ if ( $dataValue->isValid() ) { // valid DV: keep
+ $description = new SomeProperty(
+ $diProperty,
+ $dataValue->getQueryDescription( $beforePrepareValue )
+ );
+ $valueIndex++;
+ } elseif ( ( count( $values ) - $valueIndex ) == ( count( $this->dataValue->getProperties() ) - $propertyIndex ) ) {
+ $this->addError( $dataValue->getErrors() );
+ ++$valueIndex;
+ }
+
+ return $description;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/SomeValueDescriptionDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/SomeValueDescriptionDeserializer.php
new file mode 100644
index 00000000..596245b0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/SomeValueDescriptionDeserializer.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace SMW\Deserializers\DVDescriptionDeserializer;
+
+use InvalidArgumentException;
+use SMW\DIWikiPage;
+use SMWDataValue as DataValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class SomeValueDescriptionDeserializer extends DescriptionDeserializer {
+
+ /**
+ * @since 2.3
+ *
+ * {@inheritDoc}
+ */
+ public function isDeserializerFor( $serialization ) {
+ return $serialization instanceof DataValue;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $value
+ *
+ * @return Description
+ * @throws InvalidArgumentException
+ */
+ public function deserialize( $value ) {
+
+ if ( !is_string( $value ) ) {
+ throw new InvalidArgumentException( 'Value needs to be a string' );
+ }
+
+ // https://www.w3.org/TR/html4/charset.html
+ // Internally encode something like [[Help:>Foo*]] since &lt; and &gt;
+ // would throw off the Title validator; apply only in combination with
+ // a NS such as [[Help:>...]]
+ $value = str_replace( [ ':<', ':>' ], [ ':-3C', ':-3E' ], $value );
+
+ $comparator = SMW_CMP_EQ;
+ $this->prepareValue( $value, $comparator );
+
+ $this->dataValue->setOption(
+ DataValue::OPT_QUERY_COMP_CONTEXT,
+ ( $comparator !== SMW_CMP_EQ && $comparator !== SMW_CMP_NEQ )
+ );
+
+ $this->dataValue->setUserValue( $value );
+
+ if ( !$this->dataValue->isValid() ) {
+ return $this->descriptionFactory->newThingDescription();
+ }
+
+ $dataItem = $this->dataValue->getDataItem();
+
+ $description = $this->descriptionFactory->newValueDescription(
+ $dataItem,
+ $this->dataValue->getProperty(),
+ $comparator
+ );
+
+ // Ensure [[>Help:Foo]] === [[Help:>Foo]] / [[Help:~Foo*]] === [[~Help:Foo*]]
+ if ( $dataItem instanceof DIWikiPage && $dataItem->getNamespace() !== NS_MAIN ) {
+ $description = $this->findApproriateDescription( $comparator, $dataItem, $description );
+ }
+
+ return $description;
+ }
+
+ private function findApproriateDescription( $comparator, $dataItem, $description ) {
+
+ $value = $dataItem->getDBKey();
+
+ // Normalize a possible earlier encoded string part in order for the
+ // QueryComparator::extractComparatorFromString to work its magic
+ if ( $comparator === SMW_CMP_EQ || $comparator === SMW_CMP_NEQ ) {
+ $value = str_replace( [ '-3C', '-3E' ], [ '<', '>' ], $value );
+ $this->prepareValue( $value, $comparator );
+ }
+
+ // No approximate, use the normal ValueDescription
+ if ( $comparator === SMW_CMP_EQ || $comparator === SMW_CMP_NEQ ) {
+ return $description;
+ }
+
+ // The NS has been stripped, use a normal value clause in the MAIN namespace
+ $valueDescription = $this->descriptionFactory->newValueDescription(
+ $this->dataItemFactory->newDIWikiPage( $value, NS_MAIN ),
+ null,
+ $comparator
+ );
+
+ // #1652
+ // Use [[Help:~Foo*]] as conjunctive description since the comparator
+ // is only applied on the sortkey that contains the DBKey part
+ $description = $this->descriptionFactory->newConjunction( [
+ $this->descriptionFactory->newNamespaceDescription( $dataItem->getNamespace() ),
+ $valueDescription
+ ] );
+
+ return $description;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/TimeValueDescriptionDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/TimeValueDescriptionDeserializer.php
new file mode 100644
index 00000000..dd4e50ba
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializer/TimeValueDescriptionDeserializer.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace SMW\Deserializers\DVDescriptionDeserializer;
+
+use DateInterval;
+use InvalidArgumentException;
+use SMWDITime as DITime;
+use SMWTimeValue as TimeValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class TimeValueDescriptionDeserializer extends DescriptionDeserializer {
+
+ /**
+ * @since 2.3
+ *
+ * {@inheritDoc}
+ */
+ public function isDeserializerFor( $serialization ) {
+ return $serialization instanceof TimeValue;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $value
+ *
+ * @return Description
+ * @throws InvalidArgumentException
+ */
+ public function deserialize( $value ) {
+
+ if ( !is_string( $value ) ) {
+ throw new InvalidArgumentException( 'The value needs to be a string' );
+ }
+
+ $comparator = SMW_CMP_EQ;
+ $this->prepareValue( $value, $comparator );
+
+ if( $comparator !== SMW_CMP_LIKE && $comparator !== SMW_CMP_NLKE ) {
+
+ $this->dataValue->setUserValue( $value );
+
+ if ( $this->dataValue->isValid() ) {
+ return $this->descriptionFactory->newValueDescription( $this->dataValue->getDataItem(), $this->dataValue->getProperty(), $comparator );
+ } else {
+ return $this->descriptionFactory->newThingDescription();
+ }
+ }
+
+ // #1178 to support queries like [[Has date::~ Dec 2001]]
+ $this->dataValue->setOption( TimeValue::OPT_QUERY_COMP_CONTEXT, true );
+ $this->dataValue->setUserValue( $value );
+
+ if ( !$this->dataValue->isValid() ) {
+ return $this->descriptionFactory->newThingDescription();
+ }
+
+ $dataItem = $this->dataValue->getDataItem();
+ $property = $this->dataValue->getProperty();
+
+ $upperLimitDataItem = $this->getUpperLimit( $dataItem );
+
+ if ( $this->getErrors() !== [] ) {
+ return $this->descriptionFactory->newThingDescription();
+ }
+
+ if( $comparator === SMW_CMP_LIKE ) {
+ $description = $this->descriptionFactory->newConjunction( [
+ $this->descriptionFactory->newValueDescription( $dataItem, $property, SMW_CMP_GEQ ),
+ $this->descriptionFactory->newValueDescription( $upperLimitDataItem, $property, SMW_CMP_LESS )
+ ] );
+ }
+
+ if( $comparator === SMW_CMP_NLKE ) {
+ $description = $this->descriptionFactory->newDisjunction( [
+ $this->descriptionFactory->newValueDescription( $dataItem, $property, SMW_CMP_LESS ),
+ $this->descriptionFactory->newValueDescription( $upperLimitDataItem, $property, SMW_CMP_GEQ )
+ ] );
+ }
+
+ return $description;
+ }
+
+ private function getUpperLimit( $dataItem ) {
+
+ $prec = $dataItem->getPrecision();
+ $dateTime = $dataItem->asDateTime();
+
+ if ( $dateTime === false ) {
+ return $this->addError( 'Cannot compute interval for ' . $dataItem->getSerialization() );
+ }
+
+ if ( $prec === DITime::PREC_Y ) {
+ $dateTime->add( new DateInterval( 'P1Y' ) );
+ } elseif( $prec === DITime::PREC_YM ) {
+ $dateTime->add( new DateInterval( 'P1M' ) );
+ } elseif( $prec === DITime::PREC_YMD ) {
+ $dateTime->add( new DateInterval( 'P1D' ) );
+ } elseif( $prec === DITime::PREC_YMDT ) {
+
+ if ( $dataItem->getSecond() > 0 ) {
+ $dateTime->add( new DateInterval( 'PT1S' ) );
+ } elseif( $dataItem->getMinute() > 0 ) {
+ $dateTime->add( new DateInterval( 'PT1M' ) );
+ } elseif( $dataItem->getHour() > 0 ) {
+ $dateTime->add( new DateInterval( 'PT1H' ) );
+ } else {
+ $dateTime->add( new DateInterval( 'PT24H' ) );
+ }
+ }
+
+ return DITime::doUnserialize( $dataItem->getCalendarModel() . '/' . $dateTime->format( 'Y/m/d/H/i/s' ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializerRegistry.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializerRegistry.php
new file mode 100644
index 00000000..8927334b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/DVDescriptionDeserializerRegistry.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace SMW\Deserializers;
+
+use SMW\Deserializers\DVDescriptionDeserializer\DescriptionDeserializer;
+use SMW\Deserializers\DVDescriptionDeserializer\DispatchingDescriptionDeserializer;
+use SMW\Deserializers\DVDescriptionDeserializer\MonolingualTextValueDescriptionDeserializer;
+use SMW\Deserializers\DVDescriptionDeserializer\NumberValueDescriptionDeserializer;
+use SMW\Deserializers\DVDescriptionDeserializer\RecordValueDescriptionDeserializer;
+use SMW\Deserializers\DVDescriptionDeserializer\SomeValueDescriptionDeserializer;
+use SMW\Deserializers\DVDescriptionDeserializer\TimeValueDescriptionDeserializer;
+use SMWDataValue as DataValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class DVDescriptionDeserializerRegistry {
+
+ /**
+ * @var DVDescriptionDeserializerRegistry
+ */
+ private static $instance = null;
+
+ /**
+ * @var DispatchingDescriptionDeserializer
+ */
+ private $dispatchingDescriptionDeserializer = null;
+
+ /**
+ * @since 2.3
+ *
+ * @param DispatchingDescriptionDeserializer|null $dispatchingDescriptionDeserializer
+ */
+ public function __construct( DispatchingDescriptionDeserializer $dispatchingDescriptionDeserializer = null ) {
+ $this->dispatchingDescriptionDeserializer = $dispatchingDescriptionDeserializer;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return self
+ */
+ public static function getInstance() {
+
+ if ( self::$instance === null ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.3
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * @note This allows extensions to inject their own DescriptionDeserializer
+ * without further violating SRP of the DataType or DataValue.
+ *
+ * @since 2.3
+ *
+ * @param DescriptionDeserializer $descriptionDeserializer
+ */
+ public function registerDescriptionDeserializer( DescriptionDeserializer $descriptionDeserializer ) {
+
+ if ( $this->dispatchingDescriptionDeserializer === null ) {
+ $this->dispatchingDescriptionDeserializer = $this->newDispatchingDescriptionDeserializer();
+ }
+
+ $this->dispatchingDescriptionDeserializer->addDescriptionDeserializer( $descriptionDeserializer );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DataValue $dataValue
+ *
+ * @return DescriptionDeserializer
+ */
+ public function getDescriptionDeserializerBy( DataValue $dataValue ) {
+
+ if ( $this->dispatchingDescriptionDeserializer === null ) {
+ $this->dispatchingDescriptionDeserializer = $this->newDispatchingDescriptionDeserializer();
+ }
+
+ return $this->dispatchingDescriptionDeserializer->getDescriptionDeserializerBy( $dataValue );
+ }
+
+ private function newDispatchingDescriptionDeserializer() {
+
+ $dispatchingDescriptionDeserializer = new DispatchingDescriptionDeserializer();
+ $dispatchingDescriptionDeserializer->addDescriptionDeserializer( new TimeValueDescriptionDeserializer() );
+ $dispatchingDescriptionDeserializer->addDescriptionDeserializer( new NumberValueDescriptionDeserializer() );
+ $dispatchingDescriptionDeserializer->addDescriptionDeserializer( new RecordValueDescriptionDeserializer() );
+ $dispatchingDescriptionDeserializer->addDescriptionDeserializer( new MonolingualTextValueDescriptionDeserializer() );
+
+ $dispatchingDescriptionDeserializer->addDefaultDescriptionDeserializer( new SomeValueDescriptionDeserializer() );
+
+ return $dispatchingDescriptionDeserializer;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/ExpDataDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/ExpDataDeserializer.php
new file mode 100644
index 00000000..bb5906db
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/ExpDataDeserializer.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace SMW\Deserializers;
+
+use Deserializers\Deserializer;
+use OutOfBoundsException;
+use SMW\Exporter\Element\ExpElement;
+use SMWExpData as ExpData;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class ExpDataDeserializer implements Deserializer {
+
+ /**
+ * @see Deserializers::deserialize
+ *
+ * @since 2.2
+ *
+ * @return ExpData
+ * @throws OutOfBoundsException
+ */
+ public function deserialize( $serialization ) {
+
+ $expData = null;
+
+ if ( isset( $serialization['version'] ) && $serialization['version'] !== 0.1 ) {
+ throw new OutOfBoundsException( 'Serializer/Deserializer version does not match, please update your data' );
+ }
+
+ if ( isset( $serialization['subject'] ) ) {
+ $expData = $this->newExpData( $serialization['subject'] );
+ }
+
+ if ( !$expData instanceof ExpData ) {
+ throw new OutOfBoundsException( 'ExpData could not be created probably due to an invalid subject' );
+ }
+
+ $this->doDeserialize( $serialization, $expData );
+
+ return $expData;
+ }
+
+ private function newExpData( $subject ) {
+ return new ExpData( ExpElement::newFromSerialization( $subject ) );
+ }
+
+ private function doDeserialize( $serialization, $expData ) {
+
+ foreach ( $serialization['data'] as $data ) {
+
+ $property = ExpElement::newFromSerialization( $data['property'] );
+
+ foreach ( $data['children'] as $child ) {
+ $expData->addPropertyObjectValue(
+ $property,
+ $this->doDeserializeChild( $child )
+ );
+ }
+ }
+ }
+
+ private function doDeserializeChild( $serialization ) {
+
+ if ( !isset( $serialization['subject'] ) ) {
+ return ExpElement::newFromSerialization( $serialization );
+ }
+
+ $element = $this->newExpData( $serialization['subject'] );
+ $this->doDeserialize( $serialization, $element );
+
+ return $element;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/SemanticDataDeserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/SemanticDataDeserializer.php
new file mode 100644
index 00000000..2e95894a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Deserializers/SemanticDataDeserializer.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace SMW\Deserializers;
+
+use Deserializers\Deserializer;
+use OutOfBoundsException;
+use RuntimeException;
+use SMW\DataTypeRegistry;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\SemanticData;
+use SMWContainerSemanticData;
+use SMWDataItem as DataItem;
+use SMWDIContainer as DIContainer;
+use SMWErrorValue as ErrorValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class SemanticDataDeserializer implements Deserializer {
+
+ /**
+ * @var array
+ */
+ private $dataItemTypeIdCache = [];
+
+ /**
+ * @see Deserializers::deserialize
+ *
+ * @since 1.9
+ *
+ * @return SemanticData
+ * @throws OutOfBoundsException
+ * @throws RuntimeException
+ */
+ public function deserialize( $data ) {
+
+ $semanticData = null;
+
+ if ( isset( $data['version'] ) && $data['version'] !== 0.1 && $data['version'] !== 2 ) {
+ throw new OutOfBoundsException( 'Serializer/Unserializer version does not match, please update your data' );
+ }
+
+ if ( isset( $data['subject'] ) ) {
+ $semanticData = new SemanticData( DIWikiPage::doUnserialize( $data['subject'] ) );
+ }
+
+ if ( !$semanticData instanceof SemanticData ) {
+ throw new RuntimeException( 'SemanticData could not be created probably due to a missing subject' );
+ }
+
+ $this->doDeserialize( $data, $semanticData );
+
+ return $semanticData;
+ }
+
+ /**
+ * @return null
+ */
+ private function doDeserialize( $data, &$semanticData ) {
+
+ $property = null;
+
+ if ( !isset( $data['data'] ) ) {
+ return;
+ }
+
+ foreach ( $data['data'] as $values ) {
+
+ if ( is_array( $values ) ) {
+
+ foreach ( $values as $key => $value ) {
+
+ /**
+ * @var DIProperty $property
+ */
+ if ( $key === 'property' ) {
+ $property = DIProperty::doUnserialize( $value );
+ }
+
+ /**
+ * @var DataItem
+ */
+ if ( $key === 'dataitem' ) {
+ foreach ( $value as $val ) {
+ $this->doDeserializeDataItem( $property, $data, $val, $semanticData );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @return DataItem
+ */
+ private function doDeserializeDataItem( $property, $data, $value, $semanticData ) {
+
+ $dataItem = null;
+
+ if ( !is_array( $value ) ) {
+ return;
+ }
+
+ $type = $this->getDataItemId( $property );
+
+ // Verify that the current property type definition and the type of the
+ // property during serialization do match, throw an error value to avoid any
+ // exception during unserialization caused by the DataItem object due to a
+ // mismatch of type definitions
+
+ if ( $type === $value['type'] ) {
+ $dataItem = DataItem::newFromSerialization( $value['type'], $value['item'] );
+ } else {
+ $dataItem = $property->getDiWikiPage();
+ $property = new DIProperty( DIProperty::TYPE_ERROR );
+
+ $semanticData->addError( [
+ new ErrorValue( $type, 'type mismatch', $property->getLabel() )
+ ] );
+
+ }
+
+ // Check whether the current dataItem has a subobject reference
+ if ( $dataItem->getDIType() === DataItem::TYPE_WIKIPAGE && $dataItem->getSubobjectName() !== '' ) {
+
+ $dataItem = $this->doDeserializeSubSemanticData(
+ $data,
+ $value['item'],
+ new SMWContainerSemanticData( $dataItem )
+ );
+
+ }
+
+ // Ensure that errors are collected from a subobject level as well and
+ // made available at the top
+ if ( $dataItem instanceof DIContainer ) {
+ $semanticData->addError( $dataItem->getSemanticData()->getErrors() );
+ }
+
+ if ( $property !== null && $dataItem !== null ) {
+ $semanticData->addPropertyObjectValue( $property, $dataItem );
+ }
+
+ }
+
+ /**
+ * Resolves properties and dataitems assigned to a subobject recursively
+ *
+ * @note The serializer has to make sure to provide a complete data set
+ * otherwise the subobject is neglected (of course one could set an error
+ * value to the DIContainer but as of now that seems unnecessary)
+ *
+ * @return DIContainer|null
+ */
+ private function doDeserializeSubSemanticData( $data, $id, $semanticData ) {
+
+ if ( !isset( $data['sobj'] ) ) {
+ return new DIContainer( $semanticData );;
+ }
+
+ foreach ( $data['sobj'] as $subobject ) {
+ if ( isset( $subobject['subject'] ) && $subobject['subject'] === $id && isset( $subobject['data'] ) ) {
+ $this->doDeserialize( $subobject, $semanticData );
+ }
+ }
+
+ return new DIContainer( $semanticData );
+ }
+
+ /**
+ * Returns DataItemId for a property
+ *
+ * @note findPropertyTypeID is calling the Store to find the
+ * typeId reference this is costly but at the moment there is no other
+ * way to determine the typeId
+ *
+ * This check is to ensure that during unserialization the correct item
+ * in terms of its definition is being sought otherwise inconsistencies
+ * can occur due to type changes of a property between the time of
+ * the serialization and the deserialization (e.g for when the
+ * serialization object is stored in cache, DB etc.)
+ *
+ * @return integer
+ */
+ private function getDataItemId( DIProperty $property ) {
+
+ if ( !isset( $this->dataItemTypeIdCache[$property->getKey()] ) ) {
+ $this->dataItemTypeIdCache[$property->getKey()] = DataTypeRegistry::getInstance()->getDataItemId( $property->findPropertyTypeID() );
+ }
+
+ return $this->dataItemTypeIdCache[$property->getKey()];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/ElasticClientTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/ElasticClientTaskHandler.php
new file mode 100644
index 00000000..01f08ba5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/ElasticClientTaskHandler.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace SMW\Elastic\Admin;
+
+use Html;
+use SMW\MediaWiki\Specials\Admin\OutputFormatter;
+use SMW\MediaWiki\Specials\Admin\TaskHandler;
+use SMW\Message;
+use SMW\ApplicationFactory;
+use WebRequest;
+use SMW\Elastic\Indexer\ReplicationStatus;
+use SMW\Elastic\Connection\Client as ElasticClient;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ElasticClientTaskHandler extends TaskHandler {
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @var array
+ */
+ private $taskHandlers = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param OutputFormatter $outputFormatter
+ * @param array $taskHandlers
+ */
+ public function __construct( OutputFormatter $outputFormatter, array $taskHandlers = [] ) {
+ $this->outputFormatter = $outputFormatter;
+ $this->taskHandlers = $taskHandlers;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SUPPLEMENT;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+
+ // Root
+ $actions = [
+ 'elastic'
+ ];
+
+ foreach ( $this->taskHandlers as $taskHandler ) {
+ $actions[] = $taskHandler->getTask();
+ }
+
+ return in_array( $task, $actions );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $connection = $this->getStore()->getConnection( 'elastic' );
+
+ if ( !$connection->ping() ) {
+ return '';
+ }
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-elastic-title' ),
+ [ 'action' => 'elastic' ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-elastic-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $connection = $this->getStore()->getConnection( 'elastic' );
+ $action = $webRequest->getText( 'action' );
+
+ if ( !$connection->ping() ) {
+ return $this->outputNoNodesAvailable();
+ } elseif ( $action === 'elastic' ) {
+ $this->outputHead();
+ } else {
+ foreach ( $this->taskHandlers as $taskHandler ) {
+ if ( $taskHandler->isTaskFor( $action ) ) {
+
+ $taskHandler->setStore(
+ $this->getStore()
+ );
+
+ return $taskHandler->handleRequest( $webRequest );
+ }
+ }
+ }
+
+ $this->outputInfo();
+ }
+
+ private function outputNoNodesAvailable() {
+
+ $this->outputHead();
+
+ $html = Html::element(
+ 'div',
+ [ 'class' => 'smw-callout smw-callout-error' ],
+ 'Elasticsearch has no active nodes available.'
+ );
+
+ $this->outputFormatter->addHTML( $html );
+ }
+
+ private function outputHead() {
+
+ $this->outputFormatter->setPageTitle( 'Elasticsearch' );
+ $this->outputFormatter->addHelpLink( 'https://www.semantic-mediawiki.org/wiki/Help:ElasticStore' );
+
+ $this->outputFormatter->addParentLink(
+ [ 'tab' => 'supplement' ]
+ );
+
+ $html = Html::rawElement(
+ 'p',
+ [ 'class' => 'plainlinks' ],
+ $this->msg( [ 'smw-admin-supplementary-elastic-docu' ], Message::PARSE )
+ );
+
+ $this->outputFormatter->addHTML( $html );
+ }
+
+ private function outputInfo() {
+
+ $connection = $this->getStore()->getConnection( 'elastic' );
+ $html = '';
+
+ $this->outputFormatter->addAsPreformattedText(
+ $this->outputFormatter->encodeAsJson( $connection->info() )
+ );
+
+ $replicationStatus = new ReplicationStatus(
+ $connection
+ );
+
+ $jobQueue = ApplicationFactory::getInstance()->getJobQueue();
+
+ $html .= Html::element(
+ 'li',
+ [],
+ $this->msg( [ 'smw-admin-supplementary-elastic-status-last-active-replication', $replicationStatus->get( 'last_update' ) ] )
+ );
+
+ $html .= Html::rawElement(
+ 'li', [ 'class' => 'plainlinks' ],
+ $this->msg( [ 'smw-admin-supplementary-elastic-status-recovery-job-count', $jobQueue->getQueueSize( 'smw.elasticIndexerRecovery') ], Message::PARSE )
+ );
+
+ if ( $connection->getConfig()->dotGet( 'indexer.experimental.file.ingest', false ) ) {
+ $html .= Html::rawElement(
+ 'li',
+ [ 'class' => 'plainlinks' ],
+ $this->msg( [ 'smw-admin-supplementary-elastic-status-file-ingest-job-count', $jobQueue->getQueueSize( 'smw.elasticFileIngest') ], Message::PARSE )
+ );
+ }
+
+ if ( $connection->hasLock( ElasticClient::TYPE_DATA ) ) {
+ $html .= Html::rawElement(
+ 'li',
+ [ 'class' => 'plainlinks' ],
+ $this->msg( [ 'smw-admin-supplementary-elastic-status-rebuild-lock', '✓' ], Message::TEXT )
+ );
+ }
+
+ $html .= Html::element(
+ 'li',
+ [],
+ $this->msg( [ 'smw-admin-supplementary-elastic-status-refresh-interval', $replicationStatus->get( 'refresh_interval' ) ] )
+ );
+
+ $this->outputFormatter->addHTML(
+ Html::element( 'h3', [], $this->msg(
+ 'smw-admin-supplementary-elastic-status-replication' )
+ ) . Html::rawElement( 'ul', [], $html )
+ );
+
+ $list = '';
+
+ foreach ( $this->taskHandlers as $taskHandler ) {
+ $list .= $taskHandler->getHtml();
+ }
+
+ $this->outputFormatter->addHTML(
+ Html::element( 'h3', [], $this->msg( 'smw-admin-supplementary-elastic-functions' ) )
+ );
+
+ $this->outputFormatter->addHTML(
+ Html::rawElement( 'ul', [], $list )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/IndicesInfoProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/IndicesInfoProvider.php
new file mode 100644
index 00000000..aeef88cb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/IndicesInfoProvider.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace SMW\Elastic\Admin;
+
+use Html;
+use SMW\Message;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class IndicesInfoProvider extends InfoProviderHandler {
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSupplementTask() {
+ return 'indices';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-elastic-indices-title' ),
+ [ 'action' => $this->getTask() ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-elastic-indices-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $this->outputFormatter->setPageTitle( 'Elasticsearch indices' );
+
+ $this->outputFormatter->addParentLink(
+ [ 'action' => $this->getParentTask() ],
+ 'smw-admin-supplementary-elastic-title'
+ );
+
+ $this->outputInfo();
+ }
+
+ private function outputInfo() {
+
+ $connection = $this->getStore()->getConnection( 'elastic' );
+
+ $html = Html::rawElement(
+ 'p',
+ [
+ 'class' => 'plainlinks'
+ ],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-elastic-statistics-docu',
+ ],
+ Message::PARSE
+ )
+ );
+
+ $this->outputFormatter->addHtml( $html );
+
+ $this->outputFormatter->addHtml( '<h2>Indices</h2>' );
+
+ $indices = $connection->cat( 'indices' );
+ ksort( $indices );
+
+ $this->outputFormatter->addAsPreformattedText(
+ $this->outputFormatter->encodeAsJson( $indices )
+ );
+
+ $this->outputFormatter->addHtml( '<h2>Statistics</h2>' );
+
+ $this->outputFormatter->addAsPreformattedText(
+ $this->outputFormatter->encodeAsJson( $connection->stats( 'indices' ) )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/InfoProviderHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/InfoProviderHandler.php
new file mode 100644
index 00000000..0be22606
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/InfoProviderHandler.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace SMW\Elastic\Admin;
+
+use SMW\MediaWiki\Specials\Admin\OutputFormatter;
+use SMW\MediaWiki\Specials\Admin\TaskHandler;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+abstract class InfoProviderHandler extends TaskHandler {
+
+ /**
+ * @var OutputFormatter
+ */
+ protected $outputFormatter;
+
+ /**
+ * @since 3.0
+ *
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( OutputFormatter $outputFormatter ) {
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SUPPLEMENT;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === $this->getTask();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getParentTask() {
+ return 'elastic';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getTask() {
+ return $this->getParentTask() . '/' . $this->getSupplementTask();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ abstract public function getSupplementTask();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/MappingsInfoProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/MappingsInfoProvider.php
new file mode 100644
index 00000000..e09e4738
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/MappingsInfoProvider.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace SMW\Elastic\Admin;
+
+use Html;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use WebRequest;
+use SMW\Utils\HtmlTabs;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class MappingsInfoProvider extends InfoProviderHandler {
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSupplementTask() {
+ return 'mappings';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-elastic-mappings-title' ),
+ [ 'action' => $this->getTask() ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-elastic-mappings-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $this->outputFormatter->setPageTitle( 'Elasticsearch mappings' );
+
+ $this->outputFormatter->addParentLink(
+ [ 'action' => $this->getParentTask() ],
+ 'smw-admin-supplementary-elastic-title'
+ );
+
+ $this->outputInfo();
+ }
+
+ private function outputInfo() {
+
+ $connection = $this->getStore()->getConnection( 'elastic' );
+
+ $mappings = [
+ $connection->getMapping(
+ [
+ 'index' => $connection->getIndexNameByType( ElasticClient::TYPE_DATA )
+ ]
+ ),
+ $connection->getMapping(
+ [
+ 'index' => $connection->getIndexNameByType( ElasticClient::TYPE_LOOKUP )
+ ]
+ )
+ ];
+
+ $this->outputFormatter->addHtml(
+ Html::rawElement( 'p', [], $this->msg( 'smw-admin-supplementary-elastic-mappings-docu' ) )
+ );
+
+ $htmlTabs = new HtmlTabs();
+ $htmlTabs->setGroup( 'es-mapping' );
+ $htmlTabs->setActiveTab( 'summary' );
+
+ $htmlTabs->tab( 'summary', $this->msg( 'smw-admin-supplementary-elastic-mappings-summary' ) );
+
+ $htmlTabs->content(
+ 'summary',
+ '<pre>' . $this->outputFormatter->encodeAsJson( $this->buildSummary( $mappings ) ) . '</pre>'
+ );
+
+ $htmlTabs->tab( 'fields', $this->msg( 'smw-admin-supplementary-elastic-mappings-fields' ) );
+
+ $htmlTabs->content(
+ 'fields',
+ '<pre>' . $this->outputFormatter->encodeAsJson( $mappings ) . '</pre>'
+ );
+
+ $html = $htmlTabs->buildHTML( [ 'class' => 'es-mapping' ] );
+
+ $this->outputFormatter->addHtml(
+ $html
+ );
+
+ $this->outputFormatter->addInlineStyle(
+ '.es-mapping #tab-summary:checked ~ #tab-content-summary,' .
+ '.es-mapping #tab-fields:checked ~ #tab-content-fields {' .
+ 'display: block;}'
+ );
+ }
+
+ private function buildSummary( $mappings ) {
+
+ $count = [
+ ElasticClient::TYPE_DATA => [
+ 'fields' => [
+ 'property_fields' => 0,
+ 'nested_fields' => 0
+ ],
+ 'total' => 0
+ ],
+ ElasticClient::TYPE_LOOKUP => [
+ 'fields' => [
+ 'property_fields' => 0,
+ 'nested_fields' => 0
+ ],
+ 'total' => 0
+ ]
+ ];
+
+ foreach ( $mappings as $inx ) {
+ foreach ( $inx as $key => $value ) {
+
+ if ( isset( $value['mappings'][ElasticClient::TYPE_DATA] ) ) {
+ foreach ( $value['mappings'][ElasticClient::TYPE_DATA]['properties'] as $k => $val ) {
+ foreach ( $val as $p => $v ) {
+ if ( $p === 'properties' ) {
+ foreach ( $v as $field => $mappings ) {
+ if ( is_string( $field ) ) {
+ $count[ElasticClient::TYPE_DATA]['fields']['property_fields']++;
+ }
+
+ if ( isset( $mappings['fields'] ) ) {
+ $count[ElasticClient::TYPE_DATA]['fields']['nested_fields'] += count( $mappings['fields'] );
+ }
+ }
+ } elseif ( $p === 'type' ) {
+ $count[ElasticClient::TYPE_DATA]['fields']['property_fields']++;
+ } elseif ( $p === 'fields' ) {
+ $count[ElasticClient::TYPE_DATA]['fields']['nested_fields'] += count( $v );
+ }
+ }
+ }
+
+ $count[ElasticClient::TYPE_DATA]['total'] = $count[ElasticClient::TYPE_DATA]['fields']['property_fields'] +
+ $count[ElasticClient::TYPE_DATA]['fields']['nested_fields'];
+ }
+
+ if ( isset( $value['mappings'][ElasticClient::TYPE_LOOKUP] ) ) {
+ foreach ( $value['mappings'][ElasticClient::TYPE_LOOKUP]['properties'] as $k => $val ) {
+ foreach ( $val as $p => $v ) {
+
+ if ( $p === 'properties' ) {
+ foreach ( $v as $field => $mappings ) {
+ if ( is_string( $field ) ) {
+ $count[ElasticClient::TYPE_LOOKUP]['fields']['property_fields']++;
+ }
+
+ if ( isset( $mappings['fields'] ) ) {
+ $count[ElasticClient::TYPE_LOOKUP]['fields']['nested_fields'] += count( $mappings['fields'] );
+ }
+ }
+ } elseif ( $p === 'type' ) {
+ $count[ElasticClient::TYPE_LOOKUP]['fields']['property_fields']++;
+ }
+ }
+ }
+
+ $count[ElasticClient::TYPE_LOOKUP]['total'] = $count[ElasticClient::TYPE_LOOKUP]['fields']['property_fields'] +
+ $count[ElasticClient::TYPE_LOOKUP]['fields']['nested_fields'];
+ }
+ }
+ }
+
+ return $count;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/NodesInfoProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/NodesInfoProvider.php
new file mode 100644
index 00000000..ac3f6007
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/NodesInfoProvider.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace SMW\Elastic\Admin;
+
+use Html;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class NodesInfoProvider extends InfoProviderHandler {
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getSupplementTask() {
+ return 'nodes';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-elastic-nodes-title' ),
+ [ 'action' => $this->getTask() ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-elastic-nodes-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $this->outputFormatter->setPageTitle( 'Elasticsearch nodes' );
+
+ $this->outputFormatter->addParentLink(
+ [ 'action' => $this->getParentTask() ],
+ 'smw-admin-supplementary-elastic-title'
+ );
+
+ $this->outputInfo();
+ }
+
+ private function outputInfo() {
+
+ $connection = $this->getStore()->getConnection( 'elastic' );
+
+ $nodes = $connection->stats( 'nodes' );
+ ksort( $nodes );
+
+ $this->outputFormatter->addAsPreformattedText(
+ $this->outputFormatter->encodeAsJson( $nodes )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/SettingsInfoProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/SettingsInfoProvider.php
new file mode 100644
index 00000000..6a7cd76e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Admin/SettingsInfoProvider.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace SMW\Elastic\Admin;
+
+use Html;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SettingsInfoProvider extends InfoProviderHandler {
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSupplementTask() {
+ return 'settings';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-elastic-settings-title' ),
+ [ 'action' => $this->getTask() ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-elastic-settings-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $this->outputFormatter->setPageTitle( 'Elasticsearch settings' );
+
+ $this->outputFormatter->addParentLink(
+ [ 'action' => $this->getParentTask() ],
+ 'smw-admin-supplementary-elastic-title'
+ );
+
+ $this->outputInfo();
+ }
+
+ private function outputInfo() {
+
+ $connection = $this->getStore()->getConnection( 'elastic' );
+
+ $settings = [
+ $connection->getSettings(
+ [
+ 'index' => $connection->getIndexNameByType( ElasticClient::TYPE_DATA )
+ ]
+ ),
+ $connection->getSettings(
+ [
+ 'index' => $connection->getIndexNameByType( ElasticClient::TYPE_LOOKUP )
+ ]
+ )
+ ];
+
+ $this->outputFormatter->addAsPreformattedText(
+ $this->outputFormatter->encodeAsJson( $settings )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Config.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Config.php
new file mode 100644
index 00000000..7f426c76
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Config.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace SMW\Elastic;
+
+use SMW\Options;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Config extends Options {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $data
+ */
+ public function loadFromJSON( $data ) {
+
+ if ( $data === false ) {
+ return;
+ }
+
+ $data = json_decode( $data, true );
+ $merge = true;
+
+ if ( ( $error = json_last_error() ) !== JSON_ERROR_NONE ) {
+ throw new RuntimeException( 'JSON returned with a "' . json_last_error_msg() . '"' );
+ }
+
+ foreach ( $data as $key => $value ) {
+
+ if ( $merge && isset( $this->options[$key] ) && is_array( $value ) && is_array( $this->options[$key] ) ) {
+ $value = array_merge( $this->options[$key], $value );
+ }
+
+ $this->options[$key] = $value;
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ *
+ * @return string|false
+ * @throws RuntimeException
+ */
+ public function readFile( $file ) {
+
+ if ( $file === false ) {
+ return false;
+ }
+
+ $file = str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, realpath( $file ) );
+
+ if ( is_readable( $file ) ) {
+ return file_get_contents( $file );
+ }
+
+ throw new RuntimeException( "$file is inaccessible!" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/Client.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/Client.php
new file mode 100644
index 00000000..a252c2cb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/Client.php
@@ -0,0 +1,889 @@
+<?php
+
+namespace SMW\Elastic\Connection;
+
+use Elasticsearch\Client as ElasticClient;
+use Elasticsearch\Common\Exceptions\NoNodesAvailableException;
+use Exception;
+use Onoi\Cache\Cache;
+use Onoi\Cache\NullCache;
+use Psr\Log\LoggerAwareTrait;
+use Psr\Log\NullLogger;
+use SMW\Elastic\Exception\InvalidJSONException;
+use SMW\Elastic\Exception\ReplicationException;
+use SMW\Options;
+
+/**
+ * Reduced interface to the Elasticsearch client class.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Client {
+
+ use LoggerAwareTrait;
+
+ /**
+ * Identifies the cache namespace
+ */
+ const CACHE_NAMESPACE = 'smw:elastic';
+
+ const CACHE_CHECK_TTL = 3600;
+
+ /**
+ * @see https://www.elastic.co/blog/index-vs-type
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/master/removal-of-types.html
+ *
+ * " ... Indices created in Elasticsearch 6.0.0 or later may only contain a
+ * single mapping type ..."
+ */
+ const TYPE_DATA = 'data';
+
+ /**
+ * Index, type to temporary store index lookups during the execution
+ * of subqueries.
+ */
+ const TYPE_LOOKUP = 'lookup';
+
+ /**
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * @var boolean
+ */
+ private static $ping;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var boolean
+ */
+ private $inTest = false;
+
+ /**
+ * @var boolean
+ */
+ private static $hasIndex = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param ElasticClient $client
+ * @param Cache|null $cache
+ * @param Options|null $options
+ */
+ public function __construct( ElasticClient $client, Cache $cache = null, Options $options = null ) {
+ $this->client = $client;
+ $this->cache = $cache;
+ $this->options = $options;
+ $this->inTest = defined( 'MW_PHPUNIT_TEST' );
+
+ if ( $this->cache === null ) {
+ $this->cache = new NullCache();
+ }
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ $this->logger = new NullLogger();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Options
+ */
+ public function getConfig() {
+ return $this->options;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function clear() {
+ self::$ping = null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public function getIndexNameByType( $type ) {
+ return $this->getIndexName( $type );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public function getIndexName( $type ) {
+ static $indices = [];
+
+ if ( !isset( $indices[$type] ) ) {
+ $indices[$type] = "smw-$type-" . wfWikiID();
+ }
+
+ return $indices[$type];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public function getIndexDefByType( $type ) {
+ static $indexDef = [];
+
+ if ( isset( $indexDef[$type] ) ) {
+ return $indexDef[$type];
+ }
+
+ $indexDef[$type] = file_get_contents( $this->options->dotGet( "index_def.$type" ) );
+
+ // Modify settings on-the-fly
+ if ( $this->options->dotGet( "settings.$type", [] ) !== [] ) {
+ $definition = json_decode( $indexDef[$type], true );
+
+ if ( ( $error = json_last_error() ) !== JSON_ERROR_NONE ) {
+ throw new InvalidJSONException( $error, $this->options->dotGet( "index_def.$type" ) );
+ }
+
+ $definition['settings'] = $this->options->dotGet( "settings.$type" ) + $definition['settings'];
+ $indexDef[$type] = json_encode( $definition );
+ }
+
+ return $indexDef[$type];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return integer
+ */
+ public function getIndexDefFileModificationTimeByType( $type ) {
+
+ static $filemtime = [];
+
+ if ( !isset( $filemtime[$type] ) ) {
+ $filemtime[$type] = filemtime( $this->options->dotGet( "index_def.$type" ) );
+ }
+
+ return $filemtime[$type];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return integer
+ */
+ public function getVersion() {
+
+ $info = $this->info();
+
+ if ( $this->options->safeGet( 'elastic.enabled' ) && isset( $info['version']['number'] ) ) {
+ return $info['version']['number'];
+ }
+
+ return null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getSoftwareInfo() {
+ return [
+ 'component' => "[https://www.elastic.co/products/elasticsearch Elasticsearch]",
+ 'version' => $this->getVersion()
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array
+ */
+ public function info() {
+
+ if ( !$this->ping() ) {
+ return [];
+ }
+
+ try {
+ $info = $this->client->info( [] );
+ } catch( NoNodesAvailableException $e ) {
+ $info = [];
+ }
+
+ return $info;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array
+ */
+ public function stats( $type = 'indices', $params = [] ) {
+
+ $indices = [
+ $this->getIndexNameByType( self::TYPE_DATA ),
+ $this->getIndexNameByType( self::TYPE_LOOKUP )
+ ];
+
+ switch ( $type ) {
+ case 'indices':
+ $res = $this->client->indices()->stats( [ 'index' => $indices ] + $params );
+ break;
+ case 'nodes':
+ $res = $this->client->nodes()->stats( $params );
+ break;
+ default:
+ $res = [];
+ }
+
+ if ( $type === 'indices' && isset( $res['indices'] ) ) {
+ unset( $res['_all'] );
+ ksort( $res['indices'] );
+ }
+
+ if ( $type === 'nodes' && isset( $res['nodes'] ) ) {
+ foreach ( $res['nodes'] as $key => &$value ) {
+ // Remove privacy info
+ unset( $value['transport_address'] );
+ unset( $value['host'] );
+ unset( $value['ip'] );
+ }
+ }
+
+ return $res;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array
+ */
+ public function cat( $type, $params = [] ) {
+
+ $res = [];
+
+ if ( $type === 'indices' ) {
+ $indices = $this->client->cat()->indices( $params );
+
+ foreach ( $indices as $key => $value ) {
+ $res[$value['index']] = $indices[$key];
+ unset( $res[$value['index']]['index'] );
+ }
+ }
+
+ return $res;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Indices
+ */
+ public function indices() {
+ return $this->client->indices();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Ingest
+ */
+ public function ingest() {
+ return $this->client->ingest();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @param boolean
+ */
+ public function hasIndex( $type ) {
+
+ if ( isset( self::$hasIndex[$type] ) && self::$hasIndex[$type] ) {
+ return true;
+ }
+
+ $index = $this->getIndexNameByType( $type );
+
+ $ret = $this->client->indices()->exists(
+ [
+ 'index' => $index
+ ]
+ );
+
+ return self::$hasIndex[$type] = $ret;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ */
+ public function createIndex( $type ) {
+
+ $index = $this->getIndexNameByType( $type );
+ $indices = $this->client->indices();
+
+ $version = 'v1';
+
+ if ( $indices->exists( [ 'index' => "$index-$version" ] ) ) {
+ $version = 'v2';
+
+ if ( $indices->exists( [ 'index' => "$index-$version" ] ) ) {
+ $indices->delete( [ 'index' => "$index-$version" ] );
+ }
+ }
+
+ $params = [
+ 'index' => "$index-$version",
+ 'body' => $this->getIndexDefByType( $type )
+ ];
+
+ $response = $indices->create( $params );
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'user',
+ 'index' => $index,
+ 'reponse' => json_encode( $response )
+ ];
+
+ $this->logger->info( 'Created index {index} with: {reponse}', $context );
+
+ return $version;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ */
+ public function deleteIndex( $type ) {
+
+ $index = $this->getIndexNameByType( $type );
+
+ $params = [
+ 'index' => $index,
+ ];
+
+ try {
+ $response = $this->client->indices()->delete( $params );
+ } catch ( Exception $e ) {
+ $response = $e->getMessage();
+ }
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [
+ $index,
+ // A modified file causes a new cache key!
+ $this->getIndexDefFileModificationTimeByType( $type )
+ ]
+ );
+
+ $this->cache->delete( $key );
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'user',
+ 'index' => $index,
+ 'reponse' => json_encode( $response )
+ ];
+
+ $this->logger->info( 'Deleted index {index} with: {reponse}', $context );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function putSettings( array $params ) {
+ $this->client->indices()->putSettings( $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function putMapping( array $params ) {
+ $this->client->indices()->putMapping( $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function getMapping( array $params ) {
+ return $this->client->indices()->getMapping( $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function getSettings( array $params ) {
+ return $this->client->indices()->getSettings( $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function refresh( array $params ) {
+ $this->client->indices()->refresh( [ 'index' => $params['index'] ] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function validate( array $params ) {
+
+ if ( $params === [] ) {
+ return [];
+ }
+
+ $results = [];
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'index' => $params['index']
+ ];
+
+ unset( $params['body']['sort'] );
+ unset( $params['body']['_source'] );
+ unset( $params['body']['profile'] );
+ unset( $params['body']['from'] );
+ unset( $params['body']['size'] );
+
+ try {
+ $results = $this->client->indices()->validateQuery( $params );
+ } catch ( Exception $e ) {
+ $context['exception'] = $e->getMessage();
+ $this->logger->info( 'Failed the validate with: {exception}', $context );
+ }
+
+ return $results;
+ }
+
+ /**
+ * @see Client::ping
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function ping() {
+
+ if ( self::$ping !== null ) {
+ return self::$ping;
+ }
+
+ if ( $this->options->dotGet( 'connection.quick_ping' ) ) {
+ return self::$ping = $this->quick_ping();
+ }
+
+ return self::$ping = $this->client->ping( [] );
+ }
+
+ /**
+ * Check is faster than the standard Client::ping
+ *
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function quick_ping( $timeout = 2 ) {
+
+ $hosts = $this->options->get( 'endpoints' );
+
+ foreach ( $hosts as $host ) {
+
+ if ( is_string( $host ) ) {
+ $host = parse_url( $host );
+ }
+
+ $fsock = @fsockopen( $host['host'], $host['port'], $errno, $errstr, $timeout );
+
+ if ( $fsock ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @see Client::exists
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return boolean
+ */
+ public function exists( array $params ) {
+ return $this->client->exists( $params );
+ }
+
+ /**
+ * @see Client::get
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return mixed
+ */
+ public function get( array $params ) {
+ return $this->client->get( $params );
+ }
+
+ /**
+ * @see Client::delete
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return mixed
+ */
+ public function delete( array $params ) {
+ return $this->client->delete( $params );
+ }
+
+ /**
+ * @see Client::update
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return mixed
+ */
+ public function update( array $params ) {
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'index' => $params['index'],
+ 'id' => $params['id'],
+ 'response' => ''
+ ];
+
+ try {
+ $context['response'] = $this->client->update( $params );
+ } catch( Exception $e ) {
+ $context['exception'] = $e->getMessage();
+ $this->logger->info( 'Updated failed for document {id} with: {exception}, DOC: {doc}', $context );
+ }
+
+ return json_encode( $context['response'] );
+ }
+
+ /**
+ * @see Client::index
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return mixed
+ */
+ public function index( array $params ) {
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'index' => $params['index'],
+ 'id' => $params['id'],
+ 'response' => ''
+ ];
+
+ try {
+ $context['response'] = $this->client->index( $params );
+ } catch( Exception $e ) {
+ $context['exception'] = $e->getMessage();
+ $this->logger->info( 'Index failed for document {id} with: {exception}', $context );
+ }
+
+ return json_encode( $context['response'] );
+ }
+
+ /**
+ * @see Client::index
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function bulk( array $params ) {
+
+ if ( $params === [] ) {
+ return;
+ }
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'response' => ''
+ ];
+
+ if ( $this->inTest ) {
+ $params = $params + [ 'refresh' => true ];
+ }
+
+ try {
+ $response = $this->client->bulk( $params );
+
+ // No errors, just log the head otherwise show the entire
+ // response
+ if ( $response['errors'] === false ) {
+ unset( $response['items'] );
+ } else {
+
+ $throw = $this->options->dotGet(
+ 'replication.throw.exception.on.illegal.argument.error'
+ );
+
+ foreach ( $response['items'] as $value ) {
+
+ if ( !isset( $value['index'] ) ) {
+ continue;
+ }
+
+ if ( $throw && $value['index']['error']['type'] === 'illegal_argument_exception' ) {
+ throw new ReplicationException( $value['index']['error']['reason'] );
+ }
+ }
+ }
+
+ $context['response'] = $response;
+ } catch( ReplicationException $e ) {
+ throw new ReplicationException( $e->getMessage() );
+ } catch( Exception $e ) {
+ $this->logger->info( 'Bulk update failed with' . $e->getMessage(), $context );
+ }
+
+ return json_encode( $context['response'] );
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-count.html
+ * @see Client::count
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return mixed
+ */
+ public function count( array $params ) {
+
+ if ( $params === [] ) {
+ return [];
+ }
+
+ $results = [];
+ $time = -microtime( true );
+
+ // https://discuss.elastic.co/t/es-5-2-refresh-interval-doesnt-work-if-set-to-0/79248/2
+ // Make sure the replication/index lag doesn't hinder the search
+ if ( $this->inTest ) {
+ $this->client->indices()->refresh( [ 'index' => $params['index'] ] );
+ }
+
+ // ... "_source", "from", "profile", "query", "size", "sort" are not valid parameters.
+ unset( $params['body']['sort'] );
+ unset( $params['body']['_source'] );
+ unset( $params['body']['profile'] );
+ unset( $params['body']['from'] );
+ unset( $params['body']['size'] );
+
+ try {
+ $results = $this->client->count( $params );
+ } catch ( Exception $e ) {
+ $context['exception'] = $e->getMessage();
+ $this->logger->info( 'Failed the count with: {exception}', $context );
+ }
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'index' => $params['index'],
+ 'query' => json_encode( $params ),
+ 'procTime' => microtime( true ) + $time
+ ];
+
+ $this->logger->info( 'COUNT: {query}, queryTime: {procTime}', $context );
+
+ return $results;
+ }
+
+ /**
+ * @see Client::search
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return array
+ */
+ public function search( array $params ) {
+
+ if ( $params === [] ) {
+ return [];
+ }
+
+ $results = [];
+ $errors = [];
+
+ $time = -microtime( true );
+
+ // https://discuss.elastic.co/t/es-5-2-refresh-interval-doesnt-work-if-set-to-0/79248/2
+ // Make sure the replication/index lag doesn't hinder the search
+ if ( $this->inTest ) {
+ $this->client->indices()->refresh( [ 'index' => $params['index'] ] );
+ }
+
+ try {
+ $results = $this->client->search( $params );
+ } catch ( NoNodesAvailableException $e ) {
+ $errors[] = 'Elasticsearch endpoint returned with "' . $e->getMessage() . '" .';
+ } catch ( Exception $e ) {
+ $context['exception'] = $e->getMessage();
+ $this->logger->info( 'Failed the search with: {exception}', $context );
+ }
+
+ $this->logger->info(
+ [
+ 'Search',
+ '{query}, queryTime: {procTime}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'user',
+ 'index' => $params['index'],
+ 'query' => $params,
+ 'procTime' => microtime( true ) + $time
+ ]
+ );
+
+ return [ $results, $errors ];
+ }
+
+ /**
+ * @see Client::explain
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return mixed
+ */
+ public function explain( array $params ) {
+
+ if ( $params === [] ) {
+ return [];
+ }
+
+ // https://discuss.elastic.co/t/es-5-2-refresh-interval-doesnt-work-if-set-to-0/79248/2
+ // Make sure the replication/index lag doesn't hinder the search
+ if ( $this->inTest ) {
+ $this->client->indices()->refresh( [ 'index' => $params['index'] ] );
+ }
+
+ return $this->client->explain( $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ * @param string $version
+ */
+ public function setLock( $type, $version ) {
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [ 'lock', $type ]
+ );
+
+ $this->cache->save( $key, $version );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return boolean
+ */
+ public function hasLock( $type ) {
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [ 'lock', $type ]
+ );
+
+ return $this->cache->fetch( $key ) !== false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return mixed
+ */
+ public function getLock( $type ) {
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [ 'lock', $type ]
+ );
+
+ return $this->cache->fetch( $key );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ */
+ public function releaseLock( $type ) {
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [ 'lock', $type ]
+ );
+
+ $this->cache->delete( $key );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/ConnectionProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/ConnectionProvider.php
new file mode 100644
index 00000000..532f34a7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/ConnectionProvider.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace SMW\Elastic\Connection;
+
+use Elasticsearch\ClientBuilder;
+use SMW\Elastic\Exception\ClientBuilderNotFoundException;
+use SMW\ApplicationFactory;
+use SMW\Connection\ConnectionProvider as IConnectionProvider;
+use SMW\Options;
+use Psr\Log\LoggerAwareTrait;
+use Onoi\Cache\Cache;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ConnectionProvider implements IConnectionProvider {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var ElasticClient
+ */
+ private $connection;
+
+ /**
+ * @since 3.0
+ *
+ * @param Options $options
+ * @param Cache $cache
+ */
+ public function __construct( Options $options, Cache $cache ) {
+ $this->options = $options;
+ $this->cache = $cache;
+ }
+
+ /**
+ * @see ConnectionProvider::getConnection
+ *
+ * @since 3.0
+ *
+ * @return Connection
+ */
+ public function getConnection() {
+
+ if ( $this->connection !== null ) {
+ return $this->connection;
+ }
+
+ $params = [
+ 'hosts' => $this->options->get( 'endpoints' ),
+ 'retries' => $this->options->dotGet( 'connection.retries', 1 ),
+
+ 'client' => [
+
+ // controls the request timeout
+ 'timeout' => $this->options->dotGet( 'connection.timeout', 30 ),
+
+ // controls the original connection timeout duration
+ 'connect_timeout' => $this->options->dotGet( 'connection.connect_timeout', 30 )
+ ]
+
+ // Use `singleHandler` if you know you will never need async capabilities,
+ // since it will save a small amount of overhead by reducing indirection
+ // 'handler' => ClientBuilder::singleHandler()
+ ];
+
+ if ( $this->hasClientBuilder() ) {
+ $this->connection = new Client(
+ ClientBuilder::fromConfig( $params, true ),
+ $this->cache,
+ $this->options
+ );
+ } else {
+ $this->connection = new DummyClient();
+ }
+
+ $this->connection->setLogger(
+ $this->logger
+ );
+
+ $this->logger->info(
+ [
+ 'Connection',
+ '{provider} : {hosts}'
+ ],
+ [
+ 'role' => 'developer',
+ 'provider' => 'elastic',
+ 'hosts' => $params['hosts']
+ ]
+ );
+
+ return $this->connection;
+ }
+
+ /**
+ * @see ConnectionProvider::releaseConnection
+ *
+ * @since 3.0
+ */
+ public function releaseConnection() {
+ $this->connection = null;
+ }
+
+ private function hasClientBuilder() {
+
+ if ( $this->options->dotGet( 'is.elasticstore', false ) === false ) {
+ return false;
+ }
+
+ // Fail hard because someone selected the ElasticStore but forgot to install
+ // the elastic interface!
+ if ( !class_exists( '\Elasticsearch\ClientBuilder' ) ) {
+ throw new ClientBuilderNotFoundException();
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/DummyClient.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/DummyClient.php
new file mode 100644
index 00000000..2fc7d797
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Connection/DummyClient.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace SMW\Elastic\Connection;
+
+use Onoi\Cache\Cache;
+use Onoi\Cache\NullCache;
+use Psr\Log\NullLogger;
+use RuntimeException;
+use SMW\Options;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DummyClient extends Client {
+
+ /**
+ * @var Client
+ */
+ private $client;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @since 3.0
+ *
+ * @param ElasticClient $client
+ * @param Cache|null $cache
+ * @param Options|null $options
+ */
+ public function __construct( $client = null, Cache $cache = null, Options $options = null ) {
+ $this->client = $client;
+ $this->cache = $cache;
+ $this->options = $options;
+
+ if ( $this->cache === null ) {
+ $this->cache = new NullCache();
+ }
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ $this->logger = new NullLogger();
+ }
+
+ /**
+ * @see Client::getConfig
+ */
+ public function getConfig() {
+ return $this->options;
+ }
+
+ /**
+ * @see Client::getIndexName
+ */
+ public function getIndexName( $type ) {
+ return '';
+ }
+
+ /**
+ * @see Client::getIndexDefByType
+ */
+ public function getIndexDefByType( $type ) {
+ return '';
+ }
+
+ /**
+ * @see Client::getIndexDefFileModificationTimeByType
+ */
+ public function getIndexDefFileModificationTimeByType( $type ) {
+ return 0;
+ }
+
+ /**
+ * @see Client::getSoftwareInfo
+ */
+ public function getSoftwareInfo() {
+ return [
+ 'component' => "[https://www.elastic.co/products/elasticsearch Elasticsearch]",
+ 'version' => null
+ ];
+ }
+
+ /**
+ * @see Client::info
+ */
+ public function info() {
+ return [];
+ }
+
+ /**
+ * @see Client::stats
+ */
+ public function stats( $type = 'indices', $params = [] ) {
+ return [];
+ }
+
+ /**
+ * @see Client::cat
+ */
+ public function cat( $type, $params = [] ) {
+ return [];
+ }
+
+ /**
+ * @see Client::hasIndex
+ */
+ public function hasIndex( $type, $useCache = true ) {
+ return true;
+ }
+
+ /**
+ * @see Client::createIndex
+ */
+ public function createIndex( $type ) {}
+
+ /**
+ * @see Client::deleteIndex
+ */
+ public function deleteIndex( $type ) {}
+
+ /**
+ * @see Client::putSettings
+ */
+ public function putSettings( array $params ) {}
+
+ /**
+ * @see Client::putMapping
+ */
+ public function putMapping( array $params ) {}
+
+ /**
+ * @see Client::getMapping
+ */
+ public function getMapping( array $params ) {
+ return [];
+ }
+
+ /**
+ * @see Client::getSettings
+ */
+ public function getSettings( array $params ) {
+ return [];
+ }
+
+ /**
+ * @see Client::refresh
+ */
+ public function refresh( array $params ) {}
+
+ /**
+ * @see Client::validate
+ */
+ public function validate( array $params ) {
+ return [];
+ }
+
+ /**
+ * @see Client::ping
+ */
+ public function ping() {
+ return false;
+ }
+
+ /**
+ * @see Client::quick_ping
+ */
+ public function quick_ping( $timeout = 2 ) {
+ return false;
+ }
+
+ /**
+ * @see Client::exists
+ */
+ public function exists( array $params ) {
+ return false;
+ }
+
+ /**
+ * @see Client::get
+ */
+ public function get( array $params ) {
+ return [];
+ }
+
+ /**
+ * @see Client::delete
+ */
+ public function delete( array $params ) {
+ return [];
+ }
+
+ /**
+ * @see Client::update
+ */
+ public function update( array $params ) {}
+
+ /**
+ * @see Client::index
+ */
+ public function index( array $params ) {}
+
+ /**
+ * @see Client::bulk
+ */
+ public function bulk( array $params ) {}
+
+ /**
+ * @see Client::count
+ */
+ public function count( array $params ) {
+ return 0;
+ }
+
+ /**
+ * @see Client::search
+ */
+ public function search( array $params ) {
+ return [ [], [] ];
+ }
+
+ /**
+ * @see Client::explain
+ */
+ public function explain( array $params ) {
+ return [];
+ }
+
+ /**
+ * @see Client::setLock
+ */
+ public function setLock( $type, $version ) {}
+
+ /**
+ * @see Client::hasLock
+ */
+ public function hasLock( $type ) {
+ return false;
+ }
+
+ /**
+ * @see Client::getLock
+ */
+ public function getLock( $type ) {
+ return false;
+ }
+
+ /**
+ * @see Client::getLock
+ */
+ public function releaseLock( $type ) {}
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/ElasticFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/ElasticFactory.php
new file mode 100644
index 00000000..d5a79c38
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/ElasticFactory.php
@@ -0,0 +1,426 @@
+<?php
+
+namespace SMW\Elastic;
+
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\NullMessageReporter;
+use Psr\Log\LoggerInterface;
+use SMW\ApplicationFactory;
+use SMW\Elastic\Admin\ElasticClientTaskHandler;
+use SMW\Elastic\Admin\IndicesInfoProvider;
+use SMW\Elastic\Admin\MappingsInfoProvider;
+use SMW\Elastic\Admin\NodesInfoProvider;
+use SMW\Elastic\Admin\SettingsInfoProvider;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\Elastic\Connection\DummyClient;
+use SMW\Elastic\Indexer\Indexer;
+use SMW\Elastic\Indexer\FileIndexer;
+use SMW\Elastic\Indexer\Rollover;
+use SMW\Elastic\Indexer\Rebuilder;
+use SMW\Elastic\Indexer\Bulk;
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Elastic\QueryEngine\QueryEngine;
+use SMW\Elastic\QueryEngine\TermsLookup\CachingTermsLookup;
+use SMW\Elastic\QueryEngine\TermsLookup\TermsLookup;
+use SMW\Options;
+use SMW\SQLStore\PropertyTableRowMapper;
+use SMW\Store;
+use SMW\Elastic\Connection\ConnectionProvider;
+use SMW\Services\ServicesContainer;
+use SMW\Elastic\QueryEngine\DescriptionInterpreters\ClassDescriptionInterpreter;
+use SMW\Elastic\QueryEngine\DescriptionInterpreters\ConceptDescriptionInterpreter;
+use SMW\Elastic\QueryEngine\DescriptionInterpreters\ConjunctionInterpreter;
+use SMW\Elastic\QueryEngine\DescriptionInterpreters\DisjunctionInterpreter;
+use SMW\Elastic\QueryEngine\DescriptionInterpreters\NamespaceDescriptionInterpreter;
+use SMW\Elastic\QueryEngine\DescriptionInterpreters\SomePropertyInterpreter;
+use SMW\Elastic\QueryEngine\DescriptionInterpreters\ValueDescriptionInterpreter;
+use SMW\Elastic\QueryEngine\DescriptionInterpreters\SomeValueInterpreter;
+use SMW\Elastic\Lookup\ProximityPropertyValueLookup;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ElasticFactory {
+
+ /**
+ * @var Indexer
+ */
+ private $indexer;
+
+ /**
+ * @since 3.0
+ *
+ * @return Config
+ */
+ public function newConfig() {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $config = new Config(
+ $settings->get( 'smwgElasticsearchConfig' )
+ );
+
+ $isElasticstore = strpos( $settings->get( 'smwgDefaultStore' ), 'Elastic' ) !== false;
+
+ $config->set(
+ 'elastic.enabled',
+ $isElasticstore
+ );
+
+ $config->set(
+ 'is.elasticstore',
+ $isElasticstore
+ );
+
+ $config->set(
+ 'endpoints',
+ $settings->get( 'smwgElasticsearchEndpoints' )
+ );
+
+ $config->loadFromJSON(
+ $config->readFile( $settings->get( 'smwgElasticsearchProfile' ) )
+ );
+
+ return $config;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return ConnectionProvider
+ */
+ public function newConnectionProvider() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $connectionProvider = new ConnectionProvider(
+ $this->newConfig(),
+ $applicationFactory->getCache()
+ );
+
+ $connectionProvider->setLogger(
+ $applicationFactory->getMediaWikiLogger( 'smw-elastic' )
+ );
+
+ return $connectionProvider;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ *
+ * @return ProximityPropertyValueLookup
+ */
+ public function newProximityPropertyValueLookup( Store $store ) {
+ return new ProximityPropertyValueLookup( $store );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param MessageReporter|null $messageReporter
+ *
+ * @return Indexer
+ */
+ public function newIndexer( Store $store, MessageReporter $messageReporter = null ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ // Construction is postponed to the point where it is needed
+ $servicesContainer = new ServicesContainer(
+ [
+ 'Rollover' => [
+ '_service' => [ $this, 'newRollover' ],
+ '_type' => Rollover::class
+ ],
+ 'FileIndexer' => [ $this, 'newFileIndexer' ],
+ 'Bulk' => [ $this, 'newBulk' ],
+ ]
+ );
+
+ $indexer = new Indexer(
+ $store,
+ $servicesContainer
+ );
+
+ if ( $messageReporter === null ) {
+ $messageReporter = new NullMessageReporter();
+ }
+
+ $indexer->setLogger(
+ $applicationFactory->getMediaWikiLogger( 'smw-elastic' )
+ );
+
+ $indexer->setMessageReporter(
+ $messageReporter
+ );
+
+ return $indexer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ElasticClient $connection
+ *
+ * @return Rollover
+ */
+ public function newRollover( ElasticClient $connection ) {
+ return new Rollover( $connection );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ElasticClient $connection
+ *
+ * @return Bulk
+ */
+ public function newBulk( ElasticClient $connection ) {
+ return new Bulk( $connection );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Indexer $indexer
+ *
+ * @return FileIndexer
+ */
+ public function newFileIndexer( Indexer $indexer ) {
+ return new FileIndexer( $indexer );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ *
+ * @return QueryEngine
+ */
+ public function newQueryEngine( Store $store ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $options = $this->newConfig();
+
+ $queryOptions = new Options(
+ $options->safeGet( 'query', [] )
+ );
+
+ $termsLookup = new CachingTermsLookup(
+ new TermsLookup( $store, $queryOptions ),
+ $applicationFactory->getCache()
+ );
+
+ $servicesContainer = new ServicesContainer(
+ [
+ 'ConceptDescriptionInterpreter' => [ $this, 'newConceptDescriptionInterpreter' ],
+ 'SomePropertyInterpreter' => [ $this, 'newSomePropertyInterpreter' ],
+ 'ClassDescriptionInterpreter' => [ $this, 'newClassDescriptionInterpreter' ],
+ 'NamespaceDescriptionInterpreter' => [ $this, 'newNamespaceDescriptionInterpreter' ],
+ 'ValueDescriptionInterpreter' => [ $this, 'newValueDescriptionInterpreter' ],
+ 'ConjunctionInterpreter' => [ $this, 'newConjunctionInterpreter' ],
+ 'DisjunctionInterpreter' => [ $this, 'newDisjunctionInterpreter' ],
+ 'SomeValueInterpreter' => [ $this, 'newSomeValueInterpreter' ]
+ ]
+ );
+
+ $conditionBuilder = new ConditionBuilder(
+ $store,
+ $termsLookup,
+ $applicationFactory->newHierarchyLookup(),
+ $servicesContainer
+ );
+
+ $conditionBuilder->setOptions( $queryOptions );
+
+ $queryEngine = new QueryEngine(
+ $store,
+ $conditionBuilder,
+ $options
+ );
+
+ $queryEngine->setLogger(
+ $applicationFactory->getMediaWikiLogger( 'smw-elastic' )
+ );
+
+ return $queryEngine;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ *
+ * @return Rebuilder
+ */
+ public function newRebuilder( Store $store ) {
+
+ $connection = $store->getConnection( 'elastic' );
+
+ $rebuilder = new Rebuilder(
+ $connection,
+ $this->newIndexer( $store ),
+ new PropertyTableRowMapper( $store ),
+ $this->newRollover( $connection )
+ );
+
+ return $rebuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ *
+ * @return ElasticClientTaskHandler
+ */
+ public function newInfoTaskHandler( Store $store, $outputFormatter ) {
+
+ $taskHandlers = [
+ new SettingsInfoProvider( $outputFormatter ),
+ new MappingsInfoProvider( $outputFormatter ),
+ new IndicesInfoProvider( $outputFormatter ),
+ new NodesInfoProvider( $outputFormatter )
+ ];
+
+ return new ElasticClientTaskHandler( $outputFormatter, $taskHandlers );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $containerBuilder
+ *
+ * @return ConceptDescriptionInterpreter
+ */
+ public function newConceptDescriptionInterpreter( ConditionBuilder $containerBuilder ) {
+ return new ConceptDescriptionInterpreter(
+ $containerBuilder,
+ ApplicationFactory::getInstance()->newQueryParser()
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $containerBuilder
+ *
+ * @return SomePropertyInterpreter
+ */
+ public function newSomePropertyInterpreter( ConditionBuilder $containerBuilder ) {
+ return new SomePropertyInterpreter( $containerBuilder );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $containerBuilder
+ *
+ * @return ClassDescriptionInterpreter
+ */
+ public function newClassDescriptionInterpreter( ConditionBuilder $containerBuilder ) {
+ return new ClassDescriptionInterpreter( $containerBuilder );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $containerBuilder
+ *
+ * @return NamespaceDescriptionInterpreter
+ */
+ public function newNamespaceDescriptionInterpreter( ConditionBuilder $containerBuilder ) {
+ return new NamespaceDescriptionInterpreter( $containerBuilder );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $containerBuilder
+ *
+ * @return ValueDescriptionInterpreter
+ */
+ public function newValueDescriptionInterpreter( ConditionBuilder $containerBuilder ) {
+ return new ValueDescriptionInterpreter( $containerBuilder );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $containerBuilder
+ *
+ * @return SomeValueInterpreter
+ */
+ public function newSomeValueInterpreter( ConditionBuilder $containerBuilder ) {
+ return new SomeValueInterpreter( $containerBuilder );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $containerBuilder
+ *
+ * @return ConjunctionInterpreter
+ */
+ public function newConjunctionInterpreter( ConditionBuilder $containerBuilder ) {
+ return new ConjunctionInterpreter( $containerBuilder );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $containerBuilder
+ *
+ * @return DisjunctionInterpreter
+ */
+ public function newDisjunctionInterpreter( ConditionBuilder $containerBuilder ) {
+ return new DisjunctionInterpreter( $containerBuilder );
+ }
+
+ /**
+ * @see https://www.semantic-mediawiki.org/wiki/Hooks#SMW::SQLStore::EntityReferenceCleanUpComplete
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function onEntityReferenceCleanUpComplete( Store $store, $id, $subject, $isRedirect ) {
+
+ if ( !$store instanceof ElasticStore || $store->getConnection( 'elastic' ) instanceof DummyClient ) {
+ return true;
+ }
+
+ if ( $this->indexer === null ) {
+ $this->indexer = $this->newIndexer( $store );
+ }
+
+ $this->indexer->setOrigin( __METHOD__ );
+ $this->indexer->delete( [ $id ] );
+
+ return true;
+ }
+
+ /**
+ * @see https://www.semantic-mediawiki.org/wiki/Hooks#SMW::Admin::TaskHandlerFactory
+ * @since 3.0
+ */
+ public function onTaskHandlerFactory( &$taskHandlers, Store $store, $outputFormatter, $user ) {
+
+ if ( !$store instanceof ElasticStore ) {
+ return true;
+ }
+
+ $taskHandlers[] = $this->newInfoTaskHandler(
+ $store,
+ $outputFormatter
+ );
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/ElasticStore.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/ElasticStore.php
new file mode 100644
index 00000000..9de6fa6b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/ElasticStore.php
@@ -0,0 +1,353 @@
+<?php
+
+namespace SMW\Elastic;
+
+use Hooks;
+use RuntimeException;
+use SMW\DIWikiPage;
+use SMW\SemanticData;
+use SMW\SQLStore\SQLStore;
+use SMWQuery as Query;
+use Title;
+
+/**
+ * @private
+ *
+ * The `ElasticStore` is the interface to an `Elasticsearch` cluster both in
+ * regards for replicating data to a cluster as well as retrieving search results
+ * from it.
+ *
+ * `Elasticsearch` is expected:
+ * - to be used as search (aka query) engine with all other data management tasks
+ * to be carried out using the default `SQLStore`.
+ * - to inherit most of the `SQLStore` methods
+ *
+ * @see https://github.com/SemanticMediaWiki/SemanticMediaWiki/blob/master/src/Elastic/README.md
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ElasticStore extends SQLStore {
+
+ /**
+ * @var ElasticFactory
+ */
+ private $elasticFactory;
+
+ /**
+ * @var Indexer
+ */
+ private $indexer;
+
+ /**
+ * @var QueryEngine
+ */
+ private $queryEngine;
+
+ /**
+ * @since 3.0
+ */
+ public function __construct() {
+ parent::__construct();
+ $this->elasticFactory = new ElasticFactory();
+ }
+
+ /**
+ * @see Store::service
+ *
+ * {@inheritDoc}
+ */
+ public function service( $service, ...$args ) {
+
+ if ( $this->servicesContainer === null ) {
+ $this->servicesContainer = parent::newServicesContainer();
+
+ // Replace an existing (or add) SQLStore service with a ES specific
+ // optimized service
+
+ // $this->servicesContainer->add( 'ProximityPropertyValueLookup', function() {
+ // return $this->elasticFactory->newProximityPropertyValueLookup( $this );
+ // } );
+ }
+
+ return $this->servicesContainer->get( $service, ...$args );
+ }
+
+ /**
+ * @see SQLStore::deleteSubject
+ * @since 3.0
+ *
+ * @param Title $title
+ */
+ public function deleteSubject( Title $title ) {
+ parent::deleteSubject( $title );
+
+ if ( $this->indexer === null ) {
+ $this->indexer = $this->elasticFactory->newIndexer( $this, $this->messageReporter );
+ }
+
+ $this->indexer->setOrigin( 'ElasticStore::DeleteSubject' );
+ $idList = [];
+
+ if ( isset( $this->extensionData['delete.list'] ) ) {
+ $idList = $this->extensionData['delete.list'];
+ }
+
+ $this->indexer->delete( $idList, $title->getNamespace() === SMW_NS_CONCEPT );
+
+ unset( $this->extensionData['delete.list'] );
+ }
+
+ /**
+ * @see SQLStore::changeTitle
+ * @since 3.0
+ *
+ * @param Title $oldtitle
+ * @param Title $newtitle
+ * @param integer $pageid
+ * @param integer $redirid
+ */
+ public function changeTitle( Title $oldTitle, Title $newTitle, $pageId, $redirectId = 0 ) {
+ parent::changeTitle( $oldTitle, $newTitle, $pageId, $redirectId );
+
+ $id = $this->getObjectIds()->getSMWPageID(
+ $oldTitle->getDBkey(),
+ $oldTitle->getNamespace(),
+ '',
+ '',
+ false
+ );
+
+ if ( $this->indexer === null ) {
+ $this->indexer = $this->elasticFactory->newIndexer( $this, $this->messageReporter );
+ }
+
+ $this->indexer->setOrigin( 'ElasticStore::ChangeTitle' );
+ $idList = [ $id ];
+
+ if ( isset( $this->extensionData['delete.list'] ) ) {
+ $idList = array_merge( $idList, $this->extensionData['delete.list'] );
+ }
+
+ $this->indexer->delete( $idList );
+
+ // Use case [[Foo]] redirects to #REDIRECT [[Bar]] with Bar not yet being
+ // materialized and with the update not having created any reference,
+ // fulfill T:Q0604 by allowing to create a minimized document body
+ if ( $newTitle->exists() === false ) {
+ $id = $this->getObjectIds()->getSMWPageID(
+ $newTitle->getDBkey(),
+ $newTitle->getNamespace(),
+ '',
+ '',
+ false
+ );
+
+ $dataItem = DIWikiPage::newFromTitle( $newTitle );
+ $dataItem->setId( $id );
+
+ $this->indexer->create( $dataItem );
+ }
+
+ unset( $this->extensionData['delete.list'] );
+ }
+
+ /**
+ * @see SQLStore::fetchQueryResult
+ * @since 3.0
+ *
+ * @param Query $query
+ *
+ * @return QueryResult
+ */
+ public function getQueryResult( Query $query ) {
+
+ $result = null;
+ $time = -microtime( true );
+
+ $connection = $this->getConnection( 'elastic' );
+
+ if ( $this->queryEngine === null ) {
+ $this->queryEngine = $this->elasticFactory->newQueryEngine( $this );
+ }
+
+ if ( $connection->getConfig()->dotGet( 'query.fallback.no.connection' ) && !$connection->ping() ) {
+ return parent::getQueryResult( $query );
+ }
+
+ $params = [
+ $this,
+ $query,
+ &$result,
+ $this->queryEngine
+ ];
+
+ if ( Hooks::run( 'SMW::Store::BeforeQueryResultLookupComplete', $params ) ) {
+ $result = $this->queryEngine->getQueryResult( $query );
+ }
+
+ $params = [
+ $this,
+ &$result
+ ];
+
+ Hooks::run( 'SMW::ElasticStore::AfterQueryResultLookupComplete', $params );
+ Hooks::run( 'SMW::Store::AfterQueryResultLookupComplete', $params );
+
+ $query->setOption( Query::PROC_QUERY_TIME, microtime( true ) + $time );
+
+ return $result;
+ }
+
+ /**
+ * @see SQLStore::doDataUpdate
+ * @since 3.0
+ *
+ * @param SemanticData $semanticData
+ */
+ protected function doDataUpdate( SemanticData $semanticData ) {
+ parent::doDataUpdate( $semanticData );
+
+ $time = -microtime( true );
+ $config = $this->getConnection( 'elastic' )->getConfig();
+
+ if ( $this->indexer === null ) {
+ $this->indexer = $this->elasticFactory->newIndexer( $this, $this->messageReporter );
+ }
+
+ $this->indexer->setOrigin( 'ElasticStore::DoDataUpdate' );
+ $subject = $semanticData->getSubject();
+
+ if ( isset( $this->extensionData['delete.list'] ) ) {
+ $this->indexer->delete( $this->extensionData['delete.list'] );
+ }
+
+ if ( !isset( $this->extensionData['change.diff'] ) ) {
+ throw new RuntimeException( "Unable to replicate, missing a `change.diff` object!" );
+ }
+
+ $text = '';
+
+ if ( $config->dotGet( 'indexer.raw.text', false ) && ( $revID = $semanticData->getExtensionData( 'revision_id' ) ) !== null ) {
+ $text = $this->indexer->fetchNativeData( $revID );
+ }
+
+ $this->indexer->safeReplicate(
+ $this->extensionData['change.diff'],
+ $text
+ );
+
+ unset( $this->extensionData['delete.list'] );
+ unset( $this->extensionData['change.diff'] );
+
+ $this->logger->info(
+ [
+ 'ElasticStore',
+ 'Data update completed',
+ 'procTime in sec: {procTime}',
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'procTime' => microtime( true ) + $time,
+ ]
+ );
+
+ if ( $subject->getNamespace() === NS_FILE && $config->dotGet( 'indexer.experimental.file.ingest', false ) && $semanticData->getOption( 'is.fileupload' ) ) {
+ $this->indexer->getFileIndexer()->planIngestJob( $subject->getTitle() );
+ }
+ }
+
+ /**
+ * @see SQLStore::setup
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function setup( $verbose = true ) {
+
+ if ( $this->indexer === null ) {
+ $this->indexer = $this->elasticFactory->newIndexer( $this, $this->messageReporter );
+ }
+
+ $this->indexer->setup();
+
+ if ( $verbose ) {
+ $this->messageReporter->reportMessage( "\n" );
+ $this->messageReporter->reportMessage( 'Selected query engine: "SMWElasticStore"' );
+ $this->messageReporter->reportMessage( "\n" );
+ $this->messageReporter->reportMessage( "\nSetting up indices ...\n" );
+ $this->messageReporter->reportMessage( " ... done.\n" );
+ }
+
+ parent::setup( $verbose );
+ }
+
+ /**
+ * @see SQLStore::drop
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function drop( $verbose = true ) {
+
+ if ( $this->indexer === null ) {
+ $this->indexer = $this->elasticFactory->newIndexer( $this, $this->messageReporter );
+ }
+
+ $this->indexer->drop();
+
+ if ( $verbose ) {
+ $this->messageReporter->reportMessage( "\n" );
+ $this->messageReporter->reportMessage( 'Selected query engine: "SMWElasticStore"' );
+ $this->messageReporter->reportMessage( "\n" );
+ $this->messageReporter->reportMessage( "\nDropping indices ...\n" );
+ $this->messageReporter->reportMessage( " ... done.\n" );
+ }
+
+ parent::drop( $verbose );
+ }
+
+ /**
+ * @see SQLStore::clear
+ * @since 3.0
+ */
+ public function clear() {
+ parent::clear();
+ $this->indexer = null;
+ $this->queryEngine = null;
+ }
+
+ /**
+ * @see Store::getInfo
+ * @since 3.0
+ *
+ * @param string|null $type
+ *
+ * @return array
+ */
+ public function getInfo( $type = null ) {
+
+ if ( $type === 'store' ) {
+ return 'SMWElasticStore';
+ }
+
+ $database = $this->getConnection( 'mw.db' );
+ $client = $this->getConnection( 'elastic' );
+
+ if ( $type === 'db' ) {
+ return $database->getInfo();
+ }
+
+ if ( $type === 'es' ) {
+ return $client->getVersion();
+ }
+
+ return [
+ 'SMWElasticStore' => $database->getInfo() + [ 'es' => $client->getVersion() ]
+ ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/ClientBuilderNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/ClientBuilderNotFoundException.php
new file mode 100644
index 00000000..4c1d65da
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/ClientBuilderNotFoundException.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace SMW\Elastic\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ClientBuilderNotFoundException extends RuntimeException {
+
+ /**
+ * @since 3.0
+ */
+ public function __construct() {
+ parent::__construct( "The \Elasticsearch\ClientBuilder class is missing, please see https://www.semantic-mediawiki.org/wiki/Help:ElasticStore/ClientBuilder!" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/InvalidJSONException.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/InvalidJSONException.php
new file mode 100644
index 00000000..3b2db2f5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/InvalidJSONException.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace SMW\Elastic\Exception;
+
+use RuntimeException;
+use SMW\Utils\ErrorCodeFormatter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class InvalidJSONException extends RuntimeException {
+
+ /**
+ * @since 3.0
+ */
+ public function __construct( $error, $content = '' ) {
+ parent::__construct( ErrorCodeFormatter::getMessageFromJsonErrorCode( $error ) . " caused by: $content" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/NoConnectionException.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/NoConnectionException.php
new file mode 100644
index 00000000..a34e34ca
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/NoConnectionException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace SMW\Elastic\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class NoConnectionException extends RuntimeException {
+
+ /**
+ * @since 3.0
+ */
+ public function __construct() {
+ parent::__construct(
+ "Could not establish a connection to Elasticsearch using " . json_encode( $GLOBALS['smwgElasticsearchEndpoints'] )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/ReplicationException.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/ReplicationException.php
new file mode 100644
index 00000000..1b6e3731
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Exception/ReplicationException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Elastic\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ReplicationException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/AttachmentAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/AttachmentAnnotator.php
new file mode 100644
index 00000000..63fb7684
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/AttachmentAnnotator.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use SMW\DataItemFactory;
+use SMW\DIProperty;
+use SMW\PropertyAnnotator;
+use SMWContainerSemanticData as ContainerSemanticData;
+use SMWDIContainer as DIContainer;
+use SMWDITime as DITime;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class AttachmentAnnotator implements PropertyAnnotator {
+
+ /**
+ * @var ContainerSemanticData
+ */
+ private $containerSemanticData;
+
+ /**
+ * @var []
+ */
+ private $doc = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param ContainerSemanticData $containerSemanticData
+ * @param array $doc
+ */
+ public function __construct( ContainerSemanticData $containerSemanticData, array $doc ) {
+ $this->containerSemanticData = $containerSemanticData;
+ $this->doc = $doc;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return DIProperty
+ */
+ public function getProperty() {
+ return new DIProperty( '_FILE_ATTCH' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return DIContainer
+ */
+ public function getContainer() {
+ return new DIContainer( $this->containerSemanticData );
+ }
+
+ /**
+ * @see PropertyAnnotator::getSemanticData
+ * @since 3.0
+ *
+ * @return SemanticData
+ */
+ public function getSemanticData() {
+ return $this->containerSemanticData;
+ }
+
+ /**
+ * @see PropertyAnnotator::addAnnotation
+ * @since 3.0
+ *
+ * @return PropertyAnnotator
+ */
+ public function addAnnotation() {
+
+ $dataItemFactory = new DataItemFactory();
+
+ // @see https://www.elastic.co/guide/en/elasticsearch/plugins/master/using-ingest-attachment.html
+ if ( isset( $this->doc['_source']['attachment']['date'] ) ) {
+ if ( ( $dataItem = DITime::newFromTimestamp( $this->doc['_source']['attachment']['date'] ) ) instanceof DITime ) {
+ $this->containerSemanticData->addPropertyObjectValue(
+ $dataItemFactory->newDIProperty( '_CONT_DATE' ),
+ $dataItem
+ );
+ }
+ }
+
+ if ( isset( $this->doc['_source']['attachment']['content_type'] ) ) {
+ $this->containerSemanticData->addPropertyObjectValue(
+ $dataItemFactory->newDIProperty( '_CONT_TYPE' ),
+ $dataItemFactory->newDIBlob( $this->doc['_source']['attachment']['content_type'] )
+ );
+ }
+
+ if ( isset( $this->doc['_source']['attachment']['author'] ) ) {
+ $this->containerSemanticData->addPropertyObjectValue(
+ $dataItemFactory->newDIProperty( '_CONT_AUTHOR' ),
+ $dataItemFactory->newDIBlob( $this->doc['_source']['attachment']['author'] )
+ );
+ }
+
+ if ( isset( $this->doc['_source']['attachment']['title'] ) ) {
+ $this->containerSemanticData->addPropertyObjectValue(
+ $dataItemFactory->newDIProperty( '_CONT_TITLE' ),
+ $dataItemFactory->newDIBlob( $this->doc['_source']['attachment']['title'] )
+ );
+ }
+
+ if ( isset( $this->doc['_source']['attachment']['language'] ) ) {
+ $this->containerSemanticData->addPropertyObjectValue(
+ $dataItemFactory->newDIProperty( '_CONT_LANG' ),
+ $dataItemFactory->newDIBlob( $this->doc['_source']['attachment']['language'] )
+ );
+ }
+
+ if ( isset( $this->doc['_source']['attachment']['content_length'] ) ) {
+ $this->containerSemanticData->addPropertyObjectValue(
+ $dataItemFactory->newDIProperty( '_CONT_LEN' ),
+ $dataItemFactory->newDINumber( intval( $this->doc['_source']['attachment']['content_length'] ) )
+ );
+ }
+
+ if ( isset( $this->doc['_source']['attachment']['keywords'] ) ) {
+ $this->containerSemanticData->addPropertyObjectValue(
+ $dataItemFactory->newDIProperty( '_CONT_KEYW' ),
+ $dataItemFactory->newDINumber( intval( $this->doc['_source']['attachment']['keywords'] ) )
+ );
+ }
+
+ return $this;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Bulk.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Bulk.php
new file mode 100644
index 00000000..2392d1a3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Bulk.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use SMW\Elastic\Connection\Client as ElasticClient;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Bulk {
+
+ /**
+ * @var ElasticClient
+ */
+ private $connection;
+
+ /**
+ * @var array
+ */
+ private $bulk = [];
+
+ /**
+ * @var array
+ */
+ private $head = [];
+
+ /**
+ * @since 3.0
+ */
+ public function __construct( ElasticClient $connection ) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function clear() {
+ $this->bulk = [];
+ $this->head = [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function head( array $params ) {
+ $this->head = $params;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ */
+ public function delete( array $params ) {
+ $this->bulk['body'][] = [ 'delete' => $params + $this->head ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ * @param array $source
+ */
+ public function index( array $params, array $source ) {
+ $this->bulk['body'][] = [ 'index' => $params + $this->head ];
+ $this->bulk['body'][] = $source;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ * @param array $source
+ */
+ public function upsert( array $params, array $doc ) {
+ $this->bulk['body'][] = [ 'update' => $params + $this->head ];
+ $this->bulk['body'][] = [ 'doc' => $doc, "doc_as_upsert" => true ];
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function execute() {
+
+ $response = $this->connection->bulk(
+ $this->bulk
+ );
+
+ $this->bulk = [];
+
+ return $response;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function toJson( $flags = 0 ) {
+ return json_encode( $this->bulk, $flags );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/FileIndexer.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/FileIndexer.php
new file mode 100644
index 00000000..e9408dfc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/FileIndexer.php
@@ -0,0 +1,479 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use File;
+use Onoi\MessageReporter\MessageReporterAwareTrait;
+use Psr\Log\LoggerAwareTrait;
+use RuntimeException;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\Elastic\QueryEngine\FieldMapper;
+use SMW\Store;
+use SMWContainerSemanticData as ContainerSemanticData;
+use Title;
+
+/**
+ * Experimental file indexer that uses the ES ingest pipeline to ingest and retrieve
+ * data from an attachment and make file content searchable outside of a normal
+ * wiki content.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FileIndexer {
+
+ use MessageReporterAwareTrait;
+ use LoggerAwareTrait;
+
+ /**
+ * @var Indexer
+ */
+ private $indexer;
+
+ /**
+ * @var string
+ */
+ private $origin = '';
+
+ /**
+ * @var boolean
+ */
+ private $sha1Check = true;
+
+ /**
+ * @since 3.0
+ *
+ * @param Indexer $indexer
+ */
+ public function __construct( Indexer $indexer ) {
+ $this->indexer = $indexer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $origin
+ */
+ public function setOrigin( $origin ) {
+ $this->origin = $origin;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function noSha1Check() {
+ $this->sha1Check = false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param File|null $file
+ */
+ public function planIngestJob( Title $title ) {
+
+ $fileIngestJob = new FileIngestJob(
+ $title
+ );
+
+ $fileIngestJob->lazyPush();
+ }
+
+ /**
+ * The ES ingest pipeline only does create (not update) index content which
+ * means any other content is deleted after the ingest process has finished
+ * therefore:
+ *
+ * - Read the document before, and retrieve any annotations that exists for
+ * that entity
+ * - Let ES ingest the file content and attach the earlier retrieved
+ * annotations
+ * - SMW doesn't know anything about the file attachment details ES has gather
+ * from the file hence update the SQLStore (!important not the ElasticStore)
+ * with the data
+ * - After the SQLStore update make sure that those attachment details (which
+ * are represented as subobject) are added to ES manually (means not through
+ * the standard Store::updateData to avoid an update circle) otherwise there
+ * will be invisible the any SMW user
+ *
+ * @since 3.0
+ *
+ * @param DIWikiPage $dataItem
+ * @param File|null $file
+ */
+ public function index( DIWikiPage $dataItem, File $file = null ) {
+
+ if ( $dataItem->getId() == 0 ) {
+ $dataItem->setId( $this->indexer->getId( $dataItem ) );
+ }
+
+ if ( $dataItem->getId() == 0 || $dataItem->getNamespace() !== NS_FILE || $dataItem->getSubobjectName() !== '' ) {
+ return;
+ }
+
+ $time = -microtime( true );
+
+ $params = [
+ 'id' => 'attachment',
+ 'body' => [
+ 'description' => 'Extract attachment information',
+ 'processors' => [
+ [
+ 'attachment' => [
+ 'field' => 'file_content',
+ 'indexed_chars' => -1
+ ]
+ ],
+ [
+ 'remove' => [
+ "field" => "file_content"
+ ]
+ ]
+ ]
+ ],
+ ];
+
+ $connection = $this->indexer->getConnection();
+ $connection->ingest()->putPipeline( $params );
+
+ if ( $file === null ) {
+ $file = wfFindFile( $dataItem->getTitle() );
+ }
+
+ if ( $file === false || $file === null ) {
+ return;
+ }
+
+ $url = $file->getFullURL();
+ $id = $dataItem->getId();
+
+ $sha1 = $file->getSha1();
+ $ingest = true;
+
+ $index = $this->indexer->getIndexName( ElasticClient::TYPE_DATA );
+ $doc = [ '_source' => [] ];
+
+ $params = [
+ 'index' => $index,
+ 'type' => ElasticClient::TYPE_DATA,
+ 'id' => $id,
+ ];
+
+ // Do we have any existing data? The ingest pipeline will override the
+ // entire document, so rescue any data before starting the ingest.
+ if ( $connection->exists( $params ) ) {
+ $doc = $connection->get( $params + [ '_source_include' => [ 'file_sha1', 'subject', 'text_raw', 'text_copy', 'P*' ] ] );
+ }
+
+ // Is the sha1 the same? Don't do anything since the content is expected
+ // to be the same!
+ if ( $this->sha1Check && isset( $doc['_source']['file_sha1'] ) && $doc['_source']['file_sha1'] === $sha1 ) {
+ $ingest = false;
+ }
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'origin' => $this->origin,
+ 'subject' => $dataItem->getHash()
+ ];
+
+ if ( $ingest === false ) {
+
+ $msg = [
+ 'File indexer',
+ 'Skipping the ingest process',
+ 'Found identical file_sha1 ({subject})'
+ ];
+
+ return $this->logger->info( $msg, $context );
+ }
+
+ $contents = '';
+
+ // Avoid a "failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found"
+ $file_headers = @get_headers( $url );
+
+ if ( $file_headers !== false && $file_headers[0] !== 'HTTP/1.1 404 Not Found' && $file_headers[0] !== 'HTTP/1.0 404 Not Found' ) {
+ $contents = file_get_contents( $url );
+ } else {
+ $this->logger->info( [ 'File indexer', "HTTP/1.1 404 Not Found for $url" ], $context );
+ }
+
+ $params += [
+ 'pipeline' => 'attachment',
+ 'body' => [
+ 'file_content' => base64_encode( $contents ),
+ 'file_path' => $url,
+ 'file_sha1' => $sha1,
+ ] + $doc['_source']
+ ];
+
+ $context['response'] = $connection->index( $params );
+ $context['procTime'] = microtime( true ) + $time;
+
+ $msg = [
+ 'File indexer',
+ 'Ingest process completed ({subject})',
+ 'procTime (in sec): {procTime}',
+ 'Response: {response}'
+ ];
+
+ $this->logger->info( $msg, $context );
+
+ // Don't use the ElasticStore otherwise we index the added fields once more
+ // and hereby remove the content from the attachment! and start a circle
+ // since the annotation update can only happen after the information is
+ // retrieved from ES.
+ $this->addAnnotation(
+ ApplicationFactory::getInstance()->getStore( '\SMW\SQLStore\SQLStore' ),
+ $dataItem
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param DIWikiPage $dataItem
+ */
+ public function addAnnotation( Store $store, DIWikiPage $dataItem ) {
+
+ $time = -microtime( true );
+
+ if ( $dataItem->getId() == 0 ) {
+ $dataItem->setId( $this->indexer->getId( $dataItem ) );
+ }
+
+ if ( $dataItem->getId() == 0 ) {
+ throw new RuntimException( "Missing ID: " . $dataItem );
+ }
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'origin' => $this->origin,
+ 'subject' => $dataItem->getHash()
+ ];
+
+ $semanticData = $store->getSemanticData( $dataItem );
+ $connection = $this->indexer->getConnection();
+
+ $index = $this->indexer->getIndexName( ElasticClient::TYPE_DATA );
+ $doc = [ '_source' => [] ];
+
+ $params = [
+ 'index' => $index,
+ 'type' => ElasticClient::TYPE_DATA,
+ 'id' => $dataItem->getId(),
+ ];
+
+ if ( !$connection->exists( $params ) ) {
+
+ $msg = [
+ 'File indexer',
+ 'Abort annotation update',
+ 'Missing {id} document!'
+ ];
+
+ return $this->logger->info( $msg, $context + [ 'id' => $dataItem->getId() ] );
+ }
+
+ $params = $params + [
+ '_source_include' => [
+ 'file_sha1',
+ 'attachment.date',
+ 'attachment.content_type',
+ 'attachment.author',
+ 'attachment.language',
+ 'attachment.title',
+ 'attachment.content_length'
+ ]
+ ];
+
+ $doc = $connection->get( $params );
+
+ if ( !isset( $doc['_source']['file_sha1'] ) ) {
+
+ $msg = [
+ 'File indexer',
+ 'No annotation update',
+ 'Missing file_sha1!'
+ ];
+
+ return $this->logger->info( $msg, $context );
+ }
+
+ $containerSemanticData = $this->newContainerSemanticData(
+ $dataItem,
+ $doc
+ );
+
+ $attachmentAnnotator = new AttachmentAnnotator(
+ $containerSemanticData,
+ $doc
+ );
+
+ $attachmentAnnotator->addAnnotation();
+ $property = $attachmentAnnotator->getProperty();
+
+ // Remove any existing `_FILE_ATTCH` in case it was a reupload with a different
+ // content sha1
+ $semanticData->removeProperty( $property );
+
+ $semanticData->addPropertyObjectValue(
+ $property,
+ $attachmentAnnotator->getContainer()
+ );
+
+ $callableUpdate = ApplicationFactory::getInstance()->newDeferredTransactionalCallableUpdate( function() use( $store, $semanticData, $attachmentAnnotator ) {
+ // Update the SQLStore with the annotated information which will NOT
+ // trigger another ES index update BUT ...
+ $store->updateData( $semanticData );
+
+ // ... we need to replicate the container data (subobject) in order to
+ // make them usable via query engine therefore ...
+ $this->indexAttachmentInfo( $attachmentAnnotator );
+ } );
+
+ $callableUpdate->setOrigin( __METHOD__ );
+ $callableUpdate->waitOnTransactionIdle();
+ $callableUpdate->pushUpdate();
+
+ $context['procTime'] = microtime( true ) + $time;
+
+ $msg = [
+ 'File indexer',
+ 'Attachment annotation update completed ({subject})',
+ 'procTime (in sec): {procTime}'
+ ];
+
+ $this->logger->info( $msg, $context );
+ }
+
+ /**
+ * Meta assignments from a file ingest need to be republished in a SMW conform
+ * manner so that property path `[[File attachment.Content title::..]]` work
+ * as expected.
+ *
+ * @since 3.0
+ *
+ * @param AttachmentAnnotator $attachmentAnnotator
+ */
+ public function indexAttachmentInfo( AttachmentAnnotator $attachmentAnnotator ) {
+
+ $data = [];
+ $time = -microtime( true );
+
+ $semanticData = $attachmentAnnotator->getSemanticData();
+ $subject = $semanticData->getSubject();
+
+ // Find base document ID
+ $baseDocId = $this->indexer->getId( $subject->asBase() );
+
+ if ( $baseDocId == 0 ) {
+ throw new RuntimeException( "Missing ID: " . $subject );
+ }
+
+ $subject->setId( $this->indexer->getId( $subject ) );
+
+ if ( $subject->getId() == 0 ) {
+ throw new RuntimeException( "Missing ID: " . $subject );
+ }
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'origin' => $this->origin,
+ 'subject' => $subject->getHash()
+ ];
+
+ foreach ( $semanticData->getProperties() as $property ) {
+
+ $pid = $this->indexer->getId(
+ $property->getCanonicalDiWikiPage()
+ );
+
+ $pid = FieldMapper::getPID( $pid );
+ $data[$pid] = [];
+ $field = FieldMapper::getField( $property );
+
+ $data[$pid][$field] = [];
+
+ foreach ( $semanticData->getPropertyValues( $property ) as $dataItem ) {
+ $data[$pid][$field][] = $dataItem->getSortKey();
+ }
+ }
+
+ $this->indexer->create( $subject, $data );
+
+ // Attach the subobject to the base subject
+ $response = $this->upsertDoc(
+ $baseDocId,
+ $subject,
+ $attachmentAnnotator->getProperty()
+ );
+
+ $context['time'] = microtime( true ) + $time;
+ $context['response'] = $response;
+
+ $msg = [
+ 'File indexer',
+ 'Pushed attachment information to ES ({subject})',
+ 'procTime (in sec): {procTime}',
+ 'Response: {response}'
+ ];
+
+ $this->logger->info( $msg, $context );
+ }
+
+ private function upsertDoc( $baseDocId, $subject, $property ) {
+
+ $params = [
+ '_index' => $this->indexer->getIndexName( ElasticClient::TYPE_DATA ),
+ '_type' => ElasticClient::TYPE_DATA
+ ];
+
+ $bulk = $this->indexer->newBulk( $params );
+ $data = [];
+
+ $pid = $this->indexer->getId(
+ $property->getCanonicalDiWikiPage()
+ );
+
+ $pid = FieldMapper::getPID( $pid );
+ $data[$pid] = [];
+
+ // It is the ID field we want not any type related field!
+ $field = 'wpgID';
+
+ $data[$pid][$field] = [];
+ $data[$pid][$field][] = $subject->getId();
+
+ // Upsert of the base document to link subject -> subobject otherwise
+ // a property path like `File attachment.Content length`) is not going
+ // to work
+ $bulk->upsert( [ '_id' => $baseDocId ], $data );
+
+ return $bulk->execute();
+ }
+
+ private function newContainerSemanticData( $dataItem, $doc ) {
+
+ $subobjectName = '_FILE' . md5( $doc['_source']['file_sha1'] );
+
+ $subject = new DIWikiPage(
+ $dataItem->getDBkey(),
+ $dataItem->getNamespace(),
+ $dataItem->getInterwiki(),
+ $subobjectName
+ );
+
+ return new ContainerSemanticData( $subject );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/FileIngestJob.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/FileIngestJob.php
new file mode 100644
index 00000000..dfa43298
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/FileIngestJob.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Job;
+use SMW\Elastic\ElasticFactory;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\SQLStore\ChangeOp\ChangeDiff;
+use SMW\DIWikiPage;
+use Title;
+
+/**
+ * @license GNU GPL v2
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FileIngestJob extends Job {
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.elasticFileIngest', $title, $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 3.0
+ */
+ public function run() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $store = $applicationFactory->getStore();
+
+ $connection = $store->getConnection( 'elastic' );
+
+ // Make sure a node is available
+ if ( $connection->hasLock( ElasticClient::TYPE_DATA ) || !$connection->ping() ) {
+
+ if ( $connection->hasLock( ElasticClient::TYPE_DATA ) ) {
+ $this->params['retryCount'] = 0;
+ }
+
+ return $this->requeueRetry( $connection->getConfig() );
+ }
+
+ $elasticFactory = new ElasticFactory();
+
+ $indexer = $elasticFactory->newIndexer(
+ $store
+ );
+
+ $fileIndexer = $indexer->getFileIndexer();
+
+ $fileIndexer->setOrigin( __METHOD__ );
+
+ $fileIndexer->setLogger(
+ $applicationFactory->getMediaWikiLogger( 'smw-elastic' )
+ );
+
+ $file = wfFindFile( $this->getTitle() );
+
+ // File isn't available yet (or uploaded), try again!
+ if ( $file === false ) {
+ return $this->requeueRetry( $connection->getConfig() );
+ }
+
+ // It has been observed that when this job is run, the job runner can
+ // return with "Fatal error: Allowed memory size of ..." which in most
+ // cases happen when large files are involved therefore temporary lift
+ // the limitation!
+ $memory_limit = ini_get( 'memory_limit' );
+
+ if ( wfShorthandToInteger( $memory_limit ) < wfShorthandToInteger( '1024M' ) ) {
+ ini_set( 'memory_limit', '1024M' );
+ }
+
+ $fileIndexer->index(
+ DIWikiPage::newFromTitle( $this->getTitle() ),
+ $file
+ );
+
+ ini_set( 'memory_limit', $memory_limit );
+
+ return true;
+ }
+
+ private function requeueRetry( $config ) {
+
+ // Give up!
+ if ( $this->getParameter( 'retryCount' ) >= $config->dotGet( 'indexer.job.file.ingest.retries' ) ) {
+ return true;
+ }
+
+ if ( !isset( $this->params['retryCount'] ) ) {
+ $this->params['retryCount'] = 1;
+ } else {
+ $this->params['retryCount']++;
+ }
+
+ if ( !isset( $this->params['createdAt'] ) ) {
+ $this->params['createdAt'] = time();
+ }
+
+ $job = new self( $this->title, $this->params );
+ $job->setDelay( 60 * 10 );
+
+ $job->insert();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Indexer.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Indexer.php
new file mode 100644
index 00000000..98e177a9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Indexer.php
@@ -0,0 +1,777 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use Onoi\MessageReporter\MessageReporterAwareTrait;
+use Psr\Log\LoggerAwareTrait;
+use SMW\Services\ServicesContainer;
+use RuntimeException;
+use SMW\DIWikiPage;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\SQLStore\ChangeOp\ChangeDiff;
+use SMW\SQLStore\ChangeOp\ChangeOp;
+use SMW\Store;
+use SMW\Utils\CharArmor;
+use SMWDIBlob as DIBlob;
+use Title;
+use Revision;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Indexer {
+
+ use MessageReporterAwareTrait;
+ use LoggerAwareTrait;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var ServicesContainer
+ */
+ private $servicesContainer;
+
+ /**
+ * @var FileIndexer
+ */
+ private $fileIndexer;
+
+ /**
+ * @var string
+ */
+ private $origin = '';
+
+ /**
+ * @var boolean
+ */
+ private $isRebuild = false;
+
+ /**
+ * @var []
+ */
+ private $versions = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param ServicesContainer $servicesContainer
+ */
+ public function __construct( Store $store, ServicesContainer $servicesContainer ) {
+ $this->store = $store;
+ $this->servicesContainer = $servicesContainer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param [] $versions
+ */
+ public function setVersions( array $versions ) {
+ $this->versions = $versions;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $origin
+ */
+ public function setOrigin( $origin ) {
+ $this->origin = $origin;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return FileIndexer
+ */
+ public function getFileIndexer() {
+
+ if ( $this->fileIndexer === null ) {
+ $this->fileIndexer = $this->servicesContainer->get( 'FileIndexer', $this );
+ }
+
+ $this->fileIndexer->setLogger(
+ $this->logger
+ );
+
+ return $this->fileIndexer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public function getId( DIWikiPage $dataItem ) {
+ return $this->store->getObjectIds()->getId( $dataItem );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function isAccessible() {
+ return $this->isSafe();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isRebuild
+ */
+ public function isRebuild( $isRebuild = true ) {
+ $this->isRebuild = $isRebuild;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Client
+ */
+ public function getConnection() {
+ return $this->store->getConnection( 'elastic' );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function setup() {
+
+ $rollover = $this->servicesContainer->get(
+ 'Rollover',
+ $this->store->getConnection( 'elastic' )
+ );
+
+ $rollover->update(
+ ElasticClient::TYPE_DATA
+ );
+
+ $rollover->update(
+ ElasticClient::TYPE_LOOKUP
+ );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function drop() {
+
+ $rollover = $this->servicesContainer->get(
+ 'Rollover',
+ $this->store->getConnection( 'elastic' )
+ );
+
+ $rollover->delete(
+ ElasticClient::TYPE_DATA
+ );
+
+ $rollover->delete(
+ ElasticClient::TYPE_LOOKUP
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public function getIndexName( $type ) {
+
+ $index = $this->store->getConnection( 'elastic' )->getIndexNameByType(
+ $type
+ );
+
+ // If the rebuilder has set a specific version, use it to avoid writing to
+ // the alias of the index when running a rebuild.
+ if ( isset( $this->versions[$type] ) ) {
+ $index = "$index-" . $this->versions[$type];
+ }
+
+ return $index;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return Bulk
+ */
+ public function newBulk( array $params ) {
+
+ $bulk = $this->servicesContainer->get(
+ 'Bulk',
+ $this->store->getConnection( 'elastic' )
+ );
+
+ $bulk->head( $params );
+
+ return $bulk;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $idList
+ */
+ public function delete( array $idList, $isConcept = false ) {
+
+ if ( $idList === [] ) {
+ return;
+ }
+
+ $title = Title::newFromText( $this->origin . ':' . md5( json_encode( $idList ) ) );
+
+ $params = [
+ 'delete' => $idList
+ ];
+
+ if ( $this->isSafe( $title, $params ) === false ) {
+ return $this->planRecoveryJob( $title, $params );
+ }
+
+ $index = $this->getIndexName(
+ ElasticClient::TYPE_DATA
+ );
+
+ $params = [
+ '_index' => $index,
+ '_type' => ElasticClient::TYPE_DATA
+ ];
+
+ $bulk = $this->newBulk( $params );
+ $time = -microtime( true );
+
+ foreach ( $idList as $id ) {
+
+ $bulk->delete( [ '_id' => $id ] );
+
+ if ( $isConcept ) {
+ $bulk->delete(
+ [
+ '_index' => $this->getIndexName( ElasticClient::TYPE_LOOKUP ),
+ '_type' => ElasticClient::TYPE_LOOKUP,
+ '_id' => md5( $id )
+ ]
+ );
+ }
+ }
+
+ $response = $bulk->execute();
+
+ $this->logger->info(
+ [
+ 'Indexer',
+ 'Deleted list',
+ 'procTime (in sec): {procTime}',
+ 'Response: {response}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->origin,
+ 'procTime' => $time + microtime( true ),
+ 'response' => $response
+ ]
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $dataItem
+ * @param array $data
+ */
+ public function create( DIWikiPage $dataItem, array $data = [] ) {
+
+ $title = $dataItem->getTitle();
+
+ $params = [
+ 'create' => $dataItem->getHash()
+ ];
+
+ if ( $this->isSafe() === false ) {
+ return $this->planRecoveryJob( $title, $params );
+ }
+
+ if ( $dataItem->getId() == 0 ) {
+ throw new RuntimeException( "Missing ID: " . $dataItem );
+ }
+
+ $connection = $this->store->getConnection( 'elastic' );
+
+ $params = [
+ 'index' => $this->getIndexName( ElasticClient::TYPE_DATA ),
+ 'type' => ElasticClient::TYPE_DATA,
+ 'id' => $dataItem->getId()
+ ];
+
+ $data['subject'] = [
+ 'title' => str_replace( '_', ' ', $dataItem->getDBKey() ),
+ 'subobject' => $dataItem->getSubobjectName(),
+ 'namespace' => $dataItem->getNamespace(),
+ 'interwiki' => $dataItem->getInterwiki(),
+ 'sortkey' => mb_convert_encoding( $dataItem->getSortKey(), 'UTF-8', 'UTF-8' )
+ ];
+
+ $response = $connection->index( $params + [ 'body' => $data ] );
+
+ $this->logger->info(
+ [
+ 'Indexer',
+ 'Create ({subject}, {id})',
+ 'Response: {response}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->origin,
+ 'subject' => $dataItem->getHash(),
+ 'id' => $dataItem->getId(),
+ 'response' => $response
+ ]
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ChangeDiff $changeDiff
+ * @param string $text
+ */
+ public function safeReplicate( ChangeDiff $changeDiff, $text = '' ) {
+
+ $subject = $changeDiff->getSubject();
+
+ $params = [
+ 'index' => $subject->getHash()
+ ];
+
+ if ( $this->isSafe() === false ) {
+ return $this->planRecoveryJob( $subject->getTitle(), $params ) ;
+ }
+
+ $this->index( $changeDiff, $text );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage|Title|integer $id
+ *
+ * @return string
+ */
+ public function fetchNativeData( $id ) {
+
+ if ( $id instanceof DIWikiPage ) {
+ $id = $id->getTitle();
+ }
+
+ if ( $id instanceof Title ) {
+ $id = $id->getLatestRevID( \Title::GAID_FOR_UPDATE );
+ }
+
+ if ( $id == 0 ) {
+ return '';
+ };
+
+ $revision = Revision::newFromId( $id );
+
+ if ( $revision == null ) {
+ return '';
+ };
+
+ $content = $revision->getContent( Revision::RAW );
+
+ return $content->getNativeData();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ChangeDiff $changeDiff
+ * @param string $text
+ */
+ public function index( ChangeDiff $changeDiff, $text = '' ) {
+
+ $time = -microtime( true );
+ $subject = $changeDiff->getSubject();
+
+ $params = [
+ '_index' => $this->getIndexName( ElasticClient::TYPE_DATA ),
+ '_type' => ElasticClient::TYPE_DATA
+ ];
+
+ $bulk = $this->newBulk( $params );
+
+ $this->map_data( $bulk, $changeDiff );
+ $this->map_text( $bulk, $subject, $text );
+
+ $response = $bulk->execute();
+
+ // We always index (not upsert) since we want to have a complete state of
+ // an entity (and ES would delete and insert any document) so trying
+ // to filter and diff the data update has no real merit besides that it
+ // would require us to read each ID in the update from ES and wire the data
+ // back and forth which has shown to be ineffective especially when a
+ // subject has many subobjects.
+ //
+ // The disadvantage is that we loose any auxiliary data that were attached
+ // while not being part of the on-wiki information such as attachment
+ // information from a file ingest.
+ //
+ // In order to reapply those information we could read them in the same
+ // transaction before the actual update but since we expect the
+ // `attachment.content` to contain a large chunk of text, we push that
+ // into the job-queue so that the background process can take of it.
+ //
+ // Of course, this will cause a delay for the file content being searchable
+ // but that should be acceptable to avoid blocking any online transaction.
+ if ( !$this->isRebuild && $subject->getNamespace() === NS_FILE ) {
+ $this->getFileIndexer()->planIngestJob( $subject->getTitle() );
+ }
+
+ $this->logger->info(
+ [
+ 'Indexer',
+ 'Data index completed ({subject})',
+ 'procTime (in sec): {procTime}',
+ 'Response: {response}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->origin,
+ 'subject' => $subject->getHash(),
+ 'procTime' => $time + microtime( true ),
+ 'response' => $response
+ ]
+ );
+ }
+
+ /**
+ * Remove anything that resembles [[:...|foo]] to avoid distracting the indexer
+ * with internal links annotation that are not relevant.
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ public function removeLinks( $text ) {
+
+ // {{DEFAULTSORT: ... }}
+ $text = preg_replace( "/\\{\\{([^|]+?)\\}\\}/", "", $text );
+ $text = preg_replace( '/\\[\\[[\s\S]+?::/', '[[', $text );
+
+ // [[:foo|bar]]
+ $text = preg_replace( '/\\[\\[:[^|]+?\\|/', '[[', $text );
+ $text = preg_replace( "/\\{\\{([^|]+\\|)(.*?)\\}\\}/", "\\2", $text );
+ $text = preg_replace( "/\\[\\[([^|]+?)\\]\\]/", "\\1", $text );
+
+ // [[Has foo::Bar]]
+ // $text = \SMW\Parser\LinksEncoder::removeAnnotation( $text );
+
+ return $text;
+ }
+
+ private function isSafe() {
+
+ $connection = $this->store->getConnection( 'elastic' );
+
+ // Make sure a node is available and is not locked by the rebuilder
+ if ( !$connection->hasLock( ElasticClient::TYPE_DATA ) && $connection->ping() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function planRecoveryJob( $title, array $params ) {
+
+ $indexerRecoveryJob = new IndexerRecoveryJob(
+ $title,
+ $params
+ );
+
+ $indexerRecoveryJob->insert();
+
+ $this->logger->info(
+ [
+ 'Indexer',
+ 'Insert IndexerRecoveryJob: {subject}',
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'user',
+ 'origin' => $this->origin,
+ 'subject' => $title->getPrefixedDBKey()
+ ]
+ );
+ }
+
+ private function map_text( $bulk, $subject, $text ) {
+
+ if ( $text === '' ) {
+ return;
+ }
+
+ $id = $subject->getId();
+
+ if ( $id == 0 ) {
+ $id = $this->store->getObjectIds()->getSMWPageID(
+ $subject->getDBkey(),
+ $subject->getNamespace(),
+ $subject->getInterwiki(),
+ $subject->getSubobjectName(),
+ true
+ );
+ }
+
+ $bulk->upsert(
+ [
+ '_index' => $this->getIndexName( ElasticClient::TYPE_DATA ),
+ '_type' => ElasticClient::TYPE_DATA,
+ '_id' => $id
+ ],
+ [
+ 'text_raw' => $this->removeLinks( $text )
+ ]
+ );
+ }
+
+ private function map_data( $bulk, $changeDiff ) {
+
+ $dbType = $this->store->getInfo( 'db' );
+ $unescape_bytea = isset( $dbType['postgres'] );
+
+ $inserts = [];
+ $inverted = [];
+
+ // In the event that a _SOBJ (or hereafter any inherited object)
+ // is deleted, remove the reference directly from the index since
+ // the object is embedded and is therefore handled outside of the
+ // normal wikiPage delete action
+ foreach ( $changeDiff->getTableChangeOps() as $tableChangeOp ) {
+ foreach ( $tableChangeOp->getFieldChangeOps( ChangeOp::OP_DELETE ) as $fieldChangeOp ) {
+
+ if ( !$fieldChangeOp->has( 'o_id' ) ) {
+ continue;
+ }
+
+ $bulk->delete( [ '_id' => $fieldChangeOp->get( 'o_id' ) ] );
+ }
+ }
+
+ $propertyList = $changeDiff->getPropertyList( 'id' );
+
+ foreach ( $changeDiff->getDataOps() as $tableChangeOp ) {
+ foreach ( $tableChangeOp->getFieldChangeOps() as $fieldChangeOp ) {
+
+ if ( !$fieldChangeOp->has( 's_id' ) ) {
+ continue;
+ }
+
+ $this->mapRows( $fieldChangeOp, $propertyList, $inserts, $inverted, $unescape_bytea );
+ }
+ }
+
+ foreach ( $inverted as $id => $update ) {
+ $bulk->upsert( [ '_id' => $id ], $update );
+ }
+
+ foreach ( $inserts as $id => $value ) {
+ $bulk->index( [ '_id' => $id ], $value );
+ }
+ }
+
+ private function mapRows( $fieldChangeOp, $propertyList, &$insertRows, &$invertedRows, $unescape_bytea ) {
+
+ // The structure to be expected in ES:
+ //
+ // "subject": {
+ // "title": "Foaf:knows",
+ // "subobject": "",
+ // "namespace": 102,
+ // "interwiki": "",
+ // "sortkey": "Foaf:knows"
+ // },
+ // "P:8": {
+ // "txtField": [
+ // "foaf knows http://xmlns.com/foaf/0.1/ Type:Page"
+ // ]
+ // },
+ // "P:29": {
+ // "datField": [
+ // 2458150.6958333
+ // ]
+ // },
+ // "P:1": {
+ // "uriField": [
+ // "http://semantic-mediawiki.org/swivt/1.0#_wpg"
+ // ]
+ // }
+
+ // - datField (time value) is a numeric field (JD number) to allow using
+ // ranges on dates with values being representable from January 1, 4713 BC
+ // (proleptic Julian calendar)
+
+ $sid = $fieldChangeOp->get( 's_id' );
+
+ if ( !isset( $insertRows[$sid] ) ) {
+ $insertRows[$sid] = [];
+ }
+
+ if ( !isset( $insertRows[$sid]['subject'] ) ) {
+ $dataItem = $this->store->getObjectIds()->getDataItemById( $sid );
+ $sort = $dataItem->getSortKey();
+
+ // Use collated sort field if available
+ if ( $dataItem->getOption( 'sort', '' ) !== '' ) {
+ $sort = $dataItem->getOption( 'sort' );
+ }
+
+ // Avoid issue with the Ealstic serializer
+ $sort = CharArmor::removeSpecialChars(
+ CharArmor::removeControlChars( $sort )
+ );
+
+ $insertRows[$sid]['subject'] = [
+ 'title' => str_replace( '_', ' ', $dataItem->getDBKey() ),
+ 'subobject' => $dataItem->getSubobjectName(),
+ 'namespace' => $dataItem->getNamespace(),
+ 'interwiki' => $dataItem->getInterwiki(),
+ 'sortkey' => $sort
+ ];
+ }
+
+ // Avoid issues where the p_id is unknown as in case of an empty
+ // concept (red linked) as reference
+ if ( !$fieldChangeOp->has( 'p_id' ) ) {
+ return;
+ }
+
+ $ins = $fieldChangeOp->getChangeOp();
+ $pid = $fieldChangeOp->get( 'p_id' );
+
+ $prop = isset( $propertyList[$pid] ) ? $propertyList[$pid] : [];
+
+ $pid = 'P:' . $pid;
+ unset( $ins['s_id'] );
+
+ $val = 'n/a';
+ $type = 'wpgField';
+
+ if ( $fieldChangeOp->has( 'o_blob' ) && $fieldChangeOp->has( 'o_hash' ) ) {
+ $type = 'txtField';
+ $val = $ins['o_blob'] === null ? $ins['o_hash'] : $ins['o_blob'];
+
+ // Postgres requires special handling of blobs otherwise escaped
+ // text elements are used as index input
+ // Tests: P9010, Q0704, Q1206, and Q0103
+ if ( $unescape_bytea && $ins['o_blob'] !== null ) {
+ $val = pg_unescape_bytea( $val );
+ }
+
+ // #3020, 3035
+ if ( isset( $prop['_type'] ) && $prop['_type'] === '_keyw' ) {
+ $val = DIBlob::normalize( $ins['o_hash'] );
+ }
+
+ // Remove control chars and avoid Elasticsearch to throw a
+ // "SmartSerializer.php: Failed to JSON encode: 5" since JSON requires
+ // valid UTF-8
+ $val = $this->removeLinks( mb_convert_encoding( $val, 'UTF-8', 'UTF-8' ) );
+ } elseif ( $fieldChangeOp->has( 'o_serialized' ) && $fieldChangeOp->has( 'o_blob' ) ) {
+ $type = 'uriField';
+ $val = $ins['o_blob'] === null ? $ins['o_serialized'] : $ins['o_blob'];
+
+ if ( $unescape_bytea && $ins['o_blob'] !== null ) {
+ $val = pg_unescape_bytea( $val );
+ }
+
+ } elseif ( $fieldChangeOp->has( 'o_serialized' ) && $fieldChangeOp->has( 'o_sortkey' ) ) {
+ $type = strpos( $ins['o_serialized'], '/' ) !== false ? 'datField' : 'numField';
+ $val = (float)$ins['o_sortkey'];
+ } elseif ( $fieldChangeOp->has( 'o_value' ) ) {
+ $type = 'booField';
+ // Avoid a "Current token (VALUE_NUMBER_INT) not of boolean type ..."
+ $val = $ins['o_value'] ? true : false;
+ } elseif ( $fieldChangeOp->has( 'o_lat' ) ) {
+ // https://www.elastic.co/guide/en/elasticsearch/reference/6.1/geo-point.html
+ // Geo-point expressed as an array with the format: [ lon, lat ]
+ // Geo-point expressed as a string with the format: "lat,lon".
+ $type = 'geoField';
+ $val = $ins['o_serialized'];
+ } elseif ( $fieldChangeOp->has( 'o_id' ) ) {
+ $type = 'wpgField';
+ $dataItem = $this->store->getObjectIds()->getDataItemById( $ins['o_id'] );
+
+ $val = $dataItem->getSortKey();
+ $val = mb_convert_encoding( $val, 'UTF-8', 'UTF-8' );
+
+ if ( !isset( $insertRows[$sid][$pid][$type] ) ) {
+ $insertRows[$sid][$pid][$type] = [];
+ }
+
+ $insertRows[$sid][$pid][$type] = array_merge( $insertRows[$sid][$pid][$type], [ $val ] );
+ $type = 'wpgID';
+ $val = (int)$ins['o_id'];
+
+ // Create a minimal body for an inverted relation
+ //
+ // When a query `[[-Has mother::Michael]]` inquiries that relationship
+ // on the fact of `Michael` -> `[[Has mother::Carol]] with `Carol`
+ // being redlinked (not exists as page) the query can match the object
+ if ( !isset( $invertedRows[$val] ) ) {
+
+ // Ensure we have something to sort on
+ // See also Q0105#8
+ $subject = [
+ 'title' => str_replace( '_', ' ', $dataItem->getDBKey() ),
+ 'subobject' => $dataItem->getSubobjectName(),
+ 'namespace' => $dataItem->getNamespace(),
+ 'interwiki' => $dataItem->getInterwiki(),
+ 'sortkey' => mb_convert_encoding( $dataItem->getSortKey(), 'UTF-8', 'UTF-8' )
+ ];
+
+ $invertedRows[$val] = [ 'subject' => $subject ];
+ }
+
+ // A null, [] (an empty array), and [null] are all equivalent, they
+ // simply don't exists in an inverted index
+ }
+
+ if ( !isset( $insertRows[$sid][$pid][$type] ) ) {
+ $insertRows[$sid][$pid][$type] = [];
+ }
+
+ $insertRows[$sid][$pid][$type] = array_merge(
+ $insertRows[$sid][$pid][$type],
+ [ $val ]
+ );
+
+ // Replicate dates in the serialized raw_format to give aggregations a chance
+ // to filter dates by term
+ if ( $type === 'datField' && isset( $ins['o_serialized'] ) ) {
+
+ if ( !isset( $insertRows[$sid][$pid]["dat_raw"] ) ) {
+ $insertRows[$sid][$pid]["dat_raw"] = [];
+ }
+
+ $insertRows[$sid][$pid]["dat_raw"][] = $ins['o_serialized'];
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/IndexerRecoveryJob.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/IndexerRecoveryJob.php
new file mode 100644
index 00000000..7304fc1d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/IndexerRecoveryJob.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Job;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\Elastic\ElasticFactory;
+use SMW\SQLStore\ChangeOp\ChangeDiff;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class IndexerRecoveryJob extends Job {
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.elasticIndexerRecovery', $title, $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 3.0
+ */
+ public function allowRetries() {
+ return false;
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 3.0
+ */
+ public function run() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $store = $applicationFactory->getStore( '\SMW\SQLStore\SQLStore' );
+
+ $connection = $store->getConnection( 'elastic' );
+
+ // Make sure a node is available
+ if ( $connection->hasLock( ElasticClient::TYPE_DATA ) || !$connection->ping() ) {
+
+ if ( $connection->hasLock( ElasticClient::TYPE_DATA ) ) {
+ $this->params['retryCount'] = 0;
+ }
+
+ return $this->requeueRetry( $connection->getConfig() );
+ }
+
+ $elasticFactory = $applicationFactory->singleton( 'ElasticFactory' );
+
+ $this->indexer = $elasticFactory->newIndexer(
+ $store
+ );
+
+ $this->indexer->setOrigin( __METHOD__ );
+
+ $this->indexer->setLogger(
+ $applicationFactory->getMediaWikiLogger( 'smw-elastic' )
+ );
+
+ if ( $this->hasParameter( 'delete' ) ) {
+ $this->delete( $this->getParameter( 'delete' ) );
+ }
+
+ if ( $this->hasParameter( 'create' ) ) {
+ $this->create( $this->getParameter( 'create' ) );
+ }
+
+ if ( $this->hasParameter( 'index' ) ) {
+ $this->index(
+ $connection,
+ $applicationFactory->getCache(),
+ $this->getParameter( 'index' )
+ );
+ }
+
+ return true;
+ }
+
+ private function requeueRetry( $config ) {
+
+ // Give up!
+ if ( $this->getParameter( 'retryCount' ) >= $config->dotGet( 'indexer.job.recovery.retries' ) ) {
+ return true;
+ }
+
+ if ( !isset( $this->params['retryCount'] ) ) {
+ $this->params['retryCount'] = 1;
+ } else {
+ $this->params['retryCount']++;
+ }
+
+ if ( !isset( $this->params['createdAt'] ) ) {
+ $this->params['createdAt'] = time();
+ }
+
+ $job = new self( $this->title, $this->params );
+ $job->setDelay( 60 * 10 );
+
+ $job->insert();
+ }
+
+ private function delete( array $idList ) {
+ $this->indexer->delete( $idList );
+ }
+
+ private function create( $hash ) {
+ $this->indexer->create( DIWikiPage::doUnserialize( $hash ) );
+ }
+
+ private function index( $connection, $cache, $hash ) {
+
+ $subject = DIWikiPage::doUnserialize( $hash );
+ $text = '';
+
+ $changeDiff = ChangeDiff::fetch(
+ $cache,
+ $subject
+ );
+
+ if ( $connection->getConfig()->dotGet( 'indexer.raw.text', false ) ) {
+ $text = $this->indexer->fetchNativeData( $subject );
+ }
+
+ if ( $changeDiff !== false ) {
+ $this->indexer->index( $changeDiff, $text );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Rebuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Rebuilder.php
new file mode 100644
index 00000000..272f685c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Rebuilder.php
@@ -0,0 +1,421 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use Exception;
+use Onoi\MessageReporter\MessageReporterAwareTrait;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\SemanticData;
+use SMW\SQLStore\PropertyTableRowMapper;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Rebuilder {
+
+ use MessageReporterAwareTrait;
+
+ /**
+ * @var ElasticClient
+ */
+ private $client;
+
+ /**
+ * @var Indexer
+ */
+ private $indexer;
+
+ /**
+ * @var PropertyTableRowMapper
+ */
+ private $propertyTableRowMapper;
+
+ /**
+ * @var Rollover
+ */
+ private $rollover;
+
+ /**
+ * @var FileIndexer
+ */
+ private $fileIndexer;
+
+ /**
+ * @var array
+ */
+ private $settings = [];
+
+ /**
+ * @var array
+ */
+ private $versions = [];
+
+ /**
+ * @var array
+ */
+ private $options = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param ElasticClient $client
+ * @param Indexer $indexer
+ * @param PropertyTableRowMapper $propertyTableRowMapper
+ * @param Rollover $rollover
+ */
+ public function __construct( ElasticClient $client, Indexer $indexer, PropertyTableRowMapper $propertyTableRowMapper, Rollover $rollover ) {
+ $this->client = $client;
+ $this->indexer = $indexer;
+ $this->propertyTableRowMapper = $propertyTableRowMapper;
+ $this->rollover = $rollover;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function ping() {
+ return $this->client->ping();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set( $key, $value ) {
+ $this->options[$key] = $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param array $conditions
+ *
+ * @return array
+ */
+ public function select( Store $store, array $conditions ) {
+
+ $connection = $store->getConnection( 'mw.db' );
+
+ $res = $connection->select(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_id',
+ 'smw_iw'
+ ],
+ $conditions,
+ __METHOD__,
+ [ 'ORDER BY' => 'smw_id' ]
+ );
+
+ $last = $connection->selectField(
+ SQLStore::ID_TABLE,
+ 'MAX(smw_id)',
+ '',
+ __METHOD__
+ );
+
+ return [ $res, $last ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function rollover() {
+
+ if ( $this->versions === [] ) {
+ return false;
+ }
+
+ $this->rollover_version(
+ ElasticClient::TYPE_DATA,
+ $this->versions[ElasticClient::TYPE_DATA]
+ );
+
+ $this->rollover_version(
+ ElasticClient::TYPE_LOOKUP,
+ $this->versions[ElasticClient::TYPE_LOOKUP]
+ );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function prepare() {
+ $this->prepare_index( ElasticClient::TYPE_DATA );
+ $this->prepare_index( ElasticClient::TYPE_LOOKUP );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function deleteAndSetupIndices() {
+
+ $this->messageReporter->reportMessage( "\n ... deleting indices and aliases ..." );
+ $this->indexer->drop();
+
+ $this->messageReporter->reportMessage( "\n ... setting up indices and aliases ..." );
+ $this->indexer->setup();
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function createIndices() {
+ $this->create_index( ElasticClient::TYPE_DATA );
+ $this->create_index( ElasticClient::TYPE_LOOKUP );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function setDefaults() {
+
+ if ( !$this->client->hasIndex( ElasticClient::TYPE_DATA ) ) {
+ return false;
+ }
+
+ $this->messageReporter->reportMessage( "\n" . ' ... updating settings and mappings ...' );
+
+ $this->set_default( ElasticClient::TYPE_DATA );
+ $this->set_default( ElasticClient::TYPE_LOOKUP );
+
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ */
+ public function delete( $id ) {
+
+ $index = $this->client->getIndexName( ElasticClient::TYPE_DATA );
+
+ if ( isset( $this->versions[ElasticClient::TYPE_DATA] ) ) {
+ $index = $index . '-' . $this->versions[ElasticClient::TYPE_DATA];
+ }
+
+ $params = [
+ 'index' => $index,
+ 'type' => ElasticClient::TYPE_DATA,
+ 'id' => $id
+ ];
+
+ try {
+ $this->client->delete( $params );
+ } catch ( Exception $e ) {
+ // Do nothing
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param SemanticData $semanticData
+ */
+ public function rebuild( $id, SemanticData $semanticData ) {
+
+ if ( $this->fileIndexer === null ) {
+ $skip = false;
+
+ if ( isset( $this->options['skip-fileindex'] ) ) {
+ $skip = (bool)$this->options['skip-fileindex'];
+ }
+
+ if ( !$skip && $this->client->getConfig()->dotGet( 'indexer.experimental.file.ingest', false ) ) {
+ $this->fileIndexer = $this->indexer->getFileIndexer();
+ } else {
+ $this->fileIndexer = false;
+ }
+ }
+
+ $changeOp = $this->propertyTableRowMapper->newChangeOp(
+ $id,
+ $semanticData
+ );
+
+ $dataItem = $semanticData->getSubject();
+ $dataItem->setId( $id );
+
+ $this->indexer->setVersions( $this->versions );
+ $this->indexer->isRebuild();
+
+ $this->indexer->index(
+ $changeOp->newChangeDiff(),
+ $this->raw_text( $dataItem )
+ );
+
+ if ( $this->fileIndexer && $dataItem->getNamespace() === NS_FILE ) {
+ $this->fileIndexer->noSha1Check();
+ $this->fileIndexer->index( $dataItem, null );
+ }
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function refresh() {
+
+ if ( !$this->client->hasIndex( ElasticClient::TYPE_DATA ) ) {
+ return false;
+ }
+
+ $this->messageReporter->reportMessage( "\n" . ' ... refreshing indices ...' );
+
+ $this->refresh_index( ElasticClient::TYPE_DATA );
+ $this->refresh_index( ElasticClient::TYPE_LOOKUP );
+
+ return true;
+ }
+
+ private function raw_text( $dataItem ) {
+
+ if ( !$this->client->getConfig()->dotGet( 'indexer.raw.text', false ) || $dataItem->getSubobjectName() !== '' ) {
+ return '';
+ }
+
+ if ( ( $title = $dataItem->getTitle() ) !== null ) {
+ return $this->indexer->fetchNativeData( $title );
+ }
+
+ return '';
+ }
+
+ private function prepare_index( $type ) {
+
+ $index = $this->client->getIndexName( $type );
+
+ if ( isset( $this->versions[$type] ) ) {
+ $index = "$index-" . $this->versions[$type];
+ }
+
+ // @see https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html
+ $params = [
+ 'index' => $index,
+ 'body' => [
+ 'settings' => [
+ 'number_of_replicas' => 0,
+ 'refresh_interval' => -1
+ ]
+ ]
+ ];
+
+ $this->client->putSettings( $params );
+ }
+
+ private function refresh_index( $type ) {
+ $this->client->refresh( [ 'index' => $this->client->getIndexName( $type ) ] );
+ }
+
+ private function set_default( $type ) {
+
+ $indices = $this->client->indices();
+
+ $index = $this->client->getIndexName(
+ $type
+ );
+
+ $this->messageReporter->reportMessage( "\n ... '$type' index ... " );
+
+ if ( $this->client->hasLock( $type ) ) {
+ $this->rollover_version( $type, $this->client->getLock( $type ) );
+ }
+
+ $this->messageReporter->reportMessage( "\n ... closing" );
+
+ // Certain changes ( ... to define new analyzers ...) requires to close
+ // and reopen an index
+ $indices->close( [ 'index' => $index ] );
+
+ $indexDef = $this->client->getIndexDefByType(
+ $type
+ );
+
+ $indexDef = json_decode( $indexDef, true );
+
+ // Cannot be altered by a simple settings update and requires a complete
+ // rebuild
+ unset( $indexDef['settings']['number_of_shards'] );
+
+ $params = [
+ 'index' => $index,
+ 'body' => [
+ 'settings' => $indexDef['settings']
+ ]
+ ];
+
+ $this->client->putSettings( $params );
+
+ $params = [
+ 'index' => $index,
+ 'type' => $type,
+ 'body' => $indexDef['mappings']
+ ];
+
+ $this->client->putMapping( $params );
+
+ $this->messageReporter->reportMessage( ", reopening the index ... " );
+ $indices->open( [ 'index' => $index ] );
+
+
+ $this->client->releaseLock( $type );
+ }
+
+ private function create_index( $type ) {
+
+ // If for some reason a recent rebuild didn't finish, use
+ // the locked version as master
+ if ( ( $version = $this->client->getLock( $type ) ) === false ) {
+ $version = $this->client->createIndex( $type );
+ }
+
+ if ( !$this->client->hasIndex( $type ) ) {
+ $version = $this->client->createIndex( $type );
+ }
+
+ $index = $this->client->getIndexName( $type );
+ $indices = $this->client->indices();
+
+ // No Alias available, create one before the rollover
+ if ( !$indices->exists( [ 'index' => "$index" ] ) ) {
+ $actions = [
+ [ 'add' => [ 'index' => "$index-$version", 'alias' => $index ] ]
+ ];
+
+ $params['body'] = [ 'actions' => $actions ];
+
+ $indices->updateAliases( $params );
+ }
+
+ $this->versions[$type] = $version;
+ $this->client->setLock( $type, $version );
+ }
+
+ private function rollover_version( $type, $version ) {
+
+ $old = $this->rollover->rollover(
+ $type,
+ $version
+ );
+
+ $this->messageReporter->reportMessage(
+ "\n" . sprintf( " ... switching index version from %s to %s (rollover) ...", $old, $version )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/ReplicationStatus.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/ReplicationStatus.php
new file mode 100644
index 00000000..3e59c6e4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/ReplicationStatus.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\Elastic\QueryEngine\FieldMapper;
+use SMWDITime as DITime;
+use SMW\DIProperty;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ReplicationStatus {
+
+ /**
+ * @var ElasticClient
+ */
+ private $elasticClient;
+
+ /**
+ * @var FieldMapper
+ */
+ private $fieldMapper;
+
+ /**
+ * @since 3.0
+ *
+ * @param ElasticClient $elasticClient
+ */
+ public function __construct( ElasticClient $connection ) {
+ $this->connection = $connection;
+ $this->fieldMapper = new FieldMapper();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return string
+ * @throws RuntimeException
+ */
+ public function get( $key ) {
+
+ if ( !is_callable( [ $this, $key ] ) ) {
+ throw new RuntimeException( "`$key` as accessor is unknown!" );
+ }
+
+ return $this->{$key}();
+ }
+
+ /**
+ * @since 3.0
+ */
+ private function refresh_interval() {
+
+ $refresh_interval = null;
+
+ $settings = $this->connection->getSettings(
+ [
+ 'index' => $this->connection->getIndexName( ElasticClient::TYPE_DATA )
+ ]
+ );
+
+ foreach ( $settings as $key => $value ) {
+ if ( isset( $value['settings']['index']['refresh_interval'] ) ) {
+ $refresh_interval = $value['settings']['index']['refresh_interval'];
+ }
+ }
+
+ return $refresh_interval;
+ }
+
+ /**
+ * @since 3.0
+ */
+ private function last_update() {
+
+ $pid = $this->fieldMapper->getPID( \SMWSql3SmwIds::$special_ids['_MDAT'] );
+ $field = $this->fieldMapper->getField( new DIProperty( '_MDAT' ) );
+
+ $params = $this->fieldMapper->exists( "$pid.$field" );
+
+ $body = [
+ '_source' => [ "$pid.$field", "subject" ],
+ 'size' => 1,
+ 'query' => $params,
+ 'sort' => [ "$pid.$field" => [ 'order' => 'desc' ] ]
+ ];
+
+ $params = [
+ 'index' => $this->connection->getIndexName( ElasticClient::TYPE_DATA ),
+ 'type' => ElasticClient::TYPE_DATA,
+ 'body' => $body
+ ];
+
+ list( $res, $errors ) = $this->connection->search( $params );
+ $time = null;
+
+ foreach ( $res as $result ) {
+
+ if ( !isset( $result['hits'] ) ) {
+ continue;
+ }
+
+ foreach ( $result['hits'] as $key => $value ) {
+ foreach ( $value as $key => $v ) {
+ if ( $key === '_source' ) {
+ $time = DITime::newFromJD( end( $v[$pid][$field] ) );
+ }
+ }
+ }
+ }
+
+ if ( $time !== null ) {
+ $time = $time->asDateTime()->format( 'Y-m-d H:i:s' );
+ } else {
+ $time = '0000-00-00 00:00:00';
+ }
+
+ return $time;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Rollover.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Rollover.php
new file mode 100644
index 00000000..dcd10da8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Indexer/Rollover.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace SMW\Elastic\Indexer;
+
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\Elastic\Exception\NoConnectionException;
+
+/**
+ * The index uses V1/V2 to switch between versions during a rebuild allowing the
+ * index to be available while a reindex is on going and after the process has
+ * been indices will be switched (aka rollover) without down time. The index uses
+ * aliases to hide the "real" identity of the current active index.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-rollover-index.html
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Rollover {
+
+ /**
+ * @var ElasticClient
+ */
+ private $connection;
+
+ /**
+ * @since 3.0
+ *
+ * @param ElasticClient $connection
+ */
+ public function __construct( ElasticClient $connection ) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ * @param string $version
+ *
+ * @return string
+ */
+ public function rollover( $type, $version ) {
+
+ $index = $this->connection->getIndexNameByType( $type );
+ $indices = $this->connection->indices();
+
+ $params = [];
+ $actions = [];
+
+ $old = $version === 'v2' ? 'v1' : 'v2';
+ $check = false;
+
+ if ( $indices->exists( [ 'index' => "$index-$old" ] ) ) {
+ $actions = [
+ [ 'remove' => [ 'index' => "$index-$old", 'alias' => $index ] ],
+ [ 'add' => [ 'index' => "$index-$version", 'alias' => $index ] ]
+ ];
+
+ $check = true;
+ } else {
+ // No old index
+ $old = $version;
+
+ $actions = [
+ [ 'add' => [ 'index' => "$index-$version", 'alias' => $index ] ]
+ ];
+ }
+
+ $params['body'] = [ 'actions' => $actions ];
+
+ $indices->updateAliases( $params );
+
+ if ( $check && $indices->exists( [ 'index' => "$index-$old" ] ) ) {
+ $indices->delete( [ "index" => "$index-$old" ] );
+ }
+
+ $this->connection->releaseLock( $type );
+
+ return $old;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @throws NoConnectionException
+ */
+ public function update( $type ) {
+
+ // Fail hard since we expect to create an index but are unable to do so!
+ if ( !$this->connection->ping() ) {
+ throw new NoConnectionException();
+ }
+
+ $indices = $this->connection->indices();
+
+ $index = $this->connection->getIndexName(
+ $type
+ );
+
+ // Shouldn't happen but just in case where the root index is
+ // used as index but not an alias
+ if ( $indices->exists( [ 'index' => "$index" ] ) && !$indices->existsAlias( [ 'name' => "$index" ] ) ) {
+ $indices->delete( [ 'index' => "$index" ] );
+ }
+
+ // Check v1/v2 and if both exists (which shouldn't happen but most likely
+ // caused by an unfinshed rebuilder run) then use v1 as master
+ if ( $indices->exists( [ 'index' => "$index-v1" ] ) ) {
+
+ // Just in case
+ if ( $indices->exists( [ 'index' => "$index-v2" ] ) ) {
+ $indices->delete( [ 'index' => "$index-v2" ] );
+ }
+
+ $actions[] = [ 'add' => [ 'index' => "$index-v1", 'alias' => $index ] ];
+ } elseif ( $indices->exists( [ 'index' => "$index-v2" ] ) ) {
+ $actions[] = [ 'add' => [ 'index' => "$index-v2", 'alias' => $index ] ];
+ } else {
+ $version = $this->connection->createIndex( $type );
+
+ $actions = [
+ [ 'add' => [ 'index' => "$index-$version", 'alias' => $index ] ]
+ ];
+ }
+
+ $params['body'] = [ 'actions' => $actions ];
+ $indices->updateAliases( $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @throws NoConnectionException
+ */
+ public function delete( $type ) {
+
+ // Fail hard since we expect to delete an index but are unable to do so!
+ if ( !$this->connection->ping() ) {
+ throw new NoConnectionException();
+ }
+
+ $indices = $this->connection->indices();
+
+ $index = $this->connection->getIndexName(
+ $type
+ );
+
+ if ( $indices->exists( [ 'index' => "$index-v1" ] ) ) {
+ $indices->delete( [ 'index' => "$index-v1" ] );
+ }
+
+ if ( $indices->exists( [ 'index' => "$index-v2" ] ) ) {
+ $indices->delete( [ 'index' => "$index-v2" ] );
+ }
+
+ if ( $indices->exists( [ 'index' => "$index" ] ) && !$indices->existsAlias( [ 'name' => "$index" ] ) ) {
+ $indices->delete( [ 'index' => "$index" ] );
+ }
+
+ $this->connection->releaseLock( $type );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Lookup/ProximityPropertyValueLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Lookup/ProximityPropertyValueLookup.php
new file mode 100644
index 00000000..ac5005ab
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/Lookup/ProximityPropertyValueLookup.php
@@ -0,0 +1,310 @@
+<?php
+
+namespace SMW\Elastic\Lookup;
+
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\Elastic\QueryEngine\FieldMapper;
+use SMW\DataTypeRegistry;
+use SMW\DataValueFactory;
+use SMWDITime as DITime;
+use SMWDataItem as DataItem;
+use SMW\DIProperty;
+use SMW\Store;
+use SMW\RequestOptions;
+use RuntimeException;
+
+/**
+ * Experimental implementation to showcase how a Elasticsearch specific implementation
+ * for a property value lookup can be used and override the default SQL service.
+ *
+ * The class is targeted to be used for API (e.g. autocomplete etc.) intensive
+ * services.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ProximityPropertyValueLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ $this->fieldMapper = new FieldMapper();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ * @param string $value
+ * @param RequestOptions $opts
+ *
+ * @return array
+ */
+ public function lookup( DIProperty $property, $value = '', RequestOptions $opts ) {
+
+ $connection = $this->store->getConnection( 'elastic' );
+ $continueOffset = 0;
+
+ $pid = $this->fieldMapper->getPID(
+ $this->store->getObjectIds()->getSMWPropertyID( $property )
+ );
+
+ $diType = DataTypeRegistry::getInstance()->getDataItemByType(
+ $property->findPropertyTypeID()
+ );
+
+ $field = $this->fieldMapper->getField( $property );
+
+ if ( $value === '' ) {
+ // Just create a list of available values where the property exists
+ $params = $this->fieldMapper->exists( "$pid.$field" );
+
+ // Increase the range of the initial match since a property field
+ // stores are all sorts of values, this is to make sure that the
+ // aggregation has enough objects available to build a selection
+ // list that satisfies the RequestOptions::getLimit
+ $limit = 500;
+ } elseif( $diType === DataItem::TYPE_TIME ) {
+ $limit = 500;
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByProperty(
+ $property,
+ $value
+ );
+
+ $params = $this->fieldMapper->bool(
+ 'must',
+ $this->fieldMapper->range( "$pid.$field", $dataValue->getDataItem()->getJD(), SMW_CMP_GEQ )
+ );
+ } elseif( $diType === DataItem::TYPE_NUMBER ) {
+ $limit = 500;
+
+ if ( strpos( $value, '*' ) === false ) {
+ $value = "*$value*";
+ }
+
+ $params = $this->fieldMapper->bool(
+ 'must',
+ $this->fieldMapper->wildcard( "$pid.$field.keyword", $value )
+ );
+ } else {
+ $limit = 500;
+
+ if ( strpos( $value, '*' ) === false ) {
+ $value = "$value*";
+ }
+
+ $params = $this->fieldMapper->bool(
+ 'must',
+ $this->fieldMapper->match_phrase( "$pid.$field", $value )
+ );
+ }
+
+ $body = [
+ '_source' => [ "$pid.$field" ],
+ 'from' => $opts->getOffset(),
+ 'size' => $limit,
+ 'query' => $params
+ ];
+
+ $limit = $opts->getLimit() + 1;
+
+ // Aggregation is used to filter a specific value aspect from a property
+ // field contents
+ if ( $value !== '' ) {
+ // Setting size to 0 which avoids executing the fetch query of the search
+ // hereby making the request more efficient.
+ $body['size'] = 0;
+
+ $body += $this->aggs_filter( $diType, $pid, $field, $limit, $property, trim( $value, '*' ) );
+ }
+
+ if ( $opts->sort ) {
+ $body += [ 'sort' => [ "$pid.$field" => [ 'order' => $opts->sort ] ] ];
+ }
+
+ $params = [
+ 'index' => $connection->getIndexName( ElasticClient::TYPE_DATA ),
+ 'type' => ElasticClient::TYPE_DATA,
+ 'body' => $body
+ ];
+
+ list( $res, $errors ) = $connection->search( $params );
+
+ if ( isset( $res['aggregations'] ) ) {
+ list( $list, $i ) = $this->match_aggregations( $res['aggregations'], $diType, $limit );
+ } elseif ( isset( $res['hits'] ) ) {
+ list( $list, $i ) = $this->match_hits( $res['hits'], $pid, $field, $limit );
+ } else {
+ $list = [];
+ $i = 0;
+ }
+
+ if ( $list !== [] ) {
+ $list = array_values( $list );
+
+ if ( $diType === DataItem::TYPE_TIME ) {
+ foreach ( $list as $key => $value ) {
+
+ if ( strpos( $value, '/' ) !== false ) {
+ $dataItem = DITime::doUnserialize( $value );
+ } else {
+ $dataItem = DITime::newFromJD( $value );
+ }
+
+ $list[$key] = DataValueFactory::getInstance()->newDataValueByItem( $dataItem, $property )->getWikiValue();
+ }
+ }
+ }
+
+ return $list;
+ }
+
+ private function aggs_filter( $diType, $pid, $field, $limit, $property, $value ) {
+
+ // A field on ES to a property can can all different kind of values and
+ // the request is only interested in those values that match a certain
+ // prefix or affix hence use `include` to only return aggregated values
+ // that contain the search term or value
+
+ if ( $diType === DataItem::TYPE_TIME ) {
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByProperty(
+ $property,
+ $value
+ );
+
+ return [
+ 'aggs' => [
+ 'value_terms' => [
+ 'terms' => [
+ 'field' => "$pid.dat_raw",
+ 'size' => $limit,
+ "order" => [ "_key" => "asc" ],
+ 'include' => $dataValue->getDataItem()->getSerialization() . ".*"
+ ]
+ ]
+ ]
+ ];
+ }
+
+ if ( $diType === DataItem::TYPE_NUMBER ) {
+ return [
+ 'aggs' => [
+ 'value_terms' => [
+ 'terms' => [
+ 'field' => "$pid.$field.keyword",
+ 'size' => $limit,
+ "order" => [ "_key" => "asc" ],
+ 'include' => ".*" . $value . ".*"
+ ]
+ ]
+ ]
+ ];
+ }
+
+ return [
+ 'aggs' => [
+ 'value_terms' => [
+ 'terms' => [
+ 'field' => "$pid.$field.keyword",
+ 'size' => $limit,
+ 'include' =>
+ ".*" . $value . ".*|" .
+ ".*" . ucfirst( $value ) . ".*|" .
+ ".*" . mb_strtoupper( $value ) . ".*"
+ ]
+ ]
+ ]
+ ];
+ }
+
+ private function match_aggregations( $res, $diType, $limit ) {
+
+ $isNumeric = $diType === DataItem::TYPE_NUMBER;
+ $list = [];
+ $i = 0;
+
+ foreach ( $res as $aggs ) {
+ foreach ( $aggs as $val ) {
+
+ if ( !is_array( $val ) ) {
+ continue;
+ }
+
+ foreach ( $val as $v ) {
+
+ if ( $i >= $limit ) {
+ break;
+ }
+
+ if ( isset( $v['key'] ) ) {
+ $val = (string)$v['key'];
+
+ // Aggregation happens on keyword field, numerics are of type
+ // double hence is coerced as 5 -> 5.0
+ if ( $isNumeric && substr( $val, -2 ) === '.0' ) {
+ $val = substr( $val, 0, -2 );
+ }
+
+ $list[] = $val;
+ $i++;
+ }
+ }
+ }
+ }
+
+ return [ $list, $i ];
+ }
+
+ private function match_hits( $res, $pid, $field, $limit ) {
+
+ $list = [];
+ $i = 0;
+
+ foreach ( $res as $key => $value ) {
+
+ if ( $key !== 'hits' ) {
+ continue;
+ }
+
+ foreach ( $value as $v ) {
+
+ if ( !isset( $v['_source'][$pid][$field] ) ) {
+ continue;
+ }
+
+ foreach ( $v['_source'][$pid][$field] as $match ) {
+
+ if ( $i >= $limit ) {
+ break;
+ }
+
+ // Filter duplicates
+ $hash = md5( $match );
+
+ if ( isset( $list[$hash] ) ) {
+ continue;
+ }
+
+ $list[$hash] = (string)$match;
+ $i++;
+ }
+ }
+ }
+
+ return [ $list, $i ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Aggregations.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Aggregations.php
new file mode 100644
index 00000000..68beb5d6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Aggregations.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Aggregations {
+
+ /**
+ * @var array
+ */
+ private $parameters = [];
+
+ /**
+ * @var array
+ */
+ private $subAggregations = [];
+
+ /**
+ * @var boolean
+ */
+ private $plain = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param Aggregations|array $parameters
+ */
+ public function __construct( $parameters = [] ) {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Aggregations $aggregations
+ */
+ public function addSubAggregations( Aggregations $aggregations ) {
+ $this->subAggregations[] = $aggregations;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function plain() {
+ $this->plain = true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function toArray() {
+
+ $params = $this->params( $this->parameters );
+
+ foreach ( $this->subAggregations as $subAggregation ) {
+ foreach ( $params as $key => $value) {
+ $params[$key] += $subAggregation->toArray();
+ }
+ }
+
+ if ( $params === [] || $this->plain ) {
+ return $params;
+ }
+
+ return [ 'aggregations' => $params ];
+ }
+
+ private function params( &$params ) {
+
+ $aggregation = $params;
+
+ if ( $aggregation instanceof Aggregations ) {
+ $aggregation->plain();
+ $params = $aggregation->toArray();
+ }
+
+ if ( !is_array( $params ) ) {
+ return $params;
+ }
+
+ $p = [];
+
+ foreach ( $params as $k => $aggregation ) {
+ if ( $aggregation instanceof Aggregations ) {
+ $aggregation = $this->params( $aggregation );
+ }
+
+ if ( is_string( $k ) ) {
+ $p[$k] = $aggregation;
+ } else {
+ $p = array_merge( $p, $aggregation );
+ }
+ }
+
+ return $p;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function __toString() {
+ return json_encode( $this->toArray() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Condition.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Condition.php
new file mode 100644
index 00000000..a04acd11
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Condition.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Condition {
+
+ const TYPE_MUST = 'must';
+ const TYPE_SHOULD = 'should';
+ const TYPE_MUST_NOT = 'must_not';
+ const TYPE_FILTER = 'filter';
+
+ /**
+ * @var array
+ */
+ private $parameters = [];
+
+ /**
+ * @var array
+ */
+ private $logs = [];
+
+ /**
+ * @var string
+ */
+ private $type = 'must';
+
+ /**
+ * @since 3.0
+ *
+ * @param Condition|array $parameters
+ */
+ public function __construct( $parameters = [] ) {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ */
+ public function type( $type ) {
+ $this->type = $type;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param mixed $log
+ */
+ public function log( $log ) {
+ $this->logs[] = $log;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getLogs() {
+ return $this->logs;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function toArray() {
+
+ $params = $this->params( $this->parameters, $this->logs );
+
+ if ( $this->type === '' || $this->type === null || $params === [] ) {
+ return $params;
+ }
+
+ return [ 'bool' => [ $this->type => $params ] ];
+ }
+
+ private function params( $params, &$logs ) {
+
+ $condition = $params;
+
+ if ( $condition instanceof Condition ) {
+ $params = $condition->toArray();
+
+ if ( ( $rlogs = $condition->getLogs() ) !== [] ) {
+ $logs[] = $rlogs;
+ }
+ }
+
+ if ( !is_array( $params ) ) {
+ return $params;
+ }
+
+ foreach ( $params as $k => $condition ) {
+ $params[$k] = $this->params( $condition, $logs );
+ }
+
+ return $params;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function __toString() {
+ return json_encode( $this->toArray(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/ConditionBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/ConditionBuilder.php
new file mode 100644
index 00000000..09370a24
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/ConditionBuilder.php
@@ -0,0 +1,464 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+use Psr\Log\LoggerAwareTrait;
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Options;
+use SMW\HierarchyLookup;
+use SMW\Services\ServicesContainer;
+use SMW\Query\Language\ClassDescription;
+use SMW\Query\Language\ConceptDescription;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\NamespaceDescription;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ValueDescription;
+use SMW\Store;
+use SMWDataItem as DataItem;
+
+/**
+ * Build an internal representation for a SPARQL condition from individual query
+ * descriptions.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ConditionBuilder {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var TermsLookup
+ */
+ private $termsLookup;
+
+ /**
+ * @var HierarchyLookup
+ */
+ private $hierarchyLookup;
+
+ /**
+ * @var ServicesContainer
+ */
+ private $servicesContainer;
+
+ /**
+ * @var FieldMapper
+ */
+ private $fieldMapper;
+
+ /**
+ * @var ConceptDescriptionInterpreter
+ */
+ private $conceptDescriptionInterpreter;
+
+ /**
+ * @var ClassDescriptionInterpreter
+ */
+ private $classDescriptionInterpreter;
+
+ /**
+ * @var ValueDescriptionInterpreter
+ */
+ private $valueDescriptionInterpreter;
+
+ /**
+ * @var SomePropertyInterpreter
+ */
+ private $somePropertyInterpreter;
+
+ /**
+ * @var ConjunctionInterpreter
+ */
+ private $conjunctionInterpreter;
+
+ /**
+ * @var DisjunctionInterpreter
+ */
+ private $disjunctionInterpreter;
+
+ /**
+ * @var NamespaceDescriptionInterpreter
+ */
+ private $namespaceDescriptionInterpreter;
+
+ /**
+ * @var SomeValueInterpreter
+ */
+ private $someValueInterpreter;
+
+ /**
+ * @var array
+ */
+ private $sortFields = [];
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var array
+ */
+ private $queryInfo = [];
+
+ /**
+ * @var array
+ */
+ private $descriptionLog = [];
+
+ /**
+ * @var boolean
+ */
+ protected $isConstantScore = true;
+
+ /**
+ * @var boolean
+ */
+ private $initServices = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param TermsLookup $termsLookup
+ * @param HierarchyLookup $hierarchyLookup
+ * @param ServicesContainer $servicesContainer
+ */
+ public function __construct( Store $store, TermsLookup $termsLookup, HierarchyLookup $hierarchyLookup, ServicesContainer $servicesContainer ) {
+ $this->store = $store;
+ $this->termsLookup = $termsLookup;
+ $this->hierarchyLookup = $hierarchyLookup;
+ $this->servicesContainer = $servicesContainer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Options $options
+ */
+ public function setOptions( Options $options ) {
+ $this->options = $options;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function getOption( $key, $default = false ) {
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ return $this->options->safeGet( $key, $default );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $sortFields
+ */
+ public function setSortFields( array $sortFields ) {
+ $this->sortFields = $sortFields;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Store
+ */
+ public function getStore() {
+ return $this->store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return TermsLookup
+ */
+ public function getTermsLookup() {
+ return $this->termsLookup;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return FieldMapper
+ */
+ public function getFieldMapper() {
+
+ if ( $this->fieldMapper === null ) {
+ $this->fieldMapper = new FieldMapper();
+ }
+
+ return $this->fieldMapper;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param []
+ */
+ public function getQueryInfo() {
+ return $this->queryInfo;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $queryInfo
+ */
+ public function addQueryInfo( array $queryInfo ) {
+ $this->queryInfo[] = $queryInfo;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param []
+ */
+ public function getDescriptionLog() {
+ return $this->descriptionLog;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $error
+ */
+ public function addError( array $error ) {
+ $this->errors[] = $error;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return integer
+ */
+ public function getID( $dataItem ) {
+
+ if ( $dataItem instanceof DIProperty ) {
+ return (int)$this->store->getObjectIds()->getSMWPropertyID(
+ $dataItem
+ );
+ }
+
+ if ( $dataItem instanceof DIWikiPage ) {
+ return (int)$this->store->getObjectIds()->getSMWPageID(
+ $dataItem->getDBKey(),
+ $dataItem->getNamespace(),
+ $dataItem->getInterWiki(),
+ $dataItem->getSubobjectName()
+ );
+ }
+
+ return 0;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Condition|array $params
+ *
+ * @return Condition
+ */
+ public function newCondition( $params ) {
+ return new Condition( $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Description $description
+ * @param boolean $isConstantScore
+ *
+ * @return array
+ */
+ public function makeFromDescription( Description $description, $isConstantScore = true ) {
+
+ $this->errors = [];
+ $this->queryInfo = [];
+
+ $this->descriptionLog = [];
+ $this->termsLookup->clear();
+
+ if ( $this->fieldMapper === null ) {
+ $this->fieldMapper = new FieldMapper();
+ }
+
+ $this->fieldMapper->isCompatMode(
+ $this->options->safeGet( 'compat.mode', true )
+ );
+
+ // Some notes on the difference between term, match, and query
+ // string:
+ //
+ // - match, term or range queries: look for a particular value in a
+ // particular field
+ // - bool: wrap other leaf or compound queries and are used to combine
+ // multiple queries
+ // - constant_score: simply returns a constant score equal to the query
+ // boost for every document in the filter
+
+ $condition = $this->interpretDescription( $description );
+
+ if ( $condition instanceof Condition ) {
+ $query = $condition->toArray();
+ $this->descriptionLog = $condition->getLogs();
+ } else {
+ $query = $condition;
+ }
+
+ if ( $this->options->safeGet( 'sort.property.must.exists' ) && $this->sortFields !== [] ) {
+ $params = [];
+
+ foreach ( $this->sortFields as $field ) {
+ $params[] = $this->fieldMapper->exists( "$field" );
+ }
+
+ $query = $this->fieldMapper->bool( 'must', [ $query, $params ] );
+ }
+
+ // If we know we don't need any score we turn this into a `constant_score`
+ // query
+ // @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html
+ if ( $isConstantScore ) {
+ $query = $this->fieldMapper->constant_score( $query );
+ }
+
+ return $query;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DataItem|null $dataItem
+ * @param integer $hierarchyDepth
+ *
+ * @return array
+ */
+ public function findHierarchyMembers( DataItem $dataItem = null, $hierarchyDepth ) {
+
+ $ids = [];
+
+ if ( $dataItem !== null && ( $members = $this->hierarchyLookup->getConsecutiveHierarchyList( $dataItem ) ) !== [] ) {
+
+ if ( $hierarchyDepth !== null ) {
+ $members = $hierarchyDepth == 0 ? [] : array_slice( $members, 0, $hierarchyDepth );
+ }
+
+ foreach ( $members as $member ) {
+ $ids[] = $this->getID( $member );
+ }
+ }
+
+ return $ids;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Description $description
+ *
+ * @return array
+ */
+ public function interpretDescription( Description $description, $isConjunction = false ) {
+
+ $params = [];
+
+ if ( $this->initServices === false ) {
+ $this->initServices();
+ }
+
+ if ( $description instanceof SomeProperty ) {
+ $params = $this->somePropertyInterpreter->interpretDescription( $description, $isConjunction );
+ }
+
+ if ( $description instanceof ConceptDescription ) {
+ $params = $this->conceptDescriptionInterpreter->interpretDescription( $description, $isConjunction );
+ }
+
+ if ( $description instanceof ClassDescription ) {
+ $params = $this->classDescriptionInterpreter->interpretDescription( $description, $isConjunction );
+ }
+
+ if ( $description instanceof NamespaceDescription ) {
+ $params = $this->namespaceDescriptionInterpreter->interpretDescription( $description, $isConjunction );
+ }
+
+ if ( $description instanceof ValueDescription ) {
+ $params = $this->valueDescriptionInterpreter->interpretDescription( $description, $isConjunction );
+ }
+
+ if ( $description instanceof Conjunction ) {
+ $params = $this->conjunctionInterpreter->interpretDescription( $description, $isConjunction );
+ }
+
+ if ( $description instanceof Disjunction ) {
+ $params = $this->disjunctionInterpreter->interpretDescription( $description, $isConjunction );
+ }
+
+ return $params;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ValueDescription $description
+ * @param array &$options
+ *
+ * @return Condition
+ */
+ public function interpretSomeValue( ValueDescription $description, array &$options ) {
+
+ if ( $this->initServices === false ) {
+ $this->initServices();
+ }
+
+ return $this->someValueInterpreter->interpretDescription( $description, $options );
+ }
+
+ private function initServices() {
+
+ $this->somePropertyInterpreter = $this->servicesContainer->get( 'SomePropertyInterpreter', $this );
+ $this->conceptDescriptionInterpreter = $this->servicesContainer->get( 'ConceptDescriptionInterpreter', $this );
+ $this->classDescriptionInterpreter = $this->servicesContainer->get( 'ClassDescriptionInterpreter', $this );
+ $this->namespaceDescriptionInterpreter = $this->servicesContainer->get( 'NamespaceDescriptionInterpreter', $this );
+ $this->valueDescriptionInterpreter = $this->servicesContainer->get( 'ValueDescriptionInterpreter', $this );
+ $this->conjunctionInterpreter = $this->servicesContainer->get( 'ConjunctionInterpreter', $this );
+ $this->disjunctionInterpreter = $this->servicesContainer->get( 'DisjunctionInterpreter', $this );
+ $this->someValueInterpreter = $this->servicesContainer->get( 'SomeValueInterpreter', $this );
+
+ $this->initServices = true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php
new file mode 100644
index 00000000..68c49fa9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\DescriptionInterpreters;
+
+use SMW\DIProperty;
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Elastic\QueryEngine\Condition;
+use SMW\Query\Language\ClassDescription;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ClassDescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder ) {
+ $this->conditionBuilder = $conditionBuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ClassDescription $description
+ *
+ * @return Condition
+ */
+ public function interpretDescription( ClassDescription $description, $isConjunction = false ) {
+
+ $pid = 'P:' . $this->conditionBuilder->getID( new DIProperty( '_INST' ) );
+ $field = 'wpgID';
+
+ $dataItems = $description->getCategories();
+ $hierarchyDepth = $description->getHierarchyDepth();
+
+ $should = false;
+ $params = [];
+
+ // More than one member per list means OR
+ if ( count( $dataItems ) > 1 ) {
+ $should = true;
+ }
+
+ $fieldMapper = $this->conditionBuilder->getFieldMapper();
+
+ foreach ( $dataItems as $dataItem ) {
+ $value = $this->conditionBuilder->getID( $dataItem );
+
+ $p = $fieldMapper->term( "$pid.$field", $value );
+ $hierarchy = [];
+
+ $ids = $this->conditionBuilder->findHierarchyMembers( $dataItem, $hierarchyDepth );
+
+ if ( $ids !== [] ) {
+ $hierarchy[] = $fieldMapper->terms( "$pid.$field", $ids );
+ }
+
+ // Hierarchies cannot be build as part of the normal index process
+ // therefore use the consecutive list to build a chain of disjunctive
+ // (should === OR) queries to match members of the list
+ if ( $hierarchy !== [] ) {
+ $params[] = $fieldMapper->bool( Condition::TYPE_SHOULD, array_merge( [ $p ], $hierarchy ) );
+ } else {
+ $params[] = $p;
+ }
+ }
+
+ // This feature is NOT supported by the SQLStore!!
+ // Encapsulate condition for something like `[[Category:!CatTest1]] ...`
+ if ( isset( $description->isNegation ) && $description->isNegation ) {
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( Condition::TYPE_MUST_NOT );
+ } else {
+ // ??!! If the description contains more than one category then it is
+ // interpret as OR (same as the SQLStore) and only in the case of an AND
+ // it is represented as Conjunction
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( ( $should ? Condition::TYPE_SHOULD : Condition::TYPE_FILTER ) );
+ }
+
+ $condition->log( [ 'ClassDescription' => $description->getQueryString() ] );
+
+ return $condition;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php
new file mode 100644
index 00000000..76b05b36
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\DescriptionInterpreters;
+
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Query\Language\ConceptDescription;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Parser as QueryParser;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\DIProperty;
+use SMW\Options;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ConceptDescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var QueryParser
+ */
+ private $queryParser;
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $conditionBuilder
+ * @param QueryParser $queryParser
+ */
+ public function __construct( ConditionBuilder $conditionBuilder, QueryParser $queryParser ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->queryParser = $queryParser;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ConceptDescription $description
+ *
+ * @return Condition|[]
+ */
+ public function interpretDescription( ConceptDescription $description, $isConjunction = false ) {
+
+ $concept = $description->getConcept();
+
+ $value = $this->conditionBuilder->getStore()->getPropertyValues(
+ $concept,
+ new DIProperty( '_CONC' )
+ );
+
+ if ( $value === null || $value === [] ) {
+ return [];
+ }
+
+ $value = end( $value );
+
+ $description = $this->queryParser->getQueryDescription(
+ $value->getConceptQuery()
+ );
+
+ if ( $this->hasCircularConceptDescription( $description, $concept ) ) {
+ return [];
+ }
+
+ $params = $this->conditionBuilder->interpretDescription(
+ $description,
+ $isConjunction
+ );
+
+ $params = $this->terms_lookup( $description, $concept, $params );
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( '' );
+
+ $condition->log( [ 'ConceptDescription' => $description->getQueryString() ] );
+
+ return $condition;
+ }
+
+ private function terms_lookup( $description, $concept, $params ) {
+
+ $concept->setId(
+ $this->conditionBuilder->getID( $concept )
+ );
+
+ $termsLookup = $this->conditionBuilder->getTermsLookup();
+
+ $parameters = $termsLookup->newParameters(
+ [
+ 'id' => $concept->getId(),
+ 'hash' => $concept->getHash(),
+ 'query.string' => $description->getQueryString(),
+ 'fingerprint' => $description->getFingerprint(),
+ 'params' => $params,
+ ]
+ );
+
+ // Using the terms lookup to prefetch IDs from the lookup index
+ if ( $this->conditionBuilder->getOption( 'concept.terms.lookup' ) ) {
+ $params = $termsLookup->lookup( 'concept', $parameters );
+ }
+
+ if ( $parameters->has( 'query.info' ) ) {
+ $this->conditionBuilder->addQueryInfo( $parameters->get( 'query.info' ) );
+ }
+
+ return $params;
+ }
+
+ private function hasCircularConceptDescription( $description, $concept ) {
+
+ if ( $description instanceof ConceptDescription ) {
+ if ( $description->getConcept()->equals( $concept ) ) {
+ $this->conditionBuilder->addError( [ 'smw-query-condition-circular', $description->getQueryString() ] );
+ return true;
+ }
+ }
+
+ if ( $description instanceof Conjunction || $description instanceof Disjunction ) {
+ foreach ( $description->getDescriptions() as $desc ) {
+ if ( $this->hasCircularConceptDescription( $desc, $concept ) ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ConjunctionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ConjunctionInterpreter.php
new file mode 100644
index 00000000..27742e32
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ConjunctionInterpreter.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\DescriptionInterpreters;
+
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Elastic\QueryEngine\Condition;
+use SMW\Query\Language\Conjunction;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ConjunctionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder ) {
+ $this->conditionBuilder = $conditionBuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Conjunction $description
+ *
+ * @return Condition
+ */
+ public function interpretDescription( Conjunction $description ) {
+
+ $params = [];
+
+ foreach ( $description->getDescriptions() as $desc ) {
+ if ( ( $cond = $this->conditionBuilder->interpretDescription( $desc, true ) ) instanceof Condition ) {
+ $params[] = $cond;
+ }
+ }
+
+ if ( $params === [] ) {
+ return [];
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( Condition::TYPE_MUST );
+ $condition->log( [ 'Conjunction' => $description->getQueryString() ] );
+
+ return $condition;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/DisjunctionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/DisjunctionInterpreter.php
new file mode 100644
index 00000000..9cc87e07
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/DisjunctionInterpreter.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\DescriptionInterpreters;
+
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Query\Language\Disjunction;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DisjunctionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder ) {
+ $this->conditionBuilder = $conditionBuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Disjunction $description
+ *
+ * @return Condition|[]
+ */
+ public function interpretDescription( Disjunction $description, $isConjunction = false ) {
+
+ $params = [];
+ $notConditionFields = [];
+
+ foreach ( $description->getDescriptions() as $desc ) {
+
+ // Mark each as being part of a disjunction in order to to decide
+ // whether a subquery should fail as part of a conjunction or not
+ // when it relates to a disjunctive description
+ // [[Foo.bar::123]] AND [[Foobar::123]] (fails) vs.
+ // [[Foo.bar::123]] OR [[Foobar::123]]
+ $desc->isPartOfDisjunction = true;
+
+ if ( ( $param = $this->conditionBuilder->interpretDescription( $desc, true ) ) !== [] ) {
+
+ // @see SomePropertyInterpreter
+ // Collect a possible negation condition in case `must_not.property.exists`
+ // is set (which is the SMW default mode) to allow wrapping an
+ // additional condition around an OR when the existence of the
+ // queried property is required
+ if ( isset( $desc->notConditionField ) ) {
+ $notConditionFields[] = $desc->notConditionField;
+ }
+
+ $params[] = $param;
+ }
+ }
+
+ if ( $params === [] ) {
+ return [];
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( 'should' );
+
+ $condition->log( [ 'Disjunction' => $description->getQueryString() ] );
+
+ $notConditionFields = array_keys( array_flip( $notConditionFields ) );
+
+ if ( $notConditionFields === [] ) {
+ return $condition;
+ }
+
+ $existsConditions = [];
+ $fieldMapper = $this->conditionBuilder->getFieldMapper();
+
+ // Extra condition that satisfies !/OR condition (see T:Q0905#5 and
+ // T:Q1106#4)
+ //
+ // Use case: `[[Category:E-Q1106]]<q>[[Has restricted status record::!~cl*]]
+ // OR [[Has restricted status record::!~*in*]]</q>` and `[[Category:Q0905]]
+ // [[!Example/Q0905/1]] <q>[[Has page::123]] OR [[Has page::!ABCD]]</q>`
+ foreach ( $notConditionFields as $field ) {
+ $existsConditions[] = $fieldMapper->exists( $field );
+ }
+
+ // We wrap the intermediary `should` clause in an extra `must` to ensure
+ // those properties are exists for the returned documents.
+ $condition = $this->conditionBuilder->newCondition( [ $condition, $existsConditions ] );
+ $condition->type( 'must' );
+
+ return $condition;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php
new file mode 100644
index 00000000..480d2680
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\DescriptionInterpreters;
+
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Query\Language\NamespaceDescription;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class NamespaceDescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder ) {
+ $this->conditionBuilder = $conditionBuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param NamespaceDescription $description
+ *
+ * @return Condition
+ */
+ public function interpretDescription( NamespaceDescription $description, $isConjunction = false ) {
+
+ $params = [];
+ $fieldMapper = $this->conditionBuilder->getFieldMapper();
+
+ $namespace = $description->getNamespace();
+ $params = $fieldMapper->term( 'subject.namespace', $namespace );
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( '' );
+
+ if ( !$isConjunction ) {
+ $condition->type( 'filter' );
+ }
+
+ $condition->log( [ 'NamespaceDescription' => $description->getQueryString() ] );
+
+ return $condition;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php
new file mode 100644
index 00000000..85d0f646
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php
@@ -0,0 +1,464 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\DescriptionInterpreters;
+
+use Maps\Semantic\ValueDescriptions\AreaDescription;
+use SMW\DataTypeRegistry;
+use SMW\DIWikiPage;
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Elastic\QueryEngine\Condition;
+use SMW\Elastic\QueryEngine\FieldMapper;
+use SMW\Elastic\QueryEngine\QueryBuilder;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ThingDescription;
+use SMW\Query\Language\ValueDescription;
+use SMW\Query\Language\ClassDescription;
+use SMW\Query\Language\NamespaceDescription;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWDIBoolean as DIBoolean;
+use SMWDIGeoCoord as DIGeoCoord;
+use SMWDInumber as DINumber;
+use SMWDITime as DITime;
+use SMWDIUri as DIUri;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SomePropertyInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var FieldMapper
+ */
+ private $fieldMapper;
+
+ /**
+ * @var TermsLookup
+ */
+ private $termsLookup;
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder ) {
+ $this->conditionBuilder = $conditionBuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param SomeProperty $description
+ *
+ * @return array
+ */
+ public function interpretDescription( SomeProperty $description, $isConjunction = false, $isChain = false ) {
+
+ // Query types
+ //
+ // - term: query matches a single term as it is, the value is not
+ // analyzed
+ // - match_phrase: query will analyze the input, all the terms must
+ // appear in the field, they must have the same order as the input
+ // value
+
+ // Bool types
+ //
+ // - must: query must appear in matching documents and will contribute
+ // to the score
+ // - filter: query must appear in matching documents, the score
+ // of the query will be ignored
+ // - should: query should appear in the matching document
+
+ $this->fieldMapper = $this->conditionBuilder->getFieldMapper();
+ $this->termsLookup = $this->conditionBuilder->getTermsLookup();
+
+ $property = $description->getProperty();
+ $pid = $this->fieldMapper->getPID( $this->conditionBuilder->getID( $property ) );
+
+ $hierarchy = $this->conditionBuilder->findHierarchyMembers(
+ $property,
+ $description->getHierarchyDepth()
+ );
+
+ $desc = $description->getDescription();
+
+ // Copy the context
+ if ( isset( $description->isPartOfDisjunction ) ) {
+ $desc->isPartOfDisjunction = true;
+ }
+
+ $field = 'wpgID';
+ $opType = Condition::TYPE_MUST;
+
+ $field = $this->fieldMapper->getField( $property, 'Field' );
+ $params = [];
+
+ // [[Foo::Bar]]
+ if ( $desc instanceof ValueDescription ) {
+ $params = $this->interpretValueDescription( $desc, $property, $pid, $field, $opType );
+ }
+
+ // [[Foo::+]]
+ if ( $desc instanceof ThingDescription ) {
+ $params = $this->interpretThingDescription( $desc, $property, $pid, $field, $opType );
+ }
+
+ if ( $params !== [] ) {
+ $params = $this->fieldMapper->hierarchy( $params, $pid, $hierarchy );
+ }
+
+ if ( $desc instanceof ClassDescription ) {
+ $params = $this->interpretClassDescription( $desc, $property, $pid, $field );
+ }
+
+ if ( $desc instanceof NamespaceDescription ) {
+ $params = $this->interpretNamespaceDescription( $desc, $property, $pid, $field );
+ }
+
+ // [[-Person:: <q>[[Person.-Has friend.Person::Andy Mars]] [[Age::>>32]]</q> ]]
+ if ( $desc instanceof Conjunction ) {
+ $params = $this->interpretConjunction( $desc, $property, $pid, $field );
+ }
+
+ // Use case: `[[Has page-2:: <q>[[Has page-1::Value 1||Value 2]]
+ // [[Has text-1::Value 1||Value 2]]</q> || <q> [[Has page-2::Value 1||Value 2]]</q> ]]`
+ if ( $desc instanceof Disjunction ) {
+ $params = $this->interpretDisjunction( $desc, $property, $pid, $field, $opType );
+ }
+
+ if ( !$params instanceof Condition ) {
+ $condition = $this->conditionBuilder->newCondition( $params );
+ } else {
+ $condition = $params;
+ }
+
+ $condition->type( $opType );
+ $condition->log( [ 'SomeProperty' => $description->getQueryString() ] );
+
+ // [[Foo.Bar::Foobar]], [[Foo.Bar::<q>[[Foo::Bar]] OR [[Fobar::Foo]]</q>]]
+ if ( $desc instanceof SomeProperty ) {
+ $condition = $this->interpretChain( $desc, $property, $pid, $field );
+ }
+
+ if ( $condition === [] ) {
+ return [];
+ }
+
+ // Build an extra condition to restore strictness by making sure
+ // the property exist on those matched entities
+ // `[[Has text::!~foo*]]` becomes `[[Has text::!~foo*]] [[Has text::+]`
+ if ( $opType === Condition::TYPE_MUST_NOT && !$desc instanceof ThingDescription ) {
+
+ // Use case: `[[Category:Q0905]] [[!Example/Q0905/1]] <q>[[Has page::123]]
+ // OR [[Has page::!ABCD]]</q>`
+ $params = [ $this->fieldMapper->exists( "$pid.$field" ), $condition ];
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( '' );
+
+ if ( $this->conditionBuilder->getOption( 'must_not.property.exists' ) ) {
+ $description->notConditionField = "$pid.$field";
+ }
+
+ // Use case: `[[Has telephone number::!~*123*]]`
+ if ( !$isConjunction ) {
+ $condition->type( 'must' );
+ }
+ }
+
+ if ( $isChain === false ) {
+ return $condition;
+ }
+
+ if ( !isset( $description->sourceChainMemberField ) ) {
+ throw new RuntimeException( "Missing `sourceChainMemberField`" );
+ }
+
+ $parameters = $this->termsLookup->newParameters(
+ [
+ 'terms_filter.field' => $description->sourceChainMemberField,
+ 'query.string' => $description->getQueryString(),
+ 'property.key' => $property->getKey(),
+ 'params' => $condition->toArray()
+ ]
+ );
+
+ $params = $this->termsLookup->lookup( 'chain', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get( 'query.info' ) );
+
+ // Let it fail for a conjunction when the subquery returns empty!
+ if ( $params === [] && !isset( $desc->isPartOfDisjunction ) ) {
+ // Fail with a non existing condition to avoid a " ...
+ // query malformed, must start with start_object ..."
+ $params = $this->fieldMapper->exists( "empty.lookup_query" );
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->log( [ 'SomeProperty' => [ 'Chain' => $description->getQueryString() ] ] );
+
+ return $condition;
+ }
+
+ private function interpretDisjunction( $description, $property, $pid, $field, &$opType ) {
+
+ $p = [];
+ $opType = Condition::TYPE_SHOULD;
+
+ foreach ( $description->getDescriptions() as $desc ) {
+
+ $d = new SomeProperty(
+ $property,
+ $desc
+ );
+
+ $d->sourceChainMemberField = "$pid.wpgID";
+ $t = $this->conditionBuilder->interpretDescription( $d, true, true );
+
+ if ( $t !== [] ) {
+ $p[] = $t->toArray();
+ }
+ }
+
+ if ( $p === [] ) {
+ return [];
+ }
+
+ //$this->fieldMapper->bool( 'should', $p );
+ $condition = $this->conditionBuilder->newCondition( $p );
+
+ return $condition;
+ }
+
+ private function interpretClassDescription( $description, $property, $pid, $field ) {
+
+ $queryString = $description->getQueryString();
+ $condition = $this->conditionBuilder->interpretDescription( $description );
+
+ $parameters = $this->termsLookup->newParameters(
+ [
+ 'query.string' => $queryString,
+ 'field' => "$pid.wpgID",
+ 'params' => $condition
+ ]
+ );
+
+ $params = $this->termsLookup->lookup( 'predef', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get( 'query.info' ) );
+
+ if ( $params === [] ) {
+ return [];
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( Condition::TYPE_MUST );
+ $condition->log( [ 'SomeProperty' => [ 'ClassDescription' => $queryString ] ] );
+
+ return $condition;
+ }
+
+ private function interpretNamespaceDescription( $description, $property, $pid, $field ) {
+
+ $queryString = $description->getQueryString();
+ $condition = $this->conditionBuilder->interpretDescription( $description );
+
+ $parameters = $this->termsLookup->newParameters(
+ [
+ 'query.string' => $queryString,
+ 'field' => "$pid.wpgID",
+ 'params' => $condition
+ ]
+ );
+
+ $params = $this->termsLookup->lookup( 'predef', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get( 'query.info' ) );
+
+ if ( $params === [] ) {
+ return [];
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( Condition::TYPE_MUST );
+ $condition->log( [ 'SomeProperty' => [ 'NamespaceDescription' => $queryString ] ] );
+
+ return $condition;
+ }
+
+ private function interpretConjunction( $description, $property, $pid, $field ) {
+
+ $p = [];
+ $logs = [];
+ $queryString = $description->getQueryString();
+ $logs[] = $queryString;
+ $opType = Condition::TYPE_MUST;
+
+ foreach ( $description->getDescriptions() as $desc ) {
+ $params = $this->conditionBuilder->interpretDescription( $desc, true );
+
+ if ( $params !== [] ) {
+ $p[] = $params->toArray();
+ $logs = array_merge( $logs, $params->getLogs() );
+ }
+ }
+
+ if ( $p !== [] ) {
+ // We match IDs using the term lookup which is either a resource or
+ // a document field (on a txtField etc.)
+ $f = strpos( $field, 'wpg' ) !== false ? "$pid.wpgID" : "_id";
+
+ $parameters = $this->termsLookup->newParameters(
+ [
+ 'query.string' => $queryString,
+ 'field' => $f,
+ 'params' => $p
+ ]
+ );
+
+ $p = $this->termsLookup->lookup( 'predef', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get( 'query.info' ) );
+ }
+
+ // Inverse matches are always resource (aka wpgID) related
+ if ( $property->isInverse() ) {
+ $parameters = $this->termsLookup->newParameters(
+ [
+ 'query.string' => $desc->getQueryString(),
+ 'property.key' => $property->getKey(),
+ 'field' => "$pid.wpgID",
+ 'params' => $this->fieldMapper->field_filter( "$pid.wpgID", $p )
+ ]
+ );
+
+ $p = $this->termsLookup->lookup( 'inverse', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get( 'query.info' ) );
+ }
+
+ if ( $p === [] ) {
+ return [];
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $p );
+ $condition->type( '' );
+
+ $condition->log( [ 'SomeProperty' => [ 'Conjunction' => $logs ] ] );
+
+ return $condition;
+ }
+
+ private function interpretChain( $desc, $property, $pid, $field ) {
+
+ $desc->sourceChainMemberField = "$pid.wpgID";
+ $p = [];
+
+ // Use case: `[[Category:Sample-1]][[Has page-1.Has page-2:: <q>
+ // [[Has text-1::Value 1]] OR <q>[[Has text-2::Value 2]]
+ // [[Has page-2::Value 2]]</q></q> ]]`
+ if ( $desc->getDescription() instanceof Disjunction ) {
+
+ foreach ( $desc->getDescription()->getDescriptions() as $d ) {
+ $d = new SomeProperty(
+ $desc->getProperty(),
+ $d
+ );
+ $d->setMembership( $desc->getFingerprint() );
+ $d->sourceChainMemberField = "$pid.wpgID";
+
+ if ( isset( $desc->isPartOfDisjunction ) ) {
+ $d->isPartOfDisjunction = true;
+ }
+
+ $t = $this->interpretDescription( $d, true, true );
+
+ if ( $t !== [] ) {
+ $p[] = $t->toArray();
+ }
+ }
+
+ $p = $this->fieldMapper->bool( 'should', $p );
+ } else {
+ $p = $this->interpretDescription( $desc, true, true );
+ }
+
+ if ( $property->isInverse() ) {
+ $parameters = $this->termsLookup->newParameters(
+ [
+ 'query.string' => $desc->getQueryString(),
+ 'property.key' => $property->getKey(),
+ 'field' => "$pid.wpgID",
+ 'params' => $this->fieldMapper->field_filter( "$pid.wpgID", $p->toArray() )
+ ]
+ );
+
+ $p = $this->termsLookup->lookup( 'inverse', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get( 'query.info' ) );
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $p );
+ $condition->type( '' );
+
+ return $condition;
+ }
+
+ private function interpretThingDescription( $desc, $property, $pid, $field, &$opType ) {
+
+ $isResourceType = false;
+
+ if ( DataTypeRegistry::getInstance()->getDataItemByType( $property->findPropertyValueType() ) === DataItem::TYPE_WIKIPAGE ) {
+ $field = 'wpgID';
+ $isResourceType = true;
+ }
+
+ // [[Has subobject::!+]] is only supported with the ElasticStore
+ $opType = isset( $desc->isNegation ) ? Condition::TYPE_MUST_NOT : Condition::TYPE_FILTER;
+ $params = $this->fieldMapper->exists( "$pid.$field" );
+
+ // Only allow to match wpg types (aka resources) to be used as
+ // invertible query element, this matches the SQLStore behaviour
+ if ( $property->isInverse() && $isResourceType ) {
+ $parameters = $this->termsLookup->newParameters(
+ [
+ 'query.string' => $desc->getQueryString(),
+ 'property.key' => $property->getKey(),
+ 'field' => "$pid.$field",
+ 'params' => ''
+ ]
+ );
+
+ $params = $this->termsLookup->lookup( 'inverse', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get( 'query.info' ) );
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( '' );
+
+ return $condition;
+ }
+
+ private function interpretValueDescription( $desc, $property, $pid, &$field, &$type ) {
+
+ $options = [
+ 'type' => $type,
+ 'field' => $field,
+ 'pid' => $pid,
+ 'property' => $property
+ ];
+
+ $condition = $this->conditionBuilder->interpretSomeValue( $desc, $options );
+
+ $field = $options['field'];
+ $type = $options['type'];
+
+ return $condition;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/SomeValueInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/SomeValueInterpreter.php
new file mode 100644
index 00000000..54c42fd3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/SomeValueInterpreter.php
@@ -0,0 +1,538 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\DescriptionInterpreters;
+
+use Maps\Semantic\ValueDescriptions\AreaDescription;
+use SMW\DataTypeRegistry;
+use SMW\DIWikiPage;
+use SMW\DIProperty;
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Elastic\QueryEngine\Condition;
+use SMW\Elastic\QueryEngine\FieldMapper;
+use SMW\Query\Language\ValueDescription;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWDIBoolean as DIBoolean;
+use SMWDIGeoCoord as DIGeoCoord;
+use SMWDInumber as DINumber;
+use SMWDITime as DITime;
+use SMWDIUri as DIUri;
+use SMW\Utils\CharExaminer;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SomeValueInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var FieldMapper
+ */
+ private $fieldMapper;
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder ) {
+ $this->conditionBuilder = $conditionBuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ValueDescription $description
+ * @param array &$options
+ *
+ * @return Condition
+ * @throws RuntimeException
+ */
+ public function interpretDescription( ValueDescription $description, array &$options ) {
+
+ if ( !isset( $options['property'] ) || !$options['property'] instanceof DIProperty ) {
+ throw new RuntimeException( "Missing a property" );
+ }
+
+ if ( !isset( $options['pid'] ) ) {
+ throw new RuntimeException( "Missing a pid" );
+ }
+
+ $this->fieldMapper = $this->conditionBuilder->getFieldMapper();
+
+ $dataItem = $description->getDataItem();
+ $comparator = $description->getComparator();
+
+ // Normalize comparator (we don't distinguish them in Elastic)
+ if ( $comparator === SMW_CMP_PRIM_LIKE ) {
+ $comparator = SMW_CMP_LIKE;
+ }
+
+ if ( $comparator === SMW_CMP_PRIM_NLKE ) {
+ $comparator = SMW_CMP_NLKE;
+ }
+
+ if ( $comparator === SMW_CMP_NLKE || $comparator === SMW_CMP_NEQ ) {
+ $options['type'] = Condition::TYPE_MUST_NOT;
+ }
+
+ $options['comparator'] = $comparator;
+
+ if ( $dataItem instanceof DIWikiPage ) {
+ $params = $this->page( $dataItem, $options );
+ } elseif ( $dataItem instanceof DIBlob ) {
+ $params = $this->blob( $dataItem, $options );
+ } elseif ( $dataItem instanceof DIUri ) {
+ $params = $this->uri( $dataItem, $options );
+ } elseif ( $dataItem instanceof DIGeoCoord ) {
+
+ if ( $description instanceof AreaDescription ) {
+ $options['bounding_box'] = $description->getBoundingBox();
+ }
+
+ $params = $this->geo( $dataItem, $options );
+ } elseif ( $dataItem instanceof DITime ) {
+ $params = $this->plain( $dataItem->getJD(), $options );
+ } elseif ( $dataItem instanceof DIBoolean ) {
+ $params = $this->plain( $dataItem->getBoolean(), $options );
+ } elseif ( $dataItem instanceof DINumber ) {
+ $params = $this->plain( $dataItem->getNumber(), $options );
+ } else {
+ $params = $this->plain( $dataItem->getSerialization(), $options );
+ }
+
+ if ( $options['property']->isInverse() ) {
+ $options['query.string'] = $description->getQueryString();
+ $params = $this->inverse_property( $params, $options );
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+ $condition->type( $options['type'] );
+
+ return $condition;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $dataItem
+ *
+ * @return array
+ */
+ public function page( DIWikiPage $dataItem, array &$options ) {
+
+ $comparator = $options['comparator'];
+ $pid = $options['pid'];
+ $field = $options['field'];
+ $type = $options['type'];
+
+ $isSubDataType = DataTypeRegistry::getInstance()->isSubDataType(
+ $options['property']->findPropertyValueType()
+ );
+
+ if ( $comparator === SMW_CMP_EQ || $comparator === SMW_CMP_NEQ ) {
+ $field = 'wpgID';
+ $value = $this->conditionBuilder->getID( $dataItem );
+ } else {
+ $value = $dataItem->getSortKey();
+ }
+
+ if ( $this->isRange( $comparator ) ) {
+ $match = $this->fieldMapper->range( "$pid.$field", $value, $comparator );
+ } elseif ( $isSubDataType && $dataItem->getDBKey() === '' && $comparator === SMW_CMP_NEQ ) {
+ // [[Has subobject::!]] select those that are not a subobject
+ $match = $this->fieldMapper->term( "subject.subobject.keyword", '' );
+ $type = Condition::TYPE_FILTER;
+ } elseif ( $comparator === SMW_CMP_LIKE ) {
+
+ // Avoid *...* on CJK related terms so that something like
+ // [[Has text::in:名古屋]] returns a better match accuracy given that
+ // the standard analyzer splits CJK terms into single characters
+ if ( $this->conditionBuilder->getOption( 'cjk.best.effort.proximity.match', false ) && CharExaminer::isCJK( $value ) ) {
+
+ if ( $value{0} === '*' ) {
+ $value = substr( $value, 1 );
+ }
+
+ if ( substr( $value , -1 ) === '*' ) {
+ $value = substr( $value, 0, -1 );
+ }
+
+ // Use a phrase match to keep the char boundaries and avoid
+ // matching single chars
+ $value = "\"$value\"";
+ }
+
+ // Q1203
+ // [[phrase:fox jump*]] (aka ~"fox jump*") + wildcard; use match with
+ // a `multi_match` and type `phrase_prefix`
+ $isPhrase = strpos( $value, '"' ) !== false;
+ $hasWildcard = strpos( $value, '*' ) !== false;
+
+ // Match a page title, the issue is accuracy vs. proximity
+
+ // Boolean operators (+/-) are allowed? Use the query_string
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_boolean_operators
+ if ( $this->conditionBuilder->getOption( 'query_string.boolean.operators' ) && ( strpos( $value, '+' ) !== false || strpos( $value, '-' ) !== false ) ) {
+ $match = $this->fieldMapper->query_string( "$pid.$field", $value );
+ } elseif ( ( $hasWildcard && $value{0} === '*' ) && $this->conditionBuilder->getOption( 'cjk.best.effort.proximity.match', false ) && CharExaminer::isCJK( $value ) ) {
+
+ // Avoid *...* on CJK related terms so that something like
+ // [[Has page::in:名古屋]] returns a better match accuracy given that
+ // the standard analyzer splits CJK terms into single characters
+ if ( $value{0} === '*' ) {
+ $value = mb_substr( $value, 1 );
+ }
+
+ if ( mb_substr( $value , -1 ) === '*' ) {
+ $value = mb_substr( $value, 0, -1 );
+ }
+
+ // Use a phrase match to keep the char boundaries and avoid
+ // matching single chars
+ $match = $this->fieldMapper->match( "$pid.$field", "\"$value\"" );
+
+ } elseif ( ( $hasWildcard && $value{0} === '*' ) || ( strpos( $value, '~?' ) !== false && $value{0} === '?' ) ) {
+ // ES notes "... In order to prevent extremely slow wildcard queries,
+ // a wildcard term should not start with one of the wildcards
+ // * or ? ..." therefore use `query_string` instead of a
+ // `wildcard` term search
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html
+ $match = $this->fieldMapper->query_string( "$pid.$field", $value );
+ } elseif ( $hasWildcard && !$isPhrase ) {
+
+ // T:Q0910, Wildcard?
+ // - Use the term search `wildcard` with text not being
+ // analyzed which means that things like [[Has page::~Foo bar/Bar/*]]
+ // are matched strictly without manipulating the query string.
+ // - `lowercase` field with a normalizer to achieve case
+ // insensitivity
+ if ( $this->conditionBuilder->getOption( 'page.field.case.insensitive.proximity.match', true ) ) {
+ $field = "$field.lowercase";
+ } else {
+ $field = "$field.keyword";
+ }
+
+ $match = $this->fieldMapper->wildcard( "$pid.$field", $value );
+ $type = $type === Condition::TYPE_MUST ? Condition::TYPE_FILTER : $type;
+ } elseif ( $isPhrase ) {
+ $match = $this->fieldMapper->match( "$pid.$field", $value );
+ } else {
+ $match = $this->fieldMapper->query_string( "$pid.$field", $value );
+ }
+ } elseif ( $comparator === SMW_CMP_NLKE ) {
+
+ // T:Q0905, Interpreting the meaning of `!~elastic*, +sear*` which is
+ // to match non with the term `elastic*` but those that match `sear*`
+ // with the consequence that this is turned from a `must_not` to a `must`
+ if ( $this->conditionBuilder->getOption( 'query_string.boolean.operators' ) && ( strpos( $value, '+' ) !== false ) ) {
+ $type = Condition::TYPE_MUST;
+ $value = "-$value";
+ }
+
+ $match = $this->fieldMapper->query_string( "$pid.$field", $value );
+ } elseif ( $comparator === SMW_CMP_EQ ) {
+ $type = Condition::TYPE_FILTER;
+ $match = $this->fieldMapper->term( "$pid.$field", $value );
+ } elseif ( $comparator === SMW_CMP_NEQ ) {
+ $match = $this->fieldMapper->term( "$pid.$field", $value );
+ } else {
+ $match = $this->fieldMapper->match( "$pid.$field", $value, 'and' );
+ }
+
+ $options['field'] = $field;
+ $options['value'] = $value;
+ $options['type'] = $type;
+
+ return $match;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIBlob $dataItem
+ * @param array $options
+ *
+ * @return array
+ */
+ public function blob( DIBlob $dataItem, array &$options ) {
+
+ $comparator = $options['comparator'];
+ $pid = $options['pid'];
+ $field = $options['field'];
+ $type = $options['type'];
+
+ $value = $dataItem->getSerialization();
+
+ if ( $this->isRange( $comparator ) ) {
+ // Use a not_analyzed field
+ $match = $this->fieldMapper->range( "$pid.$field.keyword", $value, $comparator );
+ } elseif ( $comparator === SMW_CMP_EQ || $comparator === SMW_CMP_NEQ ) {
+
+ // #3020
+ // Use a term query where possible to allow ES to create a bitset and
+ // cache the lookup if possible
+ if ( $options['property']->findPropertyValueType() === '_keyw' ) {
+ $match = $this->fieldMapper->term( "$pid.$field.keyword", "$value" );
+ $type = $type === Condition::TYPE_MUST ? Condition::TYPE_FILTER : $type;
+ } elseif ( $this->conditionBuilder->getOption( 'text.field.case.insensitive.eq.match' ) ) {
+ // [[Has text::Template one]] == [[Has text::template one]]
+ $match = $this->fieldMapper->match_phrase( "$pid.$field", "$value" );
+ } else {
+ $match = $this->fieldMapper->term( "$pid.$field.keyword", "$value" );
+ $type = $type === Condition::TYPE_MUST ? Condition::TYPE_FILTER : $type;
+ }
+ } elseif ( $comparator === SMW_CMP_LIKE ) {
+
+ // Q1203
+ // [[phrase:fox jump*]] (aka ~"fox jump*")
+
+ // T:Q0102 Choose a `P:xxx.*` over a specific `P:xxx.txtField` field
+ // to enforce a `DisjunctionMaxQuery` as in
+ // `"(P:8316.txtField:*\\{* | P:8316.txtField.keyword:*\\{*)",`
+ $fields = [ "$pid.$field", "$pid.$field.keyword" ];
+
+ if ( $this->fieldMapper->isPhrase( $value ) ) {
+ $match = $this->fieldMapper->match( $fields, $value );
+ } else {
+ $match = $this->fieldMapper->query_string( $fields, $value );
+ }
+ } elseif ( $comparator === SMW_CMP_NLKE ) {
+
+ // T:Q0904, Interpreting the meaning of `!~elastic*, +sear*` which is
+ // to match non with the term `elastic*` but those that match `sear*`
+ // with the consequence that this is turned from a `must_not` to a `must`
+ if ( $this->conditionBuilder->getOption( 'query_string.boolean.operators' ) && ( strpos( $value, '+' ) !== false ) ) {
+ $type = Condition::TYPE_MUST;
+ $value = "-$value";
+ }
+
+ $match = $this->fieldMapper->query_string( "$pid.$field", $value );
+ } else {
+ $match = $this->fieldMapper->match( "$pid.$field", $value, 'and' );
+ }
+
+ $options['field'] = $field;
+ $options['value'] = $value;
+ $options['type'] = $type;
+
+ return $match;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIUri $dataItem
+ * @param array $options
+ *
+ * @return array
+ */
+ public function uri( DIUri $dataItem, array &$options ) {
+
+ $comparator = $options['comparator'];
+ $pid = $options['pid'];
+ $field = $options['field'];
+ $type = $options['type'];
+
+ $value = str_replace( [ '%2A' ], [ '*' ], rawurldecode( $dataItem->getUri() ) );
+
+ if ( $this->isRange( $comparator ) ) {
+ $match = $this->fieldMapper->range( "$pid.$field", $value, $comparator );
+ } elseif ( $comparator === SMW_CMP_EQ || $comparator === SMW_CMP_NEQ ) {
+
+ if ( $this->conditionBuilder->getOption( 'uri.field.case.insensitive' ) ) {
+ // As EQ, use the match_phrase to ensure that each part of the
+ // string is part of the match.
+ // T:Q0908
+ $match = $this->fieldMapper->match_phrase( "$pid.$field.lowercase", "$value" );
+ } else {
+ // Use the keyword field (not analyzed) so that the search
+ // matches the exact term
+ // T:P0419 (`http://example.org/FoO` !== `http://example.org/Foo`)
+ $match = $this->fieldMapper->term( "$pid.$field.keyword", "$value" );
+ }
+ } elseif ( $comparator === SMW_CMP_LIKE || $comparator === SMW_CMP_NLKE ) {
+
+ $value = str_replace( [ 'http://', 'https://', '=' ], [ '', '', '' ], $value );
+
+ if ( strpos( $value, 'tel:' ) !== false || strpos( $value, 'mailto:' ) !== false ) {
+ $value = str_replace( [ 'tel:', 'mailto:' ], [ '', '' ], $value );
+ $field = "$field.keyword";
+ } elseif ( $this->conditionBuilder->getOption( 'uri.field.case.insensitive' ) ) {
+ $field = "$field.lowercase";
+ }
+
+ $match = $this->fieldMapper->query_string( "$pid.$field", $value );
+ } else {
+ $match = $this->fieldMapper->match( "$pid.$field", $value, 'and' );
+ }
+
+ $options['field'] = $field;
+ $options['value'] = $value;
+
+ return $match;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIGeoCoord $dataItem
+ * @param array $options
+ *
+ * @return array
+ */
+ public function geo( DIGeoCoord $dataItem, array &$options ) {
+
+ $comparator = $options['comparator'];
+ $pid = $options['pid'];
+ $field = $options['field'];
+ $type = $options['type'];
+
+ $value = $dataItem->getSerialization();
+ $options['value'] = $value;
+
+ if ( $this->isRange( $comparator ) ) {
+ $match = $this->fieldMapper->range( "$pid.$field", $value, $comparator );
+ } elseif ( isset( $options['bounding_box'] ) ) {
+
+ // Due to "QueryShardException: Geo fields do not support exact
+ // searching, use dedicated geo queries instead" on EQ search,
+ // the geo_point is indexed as extra field geoField.point to make
+ // use of the `bounding_box` feature in ES while the standard EQ
+ // search uses the geoField string representation
+ $boundingBox = $options['bounding_box'];
+
+ $match = $this->fieldMapper->geo_bounding_box(
+ "$pid.$field.point",
+ $boundingBox['north'],
+ $boundingBox['west'],
+ $boundingBox['south'],
+ $boundingBox['east']
+ );
+ } elseif ( $comparator === SMW_CMP_LIKE ) {
+ $match = $this->fieldMapper->match( "$pid.$field", $value, 'and' );
+ } elseif ( $comparator === SMW_CMP_EQ ) {
+ $options['type'] = Condition::TYPE_FILTER;
+ $match = $this->fieldMapper->term( "$pid.$field", $value );
+ } elseif ( $comparator === SMW_CMP_NEQ ) {
+ $match = $this->fieldMapper->term( "$pid.$field", $value );
+ } else {
+ $match = $this->fieldMapper->match( "$pid.$field", $value, 'and' );
+ }
+
+ return $match;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param mixed $value
+ * @param array $options
+ *
+ * @return array
+ */
+ public function plain( $value, array &$options ) {
+
+ $comparator = $options['comparator'];
+ $pid = $options['pid'];
+ $field = $options['field'];
+
+ if ( $this->isRange( $comparator ) ) {
+ $match = $this->fieldMapper->range( "$pid.$field", $value, $comparator );
+ } elseif ( $comparator === SMW_CMP_LIKE ) {
+ $match = $this->fieldMapper->match( "$pid.$field", $value, 'and' );
+ } elseif ( $comparator === SMW_CMP_EQ ) {
+ $options['type'] = Condition::TYPE_FILTER;
+ $match = $this->fieldMapper->term( "$pid.$field", $value );
+ } elseif ( $comparator === SMW_CMP_NEQ ) {
+ $match = $this->fieldMapper->term( "$pid.$field", $value );
+ } else {
+ $match = $this->fieldMapper->match( "$pid.$field", $value, 'and' );
+ }
+
+ return $match;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param $params
+ * @param $options
+ *
+ * @return array
+ */
+ public function inverse_property( $params, $options ) {
+
+ $termsLookup = $this->conditionBuilder->getTermsLookup();
+ $comparator = $options['comparator'];
+
+ $pid = $options['pid'];
+ $property = $options['property'];
+
+ // A simple inverse is enough to fetch the inverse match for a resource
+ // [[-Has query::F0103/PageContainsAskWithTemplateUsage]]
+ if ( $comparator === SMW_CMP_EQ || $comparator === SMW_CMP_NEQ ) {
+
+ $parameters = $termsLookup->newParameters(
+ [
+ 'query.string' => $options['query.string'],
+ 'property.key' => $property->getKey(),
+ 'field' => "$pid.wpgID",
+ 'params' => $options['value']
+ ]
+ );
+
+ $params = $termsLookup->lookup( 'inverse', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get('query.info' ) );
+ } else {
+
+ $field = $options['field'];
+
+ // First we need to find entities that fulfill the condition
+ // `~*Test*` to allow to match the `-Has subobject` part from
+ // [[-Has subobject::~*Test*]]
+
+ // Either use the resource or the document field
+ $f = strpos( $field, 'wpg' ) !== false ? "$pid.wpgID" : "_id";
+
+ $parameters = $termsLookup->newParameters(
+ [
+ 'query.string' => $options['query.string'],
+ 'field' => $f,
+ 'params' => $params
+ ]
+ );
+
+ $p = $termsLookup->lookup( 'predef', $parameters );
+
+ $this->conditionBuilder->addQueryInfo( $parameters->get('query.info' ) );
+
+ $p = $this->fieldMapper->field_filter( $f, $p );
+
+ $parameters->set( 'property.key', $property->getKey() );
+ $parameters->set( 'params', $p );
+
+ $params = $termsLookup->lookup( 'inverse', $parameters );
+ $this->conditionBuilder->addQueryInfo( $parameters->get('query.info' ) );
+ }
+
+ return $params;
+ }
+
+ private function isRange( $comparator ) {
+ return $comparator === SMW_CMP_GRTR || $comparator === SMW_CMP_GEQ || $comparator === SMW_CMP_LESS || $comparator === SMW_CMP_LEQ;
+ }
+
+ private function isNot( $comparator ) {
+ return $comparator === SMW_CMP_NLKE || $comparator === SMW_CMP_NEQ;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php
new file mode 100644
index 00000000..2e817208
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php
@@ -0,0 +1,241 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\DescriptionInterpreters;
+
+use SMW\DIWikiPage;
+use SMW\Elastic\QueryEngine\ConditionBuilder;
+use SMW\Query\Language\ValueDescription;
+use SMWDIBlob as DIBlob;
+use SMWDIBoolean as DIBoolean;
+use SMWDInumber as DINumber;
+use SMWDITime as DITime;
+use SMW\Utils\CharExaminer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ValueDescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var FieldMapper
+ */
+ private $fieldMapper;
+
+ /**
+ * @since 3.0
+ *
+ * @param ConditionBuilder $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder ) {
+ $this->conditionBuilder = $conditionBuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ValueDescription $description
+ *
+ * @return Condition
+ */
+ public function interpretDescription( ValueDescription $description, $isConjunction = false ) {
+
+ $dataItem = $description->getDataItem();
+ $comparator = $description->getComparator();
+
+ $property = $description->getProperty();
+ $this->fieldMapper = $this->conditionBuilder->getFieldMapper();
+
+ $params = [];
+ $pid = false;
+ $filter = false;
+
+ if ( $property === null ) {
+ $field = "subject.sortkey";
+ } else {
+ $pid = 'P:' . $this->conditionBuilder->getID( $property );
+
+ if ( $property->isInverse() ) {
+ // Want to know if this case happens and if so we need to handle
+ // it somewhow ...
+ throw new RuntimeException( "ValueDescription with an inverted property! PID: $pid, " . $description->getQueryString() );
+ } else {
+ $field = $this->fieldMapper->getField( $property, 'Field' );
+ }
+
+ $field = "$pid.$field";
+ }
+
+ //$description->getHierarchyDepth(); ??
+ $hierarchyDepth = null;
+
+ $hierarchy = $this->conditionBuilder->findHierarchyMembers(
+ $property,
+ $hierarchyDepth
+ );
+
+ if ( $dataItem instanceof DIWikiPage && $comparator === SMW_CMP_EQ && $property === null ) {
+ // We want an exact match!
+ $field = '_id';
+ $value = $this->conditionBuilder->getID( $dataItem );
+ } elseif ( $dataItem instanceof DIWikiPage && $comparator === SMW_CMP_NEQ && $property === null ) {
+ // We want an exact match!
+ $field = '_id';
+ $value = $this->conditionBuilder->getID( $dataItem );
+ } elseif ( $dataItem instanceof DIWikiPage && $comparator === SMW_CMP_EQ ) {
+ $field = "$pid.wpgID";
+ $value = $this->conditionBuilder->getID( $dataItem );
+ } elseif ( $dataItem instanceof DIWikiPage && $comparator === SMW_CMP_NEQ ) {
+ $field = "$pid.wpgID";
+ $value = $this->conditionBuilder->getID( $dataItem );
+ } elseif ( $dataItem instanceof DIWikiPage ) {
+ $value = $dataItem->getSortKey();
+ } elseif ( $dataItem instanceof DITime ) {
+ $field = "$field.keyword";
+ $value = $dataItem->getJD();
+ } elseif ( $dataItem instanceof DIBoolean ) {
+ $value = $dataItem->getBoolean();
+ } elseif ( $dataItem instanceof DINumber ) {
+ $value = $dataItem->getNumber();
+ } else {
+ $value = $dataItem->getSerialization();
+ }
+
+ if ( $dataItem instanceof DIWikiPage && $this->isRange( $comparator ) ) {
+ $params = $this->fieldMapper->range( "$field.keyword", $value, $comparator );
+ } elseif ( $dataItem instanceof DIBlob && $comparator === SMW_CMP_EQ ) {
+ $params = $this->fieldMapper->match( "$field", "\"$value\"" );
+ } elseif ( $comparator === SMW_CMP_EQ || $comparator === SMW_CMP_NEQ ) {
+ $params = $this->fieldMapper->terms( "$field", $value );
+ } elseif ( $comparator === SMW_CMP_LIKE || $comparator === SMW_CMP_NLKE ) {
+ $params = $this->proximity_bool( $field, $value );
+ } elseif ( $this->isRange( $comparator ) ) {
+ $params = $this->fieldMapper->range( $field, $value, $comparator );
+ } else {
+ $params = $this->fieldMapper->match( $field, $value );
+ }
+
+ if ( $params !== [] && $pid ) {
+ $params = $this->fieldMapper->hierarchy( $params, $pid, $hierarchy );
+ }
+
+ $condition = $this->conditionBuilder->newCondition( $params );
+
+ if ( $this->isNot( $comparator ) && $isConjunction ) {
+ $condition->type( 'must_not' );
+ }
+
+ if ( !$isConjunction ) {
+ $condition->type( ( $this->isNot( $comparator ) ? 'must_not' : ( $filter ? 'filter' : 'must' ) ) );
+ }
+
+ $condition->log( [ 'ValueDescription' => $description->getQueryString() ] );
+
+ return $condition;
+ }
+
+ private function isRange( $comparator ) {
+ return $comparator === SMW_CMP_GRTR || $comparator === SMW_CMP_GEQ || $comparator === SMW_CMP_LESS || $comparator === SMW_CMP_LEQ;
+ }
+
+ private function isNot( $comparator ) {
+ return $comparator === SMW_CMP_NLKE || $comparator === SMW_CMP_NEQ;
+ }
+
+ private function proximity_bool( $field, $value ) {
+
+ $params = [];
+ $hasWildcard = strpos( $value, '*' ) !== false;
+
+ // Q1203
+ // [[phrase:fox jump*]] (aka ~"fox jump*") + wildcard; use match with
+ // a `multi_match` and type `phrase_prefix`
+ $isPhrase = strpos( $value, '"' ) !== false;
+ $isWide = false;
+
+ // Wide proximity uses ~~ as identifier as in [[~~ ... ]] or
+ // [[in:fox jumps]]
+ if ( $value{0} === '~' ) {
+ $isWide = true;
+
+ // Remove the ~ to avoid a `QueryShardException[Failed to parse query ...`
+ $value = substr( $value, 1 );
+
+ if ( !$hasWildcard && $this->conditionBuilder->getOption( 'wide.proximity.as.match_phrase', true ) ) {
+ $value = trim( $value, '"' );
+ $value = "\"$value\"";
+ }
+
+ $field = $this->conditionBuilder->getOption( 'wide.proximity.fields', [ 'text_copy' ] );
+ }
+
+ // Wide or simple proximity? + wildcard?
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#operator-min
+ if ( $hasWildcard && $isWide && !$isPhrase ) {
+
+ // cjk.best.effort.proximity.match
+ if ( $this->isCJK( $value ) ) {
+ // Increase match accuracy by relying on a `phrase` to define char
+ // boundaries
+ $params = $this->fieldMapper->match( $field, "\"$value\"" );
+ } else {
+ $params = $this->fieldMapper->query_string( $field, $value, [ 'minimum_should_match' => 1 ] );
+ }
+
+ } elseif ( $hasWildcard && !$isWide && !$isPhrase ) {
+ // [[~Foo/Bar/*]] (simple proximity) is only used on subject.sortkey
+ // which is why we want to use a `not_analyzed` field to exactly
+ // match the content before the *.
+ // `lowercase` uses a normalizer to achieve case insensitivity
+ if ( $this->conditionBuilder->getOption( 'page.field.case.insensitive.proximity.match', true ) ) {
+ $field = "$field.lowercase";
+ } else {
+ $field = "$field.keyword";
+ }
+
+ $params = $this->fieldMapper->wildcard( $field, $value );
+ $filter = true;
+ } else {
+ $params = $this->fieldMapper->match( $field, $value );
+ }
+
+ return $params;
+ }
+
+ /**
+ * Fields that use a standard analyzer will split CJK terms into single chars
+ * and any enclosing like *...* makes a term not applicable to the same
+ * treatment which prevents a split and hereby causing the search match to be
+ * worse off hence remove `*` in case of CJK usage.
+ */
+ private function isCJK( &$text ) {
+
+ // Only use the examiner on the standard index_def since ICU provides
+ // better CJK and may handle `*` more sufficiently
+ if ( !$this->conditionBuilder->getOption( 'cjk.best.effort.proximity.match', false ) ) {
+ return false;
+ }
+
+ if ( !CharExaminer::isCJK( $text ) ) {
+ return false;
+ }
+
+ if ( $text{0} === '*' ) {
+ $text = mb_substr( $text, 1 );
+ }
+
+ if ( mb_substr( $text , -1 ) === '*' ) {
+ $text = mb_substr( $text, 0, -1 );
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Excerpts.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Excerpts.php
new file mode 100644
index 00000000..70c7ba87
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/Excerpts.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+use SMW\DIWikiPage;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Excerpts extends \SMW\Query\Excerpts {
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage|string $hash
+ *
+ * @return string|integer|false
+ */
+ public function getExcerpt( $hash ) {
+
+ if ( $hash instanceof DIWikiPage ) {
+ $hash = $hash->getHash();
+ }
+
+ foreach ( $this->excerpts as $map ) {
+ if ( $map[0] === $hash ) {
+ return $this->format( $map[1] );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasHighlight() {
+ return $this->noHighlight ? false : true;
+ }
+
+ private function format( $v ) {
+
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html
+ // By default, highlighted text is wrapped in <em> and </em> tags
+
+ $text = '';
+
+ if ( is_array( $v ) ) {
+ foreach ( $v as $key => $value ) {
+ $text .= implode( ' ', $value ) ;
+ }
+ } else {
+ $text = $v;
+ }
+
+ if ( $this->stripTags ) {
+ $text = str_replace(
+ [ '<em>', '</em>' ],
+ [ '&lt;em&gt;', '&lt;/em&gt;' ],
+ $text
+ );
+
+ // Remove tags to avoid any output disruption
+ $text = strip_tags( $text );
+
+ $text = str_replace(
+ [ '&lt;em&gt;', '&lt;/em&gt;' ],
+ [ '<em>', '</em>' ],
+ $text
+ );
+ }
+
+ if ( $this->noHighlight ) {
+ $text = str_replace( [ '<em>', '</em>', "\n" ], [ '', '', ' ' ], $text );
+ }
+
+ return $text;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/FieldMapper.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/FieldMapper.php
new file mode 100644
index 00000000..ae3eafb5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/FieldMapper.php
@@ -0,0 +1,676 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+use SMW\DataTypeRegistry;
+use SMW\DIProperty;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FieldMapper {
+
+ const TYPE_MUST = 'must';
+ const TYPE_SHOULD = 'should';
+ const TYPE_MUST_NOT = 'must_not';
+ const TYPE_FILTER = 'filter';
+
+ /**
+ * @var boolean
+ */
+ private $isCompatMode = true;
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isCompatMode
+ */
+ public function isCompatMode( $isCompatMode ) {
+ $this->isCompatMode = $isCompatMode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ *
+ * @return string
+ */
+ public static function getPID( $id ) {
+ return "P:$id";
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ *
+ * @return string
+ */
+ public static function getFieldType( DIProperty $property ) {
+ return str_replace( [ '_' ], [ '' ], DataTypeRegistry::getInstance()->getFieldType( $property->findPropertyValueType() ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ * @param string $affix
+ *
+ * @return string
+ */
+ public static function getField( DIProperty $property, $affix = 'Field' ) {
+ return self::getFieldType( $property ) . $affix;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $value
+ *
+ * @return boolean
+ */
+ public static function isPhrase( $value = '' ) {
+ return $value{0} === '"' && substr( $value, -1 ) === '"';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $value
+ *
+ * @return boolean
+ */
+ public static function hasWildcard( $value = '' ) {
+ return strpos( $value, '*' ) !== false && strpos( $value, '\*' ) === false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $value
+ *
+ * @return boolean
+ */
+ public function containsReservedChar( $value ) {
+
+ $reservedChars = [
+ '+', '-', '=', '&&', '||', '>', '<', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '\\', '//'
+ ];
+
+ foreach ( $reservedChars as $char ) {
+ if ( strpos( $value, $char ) !== false ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @see https://stackoverflow.com/questions/9796470/random-order-pagination-elasticsearch
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
+ *
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function function_score_random( $query, $boost = 5 ) {
+ return [
+ 'function_score' => [
+ 'query' => $query,
+ "boost" => $boost,
+ "random_score" => new \stdClass(),
+ "boost_mode"=> "multiply"
+ ]
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $results
+ * @param array $params
+ *
+ * @return []
+ */
+ public function field_filter( $field, $params ) {
+
+ $idList = [];
+
+ foreach ( $params as $key => $value ) {
+
+ if ( $key === $field ) {
+ return $value;
+ }
+
+ if ( !is_array( $value ) ) {
+ return [];
+ }
+
+ return $this->field_filter( $field, $value );
+ }
+
+ return $idList;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ * @param array $params
+ *
+ * @return array
+ */
+ public function bool( $type, $params ) {
+ return [ 'bool' => [ $type => $params ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-dsl-constant-score-query.html
+ * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/filter-caching.html
+ *
+ * @since 3.0
+ *
+ * @param string $type
+ * @param array $params
+ *
+ * @return array
+ */
+ public function constant_score( $params ) {
+ return [ 'constant_score' => [ 'filter' => $params ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-filter-context.html
+ *
+ * "... filter context, a query clause ... is a simple Yes or No — no scores
+ * are calculated. Filter context is mostly used for filtering structured
+ * data ...", " ... used filters will be cached automatically ..."
+ *
+ * @since 3.0
+ *
+ * @param string $type
+ * @param array $params
+ *
+ * @return array
+ */
+ public function filter( $params ) {
+ return [ 'filter' => $params ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-dsl-geo-distance-query.html
+ *
+ * @since 3.0
+ *
+ * @param string $type
+ * @param array $params
+ *
+ * @return array
+ */
+ public function geo_distance( $field, $coordinates, $distance ) {
+ return [ 'geo_distance' => [ 'distance' => $distance, $field => $coordinates ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-dsl-geo-bounding-box-query.html
+ *
+ * @since 3.0
+ *
+ * @param string $type
+ * @param array $params
+ *
+ * @return array
+ */
+ public function geo_bounding_box( $field, $top, $left, $bottom, $right ) {
+ return [ 'geo_bounding_box' => [ $field => [ 'top' => $top , 'left' => $left, 'bottom' => $bottom, 'right' => $right ] ] ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function range( $field, $value, $comp = '' ) {
+
+ $comparators = [
+ SMW_CMP_LESS => 'lt',
+ SMW_CMP_GRTR => 'gt',
+ SMW_CMP_LEQ => 'lte',
+ SMW_CMP_GEQ => 'gte'
+ ];
+
+ return [
+ [ 'range' => [ "$field" => [ $comparators[$comp] => $value ] ] ]
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function match( $field, $value, $operator = 'or' ) {
+
+ if ( is_array( $field ) ) {
+ return $this->multi_match( $field, $value );
+ }
+
+ // Is it a phrase match as in "Foo bar"?
+ if ( $value !=='' && $value{0} === '"' && substr( $value, -1 ) === '"' ) {
+ return $this->match_phrase( $field, trim( $value, '"' ) );
+ }
+
+ if ( $operator !== 'or' ) {
+ return [
+ [ 'match' => [ "$field" => [ 'query' => $value, 'operator' => $operator ] ] ]
+ ];
+ }
+
+ return [
+ [ 'match' => [ $field => $value ] ]
+ ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html
+ *
+ * - `best_fields` Finds documents which match any field, but uses the _score
+ * from the best field
+ * - `most_fields` Finds documents which match any field and combines the
+ * _score from each field
+ * - `cross_fields` Treats fields with the same analyzer as though they were
+ * one big field and looks for each word in any field
+ * - `phrase` Runs a match_phrase query on each field and combines the _score
+ * from each field
+ * - `phrase_prefix` Runs a match_phrase_prefix query on each field and
+ * combines the _score from each field
+ *
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ * @param array $params
+ *
+ * @return string
+ */
+ public function multi_match( $fields, $value, array $params = [] ) {
+
+ //return $this->multi_match( $field, trim( $value, '"' ) , [ "type" => "phrase" ] );
+
+ if ( strpos( $value, '"' ) !== false ) {
+ $value = trim( $value, '"' );
+ $params = [ "type" => "phrase" ];
+
+ if ( strpos( $value, '*' ) !== false ) {
+ $value = trim( $value, '*' );
+ $params = [ "type" => "phrase_prefix" ];
+ }
+ }
+
+ return [
+ [ 'multi_match' => [ 'fields' => $fields, 'query' => $value ] + $params ]
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ * @param array $params
+ *
+ * @return string
+ */
+ public function match_phrase( $field, $value, array $params = [] ) {
+
+ if ( strpos( $value, '*' ) !== false ) {
+ return [
+ 'match_phrase_prefix' => [ "$field" => trim( $value, '*' ) ]
+ ];
+ }
+
+ if ( $params !== [] ) {
+ return [
+ [ 'match_phrase' => [ "$field" => [ 'query' => $value ] + $params ] ]
+ ];
+ }
+
+ return [
+ [ 'match_phrase' => [ "$field" => $value ] ]
+ ];
+ }
+
+ /**
+ * In compat mode we try to guess and normalize the query string and hereby
+ * attempt to make the search execution to match closely the SMW SQL behaviour
+ * which comes at a cost that certain ES specific constructs ((), {} etc.)
+ * cannot be used when the `compat.mode` is enabled.
+ *
+ * @since 3.0
+ *
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function query_string_compat( $value, array $params = [] ) {
+
+ $wildcard = '';
+ // $params = [];
+
+ // Reserved characters are: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /
+ // Failed the search with: {"error":{"root_cause":[{"type":"query_shard_exception","reason":"Failed to parse query [*{*]
+ // Use case: TQ0102
+ if ( $this->containsReservedChar( $value ) ) {
+ $value = str_replace(
+ [ '\\', '{', '}', '(', ')', '[', ']', '^', '=', '|', '/' , ':' ],
+ [ "\\\\", "\{", "\}", "\(", "\)", "\[", "\]", "\^", "\=", "\|", "\/", "\:" ],
+ $value
+ );
+ }
+
+ // Intended phrase or a single " char?
+ // Use case: TQ0102#13
+ if ( strpos( $value, '"' ) !== false && substr_count( $value, '"' ) < 2 ) {
+ $value = str_replace( '"' , '\"', $value );
+ } elseif ( substr_count( $value, '"' ) == 2 && strpos( $value, '~' ) !== false ) {
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_fuzziness
+ // [[Has page::phrase:some text~2]] as "some text"~2
+ list( $value, $fuzziness ) = explode( '~', $value );
+ $value = "$value\"~" . str_replace( '"', '', $fuzziness );
+ }
+
+ // In this section we add modifiers to closely emulate the "known" SMW
+ // query behaviour of matching a string by SQL in terms of %foo% ...
+
+ // Uses Boolean parameters? avoid further guessing on the behalf of the
+ // query operation as in `[[Has text::~+MariaDB -database]]`
+ if ( strpos( $value, '+' ) !== false || strpos( $value, '-' ) !== false || strpos( $value, '"' ) !== false ) {
+ // Use case: `[[Has text::~sear*, -elas*]]`
+ // The user added those parameters by themselves
+ // Avoid comma separated strings
+ $value = str_replace( ',' , '', $value );
+ } elseif ( strpos( $value, ' ' ) !== false ) {
+ // Use case: `[[Has text::~some tex*]]
+ // Intention is to search for `some` AND `tex*`
+ $value = str_replace( [ ' ', '/*' ], [ ' +', '/ *' ], $value );
+ } elseif ( strpos( $value, '/' ) !== false ) {
+ // Use case: [[~Example/0608/*]]
+ // Somehow using the input as-is returns all sorts of matches mostly
+ // due to `/` being reserved hence split the string and create a
+ // conjunction using ES boolean expression `+` (AND) as in `Example`
+ // AND `0608*`
+ // T:Q0908 `http://example.org/some_title_with_a_value` becomes
+ // `example.org +some +title +with +a +value`
+ $value = str_replace( [ '\/', '/', ' +*', '_' ], [ '/', ' +', '*', ' +' ], $value );
+ } else {
+
+ // `_` in MediaWiki represents a space therefore replace it with an
+ // `+` (AND)
+ $value = str_replace( [ '_' ], [ ' ' ], $value );
+
+ // Use case: `[[Has text::~foo*]]`, `[[Has text::~foo]]`
+ // - add Boolean + which translates into "must be present"
+ if ( $value{0} !== '*' ) {
+ $value = "+$value";
+ }
+
+ // Use case: `[[Has text::~foo bar*]]`
+ if ( strpos( $value, ' ' ) !== false && substr( $value, -1 ) === '*' ) {
+ // $value = substr( $value, 0, -1 );
+ $wildcard = '*';
+ $params[ 'analyze_wildcard'] = true;
+ }
+
+ // Use case: `[[Has text::~foo bar*]]
+ // ... ( and ) signifies precedence
+ // ... " wraps a number of tokens to signify a phrase for searching
+ if ( strpos( $value, ' ' ) !== false && strpos( $value, '"' ) === false ) {
+ // $value = "\"($value)\"$wildcard";
+ }
+ }
+
+ // Force all terms to be required by ...
+ // $params[ 'default_operator'] = 'AND';
+
+ // Disable fuzzy transpositions (ab → ba)
+ // $params[ 'fuzzy_transpositions'] = false;
+
+ return [ $value, $params ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html
+ * @since 3.0
+ *
+ * @param string|array $fields
+ * @param mixed $value
+ * @param array $params
+ *
+ * @return string
+ */
+ public function query_string( $fields, $value, array $params = [] ) {
+
+ if ( $this->isCompatMode ) {
+ list( $value, $params ) = $this->query_string_compat( $value, $params );
+ }
+
+ if ( !is_array( $fields ) ) {
+ $fields = [ $fields ];
+ }
+
+ return [
+ 'query_string' => [ 'fields' => $fields, 'query' => $value ] + $params
+ ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html
+ * @since 3.0
+ *
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function ids( $value ) {
+ return [ 'ids' => [ "values" => $value ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-dsl-term-query.html
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function term( $field, $value ) {
+ return [ 'term' => [ "$field" => $value ] ];
+ }
+
+ /**
+ * Filters documents that have fields that match any of the provided terms
+ * (not analyzed).
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-dsl-term-query.html
+ *
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function terms( $field, $value ) {
+
+ if ( !is_array( $value ) ) {
+ $value = [ $value ];
+ }
+
+ return [ 'terms' => [ "$field" => $value ] ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function wildcard( $field, $value ) {
+ return [ 'wildcard' => [ "$field" => $value ] ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $field
+ *
+ * @return string
+ */
+ public function exists( $field ) {
+ return [ 'exists' => [ "field" => "$field" ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations.html
+ *
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function aggs( $name, $params ) {
+ return [ 'aggregations' => [ "$name" => $params ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations-bucket-terms-aggregation.html
+ *
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function aggs_terms( $key, $field, $params = [] ) {
+ return [ $key => [ 'terms' => [ "field" => $field ] + $params ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations-bucket-significantterms-aggregation.html
+ *
+ * Aggregation based on terms that have undergone a significant change in
+ * popularity measured between a foreground and background set.
+ *
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public function aggs_significant_terms( $key, $field, $params = [] ) {
+ return [ $key => [ 'significant_terms' => [ "field" => $field ] + $params ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations-bucket-histogram-aggregation.html
+ *
+ * A multi-bucket values source based aggregation that can be applied on
+ * numeric values extracted from the documents. It dynamically builds fixed
+ * size (a.k.a. interval) buckets over the values.
+ *
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public static function aggs_histogram( $key, $field, $interval ) {
+ return [ $key => [ 'histogram' => [ "field" => $field, 'interval' => $interval ] ] ];
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-aggregations-bucket-datehistogram-aggregation.html
+ *
+ * A multi-bucket aggregation similar to the histogram except it can only be
+ * applied on date values.
+ *
+ * @since 3.0
+ *
+ * @param string $field
+ * @param mixed $value
+ *
+ * @return string
+ */
+ public static function aggs_date_histogram( $key, $field, $interval ) {
+ return [ $key => [ 'date_histogram' => [ "field" => $field, 'interval' => $interval ] ] ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Condition|array $params
+ * @param string $replacement
+ * @param array $hierarchy
+ *
+ * @return string
+ */
+ public function hierarchy( $params, $replacement, $hierarchy = [] ) {
+
+ if ( $hierarchy === [] ) {
+ return $params;
+ }
+
+ $str = is_array( $params ) ? json_encode( $params ) : (string)$params;
+
+ // P:, or iP:
+ list( $prefix, $id ) = explode( ':', $replacement );
+
+ $params = [];
+ $params[] = json_decode( $str, true );
+
+ foreach ( $hierarchy as $key ) {
+ // Quick and dirty to avoid iterating over an array and find a
+ // possible replacement without knowing the specific structure of
+ // an array
+ //
+ // Adding . to make it less likely that we replace a user value that
+ // appears as `P:42`
+ $params[] = json_decode( str_replace( "$replacement.", "$prefix:$key.", $str ), true );
+ }
+
+ $condition = new Condition( $params );
+
+ // Hierarchy as simple list of disjunctive (should) conditions where any
+ // of the condition is allowed to return a result. For example, a hierarchy
+ // defined as `Foo <- Foo1 <- Foo2` would be represented in terms of
+ // `[[Foo::bar]] OR [[Foo1::bar]] OR [[Foo2::bar]]`
+ $condition->type( 'should' );
+
+ return new Condition( $condition );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/QueryEngine.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/QueryEngine.php
new file mode 100644
index 00000000..8cda7893
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/QueryEngine.php
@@ -0,0 +1,369 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+use Psr\Log\LoggerAwareTrait;
+use SMW\ApplicationFactory;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\Options;
+use SMW\Query\Language\ThingDescription;
+use SMW\Query\ScoreSet;
+use SMW\QueryEngine as IQueryEngine;
+use SMW\Store;
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class QueryEngine implements IQueryEngine {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var QueryFactory
+ */
+ private $queryFactory;
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var FieldMapper
+ */
+ private $fieldMapper;
+
+ /**
+ * @var SortBuilder
+ */
+ private $sortBuilder;
+
+ /**
+ * @var array
+ */
+ private $options = [];
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var array
+ */
+ private $queryInfo = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param ConditionBuilder $conditionBuilder
+ * @param Options|null $options
+ */
+ public function __construct( Store $store, ConditionBuilder $conditionBuilder, Options $options = null ) {
+ $this->store = $store;
+ $this->options = $options;
+
+ if ( $options === null ) {
+ $this->options = new Options();
+ }
+
+ $this->queryFactory = ApplicationFactory::getInstance()->getQueryFactory();
+ $this->fieldMapper = new FieldMapper();
+
+ $this->conditionBuilder = $conditionBuilder;
+ $this->sortBuilder = new SortBuilder( $store );
+
+ $this->sortBuilder->setScoreField(
+ $this->options->dotGet( 'query.score.sortfield' )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param []
+ */
+ public function getQueryInfo() {
+ return $this->queryInfo;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ *
+ * @return QueryResult
+ */
+ public function getQueryResult( Query $query ) {
+
+// if ( ( !$this->engineOptions->get( 'smwgIgnoreQueryErrors' ) || $query->getDescription() instanceof ThingDescription ) &&
+
+ if ( ( $query->getDescription() instanceof ThingDescription ) &&
+ $query->querymode != Query::MODE_DEBUG &&
+ count( $query->getErrors() ) > 0 ) {
+ return $this->queryFactory->newQueryResult( $this->store, $query, [], false );
+ // NOTE: we check this here to prevent unnecessary work, but we check
+ // it after query processing below again in case more errors occurred.
+ } elseif ( $query->querymode == Query::MODE_NONE || $query->getLimit() < 1 ) {
+ return $this->queryFactory->newQueryResult( $this->store, $query, [], true );
+ }
+
+ $this->errors = [];
+ $body = [];
+
+ $this->queryInfo = [
+ 'smw' => [],
+ 'elastic' => [],
+ 'info' => []
+ ];
+
+ list( $sort, $sortFields, $isRandom, $isConstantScore ) = $this->sortBuilder->makeSortField(
+ $query
+ );
+
+ $description = $query->getDescription();
+
+ $this->conditionBuilder->setSortFields(
+ $sortFields
+ );
+
+ $params = $this->conditionBuilder->makeFromDescription(
+ $description,
+ $isConstantScore
+ );
+
+ $this->errors = $this->conditionBuilder->getErrors();
+ $this->queryInfo['elastic'] = $this->conditionBuilder->getQueryInfo();
+
+ if ( $isRandom ) {
+ $params = $this->fieldMapper->function_score_random( $params );
+ }
+
+ $body = [
+ // @see https://www.elastic.co/guide/en/elasticsearch/reference/6.1/search-request-source-filtering.html
+ // We only want the ID, no need for the entire document body
+ '_source' => false,
+ 'from' => $query->getOffset(),
+ 'size' => $query->getLimit() + 1, // Look ahead +1,
+ 'query' => $params
+ ];
+
+ if ( !$isRandom && $sort !== [] ) {
+ $body['sort'] = [ $sort ];
+ }
+
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html#_track_scores
+ if ( $this->sortBuilder->isScoreSort() && $query->querymode !== Query::MODE_COUNT ) {
+ $body['track_scores'] = true;
+ }
+
+ if ( $this->options->dotGet( 'query.profiling' ) ) {
+ $body['profile'] = true;
+ }
+
+ // If at all only consider the retrieval for Special:Search queries
+ if ( $query->getOption( 'highlight.fragment' ) !== false && $query->querymode !== Query::MODE_COUNT ) {
+ $this->addHighlight( $body );
+ }
+
+ $query->addErrors( $this->errors );
+
+ $connection = $this->store->getConnection( 'elastic' );
+
+ $index = $connection->getIndexNameByType(
+ ElasticClient::TYPE_DATA
+ );
+
+ $params = [
+ 'index' => $index,
+ 'type' => ElasticClient::TYPE_DATA,
+ 'body' => $body
+ ];
+
+ $this->queryInfo['elastic'][] = $params;
+
+ $this->queryInfo['smw'] = [
+ 'query' => $query->getQueryString(),
+ 'sort' => $query->getSortKeys(),
+ 'metrics' => [
+ 'query size' => $description->getSize(),
+ 'query depth' => $description->getDepth()
+ ]
+ ];
+
+ switch ( $query->querymode ) {
+ case Query::MODE_DEBUG:
+ $result = $this->newDebugQueryResult( $params );
+ break;
+ case Query::MODE_COUNT:
+ $result = $this->newCountQueryResult( $query, $params );
+ break;
+ default:
+ $result = $this->newInstanceQueryResult( $query, $params );
+ break;
+ }
+
+ return $result;
+ }
+
+ private function newDebugQueryResult( $params ) {
+
+ $params['explain'] = $this->options->dotGet( 'query.debug.explain', false );
+
+ $connection = $this->store->getConnection( 'elastic' );
+ $this->queryInfo['elastic'][] = $connection->validate( $params );
+
+ if ( ( $log = $this->conditionBuilder->getDescriptionLog() ) !== [] ) {
+ $this->queryInfo['smw']['description_log'] = $log;
+ }
+
+ if ( isset( $this->queryInfo['info'] ) && $this->queryInfo['info'] === [] ) {
+ unset( $this->queryInfo['info'] );
+ }
+
+ $info = str_replace(
+ [ '[', '<', '>', '\"', '\n' ],
+ [ '&#91;', '&lt;', '&gt;', '&quot;', '' ],
+ json_encode( $this->queryInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
+ );
+
+ $html = \Html::rawElement(
+ 'pre',
+ [
+ 'class' => 'smwpre smwpre-no-margin smw-debug-box'
+ ],
+ \Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-debug-box-header'
+ ],
+ '<big>ElasticStore debug output</big>'
+ ) . $info
+ );
+
+ return $html;
+ }
+
+ private function newCountQueryResult( $query, $params ) {
+
+ $connection = $this->store->getConnection( 'elastic' );
+ $result = $connection->count( $params );
+
+ $queryResult = $this->queryFactory->newQueryResult(
+ $this->store,
+ $query,
+ [],
+ false
+ );
+
+ $count = isset( $result['count'] ) ? $result['count'] : 0;
+ $queryResult->setCountValue( $count );
+
+ $this->queryInfo['info'] = $result;
+
+ return $queryResult;
+ }
+
+ private function newInstanceQueryResult( $query, array $params ) {
+
+ $connection = $this->store->getConnection( 'elastic' );
+ $scoreSet = new ScoreSet();
+ $excerpts = new Excerpts();
+
+ list( $res, $errors ) = $connection->search( $params );
+
+ $searchResult = new SearchResult( $res );
+
+ $results = $searchResult->getResults(
+ $query->getLimit()
+ );
+
+ $query->addErrors( $errors );
+
+ if ( $query->getOption( 'native_result' ) ) {
+ $query->native_result = json_encode( $res, JSON_PRETTY_PRINT |JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
+ }
+
+ $scores = $searchResult->get( 'scores' );
+ $excerptList = $searchResult->get( 'excerpts' );
+
+ // Use a bulk load via ` ... WHERE IN ...` instead of single requests
+ $dataItems = $this->store->getObjectIds()->getDataItemsFromList(
+ $results
+ );
+
+ // `... WHERE IN ...` doesn't guarantee to return the same order
+ $listPos = array_flip( $results );
+ $results = [];
+
+ // Relocate to the original position that returned from Elastic
+ foreach ( $dataItems as $dataItem ) {
+
+ // In case of an update lag (Elasticsearch is near real time where
+ // some shards may not yet have seen an update) make sure to hide any
+ // outdated entities we retrieve from the SQL as ID master back-end
+ if ( $dataItem->getInterwiki() === SMW_SQL3_SMWDELETEIW ) {
+ continue;
+ }
+
+ $id = $dataItem->getId();
+ $results[$listPos[$id]] = $dataItem;
+
+ if ( isset( $scores[$id] ) ) {
+ $scoreSet->addScore( $dataItem->getHash(), $scores[$id], $listPos[$id] );
+ }
+
+ if ( isset( $excerptList[$id] ) ) {
+ $excerpts->addExcerpt( $dataItem, $excerptList[$id] );
+ }
+ }
+
+ ksort( $results );
+
+ $queryResult = $this->queryFactory->newQueryResult(
+ $this->store,
+ $query,
+ $results,
+ $searchResult->get( 'continue' )
+ );
+
+ $queryResult->setScoreSet( $scoreSet );
+ $queryResult->setExcerpts( $excerpts );
+
+ return $queryResult;
+ }
+
+ private function addHighlight( &$body ) {
+
+ if ( ( $type = $this->options->dotGet( 'query.highlight.fragment.type', false ) ) === false ) {
+ return;
+ }
+
+ // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html
+ if ( !in_array( $type, [ 'plain', 'unified', 'fvh' ] ) ) {
+ return;
+ }
+
+ $body['highlight'] = [
+ 'number_of_fragments' => $this->options->dotGet( 'query.highlight.fragment.number', 1 ),
+ 'fragment_size' => $this->options->dotGet( 'query.highlight.fragment.size', 150 ),
+ 'fields' => [
+ 'attachment.content' => [ "type" => $type ],
+ 'text_raw' => [ "type" => $type ],
+ 'P*.txtField' => [ "type" => $type ]
+ ]
+ ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/SearchResult.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/SearchResult.php
new file mode 100644
index 00000000..a93142fa
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/SearchResult.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+use InvalidArgumentException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SearchResult {
+
+ /**
+ * @var []
+ */
+ private $raw = [];
+
+ /**
+ * @var []
+ */
+ private $errors = [];
+
+ /**
+ * @var []|null
+ */
+ private $results;
+
+ /**
+ * @var string
+ */
+ private $filterField = '_id';
+
+ /**
+ * @var []
+ */
+ private $container = [
+ 'info' => [],
+ 'scores' => [],
+ 'excerpts' => [],
+ 'count' => 0,
+ 'continue' => false
+ ];
+
+ /**
+ * @since 3.0
+ *
+ * @param array $raw
+ */
+ public function __construct( array $raw = [] ) {
+ $this->raw = $raw;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $errors
+ */
+ public function setErrors( array $errors ) {
+ $this->errors = $errors;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $filterField
+ */
+ public function setFilterField( $filterField ) {
+ $this->filterField = $filterField;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer|null $cutoff
+ *
+ * @return array
+ */
+ public function getResults( $cutoff = null ) {
+
+ if ( $this->results === null ) {
+ $this->filter_results( $this->raw, $cutoff );
+ }
+
+ return $this->results;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function get( $key ) {
+
+ if ( isset( $this->container[$key] ) ) {
+ return $this->container[$key];
+ }
+
+ throw new InvalidArgumentException( "`$key` is an unkown key, or is not registered." );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $cutoff
+ *
+ * @return []
+ */
+ public function filter_results( array $results, $cutoff = null ) {
+
+ $this->results = [];
+
+ $this->container = [
+ 'info' => [],
+ 'scores' => [],
+ 'excerpts' => [],
+ 'count' => 0,
+ 'continue' => false
+ ];
+
+ if ( $results === [] ) {
+ return [];
+ }
+
+ $info = $results;
+ $res = $this->filter_field( $results, $cutoff );
+
+ unset( $info['hits'] );
+ unset( $info['_shards'] );
+
+ $this->results = array_keys( $res );
+ $info['max_score'] = $results['hits']['max_score'];
+ $info['total'] = count( $res );
+
+ $this->container['info'] = $info;
+ $this->container['count'] = $info['total'];
+
+ return $this->results;
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/6.0/_search_operations.html
+ */
+ private function filter_field( $results, $cutoff ) {
+
+ $res = [];
+ $continue = false;
+
+ $scores = [];
+ $excerpts = [];
+ $i = 0;
+
+ $field = $this->filterField;
+ $pid = null;
+
+ if ( strpos( $field, '.' ) !== false ) {
+ list( $pid, $field ) = explode( '.', $field );
+ }
+
+ foreach ( $results as $key => $value ) {
+
+ if ( !isset( $value['hits'] ) ) {
+ continue;
+ }
+
+ foreach ( $value['hits'] as $k => $v ) {
+
+ if ( $cutoff !== null && $i >= $cutoff ) {
+ $continue = true;
+ break;
+ }
+
+ $ids = [];
+
+ if ( $pid !== null && isset( $v['_source'][$pid][$field] ) ) {
+ $ids = $v['_source'][$pid][$field];
+ } elseif ( isset( $v['_source'][$field] ) ) {
+ $ids = $v['_source'][$field];
+ } elseif ( isset( $v[$field] ) ) {
+ $ids = $v[$field];
+ }
+
+ $ids = (array)$ids;
+
+ foreach ( $ids as $id ) {
+ $res[$id] = true;
+
+ if ( isset( $v['_score'] ) ) {
+ $scores[$id] = $v['_score'];
+ }
+
+ if ( isset( $v['highlight'] ) ) {
+ $excerpts[$id] = $v['highlight'];
+ }
+
+ $i++;
+ }
+ }
+ }
+
+ $this->container['scores'] = $scores;
+ $this->container['count'] = 0;
+ $this->container['excerpts'] = $excerpts;
+ $this->container['continue'] = $continue;
+
+ return $res;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/SortBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/SortBuilder.php
new file mode 100644
index 00000000..6d01796e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/SortBuilder.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+use Psr\Log\LoggerAwareTrait;
+use SMW\DataTypeRegistry;
+use SMW\DIProperty;
+use SMW\Store;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SortBuilder {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var FieldMapper
+ */
+ private $fieldMapper;
+
+ /**
+ * @var string
+ */
+ private $scoreField;
+
+ /**
+ * @var boolean
+ */
+ private $isScoreSort = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ $this->fieldMapper = new FieldMapper();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $scoreField
+ */
+ public function setScoreField( $scoreField ) {
+ $this->scoreField = $scoreField;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function isScoreSort() {
+ return $this->isScoreSort;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ *
+ * @return array
+ */
+ public function makeSortField( Query $query ) {
+
+ // @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html#_memory_considerations
+ // "... the relevant sorted field values are loaded into memory. This means
+ // that per shard, there should be enough memory ... string based types,
+ // the field sorted on should not be analyzed / tokenized ... numeric
+ // types it is recommended to explicitly set the type to narrower types"
+
+ $this->isScoreSort = $query->getOption( Query::SCORE_SORT );
+
+ if ( $query->getOption( Query::SCORE_SORT ) ) {
+ return [ [ '_score' => [ 'order' => $query->getOption( Query::SCORE_SORT ) ] ], [], false, false];
+ }
+
+ return $this->getFields( $query->getSortKeys() );
+ }
+
+ private function getFields( array $sortKeys ) {
+
+ $isRandom = false;
+ $isConstantScore = true;
+ $sort = [];
+ $sortFields = [];
+ $sortKeysCount = count( $sortKeys );
+
+ foreach ( $sortKeys as $key => $order ) {
+ $order = strtolower( $order );
+ $isRandom = strpos( $order, 'rand' ) !== false;
+
+ if ( strtolower( $key ) === $this->scoreField ) {
+ $key = '_score';
+ $this->isScoreSort = true;
+ $isConstantScore = false;
+ }
+
+ if ( $key === '' || $key === '#' ) {
+ $this->addDefaultField( $sort, $order, $sortKeysCount );
+ } else {
+ $this->addField( $sort, $sortFields, $key, $order );
+ }
+ }
+
+ return [ $sort, $sortFields, $isRandom, $isConstantScore ];
+ }
+
+ private function addDefaultField( &$sort, $order, $sortKeysCount ) {
+ $sort['subject.sortkey.sort'] = [ 'order' => $order ];
+
+ // Add title as extra criteria in case an entity uses the same sortkey
+ // to clarify its relative position, @see T:P0416#8
+ // Only add the title as determining factor when no other sort parameter
+ // is available
+ if ( $sortKeysCount == 1 ) {
+ $sort['subject.title.sort'] = [ 'order' => $order ];
+ }
+ }
+
+ private function addField( &$sort, &$sortFields, $key, $order ) {
+
+ $dataTypeRegistry = DataTypeRegistry::getInstance();
+ $chain = false;
+
+ // Chain?
+ if ( strpos( $key, '.' ) !== false ) {
+ $list = explode( '.', $key );
+ $last = current( $list );
+ } else {
+ $list = [ $key ];
+ }
+
+ foreach ( $list as $key ) {
+
+ if ( $key === '_score' ) {
+ $field = '_score';
+ } else {
+ $property = DIProperty::newFromUserLabel( $key );
+
+ $field = $this->fieldMapper->getField( $property, 'Field' );
+
+ $pid = $this->fieldMapper->getPID(
+ $this->store->getObjectIds()->getSMWPropertyID( $property )
+ );
+
+ // Only record the last key to be used as possible existential
+ // enforcement
+ if ( $chain === false ) {
+ $sortFields[] = "$pid.$field";
+ }
+
+ // Use special sort field on mapped fields which is not analyzed
+ if ( $this->sort_field( $field ) ) {
+ $field = "$field.sort";
+ }
+
+ $field = "$pid.$field";
+ }
+
+ if ( !isset( $sort[$field] ) ) {
+ $sort[$field] = [ 'order' => $order ];
+ }
+
+ $chain = true;
+ }
+ }
+
+ private function sort_field( $field ) {
+ return strpos( $field, 'txt' ) !== false || strpos( $field, 'wpgField' ) !== false || strpos( $field, 'uriField' ) !== false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup.php
new file mode 100644
index 00000000..ab7177c2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine;
+
+use SMW\Elastic\QueryEngine\TermsLookup\Parameters;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+interface TermsLookup {
+
+ /**
+ * @since 3.0
+ *
+ * @return Parameters
+ */
+ public function newParameters( array $parameters = [] );
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function lookup( $key, Parameters $parameters );
+
+ /**
+ * @since 3.0
+ */
+ public function clear();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/CachingTermsLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/CachingTermsLookup.php
new file mode 100644
index 00000000..e43354e2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/CachingTermsLookup.php
@@ -0,0 +1,393 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\TermsLookup;
+
+use Onoi\Cache\Cache;
+use RuntimeException;
+use SMW\Elastic\QueryEngine\Condition;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class CachingTermsLookup extends TermsLookup {
+
+ /**
+ * Identifies the cache namespace
+ */
+ const CACHE_NAMESPACE = 'smw:elastic:lookup';
+
+ /**
+ * @var TermsLookup
+ */
+ private $termsLookup;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var []
+ */
+ private $quick_cache = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param TermsLookup $termsLookup
+ * @param Cache $cache
+ */
+ public function __construct( TermsLookup $termsLookup, Cache $cache ) {
+ $this->termsLookup = $termsLookup;
+ $this->cache = $cache;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function clear() {
+ $this->quick_cache = [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function makeCacheKey() {
+ return smwfCacheKey( self::CACHE_NAMESPACE, func_get_args() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param $type
+ * @param Parameters $parameters
+ *
+ * @return array
+ * @throws RuntimeException
+ */
+ public function lookup( $type, Parameters $parameters ) {
+
+ if ( $type === 'concept' ) {
+ return $this->concept_lookup( $parameters );
+ }
+
+ if ( $type === 'chain' ) {
+ return $this->chain_lookup( $parameters );
+ }
+
+ if ( $type === 'predef' ) {
+ return $this->predef_lookup( $parameters );
+ }
+
+ if ( $type === 'inverse' ) {
+ return $this->inverse_lookup( $parameters );
+ }
+
+ throw new RuntimeException( "$type is unknown!" );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function concept_lookup( Parameters $parameters ) {
+
+ // @see Indexer::delete
+ $parameters->set( 'id', md5( $parameters->get( 'id' ) ) );
+ $id = $parameters->get( 'id' );
+ $parameters->set( 'count', 0 );
+
+ $threshold = $this->termsLookup->getOption(
+ 'concept.terms.lookup.result.size.index.write.threshold',
+ 100
+ );
+
+ $parameters->set( 'threshold', $threshold );
+
+ $key = $this->makeCacheKey(
+ $id,
+ $threshold,
+ $parameters->get( 'fingerprint' )
+ );
+
+ if ( isset( $this->quick_cache[$key] ) ) {
+ $parameters->set( 'query.info', $this->quick_cache[$key]['info'] );
+ return $this->quick_cache[$key]['params'];
+ }
+
+ if ( ( $count = $this->cache->fetch( $key ) ) !== false ) {
+
+ $info = [
+ 'cached_concept_lookup' => $parameters->get( 'query.string' ),
+ 'count' => $count,
+ 'isFromCache' => ['id' => $id ]
+ ];
+
+ $parameters->set( 'query.info', $info );
+
+ $params = $this->termsLookup->terms_filter(
+ '_id',
+ $this->termsLookup->path_filter( $id )
+ );
+
+ $this->quick_cache[$key] = [
+ 'params' => $params,
+ 'info' => $parameters->get( 'query.info' )
+ ];
+
+ return $params;
+ }
+
+ $ttl = $this->termsLookup->getOption(
+ 'concept.terms.lookup.cache.lifetime',
+ 60
+ );
+
+ $params = $this->termsLookup->concept_index_lookup(
+ $parameters
+ );
+
+ $count = $parameters->get( 'count' );
+
+ if ( $count >= $threshold ) {
+ $this->cache->save( $key, $count, $ttl );
+ }
+
+ $this->quick_cache[$key] = [
+ 'params' => $params,
+ 'info' => $parameters->get( 'query.info' )
+ ];
+
+ if ( isset( $params['type'] ) && isset( $params['id'] ) ) {
+ $params = $this->termsLookup->terms_filter(
+ '_id',
+ $this->termsLookup->path_filter( $params['id'] )
+ );
+ }
+
+ return $params;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function chain_lookup( Parameters $parameters ) {
+
+ $params = $parameters->get( 'params' );
+
+ if ( $params instanceof Condition ) {
+ $id = 'chain:' . md5( $params->__toString() );
+ } else {
+ $id = 'chain:' . md5( json_encode( $params ) );
+ }
+
+ $parameters->set( 'id', $id );
+ $parameters->set( 'count', 0 );
+
+ $threshold = $this->termsLookup->getOption(
+ 'subquery.terms.lookup.result.size.index.write.threshold',
+ 100
+ );
+
+ $parameters->set( 'threshold', $threshold );
+
+ $key = $this->makeCacheKey(
+ $id,
+ $threshold
+ );
+
+ if ( ( $count = $this->cache->fetch( $key ) ) !== false ) {
+
+ $info = [
+ 'cached_chain_lookup' => [
+ $parameters->get( 'property.key' ),
+ $parameters->get( 'query.string' )
+ ],
+ 'count' => $count,
+ 'isFromCache' => [ 'id' => $id ]
+ ];
+
+ $parameters->set( 'query.info', $info );
+
+ $params = $this->termsLookup->terms_filter(
+ $parameters->get( 'terms_filter.field' ),
+ $this->termsLookup->path_filter( $id )
+ );
+
+ return $params;
+ }
+
+ $ttl = $this->termsLookup->getOption(
+ 'subquery.terms.lookup.cache.lifetime',
+ 60
+ );
+
+ $params = $this->termsLookup->chain_index_lookup(
+ $parameters
+ );
+
+ $count = $parameters->get( 'count' );
+
+ if ( $count >= $threshold ) {
+ $this->cache->save( $key, $count, $ttl );
+ }
+
+ return $params;
+ }
+
+ /**
+ * `[[Has monolingual text:: <q>[[Text::two]] [[Language code::fr]]</q> ]] [[Has number::123]]`
+ *
+ * @since 3.0
+ *
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function predef_lookup( Parameters $parameters ) {
+
+ $params = $parameters->get( 'params' );
+
+ if ( $params instanceof Condition ) {
+ $id = 'pre:' . md5( $params->__toString() );
+ } else {
+ $id = 'pre:' . md5( json_encode( $params ) );
+ }
+
+ $parameters->set( 'id', $id );
+ $parameters->set( 'count', 0 );
+
+ $threshold = $this->termsLookup->getOption(
+ 'subquery.terms.lookup.result.size.index.write.threshold',
+ 100
+ );
+
+ $parameters->set( 'threshold', $threshold );
+
+ $key = $this->makeCacheKey(
+ $id,
+ $threshold
+ );
+
+ if ( ( $count = $this->cache->fetch( $key ) ) !== false ) {
+
+ $info = [
+ 'cached_predefined_lookup' => $parameters->get( 'query.string' ),
+ 'count' => $count,
+ 'isFromCache' => [ 'id' => $id ]
+ ];
+
+ $parameters->set( 'query.info', $info );
+
+ $params = $this->termsLookup->terms_filter(
+ $parameters->get( 'field' ),
+ $this->termsLookup->path_filter( $id )
+ );
+
+ return $params;
+ }
+
+ $ttl = $this->termsLookup->getOption(
+ 'subquery.terms.lookup.cache.lifetime',
+ 60
+ );
+
+ $params = $this->termsLookup->predef_index_lookup(
+ $parameters
+ );
+
+ $count = $parameters->get( 'count' );
+
+ if ( $count >= $threshold ) {
+ $this->cache->save( $key, $count, $ttl );
+ }
+
+ return $params;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function inverse_lookup( Parameters $parameters ) {
+
+ $params = $parameters->get( 'params' );
+
+ if ( $params instanceof Condition ) {
+ $id = 'inv:' . md5( $parameters->get( 'field' ) . $params->__toString() );
+ } else {
+ $id = 'inv:' . md5( json_encode( [ $parameters->get( 'field' ), $params ] ) );
+ }
+
+ $parameters->set( 'id', $id );
+ $parameters->set( 'count', 0 );
+
+ $threshold = $this->termsLookup->getOption(
+ 'subquery.terms.lookup.result.size.index.write.threshold',
+ 100
+ );
+
+ $parameters->set( 'terms_filter.field', '_id' );
+ $parameters->set( 'threshold', $threshold );
+
+ $key = $this->makeCacheKey(
+ $id,
+ $threshold
+ );
+
+ if ( ( $count = $this->cache->fetch( $key ) ) !== false ) {
+
+ $info = [
+ 'cached_inverse_lookup' => [
+ $parameters->get( 'property.key' ),
+ $parameters->get( 'query.string' )
+ ],
+ 'count' => $count,
+ 'isFromCache' => [ 'id' => $id ]
+ ];
+
+ $parameters->set( 'query.info', $info );
+
+ // Return the _id field
+ $params = $this->termsLookup->terms_filter(
+ $parameters->get( 'terms_filter.field' ),
+ $this->termsLookup->path_filter( $id )
+ );
+
+ return $params;
+ }
+
+ $ttl = $this->termsLookup->getOption(
+ 'subquery.terms.lookup.cache.lifetime',
+ 60
+ );
+
+ $params = $this->termsLookup->inverse_index_lookup(
+ $parameters
+ );
+
+ $count = $parameters->get( 'count' );
+
+ if ( $count >= $threshold ) {
+ $this->cache->save( $key, $count, $ttl );
+ }
+
+ return $params;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/Parameters.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/Parameters.php
new file mode 100644
index 00000000..189dfb6a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/Parameters.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\TermsLookup;
+
+use InvalidArgumentException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Parameters {
+
+ /**
+ * @var array
+ */
+ private $parameters = [];
+
+ /**
+ * @since 3.0
+ */
+ public function __construct( array $parameters = [] ) {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set( $key, $value ) {
+ $this->parameters[$key] = $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param array $value
+ */
+ public function merge( $key, array $value ) {
+
+ if ( !isset( $this->parameters[$key] ) ) {
+ $this->parameters[$key] = [];
+ }
+
+ $this->parameters[$key] += $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function has( $key ) {
+ return isset( $this->parameters[$key] ) || array_key_exists( $key, $this->parameters );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public function get( $key ) {
+
+ if ( $this->has( $key ) ) {
+ return $this->parameters[$key];
+ }
+
+ throw new InvalidArgumentException( "$key is an unregistered key." );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/TermsLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/TermsLookup.php
new file mode 100644
index 00000000..92f656ac
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/QueryEngine/TermsLookup/TermsLookup.php
@@ -0,0 +1,415 @@
+<?php
+
+namespace SMW\Elastic\QueryEngine\TermsLookup;
+
+use Psr\Log\LoggerAwareTrait;
+use RuntimeException;
+use SMW\Elastic\Connection\Client as ElasticClient;
+use SMW\Elastic\QueryEngine\Condition;
+use SMW\Elastic\QueryEngine\TermsLookup as ITermsLookup;
+use SMW\Elastic\QueryEngine\FieldMapper;
+use SMW\Elastic\QueryEngine\SearchResult;
+use SMW\Options;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TermsLookup implements ITermsLookup {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var FieldMapper
+ */
+ private $fieldMapper;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param Options $options
+ */
+ public function __construct( Store $store, Options $options = null ) {
+ $this->store = $store;
+ $this->options = $options;
+
+ if ( $options === null ) {
+ $this->options = new Options();
+ }
+
+ $this->fieldMapper = new FieldMapper();
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function clear() {}
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ *
+ * @return Parameters
+ */
+ public function newParameters( array $parameters = [] ) {
+ return new Parameters( $parameters );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function getOption( $key, $default = false ) {
+ return $this->options->safeGet( $key, $default );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param $type
+ * @param Parameters $parameters
+ *
+ * @return array
+ * @throws RuntimeException
+ */
+ public function lookup( $type, Parameters $parameters ) {
+
+ if ( $type === 'concept' ) {
+ return $this->concept_index_lookup( $parameters );
+ }
+
+ if ( $type === 'chain' ) {
+ return $this->chain_index_lookup( $parameters );
+ }
+
+ if ( $type === 'predef' ) {
+ return $this->predef_index_lookup( $parameters );
+ }
+
+ if ( $type === 'inverse' ) {
+ return $this->inverse_index_lookup( $parameters );
+ }
+
+ throw new RuntimeException( "$type is unknown!" );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function concept_index_lookup( Parameters $parameters ) {
+
+ $params = $parameters->get( 'params' );
+ $query = $params instanceof Condition ? $params->toArray() : $params;
+
+ if ( $this->options->safeGet( 'subquery.constant.score', true ) ) {
+ $query = $this->fieldMapper->constant_score( $query );
+ }
+
+ $parameters->set( 'search.body', [ "_source" => false, 'query' => $query ] );
+ $parameters->set( 'result_filter.field', '_id' );
+
+ $info = [
+ 'concept_lookup_query' => [
+ $parameters->get( 'hash' ),
+ $parameters->get( 'query.string' )
+ ]
+ ];
+
+ $parameters->set( 'query.info', $info );
+
+ $results = $this->query_result( $parameters );
+
+ // Already in the `terms_filter` structure?
+ if ( isset( $results['type'] ) && isset( $results['id'] ) ) {
+ return $results;
+ }
+
+ return $this->ids_filter( $results );
+ }
+
+ /**
+ * Chainable queries (or better subqueries) aren't natively supported in ES.
+ *
+ * This creates its own query and executes it as independent transaction to
+ * return a list of matchable `_id` to can be fed to the source query.
+ *
+ * @since 3.0
+ *
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function chain_index_lookup( Parameters $parameters ) {
+
+ $id = $parameters->get( 'id' );
+
+ $query = $this->fieldMapper->bool( 'must', $parameters->get( 'params' ) );
+
+ if ( $this->options->safeGet( 'subquery.constant.score', true ) ) {
+ $query = $this->fieldMapper->constant_score( $query );
+ }
+
+ $parameters->set( 'search.body', [ "_source" => false, 'query' => $query ] );
+ $parameters->set( 'result_filter.field', '_id' );
+
+ $info = [
+ 'chain_lookup_query' => [
+ $parameters->get( 'property.key' ),
+ $parameters->get( 'query.string' )
+ ]
+ ];
+
+ $parameters->set( 'query.info', $info );
+
+ return $this->terms_filter( $parameters->get( 'terms_filter.field' ), $this->query_result( $parameters ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function predef_index_lookup( Parameters $parameters ) {
+
+ $id = $parameters->get( 'id' );
+ $params = $parameters->get( 'params' );
+
+ if ( $params instanceof Condition ) {
+ $query = $params->toArray();
+ } else {
+ $query = $this->fieldMapper->bool( 'must', $params );
+ }
+
+ if ( $this->options->safeGet( 'subquery.constant.score', true ) ) {
+ $query = $this->fieldMapper->constant_score( $query );
+ }
+
+ $parameters->set( 'search.body', [ "_source" => false, 'query' => $query ] );
+ $parameters->set( 'result_filter.field', '_id' );
+
+ $info = [
+ 'predef_lookup_query' => $parameters->get( 'query.string' )
+ ];
+
+ $parameters->set( 'query.info', $info );
+
+ return $this->terms_filter( $parameters->get( 'field' ), $this->query_result( $parameters ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Parameters $parameters
+ *
+ * @return array
+ */
+ public function inverse_index_lookup( Parameters $parameters ) {
+
+ $id = $parameters->get( 'id' );
+ $params = $parameters->get( 'params' );
+
+ $info = [
+ 'inverse_lookup_query' => [
+ $parameters->get( 'property.key' ),
+ $parameters->get( 'query.string' )
+ ]
+ ];
+
+ $parameters->set( 'query.info', $info + [ 'empty' ] );
+
+ if ( !is_string( $params ) && ( $params === [] || $params == 0 ) ) {
+ return [];
+ }
+
+ $field = $parameters->get( 'field' );
+
+ if ( $params === '' ) {
+ $query = $this->fieldMapper->bool( 'must', $this->fieldMapper->exists( "$field" ) );
+ // [[-Has subobject::+]] vs. [[-Has number::+]]
+ $field = strpos( $field, 'wpg' ) !== false ? $field : "_id";
+ } else {
+ $query = $this->fieldMapper->bool( 'must', $this->fieldMapper->terms( '_id', $params ) );
+ }
+
+ if ( $this->options->safeGet( 'subquery.constant.score', true ) ) {
+ $query = $this->fieldMapper->constant_score( $query );
+ }
+
+ $parameters->set( 'search.body', [ "_source" => [ $field ], 'query' => $query ] );
+ $parameters->set( 'result_filter.field', $field );
+ $parameters->set( 'query.info', $info );
+
+ return $this->terms_filter( '_id', $this->query_result( $parameters ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $field
+ * @param array $params
+ *
+ * @return array
+ */
+ public function terms_filter( $field, $params ) {
+
+ if ( $params === [] ) {
+ // Fail with a non existing condition to avoid a " ...
+ // query malformed, must start with start_object ..."
+ return $this->fieldMapper->exists( "empty.lookup_query" );
+ }
+
+ $params = $this->fieldMapper->terms(
+ $field,
+ $params
+ );
+
+ // if ( $this->options->safeGet( 'subquery.constant.score', true ) ) {
+ // $params = $this->fieldMapper->constant_score( $params );
+ // }
+
+ return $params;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return array
+ */
+ public function ids_filter( $params ) {
+
+ if ( $params === [] ) {
+ // Fail with a non existing condition to avoid a " ...
+ // query malformed, must start with start_object ..."
+ return $this->fieldMapper->exists( "empty.lookup_query" );
+ }
+
+ $params = $this->fieldMapper->ids(
+ $params
+ );
+
+ return $params;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ *
+ * @return array
+ */
+ public function path_filter( $id ) {
+
+ $connection = $this->store->getConnection( 'elastic' );
+
+ $params = [
+ 'index' => $connection->getIndexName( ElasticClient::TYPE_LOOKUP ),
+ 'type' => ElasticClient::TYPE_LOOKUP,
+ 'id' => $id
+ ];
+
+ // Define path for the terms filter
+ return $params + [ 'path' => 'id' ];
+ }
+
+ private function query_result( Parameters $parameters ) {
+
+ $connection = $this->store->getConnection( 'elastic' );
+ $info = $parameters->get( 'query.info' );
+
+ $params = [
+ 'index' => $connection->getIndexName( ElasticClient::TYPE_DATA ),
+ 'type' => ElasticClient::TYPE_DATA,
+ 'body' => $parameters->get( 'search.body' ),
+ 'size' => $this->options->safeGet( 'subquery.size', 100 )
+ ];
+
+ $info = $info + [
+ 'query' => $params,
+ 'search_info' => [ 'search_info' => [ 'total' => 0 ] ],
+ 'isFromCache' => false
+ ];
+
+ $parameters->set( 'query.info', $info );
+
+ if ( $parameters->get( 'params' ) === [] ) {
+ return [];
+ }
+
+ list( $res, $errors ) = $connection->search(
+ $params
+ );
+
+ $searchResult = new SearchResult( $res );
+ $searchResult->setFilterField( $parameters->get( 'result_filter.field' ) );
+ $searchResult->setErrors( $errors );
+
+ $results = $searchResult->getResults();
+ $count = $searchResult->get( 'count' );
+
+ if ( $count >= $parameters->get( 'threshold' ) ) {
+ $results = $this->terms_index( $parameters->get( 'id' ), $results );
+ }
+
+ $info['search_info'] = $searchResult->get( 'info' );
+
+ $parameters->set( 'query.info', $info );
+ $parameters->set( 'count', $count );
+
+ return $results;
+ }
+
+ /**
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-dsl-terms-query.html
+ */
+ private function terms_index( $id, $results ) {
+
+ $connection = $this->store->getConnection( 'elastic' );
+
+ $params = [
+ 'index' => $connection->getIndexName( ElasticClient::TYPE_LOOKUP ),
+ 'type' => ElasticClient::TYPE_LOOKUP,
+ 'id' => $id
+ ];
+
+ // https://www.elastic.co/blog/terms-filter-lookup
+ // From the documentation "... the terms filter will be fetched from a
+ // field in a document with the specified id in the specified type and
+ // index. Internally a get request is executed to fetch the values from
+ // the specified path. At the moment for this feature to work the _source
+ // needs to be stored ..."
+ $connection->index( $params + [ 'body' => [ 'id' => $results ] ] );
+
+ // Refresh to ensure results are available for the upcoming search
+ $connection->refresh( $params );
+
+ // Define path for the terms filter
+ return $params + [ 'path' => 'id' ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Elastic/README.md b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/README.md
new file mode 100644
index 00000000..ab5cf87a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Elastic/README.md
@@ -0,0 +1,519 @@
+# ElasticStore
+
+[Requirements](#requirements) | [Features](#features) | [Usage](#usage) | [Settings](#settings) | [Technical notes](#technical-notes) | [FAQ](#faq)
+
+The `ElasticStore` provides a framework to replicate Semantic MediaWiki related data to an Elasticsearch cluster and enable its `QueryEngine` to send `#ask` requests and retrieve information from Elasticsearch (aka ES) instead of the default `SQLStore`.
+
+The objective is to:
+
+- improve structured and allow unstructured content searches
+- extend and improve full-text query support (including sorting of results by [relevancy][es:relevance])
+- provide means for a scalability strategy by relying on the ES infrastructure
+
+## Requirements
+
+- Elasticsearch: Recommended 6.1+, Tested with 5.6.6
+- Semantic MediaWiki: 3.0+
+- [`elasticsearch/elasticsearch`][packagist:es] (PHP ^7.0 `~6.0` or PHP ^5.6.6 `~5.3`)
+
+We rely on the [elasticsearch php-api][es:php-api] to communicate with Elasticsearch and are therefore independent from any other vendor or MediaWiki extension that may use ES as search backend (e.g. `CirrusSearch`).
+
+It is recommended to use:
+
+- ES 6+ due to improvements to its [sparse field][es:6] handling
+- ES hardware with "... machine with 64 GB of RAM is the ideal sweet spot, but 32 GB and 16 GB machines are also common ..." as noted in the [elasticsearch guide][es:hardware]
+
+### Why Elasticsearch?
+
+- It it is relatively easy to install and run an ES instance (also on not recommended hardware).
+- ES allows to scale its cluster horizontally without requiring changes to Semantic MediaWiki or its query engine.
+- It is more likely that a user in a MediaWiki environment can provided access to an ES instance than to a `SPARQL` triple store (or a SORL/Lucence backend).
+
+## Features
+
+- Handle property type changes without the need to rebuild the entire index itself after it is ensured that all `ChangePropagation` jobs have been processed
+- Inverse queries are supported (e.g. `[[-Foo::Bar]]`)
+- Property chains and paths queries are supported (e.g. `[[Foo.Bar::Foobar]]`)
+- Category and property hierarchies are supported
+
+ES is not expected to be used as data store and therefore it is not assumed that ES returns any `_source` fields or any other data object (exception is the highlighting) besides those document IDs that match a query condition.
+
+The `ElasticStore` provides a customized serialization format to transform and transfer data, an interpreter (see [domain language][es:dsl]) allows `#ask` queries to be answered by an ES instance.
+
+## Usage
+
+The objective is to use Elasticsearch as drop-in replacement for the existing `SQLStore` based query answering but before it can provide this functionality, some settings and user actions are required:
+
+- Set `$GLOBALS['smwgDefaultStore'] = 'SMWElasticStore';`
+- Set `$GLOBALS['smwgElasticsearchEndpoints'] = [ ... ];`
+- Run `php setupStore.php` or `php update.php`
+- Rebuild the index using `php rebuildElasticIndex.php`
+
+For ES specific settings, please consult the [elasticsearch][es:conf] manual.
+
+### Indexing, updates, and refresh intervals
+
+Updates to an ES index happens instantaneously during a page action to guarantee that queries can use the latest available data set.
+
+This [page][es:create:index] decribes the index creation process where Semantic MediaWiki provides two index types:
+
+- the `data` index that hosts all user-facing queryable data (structured and unstructured content) and
+- the `lookup` index to store queries used for concept, property path, and inverse match computations
+
+#### Indexing
+
+The `rebuildElasticIndex.php` script is provided as method to replicate existing data from the `SQLStore` (fetches information directly from the property tables) to the ES backend instead of reparsing all content using the MW parser. The script operates in a [rollover mode][es:alias-zero] which is if there is already an existing index, a new index with a different version is created, leaving the current active index untouched and allowing queries to continue to operate while the index process is ongoing. Once completed, the new index switches places with the old index and is removed from the ES cluster at this point.
+
+It should be noted that __active replication__ is paused for the duration of the rebuild in order for changes to be processed after the re-index has been completed. It is __obligatory__ to run the job scheduler after the completion of the task to process any outstanding jobs.
+
+#### Safe replication
+
+The `ElasticStore` by default is set to a safe replication mode which entails that if during a page storage __no__ connection could be established to an ES cluster, a `smw.elasticIndexerRecovery` job is planned for changes that were not replicated. These jobs should be executed on a regular basis to ensure that data are kept in sync with the backend.
+
+The `job.recovery.retries` setting is set to a maximum of retry attempts in case the job itself cannot establish a connection after which the job is canceled even though it could __not__ recover.
+
+#### Refresh interval
+
+The [`refresh_interval`][es:indexing:speed] dictates how often Elasticsearch creates new [segments][stack:segments] and it set to `1s` as default. During the rebuild process the setting is changed to `-1` as recommended by the [documentation][es:indexing:speed]. If for some reason (aborted rebuild, exception etc.) the `refresh_interval` remained at `-1` then changes to an index will not be visible until a refresh has been commanded and to fix the situation it is suggested to run:
+
+- `php rebuildElasticIndex.php --update-settings`
+- `php rebuildElasticIndex.php --force-refresh`
+
+### Querying and searching
+
+`#ask` queries are system agnostic meaning that queries that worked with the `SQLStore` (or `SPARQLStore`) are expected to work equally with `ElasticStore` and not requiring any modifications to a query or its syntax.
+
+The `ElasticStore` has set its query execution to a `compat.mode` where queries are expected to return the same results as the `SQLStore`. In some instances ES could provide a different result set especially in connection with boolean query operators but the `compat.mode` warrants consistency among results retrieved from the `ElasticStore` in comparison to the `SQLStore` especially when running the same set of integration tests against each store.
+
+#### Filter and query context
+
+Most searches with a discrete value in Semantic MediaWiki will be classified as [structured search][es:structured:search] that operates with a [filter context][es:filter:context] while full-text or proximity searches use a [query context][es:query:context] that attributes to a relevancy score. A filter context will always yield a `1` relevancy score as it is translated into on a boolean operation which either matches or neglects a result as part of a set.
+
+* `[[Has page::Foo]]` (filter context) to match entities with value `Foo` for the property `Has page`
+* `[[Has page::~*Foo*]]` (query context) to match entities with any value that contains `Foo` (and `FOO`,`foo` etc. ) for the `Has page` property
+
+To improve the handling of proximity searches the following expression can be used.
+
+Expression | Interpret as | Description | Note
+------------ | ------------- | ------------- | -------------
+`in: ...` | `~~* ... *` or `~* ... *` | Find anything that contains `...` | The `in:` expression can also be combined with a property and depending on the type context is interpret differently.
+`phrase: ...` | `~~" ... "` or `~" ... "` | Find anything that contains `...` in the exact same order | The `phrase:` expression is only relevant for literal components such as text or page titles as well as unstructured text.
+`not: ...` | `!~~...` or `!~...` | Do not match any entity that matches `...` | The `not:` expression is intended to only match the exact entered term. It can be extended using `*` if necessary (e.g. `[[Has text::not:foo*]]`)
+
+A wide proximity is expressed with `~~` and the intent to search where a specific property is unknown (in case of ES it can expand the search radius to fields that have not been annotated or processed by Semantic MediaWiki prior a query request, see `indexer.raw.text` and `experimental.file.ingest`)
+
+Type | #ask | Interpret as
+------------ | ------------ | -------------
+\- | `[[in:some foo]]` | `[[~~*some foo*]]`
+Text | `[[Has text::in:some foo]]` | `[[Has text::~*some foo*]]`
+Page | `[[Has page::in:foo]]` | `[[Has text::~*foo*]]`
+Number | `[[Has number::in:99]]` | `[[Has number:: [[≥0]] [[≤99]] ]]`
+&nbsp; | `[[Has number::in:-100]]` | `[[Has number:: [[≥-100]] [[≤0]] ]]`
+Time | `[[Has date::in:2000]]` | `[[Has date:: <q>[[≥2000]] [[<<1 January 2001 00:00:00]]</q> ]]`
+
+#### Relevancy and scores
+
+[Relevancy][es:relevance] sorting is a topic of its own (and is only provided by ES and the `ElasticStore`). In order to sort results by a score, the `#ask` query needs to signal that a different context is required during the query execution. The `es.score` sortkey (see `score.sortfield` and is used as convention key) signals to the `QueryEngine` that for a non-filtered context score tracking is to be enabled.
+
+Only query constructs that use a non-filtered context (`~/!~/in/phrase/not`) provide meaningful scores that are expressive enough for sorting results otherwise results will not be distinguishable and not contribute to a meaningful overall sorting experience.
+
+<pre>
+// Find entities that contains "some text" in the property `Has text` and sort
+// by its score returned from each matched document
+
+{{#ask: [[Has text::in:some text]]
+ |sort=es.score
+ |order=desc
+}}
+</pre>
+
+#### Property chains, paths, and subqueries
+
+ES doesn't support [subqueries][es:subqueries] or [joins][es:joins] natively but the `ElasticStore` facilitates the [terms lookup][es:terms-lookup] to execute path or chain of properties and hereby builds an iterative process allowing to create a set of results that match a path condition (e.g. `Foo.bar.foobar`) with each element holding a restricted list of results from the previous execution to traverse the property path.
+
+The introduced process allows to match the `SQLStore` behaviour in terms of path queries where the `QueryEngine` is splitting each path and computes a list of elements. To avoid issues with a possible output of a vast list of matches, Semantic MediaWiki will "park" those results in the `lookup` index with the `subquery.terms.lookup.index.write.threshold` setting (default is 100) directing as to when the results are move into a separate `lookup` index.
+
+#### Hierarchies
+
+Property and category hierarchies are supported by relying on a conjunctive boolean expression for hierarchy members that are computed outside of the ES framework (the ES [parent join][es:parent-join] type is not used for this).
+
+#### Unstructured text
+
+Two experimental settings allow to handle unstructured text (content that does not provide any explicit property value annotations) using a separate field in ES.
+
+##### Raw text
+
+The `indexer.raw.text` setting enables to replicate the entire raw text of a page together with existing annotations so that unprocessed text can be searched in tandem with structured queries.
+
+##### File content
+
+This requires the ES [ingest-attachment plugin][es:ingest] and the ``indexer.experimental.file.ingest` setting.
+
+The [ingest][es:ingest] process provides a method to retrieve content from files and make them available to ES and Semantic MediaWiki without requiring the actual content to be stored within a wiki page.
+
+In case where the ingestions and extraction was successful, a `File attachment` annotation will appear on the specific `File` entity and depending on the extraction quality of ES and Tika additional annotations will be added such as:
+
+- `Content type`,
+- `Content author`,
+- `Content length`,
+- `Content language`,
+- `Content title`,
+- `Content date`, and
+- `Content keyword`
+
+Due to size and memory consumption by ES/Tika, file content ingestions exclusively happens in background using the `smw.elasticFileIngest` job. Only after the job has been executed successfully, aforementioned annotations and file content will be accessible during a query request.
+
+An "unstructured search" (i.e. searching without a property assignment) requires the wide proximity expression which conveniently are available as shortcut using `in:`, `phrase:`, or `not:`.
+
+#### Query debugging
+
+`format=debug` will output a detailed description of the `#ask` and ES DSL used for a query answering making it possible to analyze and retrieve explanations from ES about a query request.
+
+### Special:Search integration
+
+In case [SMWSearch][smw:search] was enabled, it is possible to retrieve [highlighted][es:highlighting] text snippets for matched entities from ES given that `special_search.highlight.fragment.type` is set to one of the excepted types (`plain`, `unified`, and `fvh`). Type `plain` can be used without any specific requirements, for the other types please consult the ES documentation.
+
+## Settings
+
+Accessing an ES cluster from within Semantic MediaWiki requires some settings and customization and includes:
+
+- [`$smwgElasticsearchEndpoints`](https://www.semantic-mediawiki.org/wiki/Help:$smwgElasticsearchEndpoints)
+- [`$smwgElasticsearchConfig`](https://www.semantic-mediawiki.org/wiki/Help:$smwgElasticsearchConfig)
+- [`$smwgElasticsearchProfile`](https://www.semantic-mediawiki.org/wiki/Help:$smwgElasticsearchProfile)
+
+### Endpoints
+
+`smwgElasticsearchEndpoints` is a __required__ setting and contains a list of available endpoints to create a connection with an ES cluster.
+
+<pre>
+$GLOBALS['smwgElasticsearchEndpoints'] = [
+ [ 'host' => '192.168.1.126', 'port' => 9200, 'scheme' => 'http' ],
+ 'localhost:9200'
+];
+</pre>
+
+Please consult the [reference material][es:conf:hosts] for details about the correct notation form.
+
+### Config
+
+`$smwgElasticsearchConfig` is a compound setting that collects various settings related to connection, index, and query details.
+
+<pre>
+$GLOBALS['smwgElasticsearchConfig'] = [
+
+ // Points to index and mapping definition files
+ 'index_def' => [ ... ],
+
+ // Defines connection details for ES endpoints
+ 'connection' => [ ... ],
+
+ // Holds replication details
+ 'indexer' => [ ... ],
+
+ // Used to modify ES specific settings
+ 'settings' => [ ... ],
+
+ // Section to optimize the query execution
+ 'query' => [ ... ]
+];
+</pre>
+
+A detailed list of settings and their explanations are available in the `DefaultSettings.php`. Please make sure that after changing any setting, `php rebuildElasticIndex.php --update-settings` is executed.
+
+When modifying a particular setting, use an appropriate key to change the value of a parameter otherwise it is possible that the entire configuration is replaced.
+
+<pre>
+// Uses a specific key and therefore replaces only the specific parameter
+$GLOBALS['smwgElasticsearchConfig']['query']['uri.field.case.insensitive'] = true;
+
+// !!Override!! the entire configuration
+$GLOBALS['smwgElasticsearchConfig'] = [
+ 'query' => [
+ 'uri.field.case.insensitive' => true
+ ]
+];
+</pre>
+
+#### Shards and replicas
+
+A default shards and replica configuration is applied to with:
+
+- The `data` index has two primary shards and two replicas
+- The `lookup` index has one primary shard and no replica with the documentation noting that "... consider using an index with a single shard ... lookup terms filter will prefer to execute the get request on a local node if possible ..."
+
+If it is required to change the numbers of [shards][es:shards] and replicas then use the `$smwgElasticsearchConfig` setting.
+
+<pre>
+$GLOBALS['smwgElasticsearchConfig']['settings']['data'] = [
+ 'number_of_shards' => 3,
+ 'number_of_replicas' => 3
+]
+</pre>
+
+ES comes with a precondition that any change to the `number_of_shards` requires to rebuild the entire index, so changes to that setting should be made carefully and in advance.
+
+Read-heavy wikis might want to add (without the need re-index the data) replica shards at the time ES performance is in decline. As noted, [replica shards][es:replica-shards] should be put on an extra hardware.
+
+#### Index mappings
+
+By default `index_def` points to the index definition and the `data` index is assigned the `smw-data-standard.json` to define its settings and mappings that influence how ES analyzes and index documents including fields that are identified to contain text and string elements. Those text fields use the [standard analyzer][es:standard:analyzer] and should work for most applications.
+
+The index name will be composed of a prefix such as `smw-data` (or `smw-lookup`), the wikiID, and a version indicator (used by the rollover) so that a single ES cluster can host different indices from different Semantic MediaWiki instances without interfering with each other.
+
+#### Text, languages, and analyzers
+
+For certain languages the `icu` analyzer (or any other language specific configuration) may provide better results therefore `index_def` provides a possibility to change the assignments and hereby allows custom settings such as different language [analyzer][es:lang:analyzer] to be used and increase the likelihood of better matching precision for text elements.
+
+`smw-data-icu.json` is provided as example on how to alter those settings. It should be noted that query results on text fields may differ compared to when one would use the standard analyzer and users are expected to evaluate whether those settings are more favorable or not to a query answering.
+
+Besides the different index mappings, it is recommended for a non-latin language environments to add the [analysis-icu plugin][es:icu:tokenizer] and select `smw-data-icu.json` as index definition (see also the [unicode normalization][es:unicode:normalization] guide) to make use of better unicode normalization and [case folding][es:unicode:case:folding].
+
+Please note that any change to the index or its analyzer settings __requires__ to rebuild the entire index.
+
+### Profile
+
+`$smwgElasticsearchProfile` is provided to simplify the maintenance of configuration parameters by linking to a JSON file that hosts and hereby alters individual settings.
+
+<pre>
+{
+ "indexer": {
+ "raw.text": true
+ },
+ "query": {
+ "uri.field.case.insensitive": true
+ }
+}
+</pre>
+
+The profile is loaded last and will override any default or individual settings made in `$smwgElasticsearchConfig`.
+
+## Technical notes
+
+Classes and objects related to the Elasticsearch interface and implementation are placed under the `SMW\Elastic` namespace.
+
+<pre>
+SMW\Elastic
+┃ ┠━ Admin # Classes used to extend `Special:SemanticMediaWiki`
+┃ ┠━ Exception
+┃ ┠━ Connection # Responsible for building a connection to ES
+┃ ┠━ Indexer # Contains all necessary classes for updating the ES index
+┃ ┕━ QueryEngine # Hosts the query builder and `#ask` language interpreter classes
+┃
+┠━ ElasticFactory
+┕━ ElasticStore
+</pre>
+
+### Field mapping and serialization
+
+<pre>
+{
+ "_index": "smw-data-mw-30-00-elastic-v1",
+ "_type": "data",
+ "_id": "334032",
+ "_version": 2,
+ "_source": {
+ "subject": {
+ "title": "ABC/20180716/k10011534941000",
+ "subobject": "_f21687e8bab0ebee627f71654ddd4bc4",
+ "namespace": 0,
+ "interwiki": "",
+ "sortkey": "foo ..."
+ },
+ "P:100": {
+ "txtField": [
+ "Foo bar ..."
+ ]
+ },
+ "P:4": {
+ "wpgField": [
+ "foobar"
+ ],
+ "wpgID": [
+ 334125
+ ]
+ }
+ }
+}
+</pre>
+
+It should remembered that besides specific available types in ES, text fields are generally divided into analyzed and not_analyzed fields.
+
+Semantic MediaWiki is [mapping][es:mapping] its internal structure using [`dynamic_templates`][es:dynamic:templates] to define expected data types, their attributes, and possibly add extra index fields (see [multi-fields][es:multi-fields]) to make use of certain query constructs.
+
+The naming convention follows a very pragmatic naming scheme, `P:<ID>.<type>Field` with each new field (aka property) being mapped dynamically to a corresponding field type.
+
+- `P:<ID>` identifies the property with a number which is the same as the internal ID in the `SQLStore` (`smw_id`)
+- `<type>Field` declares a typed field (e.g. `txtField` which is important in case the type changes from `wpg` to `txt` and vice versa) and holds the actual indexable data.
+- Dates are indexed using the julian day number (JDN) to allow for historic dates being applicable
+
+The `SemanticData` object is always serialized in its entirety to avoid the interface to keep delta information. Furthermore, ES itself creates always a new index document for each update therefore keeping deltas wouldn't make much difference for the update process. A complete object has the advantage to use the [bulk][es:bulk] updater making the update faster and more resilient while avoiding document comparison during an update process.
+
+To allow for exact matches as well as full-text searches on the same field most mapped fields will have at least two or three additional [multi-field][es:multi-fields] elements to store text as `not_analyzed` (or keyword) and as sortable entity.
+
+* The `text_copy` mapping (see [copy-to][es:copy-to]) is used to enable wide proximity searches on textual annotated elements. For example, `[[in:foo bar]]` (eq. `[[~~foo bar]]`) translates into "Find all entities that have `foo bar` in one of its assigned `_uri`, `_txt`, or `_wpg` properties. The `text_copy` field is a compound field for all strings to be searched when a specific property is unknown.
+* The `text_raw` (requires `indexer.raw.text` to be set `true`) contains unstructured and unprocessed raw text from an article so that it can be used in combination with the proximity operators `[[in:lorem ipsum]]` and `[[phrase:lorem ipsum]]`.
+* `attachment.{...}` will be added by the ingest processor
+
+### ES DSL mapping
+
+For example, the ES DSL for a `[[in:lorem ipsum]]` query (find all entities that contains `lorem ipsum`) on structured and unstructured fields will look similar to:
+
+<pre>
+"bool": {
+ "must": {
+ "query_string": {
+ "fields": [
+ "subject.title^8",
+ "text_copy^5",
+ "text_raw",
+ "attachment.title^3",
+ "attachment.content"
+ ],
+ "query": "*lorem ipsum*",
+ "minimum_should_match": 1
+ }
+ }
+}
+</pre>
+
+The term `lorem ipsum` will be queried in different fields with different boost factors to highlight preferences when a term is among a title or only part of a text field.
+
+A request for a structured term (assigned to a property e.g. `[[Has text::lorem ipsum]]`) will generate a different ES DSL query.
+
+<pre>
+"bool": {
+ "filter": {
+ "term": {
+ "P:100.txtField.keyword": "lorem ipsum"
+ }
+ }
+}
+</pre>
+
+While `P:100.txtField` contains the text component that is assigned to `Has text` and by default is an analyzed field, the `keyword` field is selected to execute the query on a not analyzed content to match the exact term. Exact term matching means that the matching process distinguishes between `lorem ipsum` and `Lorem ipsum`.
+
+
+On the contrary, a proximity request (e.g. `[[Has text::~lorem ipsum*]]`) has different requirements including case folding, lower, and upper case matching and therefore includes the analyzed field with an ES DSL output that is comparable to:
+
+<pre>
+"bool": {
+ "must": {
+ "query_string": {
+ "fields": [
+ "P:100.txtField",
+ "P:100.txtField.keyword"
+ ],
+ "query": "lorem +ipsum*"
+ }
+ }
+}
+</pre>
+
+### Monitoring
+
+To make it easier for administrators to monitor the interface between Semantic MediaWiki and ES, several service links are provided for a convenient access to selected information.
+
+The main access point is defined with `Special:SemanticMediaWiki/elastic` but only users with the `smw-admin` right (which is required for the `Special:SemanticMediaWiki` page) can access the information and only when an ES cluster is available.
+
+### Logging
+
+The enable connector specific logging, please use the `smw-elastic` identifier in your LocalSettings.
+
+<pre>
+$wgDebugLogGroups = [
+ 'smw-elastic' => ".../logs/smw-elastic-{$wgDBname}.log",
+];
+</pre>
+
+## FAQ
+
+> Why not combine the `SQLStore` and ES search where ES only handles the text search?
+
+The need to support ordering of results requires that the sorting needs to happen over the entire set of results that match a condition. It is not possible to split a search between two systems while retaining consistency for the offset (from where result starts and end) pointer.
+
+> Why not use ES as a replacement?
+
+Because at this point of implementation ES is used search engine and not a storage backend therefore the data storage and management remains part of the `SQLStore`. The `SQLStore` is responsible for creating IDs, storing data objects, and provide answers to requests that doesn't involve the `QueryEngine`.
+
+> Limit of total fields [3000] in index [...] has been exceeded
+
+If the rebuilder or ES returns with a similar message then the preconfigured limit needs to be changed which is most likely caused by an excessive use of property declarations. The user should question such usage patterns and analyze why so many properties are used and whether or not some can
+be merged or properties are in fact misused as fact statements.
+
+The limit is set to prevent [mapping explosion][es:map:explosion] but can be readjusted using the [index.mapping.total_fields.limit][es:mapping] (maximum number of fields in an index) setting.
+
+<pre>
+$GLOBALS['smwgElasticsearchConfig']['settings']['data'] = [
+ 'index.mapping.total_fields.limit' => 6000
+];
+</pre>
+
+After changing those settings, ensure to run `php rebuildElasticIndex.php --update-settings`.
+
+> Your version of PHP / json-ext does not support the constant 'JSON_PRESERVE_ZERO_FRACTION', which is important for proper type mapping in Elasticsearch. Please upgrade your PHP or json-ext.
+
+[elasticsearch-php#534](https://github.com/elastic/elasticsearch-php/issues/534) has some details about the issue. Please check the [version matrix][es:version:matrix] to see which version is compatible with your PHP environment.
+
+> "Connection.php: {"error":{"root_cause":[{"type":"parse_exception","reason":"No processor type exists with name [attachment]","header":{"processor_type":"attachment"}}] ..."
+
+The file indexer (`experimental.file.ingest`) was enabled but the required ES [ingest-plugin][es:ingest] was not installed.
+
+> I use CirrusSearch, can I search SMW (or its data) via CirrusSearch?
+
+No, because first of all SMW doesn't rely on CirrusSearch at all and even if a user has CirrusSearch installed both extensions have different requirements and different indices and are not designed to share content with each other.
+
+> Can I use `Special:Search` together with SMW and CirrusSearch?
+
+Yes, by adding `$wgSearchType = 'SMWSearch';` one can use the `#ask` syntax (e.g. `[[Has date::>1970]]`) and execute structured or unstructured searches. Using the [extended profile](https://www.semantic-mediawiki.org/wiki/Help:SMWSearch/Extended_profile) or `#ask` constructs a search input that will retrieved results via Semantic MediaWiki (and hereby ES).
+
+### Glossary
+
+- `Document` is called in ES a content container to holds indexable content and is equivalent to an entity (subject) in Semantic MediaWiki
+- `Index` holds all documents within a collection of types and contains inverted indices to search across everything within those documents at once
+- `Node` is a running instance of Elasticsearch
+- `Cluster` is a group of nodes
+
+### Other recommendations
+
+- Analysis ICU ( tokenizer and token filters from the Unicode ICU library), see `bin/elasticsearch-plugin install analysis-icu`
+- A [curated list](https://github.com/dzharii/awesome-elasticsearch) of useful resources about elasticsearch including articles, videos, blogs, tips and tricks, use cases
+- [Elasticsearch: The Definitive Guide](http://shop.oreilly.com/product/0636920028505.do) by Clinton Gormley and Zachary Tonge should provide insights in how to run and use Elasticsearch
+- [10 Elasticsearch metrics to watch][oreilly:es-metrics-to-watch] describes key metrics to keep Elasticsearch running smoothly
+
+[es:conf]: https://www.elastic.co/guide/en/elasticsearch/reference/6.1/system-config.html
+[es:conf:hosts]: https://www.elastic.co/guide/en/elasticsearch/client/php-api/6.0/_configuration.html#_extended_host_configuration
+[es:php-api]: https://www.elastic.co/guide/en/elasticsearch/client/php-api/6.0/_installation_2.html
+[es:joins]: https://github.com/elastic/elasticsearch/issues/6769
+[es:subqueries]: https://discuss.elastic.co/t/question-about-subqueries/20767/2
+[es:terms-lookup]: https://www.elastic.co/blog/terms-filter-lookup
+[es:dsl]: https://www.elastic.co/guide/en/elasticsearch/reference/6.1/query-dsl.html
+[es:mapping]: https://www.elastic.co/guide/en/elasticsearch/reference/6.1/mapping.html
+[es:multi-fields]: https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html
+[es:map:explosion]: https://www.elastic.co/blog/found-crash-elasticsearch#mapping-explosion
+[es:indexing:speed]: https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html
+[es:create:index]: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
+[es:dynamic:templates]: https://www.elastic.co/guide/en/elasticsearch/reference/6.1/dynamic-templates.html
+[es:version:matrix]: https://www.elastic.co/guide/en/elasticsearch/client/php-api/6.0/_installation_2.html#_version_matrix
+[es:hardware]: https://www.elastic.co/guide/en/elasticsearch/guide/2.x/hardware.html#_memory
+[es:standard:analyzer]: https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-analyzer.html
+[es:lang:analyzer]: https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html
+[es:icu:tokenizer]: https://www.elastic.co/guide/en/elasticsearch/plugins/6.1/analysis-icu-tokenizer.html
+[es:unicode:normalization]: https://www.elastic.co/guide/en/elasticsearch/guide/current/unicode-normalization.html
+[es:unicode:case:folding]: https://www.elastic.co/guide/en/elasticsearch/guide/current/case-folding.html
+[es:shards]: https://www.elastic.co/guide/en/elasticsearch/reference/current/_basic_concepts.html#getting-started-shards-and-replicas
+[es:alias-zero]: https://www.elastic.co/guide/en/elasticsearch/guide/master/index-aliases.html
+[es:bulk]: https://www.elastic.co/guide/en/elasticsearch/reference/6.2/docs-bulk.html
+[es:structured:search]: https://www.elastic.co/guide/en/elasticsearch/guide/current/structured-search.html
+[es:filter:context]: https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-filter-context.html
+[es:query:context]: https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-filter-context.html
+[es:relevance]: https://www.elastic.co/guide/en/elasticsearch/guide/master/relevance-intro.html
+[es:copy-to]: https://www.elastic.co/guide/en/elasticsearch/reference/master/copy-to.html
+[oreilly:es-metrics-to-watch]: https://www.oreilly.com/ideas/10-elasticsearch-metrics-to-watch
+[stack:segments]: https://stackoverflow.com/questions/15426441/understanding-segments-in-elasticsearch
+[es:6]: https://www.elastic.co/blog/minimize-index-storage-size-elasticsearch-6-0
+[packagist:es]:https://packagist.org/packages/elasticsearch/elasticsearch
+[es:ingest]:https://www.elastic.co/guide/en/elasticsearch/plugins/master/ingest-attachment.html
+[es:parent-join]: https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html
+[es:replica-shards]:https://www.elastic.co/guide/en/elasticsearch/guide/current/replica-shards.html
+[es:highlighting]: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html
+[smw:search]: https://www.semantic-mediawiki.org/wiki/Help:SMWSearch
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Encoder.php b/www/wiki/extensions/SemanticMediaWiki/src/Encoder.php
new file mode 100644
index 00000000..6abbd1f6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Encoder.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace SMW;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class Encoder {
+
+ /**
+ * @see SMWInfolink::encodeParameters
+ *
+ * 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
+ * [ ] < > &lt; &gt; '' |: problematic in MW titles
+ * & : sometimes problematic in MW titles ([[&amp;]] 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
+ *
+ * @since 2.2
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ public static function escape( $string ) {
+
+ $value = str_replace(
+ [ '-', '#', "\n", ' ', '/', '[', ']', '<', '>', '&lt;', '&gt;', '&amp;', '\'\'', '|', '&', '%', '?', '$', "\\", ";", '_' ],
+ [ '-2D', '-23', '-0A', '-20', '-2F', '-5B', '-5D', '-3C', '-3E', '-3C', '-3E', '-26', '-27-27', '-7C', '-26', '-25', '-3F', '-24', '-5C', "-3B", '-5F' ],
+ $string
+ );
+
+ return $value;
+ }
+
+ /**
+ * Reverse of self::escape
+ *
+ * @since 2.5
+ *
+ * @param $string
+ *
+ * @return string
+ */
+ public static function unescape( $string ) {
+
+ $value = str_replace(
+ [ '-20', '-23', '-0A', '-2F', '-5B', '-5D', '-3C', '-3E', '-3C', '-3E', '-26', '-27-27', '-7C', '-26', '-25', '-3F', '-24', '-5C', "-3B", "-3A", '-5F', '-2D' ],
+ [ ' ', '#', "\n", '/', '[', ']', '<', '>', '&lt;', '&gt;', '&', '\'\'', '|', '&', '%', '?', '$', "\\", ";", ":", "_", '-' ],
+ $string
+ );
+
+ return $value;
+ }
+
+ /**
+ * @see SMWInfolink::encodeParameters
+ *
+ * @since 2.2
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ public static function encode( $string ) {
+ return rawurlencode( $string );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ public static function decode( $string ) {
+
+ // Apply decoding for SMW's own url encoding strategy (see SMWInfolink)
+ $string = str_replace( '%', '-', rawurldecode( str_replace( '-', '%', $string ) ) );
+
+ $string = str_replace( [ '-2D', '-3A' ], [ '-', ':' ], $string );
+
+ // Sanitize remaining string content
+ $string = trim( htmlspecialchars( $string, ENT_NOQUOTES ) );
+ $string = str_replace( '&nbsp;', ' ', str_replace( [ '&#160;', '&amp;' ], [ ' ', '&' ], $string ) );
+
+ return $string;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/EntityLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/EntityLookup.php
new file mode 100644
index 00000000..c7531813
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/EntityLookup.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace SMW;
+
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+interface EntityLookup {
+
+ /**
+ * Retrieve all data stored about the given subject and return it as a
+ * SemanticData container. There are no options: it just returns all
+ * available data as shown in the page's Factbox.
+ * $filter is an array of strings that are datatype IDs. If given, the
+ * function will avoid any work that is not necessary if only
+ * properties of these types are of interest.
+ *
+ * @note There is no guarantee that the store does not retrieve more
+ * data than requested when a filter is used. Filtering just ensures
+ * that only necessary requests are made, i.e. it improves performance.
+ *
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ * @param RequestOptions|string[]|bool $filter
+ *
+ * @return SemanticData
+ */
+ public function getSemanticData( DIWikiPage $subject, $filter = false );
+
+ /**
+ * Get an array of all properties for which the given subject has some
+ * value. The result is an array of DIProperty objects.
+ *
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return DataItem[]|[]
+ */
+ public function getProperties( DIWikiPage $subject, RequestOptions $requestOptions = null );
+
+ /**
+ * Get an array of all property values stored for the given subject and
+ * property. The result is an array of DataItem objects.
+ *
+ * If called with $subject == null, all values for the given property
+ * are returned.
+ *
+ * @since 2.5
+ *
+ * @param DIWikiPage|null $subject
+ * @param DIProperty $property
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return DataItem[]|[]|Iterator
+ */
+ public function getPropertyValues( DIWikiPage $subject = null, DIProperty $property, RequestOptions $requestOptions = null );
+
+ /**
+ * Get an array of all subjects that have the given value for the given
+ * property. The result is an array of DIWikiPage objects. If null
+ * is given as a value, all subjects having that property are returned.
+ *
+ * @since 2.5
+ *
+ * @param DIWikiPage|null $subject
+ * @param DIProperty $property
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return DIWikiPage[]|[]|Iterator
+ */
+ public function getPropertySubjects( DIProperty $property, DataItem $dataItem = null, RequestOptions $requestOptions = null );
+
+ /**
+ * Get an array of all subjects that have some value for the given
+ * property. The result is an array of DIWikiPage objects.
+ *
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return DIWikiPage[]|Iterator
+ */
+ public function getAllPropertySubjects( DIProperty $property, RequestOptions $requestOptions = null );
+
+ /**
+ * Get an array of all properties for which there is some subject that
+ * relates to the given value. The result is an array of DIWikiPage
+ * objects.
+ *
+ * @note In some stores, this function might be implemented partially
+ * so that only values of type Page (_wpg) are supported.
+ *
+ * @since 2.5
+ *
+ * @param DataItem $object
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return DataItem[]|[]
+ */
+ public function getInProperties( DataItem $object, RequestOptions $requestOptions = null );
+
+ /**
+ * @since 3.0
+ */
+ public function invalidateCache();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Enum.php b/www/wiki/extensions/SemanticMediaWiki/src/Enum.php
new file mode 100644
index 00000000..6a4bdaea
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Enum.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace SMW;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Enum {
+
+ /**
+ * Option that allows to suspend the page purge
+ */
+ const OPT_SUSPEND_PURGE = 'smw.opt.suspend.purge';
+
+ /**
+ * Indicates to purge an associated parser cache
+ */
+ const PURGE_ASSOC_PARSERCACHE = 'smw.purge.assoc.parsercache';
+
+ /**
+ * Indicates whether to proceed with the cache warming or not
+ */
+ const SUSPEND_CACHE_WARMUP = 'smw.suspend.cache.warmup';
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/EventHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/EventHandler.php
new file mode 100644
index 00000000..b0bdfa55
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/EventHandler.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace SMW;
+
+use Onoi\EventDispatcher\EventDispatcher;
+use Onoi\EventDispatcher\EventDispatcherFactory;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class EventHandler {
+
+ /**
+ * @var EventHandler
+ */
+ private static $instance = null;
+
+ /**
+ * @var EventDispatcher
+ */
+ private $eventDispatcher = null;
+
+ /**
+ * @since 2.2
+ *
+ * @param EventDispatcher $eventDispatcher
+ */
+ public function __construct( EventDispatcher $eventDispatcher ) {
+ $this->eventDispatcher = $eventDispatcher;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return self
+ */
+ public static function getInstance() {
+
+ if ( self::$instance === null ) {
+ self::$instance = new self( self::newEventDispatcher() );
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.2
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return EventDispatcher
+ */
+ public function getEventDispatcher() {
+ return $this->eventDispatcher;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return DispatchContext
+ */
+ public function newDispatchContext() {
+ return EventDispatcherFactory::getInstance()->newDispatchContext();
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $event
+ * @param Closure $callback
+ */
+ public function addCallbackListener( $event, \Closure $callback ) {
+
+ $listener = EventDispatcherFactory::getInstance()->newGenericCallbackEventListener();
+ $listener->registerCallback( $callback );
+
+ $this->getEventDispatcher()->addListener(
+ $event,
+ $listener
+ );
+ }
+
+ private static function newEventDispatcher() {
+
+ $eventListenerRegistry = new EventListenerRegistry(
+ EventDispatcherFactory::getInstance()->newGenericEventListenerCollection()
+ );
+
+ $eventDispatcher = EventDispatcherFactory::getInstance()->newGenericEventDispatcher();
+ $eventDispatcher->addListenerCollection( $eventListenerRegistry );
+
+ return $eventDispatcher;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/EventListenerRegistry.php b/www/wiki/extensions/SemanticMediaWiki/src/EventListenerRegistry.php
new file mode 100644
index 00000000..28e4a2c1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/EventListenerRegistry.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace SMW;
+
+use Onoi\EventDispatcher\EventListenerCollection;
+use SMW\Query\QueryComparator;
+use SMW\SQLStore\QueryDependency\DependencyLinksUpdateJournal;
+use SMWExporter as Exporter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class EventListenerRegistry implements EventListenerCollection {
+
+ /**
+ * @var EventListenerCollection
+ */
+ private $eventListenerCollection = null;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @since 2.2
+ *
+ * @param EventListenerCollection $eventListenerCollection
+ */
+ public function __construct( EventListenerCollection $eventListenerCollection ) {
+ $this->eventListenerCollection = $eventListenerCollection;
+ }
+
+ /**
+ * @see EventListenerCollection::getCollection
+ *
+ * @since 2.2
+ */
+ public function getCollection() {
+ return $this->addListenersToCollection()->getCollection();
+ }
+
+ private function addListenersToCollection() {
+
+ $this->logger = ApplicationFactory::getInstance()->getMediaWikiLogger();
+
+ /**
+ * Emitted during UpdateJob, ArticlePurge
+ */
+ $this->eventListenerCollection->registerCallback(
+ 'factbox.cache.delete', function( $dispatchContext ) {
+
+ if ( $dispatchContext->has( 'subject' ) ) {
+ $title = $dispatchContext->get( 'subject' )->getTitle();
+ } else {
+ $title = $dispatchContext->get( 'title' );
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $applicationFactory->getCache()->delete(
+ \SMW\Factbox\CachedFactbox::makeCacheKey( $title )
+ );
+ }
+ );
+
+ $this->eventListenerCollection->registerCallback(
+ 'exporter.reset', function() {
+ Exporter::getInstance()->clear();
+ }
+ );
+
+ $this->eventListenerCollection->registerCallback(
+ 'query.comparator.reset', function() {
+ QueryComparator::getInstance()->clear();
+ }
+ );
+
+ /**
+ * Emitted during UpdateJob
+ */
+ $this->eventListenerCollection->registerCallback(
+ 'cached.propertyvalues.prefetcher.reset', function( $dispatchContext ) {
+
+ if ( $dispatchContext->has( 'title' ) ) {
+ $subject = DIWikiPage::newFromTitle( $dispatchContext->get( 'title' ) );
+ } else{
+ $subject = $dispatchContext->get( 'subject' );
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $logContext = [
+ 'role' => 'developer',
+ 'event' => 'cached.propertyvalues.prefetcher.reset',
+ 'origin' => $subject
+ ];
+
+ $this->logger->info( '[Event] {event}: {origin}', $logContext );
+
+ $applicationFactory->singleton( 'CachedPropertyValuesPrefetcher' )->resetCacheBy(
+ $subject
+ );
+
+ $dispatchContext->set( 'propagationstop', true );
+ }
+ );
+
+ /**
+ * Emitted during NewRevisionFromEditComplete, ArticleDelete, TitleMoveComplete,
+ * PropertyTableIdReferenceDisposer, ArticlePurge
+ */
+ $this->eventListenerCollection->registerCallback(
+ 'cached.prefetcher.reset', function( $dispatchContext ) {
+
+ if ( $dispatchContext->has( 'title' ) ) {
+ $subject = DIWikiPage::newFromTitle( $dispatchContext->get( 'title' ) );
+ } else{
+ $subject = $dispatchContext->get( 'subject' );
+ }
+
+ $context = $dispatchContext->has( 'context' ) ? $dispatchContext->get( 'context' ) : '';
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $logContext = [
+ 'role' => 'developer',
+ 'event' => 'cached.prefetcher.reset',
+ 'origin' => $subject
+ ];
+
+ $this->logger->info( '[Event] {event}: {origin}', $logContext );
+
+ $applicationFactory->singleton( 'CachedPropertyValuesPrefetcher' )->resetCacheBy(
+ $subject
+ );
+
+ $applicationFactory->singleton( 'CachedQueryResultPrefetcher' )->resetCacheBy(
+ $subject,
+ $context
+ );
+
+ if ( $dispatchContext->has( 'ask' ) ) {
+ $applicationFactory->singleton( 'CachedQueryResultPrefetcher' )->resetCacheBy(
+ $dispatchContext->get( 'ask' ),
+ $context
+ );
+ }
+
+ $dispatchContext->set( 'propagationstop', true );
+ }
+ );
+
+ $this->registerStateChangeEvents();
+
+ return $this->eventListenerCollection;
+ }
+
+ private function registerStateChangeEvents() {
+
+ /**
+ * Emitted during ArticleDelete
+ */
+ $this->eventListenerCollection->registerCallback(
+ 'cached.update.marker.delete', function( $dispatchContext ) {
+
+ $cache = ApplicationFactory::getInstance()->getCache();
+
+ if ( $dispatchContext->has( 'subject' ) ) {
+ $cache->delete(
+ DependencyLinksUpdateJournal::makeKey(
+ $dispatchContext->get( 'subject' )
+ )
+ );
+
+ $cache->delete(
+ smwfCacheKey(
+ ParserData::CACHE_NAMESPACE,
+ $dispatchContext->get( 'subject' )->getHash()
+ )
+ );
+ }
+ }
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataItemDeserializationException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataItemDeserializationException.php
new file mode 100644
index 00000000..e05cafb1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataItemDeserializationException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace SMW\Exception;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DataItemDeserializationException extends DataItemException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataItemException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataItemException.php
new file mode 100644
index 00000000..46b67ff4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataItemException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class DataItemException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataTypeLookupException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataTypeLookupException.php
new file mode 100644
index 00000000..4311de88
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/DataTypeLookupException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DataTypeLookupException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotFoundException.php
new file mode 100644
index 00000000..7e1816b7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FileNotFoundException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotReadableException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotReadableException.php
new file mode 100644
index 00000000..83e2ad68
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotReadableException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FileNotReadableException extends RuntimeException {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ */
+ public function __construct( $file ) {
+ parent::__construct( "$file is not readable." );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotWritableException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotWritableException.php
new file mode 100644
index 00000000..a9ccb44f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/FileNotWritableException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FileNotWritableException extends RuntimeException {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ */
+ public function __construct( $file ) {
+ parent::__construct( "$file is not writable." );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/ParameterNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/ParameterNotFoundException.php
new file mode 100644
index 00000000..502275b4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/ParameterNotFoundException.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace SMW\Exception;
+
+use InvalidArgumentException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ParameterNotFoundException extends InvalidArgumentException {
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @since 3.0
+ *
+ * @param string $name
+ */
+ public function __construct( $name ) {
+ $this->name = $name;
+ parent::__construct( " $name is missing as argument!" );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/PredefinedPropertyLabelMismatchException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/PredefinedPropertyLabelMismatchException.php
new file mode 100644
index 00000000..907a5ff7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/PredefinedPropertyLabelMismatchException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace SMW\Exception;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PredefinedPropertyLabelMismatchException extends PropertyLabelNotResolvedException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/PropertyLabelNotResolvedException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/PropertyLabelNotResolvedException.php
new file mode 100644
index 00000000..8e7aa20e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/PropertyLabelNotResolvedException.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace SMW\Exception;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyLabelNotResolvedException extends DataItemException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/PropertyNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/PropertyNotFoundException.php
new file mode 100644
index 00000000..e1ebe694
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/PropertyNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyNotFoundException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/RedirectTargetUnresolvableException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/RedirectTargetUnresolvableException.php
new file mode 100644
index 00000000..f9ee1978
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/RedirectTargetUnresolvableException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class RedirectTargetUnresolvableException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/SemanticDataImportException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/SemanticDataImportException.php
new file mode 100644
index 00000000..61579eb8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/SemanticDataImportException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SemanticDataImportException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/SettingNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/SettingNotFoundException.php
new file mode 100644
index 00000000..9b852d03
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/SettingNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SettingNotFoundException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/StoreNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/StoreNotFoundException.php
new file mode 100644
index 00000000..c3bd2409
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/StoreNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class StoreNotFoundException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exception/SubSemanticDataException.php b/www/wiki/extensions/SemanticMediaWiki/src/Exception/SubSemanticDataException.php
new file mode 100644
index 00000000..5f2dadd4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exception/SubSemanticDataException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SubSemanticDataException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ConceptMapper.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ConceptMapper.php
new file mode 100644
index 00000000..20e43dde
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ConceptMapper.php
@@ -0,0 +1,306 @@
+<?php
+
+namespace SMW\Exporter;
+
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DIConcept;
+use SMW\DIProperty;
+use SMW\Exporter\Element\ExpResource;
+use SMW\Query\Language\ClassDescription;
+use SMW\Query\Language\ConceptDescription;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ThingDescription;
+use SMW\Query\Language\ValueDescription;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+use SMWExporter as Exporter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ConceptMapper {
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.4
+ *
+ * @param Exporter|null $exporter
+ */
+ public function __construct( Exporter $exporter = null ) {
+ $this->exporter = $exporter;
+
+ if ( $this->exporter === null ) {
+ $this->exporter = Exporter::getInstance();
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DataItem $dataItem
+ *
+ * @return boolean
+ */
+ public function isMapperFor( DataItem $dataItem ) {
+ return $dataItem instanceof DIConcept;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIConcept $concept
+ *
+ * @return ExpData|null
+ */
+ public function getElementFor( DIConcept $concept ) {
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $concept
+ );
+
+ if ( !$dataValue->isValid() ) {
+ return null;
+ }
+
+ $description = ApplicationFactory::getInstance()->newQueryParser()->getQueryDescription(
+ $dataValue->getWikiValue()
+ );
+
+ $exact = true;
+ $owlDescription = $this->getExpDataFromDescription( $description, $exact );
+
+ if ( $owlDescription === false ) {
+ $result = new ExpData(
+ $this->exporter->getSpecialNsResource( 'owl', 'Thing' )
+ );
+
+ return $result;
+ }
+
+ if ( $exact ) {
+ return $owlDescription;
+ }
+
+ $result = new ExpData(
+ new ExpResource( '' )
+ );
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'rdf', 'type' ),
+ new ExpData( $this->exporter->getSpecialNsResource( 'owl', 'Class' ) )
+ );
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'rdfs', 'subClassOf' ),
+ $owlDescription
+ );
+
+ return $result;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Description $description
+ *
+ * @param string &$exact
+ *
+ * @return Element|false
+ */
+ public function getExpDataFromDescription( Description $description, &$exact ) {
+
+ if ( ( $description instanceof Conjunction ) || ( $description instanceof Disjunction ) ) {
+ $result = $this->doMapConjunctionDisjunction( $description, $exact );
+ } elseif ( $description instanceof ClassDescription ) {
+ $result = $this->doMapClassDescription( $description, $exact );
+ } elseif ( $description instanceof ConceptDescription ) {
+ $result = $this->doMapConceptDescription( $description, $exact );
+ } elseif ( $description instanceof SomeProperty ) {
+ $result = $this->doMapSomeProperty( $description, $exact );
+ } elseif ( $description instanceof ValueDescription ) {
+ $result = $this->doMapValueDescription( $description, $exact );
+ } elseif ( $description instanceof ThingDescription ) {
+ $result = false;
+ } else {
+ $result = false;
+ $exact = false;
+ }
+
+ return $result;
+ }
+
+ private function doMapValueDescription( ValueDescription $description, &$exact ) {
+
+ if ( $description->getComparator() === SMW_CMP_EQ ) {
+ $result = $this->exporter->getDataItemExpElement( $description->getDataItem() );
+ } else {
+ // OWL cannot represent <= and >= ...
+ $exact = false;
+ $result = false;
+ }
+
+ return $result;
+ }
+
+ private function doMapConceptDescription( ConceptDescription $description, &$exact ) {
+
+ $result = new ExpData(
+ $this->exporter->getResourceElementForWikiPage( $description->getConcept() )
+ );
+
+ return $result;
+ }
+
+ private function doMapSomeProperty( SomeProperty $description, &$exact ) {
+
+ $result = new ExpData(
+ new ExpResource( '' )
+ );
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'rdf', 'type' ),
+ new ExpData( $this->exporter->getSpecialNsResource( 'owl', 'Restriction' ) )
+ );
+
+ $property = $description->getProperty();
+
+ if ( $property->isInverse() ) {
+ $property = new DIProperty( $property->getKey() );
+ }
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'owl', 'onProperty' ),
+ new ExpData(
+ $this->exporter->getResourceElementForProperty( $property )
+ )
+ );
+
+ $subdata = $this->getExpDataFromDescription(
+ $description->getDescription(),
+ $exact
+ );
+
+ if ( ( $description->getDescription() instanceof ValueDescription ) &&
+ ( $description->getDescription()->getComparator() === SMW_CMP_EQ ) ) {
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'owl', 'hasValue' ),
+ $subdata
+ );
+ } else {
+ if ( $subdata === false ) {
+
+ $owltype = $this->exporter->getOWLPropertyType(
+ $description->getProperty()->findPropertyTypeID()
+ );
+
+ if ( $owltype == 'ObjectProperty' ) {
+ $subdata = new ExpData(
+ $this->exporter->getSpecialNsResource( 'owl', 'Thing' )
+ );
+ } elseif ( $owltype == 'DatatypeProperty' ) {
+ $subdata = new ExpData(
+ $this->exporter->getSpecialNsResource( 'rdfs', 'Literal' )
+ );
+ } else { // no restrictions at all with annotation properties ...
+ return new ExpData(
+ $this->exporter->getSpecialNsResource( 'owl', 'Thing' )
+ );
+ }
+ }
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'owl', 'someValuesFrom' ),
+ $subdata
+ );
+ }
+
+ return $result;
+ }
+
+ private function doMapClassDescription( ClassDescription $description, &$exact ) {
+
+ if ( count( $description->getCategories() ) == 1 ) { // single category
+ $categories = $description->getCategories();
+ $result = new ExpData(
+ $this->exporter->getResourceElementForWikiPage( end( $categories ) )
+ );
+ } else { // disjunction of categories
+
+ $result = new ExpData(
+ new ExpResource( '' )
+ );
+
+ $elements = [];
+
+ foreach ( $description->getCategories() as $cat ) {
+ $elements[] = new ExpData(
+ $this->exporter->getResourceElementForWikiPage( $cat )
+ );
+ }
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'owl', 'unionOf' ),
+ ExpData::makeCollection( $elements )
+ );
+ }
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'rdf', 'type' ),
+ new ExpData(
+ $this->exporter->getSpecialNsResource( 'owl', 'Class' )
+ )
+ );
+
+ return $result;
+ }
+
+ private function doMapConjunctionDisjunction( Description $description, &$exact ) {
+
+ $result = new ExpData(
+ new ExpResource( '' )
+ );
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'rdf', 'type' ),
+ new ExpData(
+ $this->exporter->getSpecialNsResource( 'owl', 'Class' )
+ )
+ );
+
+ $elements = [];
+
+ foreach ( $description->getDescriptions() as $subdesc ) {
+ $element = $this->getExpDataFromDescription( $subdesc, $exact );
+
+ if ( $element === false ) {
+ $element = new ExpData(
+ $this->exporter->getSpecialNsResource( 'owl', 'Thing' )
+ );
+ }
+
+ $elements[] = $element;
+ }
+
+ $prop = $description instanceof Conjunction ? 'intersectionOf' : 'unionOf';
+
+ $result->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'owl', $prop ),
+ ExpData::makeCollection( $elements )
+ );
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/DataItemMatchFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/DataItemMatchFinder.php
new file mode 100644
index 00000000..24cd2450
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/DataItemMatchFinder.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace SMW\Exporter;
+
+use SMW\DIWikiPage;
+use SMW\Exporter\Element\ExpElement;
+use SMW\Exporter\Element\ExpResource;
+use SMW\Localizer;
+use SMW\Store;
+use SMWDataItem as DataItem;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class DataItemMatchFinder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var string
+ */
+ private $wikiNamespace;
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ * @param string $wikiNamespace
+ */
+ public function __construct( Store $store, $wikiNamespace = '' ) {
+ $this->store = $store;
+ $this->wikiNamespace = $wikiNamespace;
+ }
+
+ /**
+ * Try to map an ExpElement to a representative DataItem which may return null
+ * if the attempt fails.
+ *
+ * @since 2.4
+ *
+ * @param ExpElement $expElement
+ *
+ * @return DataItem|null
+ */
+ public function matchExpElement( ExpElement $expElement ) {
+
+ $dataItem = null;
+
+ if ( !$expElement instanceof ExpResource ) {
+ return $dataItem;
+ }
+
+ $uri = $expElement->getUri();
+
+ if ( strpos( $uri, $this->wikiNamespace ) !== false ) {
+ $dataItem = $this->matchToWikiNamespaceUri( $uri );
+ } else {
+ // Not in wikiNamespace therefore most likely an imported URI
+ $dataItem = $this->matchToUnknownWikiNamespaceUri( $uri );
+ }
+
+ return $dataItem;
+ }
+
+ private function matchToWikiNamespaceUri( $uri ) {
+
+ $dataItem = null;
+ $localName = substr( $uri, strlen( $this->wikiNamespace ) );
+
+ $dbKey = rawurldecode( Escaper::decodeUri( $localName ) );
+ $parts = explode( '#', $dbKey, 2 );
+
+ if ( count( $parts ) == 2 ) {
+ $dbKey = $parts[0];
+ $subobjectname = $parts[1];
+ } else {
+ $subobjectname = '';
+ }
+
+ $parts = explode( ':', $dbKey, 2 );
+
+ // No extra NS
+ if ( count( $parts ) == 1 ) {
+ return new DIWikiPage( $dbKey, NS_MAIN, '', $subobjectname );
+ }
+
+ $namespaceId = $this->matchToNamespaceName( $parts[0] );
+
+ if ( $namespaceId != -1 && $namespaceId !== false ) {
+ $dataItem = new DIWikiPage( $parts[1], $namespaceId, '', $subobjectname );
+ } else {
+ $title = Title::newFromDBkey( $dbKey );
+
+ if ( $title !== null ) {
+ $dataItem = new DIWikiPage( $title->getDBkey(), $title->getNamespace(), $title->getInterwiki(), $subobjectname );
+ }
+ }
+
+ return $dataItem;
+ }
+
+ private function matchToNamespaceName( $name ) {
+ // try the by far most common cases directly before using Title
+ $namespaceName = str_replace( '_', ' ', $name );
+
+ if ( ( $namespaceId = Localizer::getInstance()->getNamespaceIndexByName( $name ) ) !== false ) {
+ return $namespaceId;
+ }
+
+ foreach ( [ SMW_NS_PROPERTY, NS_CATEGORY, NS_USER, NS_HELP ] as $nsId ) {
+ if ( $namespaceName == Localizer::getInstance()->getNamespaceTextById( $nsId ) ) {
+ $namespaceId = $nsId;
+ break;
+ }
+ }
+
+ return $namespaceId;
+ }
+
+ private function matchToUnknownWikiNamespaceUri( $uri ) {
+
+ $dataItem = null;
+
+ // Sesame: Not a valid (absolute) URI: _node1abjt1k9bx17
+ if ( filter_var( $uri, FILTER_VALIDATE_URL ) === false ) {
+ return $dataItem;
+ }
+
+ $respositoryResult = $this->store->getConnection( 'sparql' )->select(
+ '?v1 ?v2',
+ "<$uri> rdfs:label ?v1 . <$uri> swivt:wikiNamespace ?v2",
+ [ 'LIMIT' => 1 ]
+ );
+
+ $expElements = $respositoryResult->current();
+
+ if ( $expElements !== false ) {
+
+ // ?v1
+ if ( isset( $expElements[0] ) ) {
+ $dbKey = $expElements[0]->getLexicalForm();
+ } else {
+ $dbKey = 'UNKNOWN';
+ }
+
+ // ?v2
+ if ( isset( $expElements[1] ) ) {
+ $namespace = strval( $expElements[1]->getLexicalForm() );
+ } else {
+ $namespace = NS_MAIN;
+ }
+
+ $dataItem = new DIWikiPage(
+ $this->getFittingDBKey( $dbKey, $namespace ),
+ $namespace
+ );
+ }
+
+ return $dataItem;
+ }
+
+ private function getFittingDBKey( $dbKey, $namespace ) {
+
+ // https://www.mediawiki.org/wiki/Manual:$wgCapitalLinks
+ // https://www.mediawiki.org/wiki/Manual:$wgCapitalLinkOverrides
+ if ( $GLOBALS['wgCapitalLinks'] || ( isset( $GLOBALS['wgCapitalLinkOverrides'][$namespace] ) && $GLOBALS['wgCapitalLinkOverrides'][$namespace] ) ) {
+ return mb_strtoupper( mb_substr( $dbKey, 0, 1 ) ) . mb_substr( $dbKey, 1 );
+ }
+
+ return $dbKey;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element.php
new file mode 100644
index 00000000..9443f5c5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace SMW\Exporter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+interface Element {
+
+ /**
+ * A single resource (individual) for export, as defined by a URI.
+ */
+ const TYPE_RESOURCE = 0;
+
+ /**
+ * A single resource (individual) for export, defined by a URI for which there
+ * also is a namespace abbreviation.
+ */
+ const TYPE_NSRESOURCE = 1;
+
+ /**
+ * A single datatype literal for export. Defined by a literal value and a
+ * datatype URI.
+ */
+ const TYPE_LITERAL = 2;
+
+ /**
+ * A dataItem an export element is associated with
+ *
+ * @since 2.2
+ *
+ * @return DataItem|null
+ */
+ public function getDataItem();
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getHash();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpElement.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpElement.php
new file mode 100644
index 00000000..1e37f10d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpElement.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace SMW\Exporter\Element;
+
+use RuntimeException;
+use SMW\Exporter\Element;
+use SMWDataItem as DataItem;
+
+/**
+ * ExpElement is a class for representing single elements that appear in
+ * exported data, such as individual resources, data literals, or blank nodes.
+ *
+ * A single element for export, e.g. a data literal, instance name, or blank
+ * node. This abstract base class declares the basic common functionality of
+ * export elements (which is not much, really).
+ * @note This class should not be instantiated directly.
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+abstract class ExpElement implements Element {
+
+ /**
+ * The DataItem that this export element is associated with, if
+ * any. Might be unset if not given yet.
+ *
+ * @var DataItem|null
+ */
+ protected $dataItem;
+
+ /**
+ * @since 1.6
+ *
+ * @param DataItem|null $dataItem
+ */
+ public function __construct( DataItem $dataItem = null ) {
+ $this->dataItem = $dataItem;
+ }
+
+ /**
+ * Get a DataItem object that represents the contents of this export
+ * element in SMW, or null if no such data item could be found.
+ *
+ * @return DataItem|null
+ */
+ public function getDataItem() {
+ return $this->dataItem;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getHash() {
+ return md5( json_encode( $this->getSerialization() ) );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getSerialization() {
+
+ $dataItem = null;
+
+ if ( $this->getDataItem() !== null ) {
+ $dataItem = [
+ 'type' => $this->getDataItem()->getDIType(),
+ 'item' => $this->getDataItem()->getSerialization()
+ ];
+ }
+
+ return [
+ 'dataitem' => $dataItem
+ ];
+ }
+
+ /**
+ * @see ExpElement::newFromSerialization
+ */
+ protected static function deserialize( $serialization ) {
+
+ $dataItem = null;
+
+ if ( !array_key_exists( 'dataitem', $serialization ) ) {
+ throw new RuntimeException( "The serialization format is missing a dataitem element" );
+ }
+
+ // If it is null, isset will ignore it
+ if ( isset( $serialization['dataitem'] ) ) {
+ $dataItem = DataItem::newFromSerialization(
+ $serialization['dataitem']['type'],
+ $serialization['dataitem']['item']
+ );
+ }
+
+ return $dataItem;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param array $serialization
+ *
+ * @return ExpElement
+ */
+ public static function newFromSerialization( array $serialization ) {
+
+ if ( !isset( $serialization['type'] ) ) {
+ throw new RuntimeException( "The serialization format is missing a type element" );
+ }
+
+ switch ( $serialization['type'] ) {
+ case Element::TYPE_RESOURCE:
+ $elementClass = ExpResource::class;
+ break;
+ case Element::TYPE_NSRESOURCE:
+ $elementClass = ExpNsResource::class;
+ break;
+ case Element::TYPE_LITERAL:
+ $elementClass = ExpLiteral::class;
+ break;
+ default:
+ throw new RuntimeException( "Unknown type" );
+ }
+
+ $serialization['dataitem'] = self::deserialize( $serialization );
+
+ return $elementClass::deserialize( $serialization );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpLiteral.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpLiteral.php
new file mode 100644
index 00000000..f800c482
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpLiteral.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace SMW\Exporter\Element;
+
+use InvalidArgumentException;
+use RuntimeException;
+use SMWDataItem as DataItem;
+
+/**
+ * A single datatype literal for export. Defined by a literal value and a
+ * datatype URI.
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ExpLiteral extends ExpElement {
+
+ /**
+ * Lexical form of the literal.
+ * @var string
+ */
+ private $lexicalForm;
+
+ /**
+ * Datatype URI for the literal.
+ * @var string
+ */
+ private $datatype;
+
+ /**
+ * @var string
+ */
+ private $lang = '';
+
+ /**
+ * @note The given lexical form should be the plain string for
+ * representing the literal without datatype or language information.
+ * It must not use any escaping or abbreviation mechanisms.
+ *
+ * @param string $lexicalForm lexical form
+ * @param string $datatype Data type URI or empty for untyped literals
+ * @param string $lang
+ * @param DataItem|null $dataItem
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct( $lexicalForm, $datatype = '', $lang = '', DataItem $dataItem = null ) {
+
+ if ( !is_string( $lexicalForm ) ) {
+ throw new InvalidArgumentException( '$lexicalForm needs to be a string' );
+ }
+
+ if ( !is_string( $datatype ) ) {
+ throw new InvalidArgumentException( '$datatype needs to be a string' );
+ }
+
+ if ( !is_string( $lang ) ) {
+ throw new InvalidArgumentException( '$lang needs to be a string and $datatype has to be of langString type' );
+ }
+
+ parent::__construct( $dataItem );
+
+ $this->lexicalForm = $lexicalForm;
+ $this->datatype = $datatype;
+
+ // 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString'
+ // can also be used instead of the simple Foo@lang-tag convention
+
+ // https://www.w3.org/TR/2004/REC-rdf-concepts-20040210/#dfn-language-identifier
+ // "...Plain literals have a lexical form and optionally a language tag as
+ // defined by [RFC-3066], normalized to lowercase..."
+ // https://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal
+ // "...Lexical representations of language tags may be converted to
+ // lower case. The value space of language tags is always in lower case..."
+ $this->lang = strtolower( $lang );
+ }
+
+ /**
+ * Returns a language tag with the language tag must be well-formed according
+ * to BCP47
+ *
+ * @return string
+ */
+ public function getLang() {
+ return $this->lang;
+ }
+
+ /**
+ * Return the URI of the datatype used, or the empty string if untyped.
+ *
+ * @return string
+ */
+ public function getDatatype() {
+ return $this->datatype;
+ }
+
+ /**
+ * Return the lexical form of the literal. The result does not use
+ * any escapings and might still need to be escaped in some contexts.
+ * The lexical form is not validated or canonicalized.
+ *
+ * @return string
+ */
+ public function getLexicalForm() {
+ return $this->lexicalForm;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getSerialization() {
+
+ $serialization = [
+ 'type' => self::TYPE_LITERAL,
+ 'lexical' => $this->lexicalForm,
+ 'datatype' => $this->datatype,
+ 'lang' => $this->lang
+ ];
+
+ return $serialization + parent::getSerialization();
+ }
+
+ /**
+ * @see ExpElement::newFromSerialization
+ */
+ protected static function deserialize( $serialization ) {
+
+ if ( !isset( $serialization['lexical'] ) || !isset( $serialization['datatype'] ) || !isset( $serialization['lang'] ) ) {
+ throw new RuntimeException( "Invalid format caused by a missing lexical/datatype element" );
+ }
+
+ return new self(
+ $serialization['lexical'],
+ $serialization['datatype'],
+ $serialization['lang'],
+ $serialization['dataitem']
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpNsResource.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpNsResource.php
new file mode 100644
index 00000000..18e7f174
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpNsResource.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace SMW\Exporter\Element;
+
+use InvalidArgumentException;
+use RuntimeException;
+use SMWDataItem as DataItem;
+
+/**
+ * A single resource (individual) for export, defined by a URI for which there
+ * also is a namespace abbreviation.
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ExpNsResource extends ExpResource {
+
+ /**
+ * Local part of the abbreviated URI
+ * @var string
+ */
+ private $localName;
+
+ /**
+ * Namespace URI prefix of the abbreviated URI
+ * @var string
+ */
+ private $namespace;
+
+ /**
+ * Namespace abbreviation of the abbreviated URI
+ * @var string
+ */
+ private $namespaceId;
+
+ /**
+ * @note The given URI must not contain serialization-specific
+ * abbreviations or escapings, such as XML entities.
+ *
+ * @param string $localName Local part of the abbreviated URI
+ * @param string $namespace Namespace URI prefix of the abbreviated URI
+ * @param string $namespaceId Namespace abbreviation of the abbreviated URI
+ * @param DataItem|null $dataItem
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct( $localName, $namespace, $namespaceId, DataItem $dataItem = null ) {
+
+ if ( !is_string( $localName ) ) {
+ throw new InvalidArgumentException( '$localName needs to be a string' );
+ }
+
+ if ( !is_string( $namespace ) ) {
+ throw new InvalidArgumentException( '$namespace needs to be a string' );
+ }
+
+ if ( !is_string( $namespaceId ) ) {
+ throw new InvalidArgumentException( '$namespaceId needs to be a string' );
+ }
+
+ parent::__construct( $namespace . $localName, $dataItem );
+
+ $this->localName = $localName;
+ $this->namespace = $namespace;
+ $this->namespaceId = $namespaceId;
+ }
+
+ /**
+ * Return a qualified name for the element.
+ *
+ * @return string
+ */
+ public function getQName() {
+ return $this->namespaceId . ':' . $this->localName;
+ }
+
+ /**
+ * Get the namespace identifier used (the part before :).
+ *
+ * @return string
+ */
+ public function getNamespaceId() {
+ return $this->namespaceId;
+ }
+
+ /**
+ * Get the namespace URI that is used in the abbreviation.
+ *
+ * @return string
+ */
+ public function getNamespace() {
+ return $this->namespace;
+ }
+
+ /**
+ * Get the local name (the part after :).
+ *
+ * @return string
+ */
+ public function getLocalName() {
+ return $this->localName;
+ }
+
+ /**
+ * Check if the local name is qualifies as a local name in XML and
+ * Turtle. The function returns true if this is surely the case, and
+ * false if it may not be the case. However, we do not check the whole
+ * range of allowed Unicode entities for performance reasons.
+ *
+ * @return boolean
+ */
+ public function hasAllowedLocalName() {
+ return preg_match( '/^[A-Za-z_][-A-Za-z_0-9]*$/u', $this->localName );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getSerialization() {
+
+ // Use '|' as divider as it is unlikely that symbol appears within a uri
+ $serialization = [
+ 'type' => self::TYPE_NSRESOURCE,
+ 'uri' => $this->localName . '|' . $this->namespace . '|' . $this->namespaceId
+ ];
+
+ return $serialization + parent::getSerialization();
+ }
+
+ /**
+ * @see ExpElement::newFromSerialization
+ */
+ protected static function deserialize( $serialization ) {
+
+ if ( !isset( $serialization['uri'] ) ) {
+ throw new RuntimeException( "Invalid serialization format, missing a uri element" );
+ }
+
+ if ( substr_count( $serialization['uri'], '|') < 2 ) {
+ throw new RuntimeException( "Invalid uri format, expected two '|' dividers" );
+ }
+
+ list( $localName, $namespace, $namespaceId ) = explode( '|', $serialization['uri'], 3 );
+
+ return new self(
+ $localName,
+ $namespace,
+ $namespaceId,
+ $serialization['dataitem']
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpResource.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpResource.php
new file mode 100644
index 00000000..f991b7ea
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Element/ExpResource.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace SMW\Exporter\Element;
+
+use InvalidArgumentException;
+use RuntimeException;
+use SMWDataItem as DataItem;
+
+/**
+ * A single resource (individual) for export, as defined by a URI.
+ * This class can also be used to represent blank nodes: It is assumed that all
+ * objects of class ExpElement or any of its subclasses represent a blank
+ * node if their name is empty or of the form "_id" where "id" is any
+ * identifier string. IDs are local to the current context, such as a list of
+ * triples or an SMWExpData container.
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ExpResource extends ExpElement {
+
+ /**
+ * @var string
+ */
+ private $uri;
+
+ /**
+ * @var boolean
+ */
+ public $isImported = false;
+
+ /**
+ * @note The given URI must not contain serialization-specific
+ * abbreviations or escapings, such as XML entities.
+ *
+ * @param string $uri The full URI
+ * @param DataItem|null $dataItem
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct( $uri, DataItem $dataItem = null ) {
+
+ if ( !is_string( $uri ) ) {
+ throw new InvalidArgumentException( '$uri needs to be a string' );
+ }
+
+ parent::__construct( $dataItem );
+
+ // https://www.w3.org/2011/rdf-wg/wiki/IRIs/RDFConceptsProposal
+ // "... characters “<”, “>”, “{”, “}”, “|”, “\”, “^”, “`”, ‘“’ (double quote),
+ // and “ ” (space) were allowed ... are not allowed in IRIs, Data
+ // containing these characters in %-encoded form is fine ..."
+ $this->uri = str_replace( [ '"' ], [ '%22' ], $uri );
+ }
+
+ /**
+ * Return true if this resource represents a blank node.
+ *
+ * @return boolean
+ */
+ public function isBlankNode() {
+ return $this->uri === '' || $this->uri{0} == '_';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function isImported() {
+ return $this->isImported;
+ }
+
+ /**
+ * Get the URI of this resource. The result is a UTF-8 encoded URI (or
+ * IRI) without any escaping.
+ *
+ * @return string
+ */
+ public function getUri() {
+ return $this->uri;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getSerialization() {
+
+ $serialization = [
+ 'type' => self::TYPE_RESOURCE,
+ 'uri' => $this->getUri()
+ ];
+
+ return $serialization + parent::getSerialization();
+ }
+
+ /**
+ * @see ExpElement::newFromSerialization
+ */
+ protected static function deserialize( $serialization ) {
+
+ if ( !isset( $serialization['uri'] ) ) {
+ throw new RuntimeException( "Invalid serialization format" );
+ }
+
+ return new self(
+ $serialization['uri'],
+ $serialization['dataitem']
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ElementFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ElementFactory.php
new file mode 100644
index 00000000..bfb45077
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ElementFactory.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace SMW\Exporter;
+
+use RuntimeException;
+use SMW\Exporter\Element\ExpLiteral;
+use SMW\Exporter\Element\ExpResource;
+use SMWDataItem as DataItem;
+use SMWDITime as DITime;
+use SMWExporter as Exporter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class ElementFactory {
+
+ /**
+ * @var array
+ */
+ private $dataItemMapper = [];
+
+ /**
+ * @var array
+ */
+ private $dataItemToElementMapper = [];
+
+ /**
+ * @since 2.2
+ *
+ * @param integer $type
+ * @param Closure $dataItemEncoder
+ */
+ public function registerDataItemMapper( $type, \Closure $dataItemEncoder ) {
+ $this->dataItemMapper[$type] = $dataItemEncoder;
+ }
+
+ /**
+ * Create an Element that encodes the data for the given dataitem object.
+ * This method is meant to be used when exporting a dataitem as a subject
+ * or object.
+ *
+ * @param DataItem $dataItem
+ *
+ * @return Element|null
+ * @throws RuntimeException
+ */
+ public function newFromDataItem( DataItem $dataItem ) {
+
+ if ( $this->dataItemMapper === [] ) {
+ $this->initDataItemMap();
+ }
+
+ if ( $this->dataItemToElementMapper === [] ) {
+ $this->initDataItemToElementMapper();
+ }
+
+ $element = $this->findElementByDataItem( $dataItem );
+
+ if ( $element instanceof Element || $element === null ) {
+ return $element;
+ }
+
+ throw new RuntimeException( 'Encoder did not return a valid element' );
+ }
+
+ private function findElementByDataItem( $dataItem ) {
+
+ foreach ( $this->dataItemToElementMapper as $dataItemToElementMapper ) {
+ if ( $dataItemToElementMapper->isMapperFor( $dataItem ) ) {
+ return $dataItemToElementMapper->getElementFor( $dataItem );
+ }
+ }
+
+ foreach ( $this->dataItemMapper as $type => $callback ) {
+ if ( $type === $dataItem->getDIType() ) {
+ return $callback( $dataItem );
+ }
+ }
+
+ return null;
+ }
+
+ private function initDataItemToElementMapper() {
+ $this->dataItemToElementMapper[] = new ConceptMapper();
+ }
+
+ private function initDataItemMap() {
+
+ $lang = '';
+ $xsdValueMapper = new XsdValueMapper();
+
+ $this->registerDataItemMapper( DataItem::TYPE_NUMBER, function( $dataItem ) use ( $lang, $xsdValueMapper ) {
+
+ $xsdValueMapper->map( $dataItem );
+
+ return new ExpLiteral(
+ $xsdValueMapper->getXsdValue(),
+ $xsdValueMapper->getXsdType(),
+ $lang,
+ $dataItem
+ );
+ } );
+
+ $this->registerDataItemMapper( DataItem::TYPE_BLOB, function( $dataItem ) use ( $lang, $xsdValueMapper ) {
+
+ $xsdValueMapper->map( $dataItem );
+
+ return new ExpLiteral(
+ $xsdValueMapper->getXsdValue(),
+ $xsdValueMapper->getXsdType(),
+ $lang,
+ $dataItem
+ );
+ } );
+
+ $this->registerDataItemMapper( DataItem::TYPE_BOOLEAN, function( $dataItem ) use ( $lang, $xsdValueMapper ) {
+
+ $xsdValueMapper->map( $dataItem );
+
+ return new ExpLiteral(
+ $xsdValueMapper->getXsdValue(),
+ $xsdValueMapper->getXsdType(),
+ $lang,
+ $dataItem
+ );
+ } );
+
+ $this->registerDataItemMapper( DataItem::TYPE_URI, function( $dataItem ) {
+ return new ExpResource(
+ $dataItem->getURI(),
+ $dataItem
+ );
+ } );
+
+ $this->registerDataItemMapper( DataItem::TYPE_TIME, function( $dataItem ) use ( $lang, $xsdValueMapper ) {
+
+ $gregorianTime = $dataItem->getForCalendarModel( DITime::CM_GREGORIAN );
+ $xsdValueMapper->map( $gregorianTime );
+
+ return new ExpLiteral(
+ $xsdValueMapper->getXsdValue(),
+ $xsdValueMapper->getXsdType(),
+ $lang,
+ $gregorianTime
+ );
+ } );
+
+ $this->registerDataItemMapper( DataItem::TYPE_CONTAINER, function( $dataItem ) {
+ return Exporter::getInstance()->makeExportData(
+ $dataItem->getSemanticData()
+ );
+ } );
+
+ $this->registerDataItemMapper( DataItem::TYPE_WIKIPAGE, function( $dataItem ) {
+ return Exporter::getInstance()->getResourceElementForWikiPage(
+ $dataItem
+ );
+ } );
+
+ $this->registerDataItemMapper( DataItem::TYPE_PROPERTY, function( $dataItem ) {
+ return Exporter::getInstance()->getResourceElementForProperty(
+ $dataItem
+ );
+ } );
+
+ // Not implemented
+ $this->registerDataItemMapper( DataItem::TYPE_GEO, function( $dataItem ) {
+ return null;
+ } );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Escaper.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Escaper.php
new file mode 100644
index 00000000..ffbe97f9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/Escaper.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace SMW\Exporter;
+
+use SMW\DIWikiPage;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ */
+class Escaper {
+
+ /**
+ * @since 2.2
+ *
+ * @param DIWikiPage $diWikiPage
+ *
+ * @return string
+ */
+ static public function encodePage( DIWikiPage $diWikiPage ) {
+
+ $localName = '';
+
+ if ( $diWikiPage->getInterwiki() !== '' ) {
+ $localName = $diWikiPage->getInterwiki() . ':';
+ }
+
+ if ( $diWikiPage->getNamespace() === SMW_NS_PROPERTY ) {
+ $localName .= 'Property' . ':' . $diWikiPage->getDBkey();
+ } elseif ( $diWikiPage->getNamespace() === NS_CATEGORY ) {
+ $localName .= 'Category' . ':' . $diWikiPage->getDBkey();
+ } elseif ( $diWikiPage->getNamespace() !== NS_MAIN ) {
+ $localName .= str_replace( ' ', '_', $GLOBALS['wgContLang']->getNSText( $diWikiPage->getNamespace() ) ) . ':' . $diWikiPage->getDBkey();
+ } else {
+ $localName .= $diWikiPage->getDBkey();
+ }
+
+ return self::encodeUri( $localName );
+ }
+
+ /**
+ * @param string
+ *
+ * @return string
+ */
+ static public function armorChars( $string ) {
+ return str_replace( [ '/' ], [ '-2F' ], $string );
+ }
+
+ /**
+ * This function escapes symbols that might be problematic in XML in a uniform
+ * and injective way.
+ *
+ * @param string
+ *
+ * @return string
+ */
+ static public function encodeUri( $uri ) {
+
+ $uri = $GLOBALS['smwgExportResourcesAsIri'] ? $uri : wfUrlencode( $uri );
+
+ $uri = str_replace(
+ [ '-', ' ' ],
+ [ '-2D', '_' ],
+ $uri
+ );
+
+ $uri = str_replace(
+ [ '*', ',' , ';', '<', '>', '(', ')', '[', ']', '{', '}', '\\', '$', '^', ':', '"', '#', '&', "'", '+', '!', '%' ],
+ [ '-2A', '-2C', '-3B', '-3C', '-3E', '-28', '-29', '-5B', '-5D', '-7B', '-7D', '-5C', '-24', '-5E', '-3A', '-22', '-23', '-26', '-27', '-2B', '-21', '-' ],
+ $uri
+ );
+
+ return $uri;
+ }
+
+ /**
+ * This function unescapes URIs generated with Escaper::decodeUri.
+ *
+ * @param string
+ *
+ * @return string
+ */
+ static public function decodeUri( $uri ) {
+
+ $uri = str_replace(
+ [ '-2A', '-2C', '-3B', '-3C', '-3E', '-28', '-29', '-5B', '-5D', '-7B', '-7D', '-5C', '-24', '-5E', '-3A', '-22', '-23', '-26', '-27', '-2B', '-21', '-25', '-' ],
+ [ '*', ',' , ';', '<', '>', '(', ')', '[', ']', '{', '}', '\\', '$', '^', ':', '"', '#', '&', "'", '+', '!', '%', '%' ],
+ $uri
+ );
+
+ $uri = str_replace( '%2D', '-', $uri );
+
+ return $uri;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ExpResourceMapper.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ExpResourceMapper.php
new file mode 100644
index 00000000..954b048a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ExpResourceMapper.php
@@ -0,0 +1,295 @@
+<?php
+
+namespace SMW\Exporter;
+
+use RuntimeException;
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Exporter\Element\ExpNsResource;
+use SMW\Exporter\Element\ExpResource;
+use SMW\InMemoryPoolCache;
+use SMW\Store;
+use SMWDataItem as DataItem;
+use SMWExporter as Exporter;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class ExpResourceMapper {
+
+ /**
+ * Identifies auxiliary data (helper values)
+ */
+ const AUX_MARKER = 'aux';
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var DataValueFactory
+ */
+ private $dataValueFactory;
+
+ /**
+ * @var InMemoryPoolCache
+ */
+ private $inMemoryPoolCache;
+
+ /**
+ * @note Legacy setting expected to vanish with 3.0
+ *
+ * @var boolean
+ */
+ private $bcAuxiliaryUse = true;
+
+ /**
+ * @var boolean
+ */
+ private $seekImportVocabulary = true;
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ $this->dataValueFactory = DataValueFactory::getInstance();
+ $this->inMemoryPoolCache = InMemoryPoolCache::getInstance();
+ }
+
+ /**
+ * @since 2.3
+ */
+ public function reset() {
+ $this->inMemoryPoolCache->resetPoolCacheById( 'exporter.expresource.mapper' );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param boolean $bcAuxiliaryUse
+ */
+ public function setBCAuxiliaryUse( $bcAuxiliaryUse ) {
+ $this->bcAuxiliaryUse = (bool)$bcAuxiliaryUse;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param DIWikiPage $subject
+ */
+ public function invalidateCache( DIWikiPage $subject ) {
+
+ $hash = $subject->getHash();
+
+ $poolCache = $this->inMemoryPoolCache->getPoolCacheById(
+ 'exporter.expresource.mapper'
+ );
+
+ foreach ( [ $hash, $hash . self::AUX_MARKER . $this->seekImportVocabulary ] as $key ) {
+ $poolCache->delete( $key );
+ }
+ }
+
+ /**
+ * Create an ExpElement for some internal resource, given by an
+ * DIProperty object.
+ *
+ * This code is only applied to user-defined properties, since the
+ * code for special properties in
+ * Exporter::getSpecialPropertyResource may require information
+ * about the namespace in which some special property is used.
+ *
+ * @note $useAuxiliaryModifier is to determine whether an auxiliary
+ * property resource is to store a helper value
+ * (see Exporter::getDataItemHelperExpElement) should be generated
+ *
+ * @param DIProperty $property
+ * @param boolean $useAuxiliaryModifier
+ * @param boolean $seekImportVocabulary
+ *
+ * @return ExpResource
+ * @throws RuntimeException
+ */
+ public function mapPropertyToResourceElement( DIProperty $property, $useAuxiliaryModifier = false, $seekImportVocabulary = true ) {
+
+ // We want the a canonical representation to ensure that resources
+ // are language independent
+ $this->seekImportVocabulary = $seekImportVocabulary;
+ $diWikiPage = $property->getCanonicalDiWikiPage();
+
+ if ( $diWikiPage === null ) {
+ throw new RuntimeException( 'Only non-inverse, user-defined properties are permitted.' );
+ }
+
+ // No need for any aux properties besides those listed here
+ if ( !$this->bcAuxiliaryUse && $property->findPropertyTypeID() !== '_dat' && $property->findPropertyTypeID() !== '_geo' ) {
+ $useAuxiliaryModifier = false;
+ }
+
+ $expResource = $this->mapWikiPageToResourceElement( $diWikiPage, $useAuxiliaryModifier );
+ $this->seekImportVocabulary = true;
+
+ return $expResource;
+ }
+
+ /**
+ * Create an ExpElement for some internal resource, given by an
+ * DIWikiPage object. This is the one place in the code where URIs
+ * of wiki pages and user-defined properties are determined. A modifier
+ * can be given to make variants of a URI, typically done for
+ * auxiliary properties. In this case, the URI is modiied by appending
+ * "-23$modifier" where "-23" is the URI encoding of "#" (a symbol not
+ * occurring in MW titles).
+ *
+ * @param DIWikiPage $diWikiPage
+ * @param boolean $useAuxiliaryModifier
+ *
+ * @return ExpResource
+ */
+ public function mapWikiPageToResourceElement( DIWikiPage $diWikiPage, $useAuxiliaryModifier = false ) {
+
+ $modifier = $useAuxiliaryModifier ? self::AUX_MARKER : '';
+
+ $hash = $diWikiPage->getHash() . $modifier . $this->seekImportVocabulary;
+
+ $poolCache = $this->inMemoryPoolCache->getPoolCacheById( 'exporter.expresource.mapper' );
+
+ if ( $poolCache->contains( $hash ) ) {
+ return $poolCache->fetch( $hash );
+ }
+
+ if ( $diWikiPage->getSubobjectName() !== '' ) {
+ $modifier = $diWikiPage->getSubobjectName();
+ }
+
+ $resource = $this->newExpNsResource(
+ $diWikiPage,
+ $modifier
+ );
+
+ $poolCache->save(
+ $hash,
+ $resource
+ );
+
+ return $resource;
+ }
+
+ private function newExpNsResource( $diWikiPage, $modifier ) {
+
+ $importDataItem = $this->findImportDataItem( $diWikiPage, $modifier );
+
+ if ( $this->seekImportVocabulary && $importDataItem instanceof DataItem ) {
+ list( $localName, $namespace, $namespaceId ) = $this->defineElementsForImportDataItem( $importDataItem );
+ } else {
+ list( $localName, $namespace, $namespaceId ) = $this->defineElementsForDiWikiPage( $diWikiPage, $modifier );
+ }
+
+ $resource = new ExpNsResource(
+ $localName,
+ $namespace,
+ $namespaceId,
+ $diWikiPage
+ );
+
+ $resource->isImported = $importDataItem instanceof DataItem;
+ $dbKey = $diWikiPage->getDBkey();
+
+ if ( $diWikiPage->getNamespace() === SMW_NS_PROPERTY && $dbKey !== '' && $dbKey{0} !== '-' ) {
+ $resource->isUserDefined = DIProperty::newFromUserLabel( $diWikiPage->getDBkey() )->isUserDefined();
+ }
+
+ return $resource;
+ }
+
+ private function defineElementsForImportDataItem( DataItem $dataItem ) {
+
+ $importValue = $this->dataValueFactory->newDataValueByItem(
+ $dataItem,
+ new DIProperty( '_IMPO' )
+ );
+
+ return [
+ $importValue->getLocalName(),
+ $importValue->getNS(),
+ $importValue->getNSID()
+ ];
+ }
+
+ private function defineElementsForDiWikiPage( DIWikiPage $diWikiPage, $modifier ) {
+
+ $localName = '';
+ $hasFixedNamespace = false;
+
+ if ( $diWikiPage->getNamespace() === NS_CATEGORY ) {
+ $namespace = Exporter::getInstance()->getNamespaceUri( 'category' );
+ $namespaceId = 'category';
+ $localName = Escaper::encodeUri( $diWikiPage->getDBkey() );
+ $hasFixedNamespace = true;
+ }
+
+ if ( $diWikiPage->getNamespace() === SMW_NS_PROPERTY ) {
+ $namespace = Exporter::getInstance()->getNamespaceUri( 'property' );
+ $namespaceId = 'property';
+ $localName = Escaper::encodeUri( $diWikiPage->getDBkey() );
+ $hasFixedNamespace = true;
+ }
+
+ if ( ( $localName === '' ) ||
+ ( in_array( $localName{0}, [ '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ] ) ) ||
+ ( $hasFixedNamespace && strpos( $localName, '/' ) !== false )
+ ) {
+ $namespace = Exporter::getInstance()->getNamespaceUri( 'wiki' );
+ $namespaceId = 'wiki';
+ $localName = Escaper::encodePage( $diWikiPage );
+ }
+
+ if ( $hasFixedNamespace && strpos( $localName, '/' ) !== false ) {
+ $namespace = Exporter::getInstance()->getNamespaceUri( 'wiki' );
+ $namespaceId = 'wiki';
+ $localName = Escaper::armorChars( Escaper::encodePage( $diWikiPage ) );
+ }
+
+ // "-23$modifier" where "-23" is the URI encoding of "#" (a symbol not
+ // occurring in MW titles).
+ if ( $modifier !== '' ) {
+ $localName .= '-23' . Escaper::encodeUri( $modifier );
+ }
+
+ return [
+ $localName,
+ $namespace,
+ $namespaceId
+ ];
+ }
+
+ private function findImportDataItem( DIWikiPage $diWikiPage, $modifier ) {
+
+ $importDataItems = null;
+
+ // Only try to find an import vocab for a matchable entity
+ if ( $this->seekImportVocabulary && $diWikiPage->getNamespace() === NS_CATEGORY || $diWikiPage->getNamespace() === SMW_NS_PROPERTY ) {
+ $importDataItems = $this->store->getPropertyValues(
+ $diWikiPage,
+ new DIProperty( '_IMPO' )
+ );
+ }
+
+ if ( $importDataItems !== null && $importDataItems !== [] ) {
+ $importDataItems = current( $importDataItems );
+ }
+
+ return $importDataItems;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilder.php
new file mode 100644
index 00000000..5ab06fec
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilder.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace SMW\Exporter;
+
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+interface ResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return boolean
+ */
+ public function isResourceBuilderFor( DIProperty $property );
+
+ /**
+ * @since 2.5
+ *
+ * @param ExpData $expData
+ * @param DIProperty $property
+ * @param DataItem $dataItem
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/AuxiliaryPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/AuxiliaryPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..c39743fd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/AuxiliaryPropertyValueResourceBuilder.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class AuxiliaryPropertyValueResourceBuilder extends PredefinedPropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return !$property->isUserDefined() && $this->requiresAuxiliary( $property->getKey() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ $expElement = $this->exporter->getDataItemExpElement(
+ $dataItem
+ );
+
+ if ( $expElement === null ) {
+ return;
+ }
+
+ if ( $property->getKey() === $property->findPropertyTypeID() ) {
+ // Ensures that Boolean remains Boolean and not localized canonical
+ // representation such as "Booléen" when the content languageis not
+ // English
+ $expNsResource = $this->getResourceElementForProperty(
+ new DIProperty( $property->getCanonicalDiWikiPage()->getDBKey() )
+ );
+ } else {
+ $expNsResource = $this->getResourceElementHelperForProperty( $property );
+ }
+
+ $expData->addPropertyObjectValue(
+ $expNsResource,
+ $expElement
+ );
+
+ $this->addResourceHelperValue(
+ $expData,
+ $property,
+ $dataItem
+ );
+ }
+
+ protected function requiresAuxiliary( $key ) {
+ return !in_array( $key, [ '_SKEY', '_INST', '_MDAT', '_CDAT', '_SUBC', '_SUBP', '_TYPE', '_IMPO', '_URI' ] );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ConceptPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ConceptPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..722cd834
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ConceptPropertyValueResourceBuilder.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ConceptPropertyValueResourceBuilder extends PredefinedPropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->getKey() === '_CONC';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ $expElement = $this->exporter->getDataItemExpElement(
+ $dataItem
+ );
+
+ if ( $expData->getSubject()->getUri() === '' || $expElement === null ) {
+ return;
+ }
+
+ foreach ( $expElement->getProperties() as $subp ) {
+ if ( $subp->getUri() != $this->exporter->getSpecialNsResource( 'rdf', 'type' )->getUri() ) {
+ foreach ( $expElement->getValues( $subp ) as $subval ) {
+ $expData->addPropertyObjectValue( $subp, $subval );
+ }
+ }
+ }
+
+ $this->addResourceHelperValue(
+ $expData,
+ $property,
+ $dataItem
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/DispatchingResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/DispatchingResourceBuilder.php
new file mode 100644
index 00000000..ba4e11f5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/DispatchingResourceBuilder.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DIProperty;
+use SMW\Exporter\ResourceBuilder;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DispatchingResourceBuilder implements ResourceBuilder {
+
+ /**
+ * @var ResourceBuilder[]
+ */
+ private $resourceBuilders = [];
+
+ /**
+ * @var ResourceBuilder
+ */
+ private $defaultResourceBuilder = null;
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return boolean
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+
+ if ( $this->resourceBuilders === [] ) {
+ $this->initResourceBuilders();
+ }
+
+ foreach ( $this->resourceBuilders as $resourceBuilder ) {
+ if ( $resourceBuilder->isResourceBuilderFor( $property ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+ return $this->findResourceBuilder( $property )->addResourceValue( $expData, $property, $dataItem );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return ResourceBuilder $resourceBuilder
+ */
+ public function findResourceBuilder( DIProperty $property ) {
+
+ if ( $this->resourceBuilders === [] ) {
+ $this->initResourceBuilders();
+ }
+
+ foreach ( $this->resourceBuilders as $resourceBuilder ) {
+ if ( $resourceBuilder->isResourceBuilderFor( $property ) ) {
+ return $resourceBuilder;
+ }
+ }
+
+ return $this->defaultResourceBuilder;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ResourceBuilder $resourceBuilder
+ */
+ public function addResourceBuilder( ResourceBuilder $resourceBuilder ) {
+ $this->resourceBuilders[] = $resourceBuilder;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ResourceBuilder $defaultResourceBuilder
+ */
+ public function addDefaultResourceBuilder( ResourceBuilder $defaultResourceBuilder ) {
+ $this->defaultResourceBuilder = $defaultResourceBuilder;
+ }
+
+ private function initResourceBuilders() {
+
+ $this->addResourceBuilder( new UniquenessConstraintPropertyValueResourceBuilder() );
+
+ $sortPropertyValueResourceBuilder = new SortPropertyValueResourceBuilder();
+
+ $sortPropertyValueResourceBuilder->enabledCollationField(
+ ( (int)$GLOBALS['smwgSparqlQFeatures'] & SMW_SPARQL_QF_COLLATION ) != 0
+ );
+
+ $this->addResourceBuilder( $sortPropertyValueResourceBuilder );
+
+ $this->addResourceBuilder( new PropertyDescriptionValueResourceBuilder() );
+ $this->addResourceBuilder( new PreferredPropertyLabelResourceBuilder() );
+
+ $this->addResourceBuilder( new ExternalIdentifierPropertyValueResourceBuilder() );
+ $this->addResourceBuilder( new KeywordPropertyValueResourceBuilder() );
+
+ $this->addResourceBuilder( new MonolingualTextPropertyValueResourceBuilder() );
+ $this->addResourceBuilder( new ConceptPropertyValueResourceBuilder() );
+
+ $this->addResourceBuilder( new ImportFromPropertyValueResourceBuilder() );
+ $this->addResourceBuilder( new RedirectPropertyValueResourceBuilder() );
+
+ $this->addResourceBuilder( new AuxiliaryPropertyValueResourceBuilder() );
+ $this->addResourceBuilder( new PredefinedPropertyValueResourceBuilder() );
+
+ $this->addDefaultResourceBuilder( new PropertyValueResourceBuilder() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ExternalIdentifierPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ExternalIdentifierPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..a27f766f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ExternalIdentifierPropertyValueResourceBuilder.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWDIUri as DIUri;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ExternalIdentifierPropertyValueResourceBuilder extends PropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->findPropertyTypeID() === '_eid';
+ }
+
+ /**
+ * Instead of representing an external identifier as "owl:sameAs", the weaker
+ * declarative axiom "skos:exactMatch" has been chosen to avoid potential
+ * issues with undesirable entailments.
+ *
+ * "skos:exactMatch" has been defined as "... indicating a high degree of
+ * confidence that the concepts can be used interchangeably across a wide
+ * range of information retrieval applications ..."
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ parent::addResourceValue( $expData, $property, $dataItem );
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ $uri = $dataValue->getUri();
+
+ if ( $uri instanceof DIUri ) {
+ $expData->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'skos', 'exactMatch' ),
+ $this->exporter->getDataItemExpElement( $uri )
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ImportFromPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ImportFromPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..bacc7c81
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/ImportFromPropertyValueResourceBuilder.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWExpData as ExpData;
+use SMWImportValue as ImportValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ImportFromPropertyValueResourceBuilder extends PredefinedPropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->getKey() === '_IMPO';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ $diSubject = $expData->getSubject()->getDataItem();
+
+ if ( $diSubject === null ) {
+ return;
+ }
+
+ $expNsResource = $this->exporter->getSpecialPropertyResource(
+ $property->getKey(),
+ $diSubject->getNamespace()
+ );
+
+
+ if ( $expNsResource === null ) {
+ return;
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ if ( !$dataValue instanceof ImportValue ) {
+ return;
+ }
+
+ $expData->addPropertyObjectValue(
+ $expNsResource,
+ $this->exporter->getDataItemExpElement( new DIBlob( $dataValue->getImportReference() ) )
+ );
+
+ $this->addResourceHelperValue(
+ $expData,
+ $property,
+ $dataItem
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/KeywordPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/KeywordPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..7855be2b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/KeywordPropertyValueResourceBuilder.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWDIUri as DIUri;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class KeywordPropertyValueResourceBuilder extends PropertyValueResourceBuilder {
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->findPropertyTypeID() === '_keyw';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ $dataItem = new DIBlob(
+ DIBlob::normalize( $dataItem->getString() )
+ );
+
+ parent::addResourceValue( $expData, $property, $dataItem );
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ $uri = $dataValue->getUri();
+
+ /**
+ * @see https://www.w3.org/2009/08/skos-reference/skos.rdf
+ *
+ * "skos:relatedMatch" has been defined as "... used to state an associative
+ * mapping link between two conceptual resources in different concept
+ * schemes ..."
+ */
+ if ( $uri instanceof DIUri ) {
+ $expData->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'skos', 'relatedMatch' ),
+ $this->exporter->getDataItemExpElement( $uri )
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/MonolingualTextPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/MonolingualTextPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..7ab22b51
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/MonolingualTextPropertyValueResourceBuilder.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+use SMWExpLiteral as ExpLiteral;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class MonolingualTextPropertyValueResourceBuilder extends PropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->findPropertyTypeID() === '_mlt_rec';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ $expResourceElement = $this->exporter->getResourceElementForWikiPage(
+ $property->getCanonicalDiWikiPage(),
+ true
+ );
+
+ // Avoid that an imported vocabulary is pointing to an internal resource.
+ //
+ // For example: <Has_alternative_label> imported from <skos:altLabel>
+ // with "Monolingual text" type is expected to produce:
+ //
+ // - <property:Has_alternative_label rdf:resource="http://example.org/id/Foo_MLa9c103f4379a94bfab97819dacd3c182"/>
+ // - <skos:altLabel xml:lang="en">Foo</skos:altLabel>
+ if ( $expResourceElement->isImported() ) {
+ $seekImportVocabulary = false;
+
+ $expData->addPropertyObjectValue(
+ $this->exporter->getResourceElementForProperty( $property, false, $seekImportVocabulary ),
+ $this->exporter->getDataItemExpElement( $dataItem )
+ );
+ } else {
+ parent::addResourceValue( $expData, $property, $dataItem );
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ $list = $dataValue->toArray();
+
+ if ( !isset( $list['_TEXT'] ) || !isset( $list['_LCODE'] ) ) {
+ return;
+ }
+
+ $expData->addPropertyObjectValue(
+ $expResourceElement,
+ new ExpLiteral(
+ (string)$list['_TEXT'],
+ 'http://www.w3.org/2001/XMLSchema#string',
+ (string)$list['_LCODE'],
+ $dataItem
+ )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PredefinedPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PredefinedPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..6683de03
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PredefinedPropertyValueResourceBuilder.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PredefinedPropertyValueResourceBuilder extends PropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return !$property->isUserDefined();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ $diSubject = $expData->getSubject()->getDataItem();
+
+ if ( $diSubject === null ) {
+ return;
+ }
+
+ $expNsResource = $this->exporter->getSpecialPropertyResource(
+ $property->getKey(),
+ $diSubject->getNamespace()
+ );
+
+ $expElement = $this->exporter->getDataItemExpElement(
+ $dataItem
+ );
+
+ if ( $expElement === null || $expNsResource === null ) {
+ return;
+ }
+
+ $expData->addPropertyObjectValue(
+ $expNsResource,
+ $expElement
+ );
+
+ $this->addResourceHelperValue(
+ $expData,
+ $property,
+ $dataItem
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PreferredPropertyLabelResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PreferredPropertyLabelResourceBuilder.php
new file mode 100644
index 00000000..83c534bd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PreferredPropertyLabelResourceBuilder.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+use SMWExpLiteral as ExpLiteral;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PreferredPropertyLabelResourceBuilder extends PropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->getKey() === '_PPLB';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ parent::addResourceValue( $expData, $property, $dataItem );
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ $list = $dataValue->toArray();
+
+ if ( !isset( $list['_TEXT'] ) || !isset( $list['_LCODE'] ) ) {
+ return;
+ }
+
+ // https://www.w3.org/TR/2009/NOTE-skos-primer-20090818/#secpref
+ //
+ // "skos:prefLabel ... implies that a resource can only have one such
+ // label per language tag ... it is recommended that no two concepts in
+ // the same KOS be given the same preferred lexical label for any given
+ // language tag ..."
+
+ $expData->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'skos', 'prefLabel' ),
+ new ExpLiteral(
+ (string)$list['_TEXT'],
+ 'http://www.w3.org/2001/XMLSchema#string',
+ (string)$list['_LCODE'],
+ $dataItem
+ )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PropertyDescriptionValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PropertyDescriptionValueResourceBuilder.php
new file mode 100644
index 00000000..a99c77b6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PropertyDescriptionValueResourceBuilder.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+use SMWExpLiteral as ExpLiteral;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyDescriptionValueResourceBuilder extends PropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->getKey() === '_PDESC';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ parent::addResourceValue( $expData, $property, $dataItem );
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ $list = $dataValue->toArray();
+
+ if ( !isset( $list['_TEXT'] ) || !isset( $list['_LCODE'] ) ) {
+ return;
+ }
+
+ // Ussing `skos:scopeNote` instead of `skos:definition` since we can not
+ // ensure that the description given by a user is complete.
+ //
+ // "skos:scopeNote supplies some, possibly partial, information about the
+ // intended meaning of a concept ..."
+ //
+ // "skos:definition supplies a complete explanation of the intended
+ // meaning of a concept."
+ //
+ // According to https://www.w3.org/TR/2009/NOTE-skos-primer-20090818/#secdocumentation
+
+ $expData->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'skos', 'scopeNote' ),
+ new ExpLiteral(
+ (string)$list['_TEXT'],
+ 'http://www.w3.org/2001/XMLSchema#string',
+ (string)$list['_LCODE'],
+ $dataItem
+ )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PropertyValueResourceBuilder.php
new file mode 100644
index 00000000..d8eb3dc4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/PropertyValueResourceBuilder.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\Exporter\ResourceBuilder;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+use SMWExporter as Exporter;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyValueResourceBuilder implements ResourceBuilder {
+
+ /**
+ * @var Exporter
+ */
+ protected $exporter;
+
+ /**
+ * @var InMemoryPoolCache
+ */
+ private $inMemoryPoolCache;
+
+ /**
+ * @since 2.5
+ *
+ * @param Exporter|null $exporter
+ */
+ public function __construct( Exporter $exporter = null ) {
+ $this->exporter = $exporter;
+
+ if ( $this->exporter === null ) {
+ $this->exporter = Exporter::getInstance();
+ }
+
+ $this->inMemoryPoolCache = ApplicationFactory::getInstance()->getInMemoryPoolCache()->getPoolCacheById(
+ Exporter::POOLCACHE_ID
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ $expElement = $this->exporter->getDataItemExpElement(
+ $dataItem
+ );
+
+ if ( $expElement !== null ) {
+ $expData->addPropertyObjectValue(
+ $this->getResourceElementForProperty( $property ),
+ $expElement
+ );
+ }
+
+ $this->addResourceHelperValue(
+ $expData,
+ $property,
+ $dataItem
+ );
+ }
+
+ protected function addResourceHelperValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ $expElementHelper = $this->exporter->getDataItemHelperExpElement(
+ $dataItem
+ );
+
+ if ( $expElementHelper !== null ) {
+ $expData->addPropertyObjectValue(
+ $this->getResourceElementHelperForProperty( $property ),
+ $expElementHelper
+ );
+ }
+ }
+
+ protected function getResourceElementForProperty( $property ) {
+
+ $key = 'resource:builder:' . $property->getKey();
+
+ if ( ( $resourceElement = $this->inMemoryPoolCache->fetch( $key ) ) !== false ) {
+ return $resourceElement;
+ }
+
+ $resourceElement = $this->exporter->getResourceElementForProperty( $property );
+
+ $this->inMemoryPoolCache->save(
+ $key,
+ $resourceElement
+ );
+
+ return $resourceElement;
+ }
+
+ protected function getResourceElementHelperForProperty( $property ) {
+
+ $key = 'resource:builder:aux:' . $property->getKey();
+
+ if ( ( $resourceElement = $this->inMemoryPoolCache->fetch( $key ) ) !== false ) {
+ return $resourceElement;
+ }
+
+ $resourceElement = $this->exporter->getResourceElementForProperty( $property, true );
+
+ $this->inMemoryPoolCache->save(
+ $key,
+ $resourceElement
+ );
+
+ return $resourceElement;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/RedirectPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/RedirectPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..a489beea
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/RedirectPropertyValueResourceBuilder.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class RedirectPropertyValueResourceBuilder extends PredefinedPropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->getKey() === '_REDI';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ parent::addResourceValue( $expData, $property, $dataItem );
+
+ $expElement = $this->exporter->getDataItemExpElement(
+ $dataItem
+ );
+
+ if ( $expElement === null ) {
+ return;
+ }
+
+ $expData->addPropertyObjectValue(
+ $this->exporter->getSpecialPropertyResource( '_URI' ),
+ $expElement
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/SortPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/SortPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..3a91d0fb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/SortPropertyValueResourceBuilder.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DIProperty;
+use SMW\MediaWiki\Collator;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWExpData as ExpData;
+use SMWExpLiteral as ExpLiteral;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SortPropertyValueResourceBuilder extends PredefinedPropertyValueResourceBuilder {
+
+ /**
+ * @var boolean
+ */
+ private $enabledCollationField = false;
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->getKey() === '_SKEY';
+ }
+
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $enabledCollationField
+ */
+ public function enabledCollationField( $enabledCollationField ) {
+ $this->enabledCollationField = (bool)$enabledCollationField;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ if ( !$dataItem instanceof DIBlob ) {
+ $dataItem = new DIBlob( $dataItem->getSortKey() );
+ }
+
+ parent::addResourceValue( $expData, $property, $dataItem );
+
+ if ( $this->enabledCollationField === false ) {
+ return;
+ }
+
+ $sort = Collator::singleton()->armor(
+ Collator::singleton()->getSortKey( $dataItem->getSortKey() )
+ );
+
+ $expData->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'swivt', 'sort' ),
+ new ExpLiteral(
+ $sort,
+ 'http://www.w3.org/2001/XMLSchema#string'
+ )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/UniquenessConstraintPropertyValueResourceBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/UniquenessConstraintPropertyValueResourceBuilder.php
new file mode 100644
index 00000000..40d4f55a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/ResourceBuilders/UniquenessConstraintPropertyValueResourceBuilder.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace SMW\Exporter\ResourceBuilders;
+
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+use SMWExpData as ExpData;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class UniquenessConstraintPropertyValueResourceBuilder extends PropertyValueResourceBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isResourceBuilderFor( DIProperty $property ) {
+ return $property->getKey() === '_PVUC';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function addResourceValue( ExpData $expData, DIProperty $property, DataItem $dataItem ) {
+
+ parent::addResourceValue( $expData, $property, $dataItem );
+
+ // https://www.w3.org/TR/2004/REC-owl-ref-20040210/#FunctionalProperty-def
+ //
+ // "A functional property is a property that can have only one (unique)
+ // value y for each instance x ..."
+
+ $expData->addPropertyObjectValue(
+ $this->exporter->getSpecialNsResource( 'rdf', 'type' ),
+ $this->exporter->getSpecialNsResource( 'owl', 'FunctionalProperty' )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Exporter/XsdValueMapper.php b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/XsdValueMapper.php
new file mode 100644
index 00000000..80eb5795
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Exporter/XsdValueMapper.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace SMW\Exporter;
+
+use RuntimeException;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWDIBoolean as DIBoolean;
+use SMWDINumber as DINumber;
+use SMWDITime as DITime;
+
+/**
+ * This class only maps primitive types (string, boolean, integers ) mostly to
+ * be encoded as literal and all other dataitems are handled separately.
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class XsdValueMapper {
+
+ /**
+ * @var string
+ */
+ private $xsdValue = '';
+
+ /**
+ * @var string
+ */
+ private $xsdType = '';
+
+ /**
+ * @since 2.2
+ *
+ * @param DataItem $dataItem
+ *
+ * @throws RuntimeException
+ */
+ public function map( DataItem $dataItem ) {
+
+ if ( $dataItem instanceof DIBoolean ) {
+ $this->parseToBooleanValue( $dataItem );
+ } elseif ( $dataItem instanceof DINumber ) {
+ $this->parseToDoubleValue( $dataItem );
+ } elseif ( $dataItem instanceof DIBlob ) {
+ $this->parseToStringValue( $dataItem );
+ } elseif ( $dataItem instanceof DITime && $dataItem->getCalendarModel() === DITime::CM_GREGORIAN ) {
+ $this->parseToTimeValueForGregorianCalendarModel( $dataItem );
+ } else {
+ throw new RuntimeException( "Cannot match the dataItem of type " . $dataItem->getDIType() );
+ }
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getXsdValue() {
+ return $this->xsdValue;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getXsdType() {
+ return $this->xsdType;
+ }
+
+ private function parseToStringValue( DIBlob $dataItem ) {
+ $this->xsdValue = smwfHTMLtoUTF8( $dataItem->getString() );
+ $this->xsdType = 'http://www.w3.org/2001/XMLSchema#string';
+ }
+
+ private function parseToDoubleValue( DINumber $dataItem ) {
+ $this->xsdValue = strval( $dataItem->getNumber() );
+ $this->xsdType = 'http://www.w3.org/2001/XMLSchema#double';
+ }
+
+ private function parseToBooleanValue( DIBoolean $dataItem ) {
+ $this->xsdValue = $dataItem->getBoolean() ? 'true' : 'false';
+ $this->xsdType = 'http://www.w3.org/2001/XMLSchema#boolean';
+ }
+
+ private function parseToTimeValueForGregorianCalendarModel( DITime $dataItem ) {
+
+ if ( $dataItem->getYear() > 0 ) {
+ $xsdvalue = str_pad( $dataItem->getYear(), 4, "0", STR_PAD_LEFT );
+ } else {
+ $xsdvalue = '-' . str_pad( 1 - $dataItem->getYear(), 4, "0", STR_PAD_LEFT );
+ }
+
+ $xsdtype = 'http://www.w3.org/2001/XMLSchema#gYear';
+
+ if ( $dataItem->getPrecision() >= DITime::PREC_YM ) {
+ $xsdtype = 'http://www.w3.org/2001/XMLSchema#gYearMonth';
+ $xsdvalue .= '-' . str_pad( $dataItem->getMonth(), 2, "0", STR_PAD_LEFT );
+ if ( $dataItem->getPrecision() >= DITime::PREC_YMD ) {
+ $xsdtype = 'http://www.w3.org/2001/XMLSchema#date';
+ $xsdvalue .= '-' . str_pad( $dataItem->getDay(), 2, "0", STR_PAD_LEFT );
+ if ( $dataItem->getPrecision() == DITime::PREC_YMDT ) {
+ $xsdtype = 'http://www.w3.org/2001/XMLSchema#dateTime';
+ $xsdvalue .= 'T' .
+ sprintf( "%02d", $dataItem->getHour() ) . ':' .
+ sprintf( "%02d", $dataItem->getMinute()) . ':' .
+ sprintf( "%02d", $dataItem->getSecond() );
+ }
+
+ // https://www.w3.org/TR/2005/NOTE-timezone-20051013/
+ // "Time zone identification in the date and time types relies
+ // entirely on time zone offset from UTC."
+ // Zone offset Z indicates UTC
+ $xsdvalue .= 'Z';
+ }
+ }
+
+ $this->xsdValue = $xsdvalue;
+ $this->xsdType = $xsdtype;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Factbox/CachedFactbox.php b/www/wiki/extensions/SemanticMediaWiki/src/Factbox/CachedFactbox.php
new file mode 100644
index 00000000..11c5de72
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Factbox/CachedFactbox.php
@@ -0,0 +1,321 @@
+<?php
+
+namespace SMW\Factbox;
+
+use Onoi\Cache\Cache;
+use OutputPage;
+use ParserOutput;
+use SMW\ApplicationFactory;
+use SMW\Parser\InTextAnnotationParser;
+use Title;
+use Language;
+
+/**
+ * Factbox output caching
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class CachedFactbox {
+
+ const CACHE_NAMESPACE = 'smw:fc';
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var boolean
+ */
+ private $isCached = false;
+
+ /**
+ * @var boolean
+ */
+ private $isEnabled = true;
+
+ /**
+ * @var integer
+ */
+ private $featureSet = 0;
+
+ /**
+ * @var integer
+ */
+ private $expiryInSeconds = 0;
+
+ /**
+ * @var integer
+ */
+ private $timestamp;
+
+ /**
+ * @since 1.9
+ *
+ * @param Cache $cache
+ */
+ public function __construct( Cache $cache ) {
+ $this->cache = $cache;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title|integer $id
+ *
+ * @return string
+ */
+ public static function makeCacheKey( $id ) {
+
+ if ( $id instanceof Title ) {
+ $id = $id->getArticleID();
+ }
+
+ return smwfCacheKey( self::CACHE_NAMESPACE, $id );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function isCached() {
+ return $this->isCached;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $featureSet
+ */
+ public function setFeatureSet( $featureSet ) {
+ $this->featureSet = $featureSet;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function setExpiryInSeconds( $expiryInSeconds ) {
+ $this->expiryInSeconds = $expiryInSeconds;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function isEnabled( $isEnabled ) {
+ $this->isEnabled = $isEnabled;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return integer
+ */
+ public function getTimestamp() {
+ return $this->timestamp;
+ }
+
+ /**
+ * Prepare and update the OutputPage property
+ *
+ * Factbox content is either retrieved from a CacheStore or re-parsed from
+ * the Factbox object
+ *
+ * Altered content is tracked using the revision Id, getLatestRevID() only
+ * changes after a content modification has occurred.
+ *
+ * @since 1.9
+ *
+ * @param OutputPage &$outputPage
+ * @param Language $language
+ * @param ParserOutput $parserOutput
+ */
+ public function prepareFactboxContent( OutputPage &$outputPage, Language $language, ParserOutput $parserOutput ) {
+
+ $content = '';
+ $title = $outputPage->getTitle();
+
+ $rev_id = $this->findRevId( $title, $outputPage->getContext() );
+ $lang = $language->getCode();
+
+ $key = self::makeCacheKey( $title );
+
+ if ( $this->cache->contains( $key ) ) {
+ $content = $this->retrieveFromCache( $key );
+ }
+
+ if ( $this->hasCachedContent( $rev_id, $lang, $content, $outputPage->getContext() ) ) {
+ return $outputPage->mSMWFactboxText = $content['text'];
+ }
+
+ $text = $this->rebuild(
+ $title,
+ $parserOutput,
+ $outputPage->getContext()
+ );
+
+ $this->addContentToCache(
+ $key,
+ $text,
+ $rev_id,
+ $lang,
+ $this->featureSet
+ );
+
+ $outputPage->mSMWFactboxText = $text;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $key
+ * @param string $text
+ * @param integer|null $revisionId
+ */
+ public function addContentToCache( $key, $text, $revisionId = null, $lang = 'en', $fset = null ) {
+ $this->saveToCache(
+ $key,
+ [
+ 'revId' => $revisionId,
+ 'lang' => $lang,
+ 'fset' => $fset,
+ 'text' => $text
+ ]
+ );
+ }
+
+ /**
+ * Returns parsed Factbox content from either the OutputPage property
+ * or from the Cache
+ *
+ * @since 1.9
+ *
+ * @param OutputPage $outputPage
+ *
+ * @return string
+ */
+ public function retrieveContent( OutputPage $outputPage ) {
+
+ $text = '';
+ $title = $outputPage->getTitle();
+
+ if ( $title instanceof Title && ( $title->isSpecialPage() || !$title->exists() ) ) {
+ return $text;
+ }
+
+ if ( isset( $outputPage->mSMWFactboxText ) ) {
+ $text = $outputPage->mSMWFactboxText;
+ } elseif ( $title instanceof Title ) {
+
+ $content = $this->retrieveFromCache(
+ self::makeCacheKey( $title )
+ );
+
+ $text = isset( $content['text'] ) ? $content['text'] : '';
+ }
+
+ return $text;
+ }
+
+ /**
+ * Return a revisionId either from the WebRequest object (display an old
+ * revision or permalink etc.) or from the title object
+ */
+ private function findRevId( Title $title, $requestContext ) {
+
+ if ( $requestContext->getRequest()->getCheck( 'oldid' ) ) {
+ return (int)$requestContext->getRequest()->getVal( 'oldid' );
+ }
+
+ return $title->getLatestRevID();
+ }
+
+ /**
+ * Processing and reparsing of the Factbox content
+ */
+ private function rebuild( Title $title, ParserOutput $parserOutput, $requestContext ) {
+
+ $text = null;
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $factbox = $applicationFactory->singleton( 'FactboxFactory' )->newFactbox(
+ $title,
+ $parserOutput
+ );
+
+ $factbox->setPreviewFlag(
+ $requestContext->getRequest()->getCheck( 'wpPreview' )
+ );
+
+ if ( $factbox->doBuild()->isVisible() ) {
+
+ $contentParser = $applicationFactory->newContentParser( $title );
+ $contentParser->parse( $factbox->getContent() );
+
+ $text = InTextAnnotationParser::removeAnnotation(
+ $contentParser->getOutput()->getText()
+ );
+
+ $text = $factbox->tabs( $text );
+ }
+
+ return $text;
+ }
+
+ private function hasCachedContent( $revId, $lang, $content, $requestContext ) {
+
+ if ( $requestContext->getRequest()->getVal( 'action' ) === 'edit' ) {
+ return $this->isCached = false;
+ }
+
+ if ( $revId !== 0 && isset( $content['revId'] ) && ( $content['revId'] === $revId ) && $content['text'] !== null ) {
+
+ if (
+ ( isset( $content['lang'] ) && $content['lang'] === $lang ) &&
+ ( isset( $content['fset'] ) && $content['fset'] === $this->featureSet ) ) {
+ return $this->isCached = true;
+ }
+ }
+
+ return $this->isCached = false;
+ }
+
+ private function retrieveFromCache( $key ) {
+
+ if ( !$this->cache->contains( $key ) || !$this->isEnabled ) {
+ return [];
+ }
+
+ $data = $this->cache->fetch( $key );
+
+ $this->isCached = true;
+ $this->timestamp = $data['time'];
+
+ return unserialize( $data['content'] );
+ }
+
+ /**
+ * Cached content is serialized in an associative array following:
+ * { 'revId' => $revisionId, 'text' => (...) }
+ */
+ private function saveToCache( $key, array $content ) {
+
+ $this->timestamp = wfTimestamp( TS_UNIX );
+ $this->isCached = false;
+
+ $data = [
+ 'time' => $this->timestamp,
+ 'content' => serialize( $content )
+ ];
+
+ $this->cache->save( $key, $data, $this->expiryInSeconds );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Factbox/Factbox.php b/www/wiki/extensions/SemanticMediaWiki/src/Factbox/Factbox.php
new file mode 100644
index 00000000..feae3999
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Factbox/Factbox.php
@@ -0,0 +1,461 @@
+<?php
+
+namespace SMW\Factbox;
+
+use Html;
+use Sanitizer;
+use Title;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Localizer;
+use SMW\Message;
+use SMW\ParserData;
+use SMW\Profiler;
+use SMW\SemanticData;
+use SMW\Store;
+use SMW\Utils\HtmlDivTable;
+use SMW\Utils\HtmlTabs;
+use SMWInfolink;
+use SMWSemanticData;
+
+/**
+ * Class handling the "Factbox" content rendering
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class Factbox {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var ParserData
+ */
+ private $parserData;
+
+ /**
+ * @var ApplicationFactory
+ */
+ private $applicationFactory;
+
+ /**
+ * @var DataValueFactory
+ */
+ private $dataValueFactory;
+
+ /**
+ * @var integer
+ */
+ private $featureSet = 0;
+
+ /**
+ * @var boolean
+ */
+ protected $isVisible = false;
+
+ /**
+ * @var string
+ */
+ protected $content = null;
+
+ /**
+ * @var boolean
+ */
+ private $previewFlag = false;
+
+ /**
+ * @since 1.9
+ *
+ * @param Store $store
+ * @param ParserData $parserData
+ */
+ public function __construct( Store $store, ParserData $parserData ) {
+ $this->store = $store;
+ $this->parserData = $parserData;
+ $this->applicationFactory = ApplicationFactory::getInstance();
+ $this->dataValueFactory = DataValueFactory::getInstance();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $featureSet
+ */
+ public function setFeatureSet( $featureSet ) {
+ $this->featureSet = $featureSet;
+ }
+
+ /**
+ * @note contains information about wpPreview
+ *
+ * @since 2.1
+ *
+ * @param boolean $previewFlag
+ */
+ public function setPreviewFlag( $previewFlag ) {
+ $this->previewFlag = $previewFlag;
+ }
+
+ /**
+ * Builds content suitable for rendering a Factbox and
+ * updating the ParserOutput accordingly
+ *
+ * @since 1.9
+ *
+ * @return Factbox
+ */
+ public function doBuild() {
+
+ $this->content = $this->fetchContent( $this->getMagicWords() );
+
+ if ( $this->content !== '' ) {
+ $this->parserData->getOutput()->addModules( $this->getModules() );
+ $this->parserData->pushSemanticDataToParserOutput();
+ $this->isVisible = true;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns Title object
+ *
+ * @since 1.9
+ *
+ * @return string|null
+ */
+ public function getTitle() {
+ return $this->parserData->getTitle();
+ }
+
+ /**
+ * Returns content
+ *
+ * @since 1.9
+ *
+ * @return string|null
+ */
+ public function getContent() {
+ return $this->content;
+ }
+
+ /**
+ * Returns if content is visible
+ *
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function isVisible() {
+ return $this->isVisible;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $rendered
+ * @param string $derived
+ *
+ * @return string
+ */
+ public static function tabs( $rendered, $derived = '' ) {
+
+ $htmlTabs = new HtmlTabs();
+ $htmlTabs->setActiveTab( 'facts-rendered' );
+ $htmlTabs->tab(
+ 'facts-rendered',
+ Message::get( 'smw-factbox-facts' , Message::TEXT, Message::USER_LANGUAGE ),
+ [
+ 'title' => Message::get( 'smw-factbox-facts-help' , Message::TEXT, Message::USER_LANGUAGE )
+ ]
+ );
+
+ $htmlTabs->content( 'facts-rendered', $rendered );
+
+ $htmlTabs->tab(
+ 'facts-derived',
+ Message::get( 'smw-factbox-derived' , Message::TEXT, Message::USER_LANGUAGE ),
+ [
+ 'hide' => $derived === '' ? true : false
+ ]
+ );
+
+ $htmlTabs->content( 'facts-derived', $derived );
+
+ return $htmlTabs->buildHTML(
+ [
+ 'class' => 'smw-factbox'
+ ]
+ );
+ }
+
+ /**
+ * Returns magic words attached to the ParserOutput object
+ *
+ * @since 1.9
+ *
+ * @return string|null
+ */
+ protected function getMagicWords() {
+
+ $settings = $this->applicationFactory->getSettings();
+ $parserOutput = $this->parserData->getOutput();
+
+ // Prior MW 1.21 mSMWMagicWords is used (see SMW\ParserTextProcessor)
+ if ( method_exists( $parserOutput, 'getExtensionData' ) ) {
+ $smwMagicWords = $parserOutput->getExtensionData( 'smwmagicwords' );
+ $mws = $smwMagicWords === null ? [] : $smwMagicWords;
+ } else {
+ // @codeCoverageIgnoreStart
+ $mws = isset( $parserOutput->mSMWMagicWords ) ? $parserOutput->mSMWMagicWords : [];
+ // @codeCoverageIgnoreEnd
+ }
+
+ if ( in_array( 'SMW_SHOWFACTBOX', $mws ) ) {
+ $showfactbox = SMW_FACTBOX_NONEMPTY;
+ } elseif ( in_array( 'SMW_NOFACTBOX', $mws ) ) {
+ $showfactbox = SMW_FACTBOX_HIDDEN;
+ } elseif ( $this->previewFlag ) {
+ $showfactbox = $settings->get( 'smwgShowFactboxEdit' );
+ } else {
+ $showfactbox = $settings->get( 'smwgShowFactbox' );
+ }
+
+ return $showfactbox;
+ }
+
+ /**
+ * Returns required resource modules
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ protected function getModules() {
+ return [
+ 'ext.smw.style',
+ 'ext.smw.table.styles'
+ ];
+ }
+
+ /**
+ * Returns content found for a given ParserOutput object and if the required
+ * custom data was not available then semantic data are retrieved from
+ * the store for a given subject.
+ *
+ * The method checks whether the given setting of $showfactbox requires
+ * displaying the given data at all.
+ *
+ * @since 1.9
+ *
+ * @return integer $showFactbox
+ *
+ * @return string|null
+ */
+ protected function fetchContent( $showFactbox = SMW_FACTBOX_NONEMPTY ) {
+
+ if ( $showFactbox === SMW_FACTBOX_HIDDEN ) {
+ return '';
+ }
+
+ $semanticData = $this->parserData->getSemanticData();
+
+ if ( $semanticData === null || $semanticData->stubObject || $this->isEmpty( $semanticData ) ) {
+ $semanticData = $this->store->getSemanticData( $this->parserData->getSubject() );
+ }
+
+ if ( $showFactbox === SMW_FACTBOX_SPECIAL && !$semanticData->hasVisibleSpecialProperties() ) {
+ // show only if there are special properties
+ return '';
+ } elseif ( $showFactbox === SMW_FACTBOX_NONEMPTY && !$semanticData->hasVisibleProperties() ) {
+ // show only if non-empty
+ return '';
+ }
+
+ return $this->createTable( $semanticData );
+ }
+
+ /**
+ * Returns a formatted factbox table
+ *
+ * @since 1.9
+ *
+ * @param SMWSemanticData $semanticData
+ *
+ * @return string|null
+ */
+ protected function createTable( SemanticData $semanticData ) {
+
+ $html = '';
+
+ // Hook deprecated with SMW 1.9 and will vanish with SMW 1.11
+ \Hooks::run( 'smwShowFactbox', [ &$html, $semanticData ] );
+
+ // Hook since 1.9
+ if ( \Hooks::run( 'SMW::Factbox::BeforeContentGeneration', [ &$html, $semanticData ] ) ) {
+
+ $header = $this->createHeader( $semanticData->getSubject() );
+ $rows = $this->createRows( $semanticData );
+
+ $html .= Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smwfact',
+ 'style' => 'display:block;'
+ ],
+ $header . HtmlDivTable::table(
+ $rows,
+ [
+ 'class' => 'smwfacttable'
+ ]
+ )
+ );
+ }
+
+ return $html;
+ }
+
+ private function createHeader( DIWikiPage $subject ) {
+
+ $dataValue = $this->dataValueFactory->newDataValueByItem( $subject, null );
+
+ $browselink = SMWInfolink::newBrowsingLink(
+ $dataValue->getPreferredCaption(),
+ $dataValue->getWikiValue(),
+ ''
+ );
+
+ $header = Html::rawElement(
+ 'div',
+ [ 'class' => 'smwfactboxhead' ],
+ Message::get( [ 'smw-factbox-head', $browselink->getWikiText() ], Message::TEXT, Message::USER_LANGUAGE )
+ );
+
+ $rdflink = SMWInfolink::newInternalLink(
+ Message::get( 'smw_viewasrdf', Message::TEXT, Message::USER_LANGUAGE ),
+ Localizer::getInstance()->getNamespaceTextById( NS_SPECIAL ) . ':ExportRDF/' . $dataValue->getWikiValue(),
+ 'rdflink'
+ );
+
+ $header .= Html::rawElement(
+ 'div',
+ [ 'class' => 'smwrdflink' ],
+ $rdflink->getWikiText()
+ );
+
+ return $header;
+ }
+
+ private function createRows( SemanticData $semanticData ) {
+
+ $rows = '';
+ $attributes = [];
+
+ $comma = Message::get(
+ 'comma-separator',
+ Message::ESCAPED,
+ Message::USER_LANGUAGE
+ );
+
+ $and = Message::get(
+ 'and',
+ Message::ESCAPED,
+ Message::USER_LANGUAGE
+ );
+
+ foreach ( $semanticData->getProperties() as $property ) {
+
+ if ( $property->getKey() === '_SOBJ' && !$this->hasFeature( SMW_FACTBOX_DISPLAY_SUBOBJECT ) ) {
+ continue;
+ }
+
+ $propertyDv = $this->dataValueFactory->newDataValueByItem( $property, null );
+ $row = '';
+
+ if ( !$property->isShown() ) {
+ // showing this is not desired, hide
+ continue;
+ } elseif ( $property->isUserDefined() ) {
+ $propertyDv->setCaption( $propertyDv->getWikiValue() );
+ $attributes['property'] = [ 'class' => 'smwpropname' ];
+ $attributes['values'] = [ 'class' => 'smwprops' ];
+ } elseif ( $propertyDv->isVisible() ) {
+ // Predefined property
+ $attributes['property'] = [ 'class' => 'smwspecname' ];
+ $attributes['values'] = [ 'class' => 'smwspecs' ];
+ } else {
+ // predefined, internal property
+ // @codeCoverageIgnoreStart
+ continue;
+ // @codeCoverageIgnoreEnd
+ }
+
+ $list = [];
+ $html = '';
+
+ foreach ( $semanticData->getPropertyValues( $property ) as $dataItem ) {
+
+ $dataValue = $this->dataValueFactory->newDataValueByItem( $dataItem, $property );
+
+ $outputFormat = $dataValue->getOutputFormat();
+ $dataValue->setOutputFormat( $outputFormat ? $outputFormat : 'LOCL' );
+
+ $dataValue->setOption( $dataValue::OPT_DISABLE_SERVICELINKS, true );
+
+ if ( $dataValue->isValid() ) {
+ $list[] = $dataValue->getLongWikiText( true ) . $dataValue->getInfolinkText( SMW_OUTPUT_WIKI );
+ }
+ }
+
+ if ( $list !== [] ) {
+ $last = array_pop( $list );
+
+ if ( $list === [] ) {
+ $html = $last;
+ } else {
+ $html = implode( $comma, $list ) . '&nbsp;' . $and . '&nbsp;' . $last;
+ }
+ }
+
+ $row .= HtmlDivTable::cell(
+ $propertyDv->getShortWikiText( true ),
+ $attributes['property']
+ );
+
+ $row .= HtmlDivTable::cell(
+ $html,
+ $attributes['values']
+ );
+
+ $rows .= HtmlDivTable::row(
+ $row
+ );
+ }
+
+ return $rows;
+ }
+
+ private function isEmpty( SemanticData $semanticData ) {
+
+ // MW's internal Parser does iterate the ParserOutput object several times
+ // which can leave a '_SKEY' property while in fact the container is empty.
+ $semanticData->removeProperty(
+ new DIProperty( '_SKEY' )
+ );
+
+ return $semanticData->isEmpty();
+ }
+
+
+ private function hasFeature( $feature ) {
+ return ( (int)$this->featureSet & $feature ) != 0;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Factbox/FactboxFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Factbox/FactboxFactory.php
new file mode 100644
index 00000000..239d037a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Factbox/FactboxFactory.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace SMW\Factbox;
+
+use IContextSource;
+use OutputPage;
+use SMW\ApplicationFactory;
+use Title;
+use ParserOutput;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class FactboxFactory {
+
+ /**
+ * @since 2.0
+ *
+ * @return CachedFactbox
+ */
+ public function newCachedFactbox() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ $cachedFactbox = new CachedFactbox(
+ $applicationFactory->getCache(
+ $settings->get( 'smwgMainCacheType' )
+ )
+ );
+
+ // Month = 30 * 24 * 3600
+ $cachedFactbox->setExpiryInSeconds( 2592000 );
+
+ $cachedFactbox->isEnabled(
+ $settings->isFlagSet( 'smwgFactboxFeatures', SMW_FACTBOX_CACHE )
+ );
+
+ $cachedFactbox->setFeatureSet(
+ $settings->get( 'smwgFactboxFeatures' )
+ );
+
+ return $cachedFactbox;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param Title $title
+ * @param ParserOutput $parserOutput
+ *
+ * @return Factbox
+ */
+ public function newFactbox( Title $title, ParserOutput $parserOutput ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $factbox = new Factbox(
+ $applicationFactory->getStore(),
+ $applicationFactory->newParserData( $title, $parserOutput )
+ );
+
+ $factbox->setFeatureSet(
+ $applicationFactory->getSettings()->get( 'smwgFactboxFeatures' )
+ );
+
+ return $factbox;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/GlobalFunctions.php b/www/wiki/extensions/SemanticMediaWiki/src/GlobalFunctions.php
new file mode 100644
index 00000000..7fa3a9e8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/GlobalFunctions.php
@@ -0,0 +1,282 @@
+<?php
+
+use SMW\CompatibilityMode;
+use SMW\DataValues\Number\IntlNumberFormatter;
+use SMW\Highlighter;
+use SMW\NamespaceManager;
+use SMW\ProcessingErrorMsgHandler;
+
+/**
+ * Global functions specified and used by Semantic MediaWiki. In general, it is
+ * tried to fit functions in suitable classes as static methods if they clearly
+ * belong to some particular sub-function of SMW. Most functions here are used
+ * in diverse contexts so that they do not have fonud a place in any such class
+ * yet.
+ * @ingroup SMW
+ */
+
+/**
+ * Takes a title text and turns it safely into its DBKey. This function
+ * reimplements most of the title normalization as done in Title.php in order
+ * to achieve conversion with less overhead. The official code could be called
+ * here if more advanced normalization is needed.
+ *
+ * @param string $text
+ */
+function smwfNormalTitleDBKey( $text ) {
+ global $wgCapitalLinks;
+
+ $text = trim( $text );
+
+ if ( $wgCapitalLinks ) {
+ $text = ucfirst( $text );
+ }
+
+ return str_replace( ' ', '_', $text );
+}
+
+/**
+ * Takes a text and turns it into a normalised version. This function
+ * reimplements the title normalization as done in Title.php in order to
+ * achieve conversion with less overhead. The official code could be called
+ * here if more advanced normalization is needed.
+ *
+ * @param string $text
+ */
+function smwfNormalTitleText( $text ) {
+ global $wgCapitalLinks, $wgContLang;
+
+ $text = trim( $text );
+
+ if ( $wgCapitalLinks ) {
+ $text = $wgContLang->ucfirst( $text );
+ }
+
+ return str_replace( '_', ' ', $text );
+}
+
+/**
+ * Escapes text in a way that allows it to be used as XML content (e.g. as a
+ * string value for some property).
+ *
+ * @param string $text
+ */
+function smwfXMLContentEncode( $text ) {
+ return str_replace( [ '&', '<', '>' ], [ '&amp;', '&lt;', '&gt;' ], Sanitizer::decodeCharReferences( $text ) );
+}
+
+/**
+ * Decodes character references and inserts Unicode characters instead, using
+ * the MediaWiki Sanitizer.
+ *
+ * @param string $text
+ */
+function smwfHTMLtoUTF8( $text ) {
+ return Sanitizer::decodeCharReferences( $text );
+}
+
+/**
+ * @deprecated since 2.1, use NumberFormatter instead
+ */
+function smwfNumberFormat( $value, $decplaces = 3 ) {
+ return IntlNumberFormatter::getInstance()->getLocalizedFormattedNumber( $value, $decplaces );
+}
+
+/**
+ * @since 3.0
+ *
+ * @param string $text
+ */
+function smwfAbort( $text ) {
+
+ if ( PHP_SAPI === 'cli' && PHP_SAPI === 'phpdbg' ) {
+ die( $text );
+ }
+
+ $html = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
+ $html .= "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\" dir=\"ltr\">\n";
+ $html .= "<head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />";
+ $html .= "<title>Error</title></head><body><h2>Error</h2><hr style='border: 0; height: 0; border-top: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.3);'>";
+ $html .= "<p>{$text}</p></body></html>";
+
+ die( $html );
+}
+
+/**
+ * Formats an array of message strings so that it appears as a tooltip.
+ * $icon should be one of: 'warning' (default), 'info'.
+ *
+ * @param array $messages
+ * @param string $icon Acts like an enum. Callers must ensure safety, since this value is used directly in the output.
+ * @param string $seperator
+ * @param boolean $escape Should the messages be escaped or not (ie when they already are)
+ *
+ * @return string
+ */
+function smwfEncodeMessages( array $messages, $type = 'warning', $seperator = ' <!--br-->', $escape = true ) {
+
+ $messages = ProcessingErrorMsgHandler::normalizeAndDecodeMessages( $messages );
+
+ if ( $messages === [] ) {
+ return '';
+ }
+
+ if ( $escape ) {
+ $messages = array_map( 'htmlspecialchars', $messages );
+ }
+
+ if ( count( $messages ) == 1 ) {
+ $content = $messages[0];
+ } else {
+ foreach ( $messages as &$message ) {
+ $message = '<li>' . $message . '</li>';
+ }
+
+ $content = '<ul>' . implode( $seperator, $messages ) . '</ul>';
+ }
+
+ // Stop when a previous processing produced an error and it is expected to be
+ // added to a new tooltip (e.g {{#info {{#show ...}} }} ) instance
+ if ( Highlighter::hasHighlighterClass( $content, 'warning' ) ) {
+ return $content;
+ }
+
+ $highlighter = Highlighter::factory( $type );
+
+ $highlighter->setContent( [
+ 'caption' => null,
+ 'content' => Highlighter::decode( $content )
+ ] );
+
+ return $highlighter->getHtml();
+}
+
+/**
+ * Returns an instance for the storage back-end
+ *
+ * @return SMWStore
+ */
+function &smwfGetStore() {
+ $store = \SMW\StoreFactory::getStore();
+ return $store;
+}
+
+/**
+ * @since 3.0
+ *
+ * @param string $namespace
+ * @param string $key
+ *
+ * @return string
+ */
+function smwfCacheKey( $namespace, $key ) {
+
+ $cachePrefix = $GLOBALS['wgCachePrefix'] === false ? wfWikiID() : $GLOBALS['wgCachePrefix'];
+
+ if ( $namespace{0} !== ':' ) {
+ $namespace = ':' . $namespace;
+ }
+
+ if ( is_array( $key ) ) {
+ $key = json_encode( $key );
+ }
+
+ return $cachePrefix . $namespace . ':' . md5( $key );
+}
+
+/**
+ * Compatibility helper for using Linker methods.
+ * MW 1.16 has a Linker with non-static methods,
+ * where in MW 1.19 they are static, and a DummyLinker
+ * class is introduced, which can be instantiated for
+ * compat reasons. As of MW 1.28, DummyLinker is being
+ * deprecated, so always use Linker.
+ *
+ * @since 1.6
+ *
+ * @return Linker
+ */
+function smwfGetLinker() {
+ static $linker = false;
+
+ if ( $linker === false ) {
+ $linker = new Linker();
+ }
+
+ return $linker;
+}
+
+/**
+ * @private
+ *
+ * Copied from wfCountDown as it became deprecated in 1.31
+ *
+ * @since 3.0
+ */
+function swfCountDown( $seconds ) {
+ for ( $i = $seconds; $i >= 0; $i-- ) {
+ if ( $i != $seconds ) {
+ echo str_repeat( "\x08", strlen( $i + 1 ) );
+ }
+ echo $i;
+ flush();
+ if ( $i ) {
+ sleep( 1 );
+ }
+ }
+ echo "\n";
+}
+
+/**
+ * Function to switch on Semantic MediaWiki. This function must be called in
+ * LocalSettings.php after including SMW_Settings.php. It is used to ensure
+ * that required parameters for SMW are really provided explicitly. For
+ * readability, this is the only global function that does not adhere to the
+ * naming conventions.
+ *
+ * This function also sets up all autoloading, such that all SMW classes are
+ * available as early on. Moreover, jobs and special pages are registered.
+ *
+ * @param mixed $namespace
+ * @param boolean $complete
+ *
+ * @return true
+ *
+ * @codeCoverageIgnore
+ */
+function enableSemantics( $namespace = null, $complete = false ) {
+ global $smwgNamespace;
+
+ // #1732 + #2813
+ wfLoadExtension( 'SemanticMediaWiki', dirname( __DIR__ ) . '/extension.json' );
+
+ // Apparently this is required (1.28+) as the earliest possible execution
+ // point in order for settings that refer to the SMW_NS_PROPERTY namespace
+ // to be available in LocalSettings
+ NamespaceManager::initCustomNamespace( $GLOBALS );
+
+ if ( !$complete && ( $smwgNamespace !== '' ) ) {
+ // The dot tells that the domain is not complete. It will be completed
+ // in the Export since we do not want to create a title object here when
+ // it is not needed in many cases.
+ $smwgNamespace = '.' . $namespace;
+ } else {
+ $smwgNamespace = $namespace;
+ }
+
+ $GLOBALS['smwgSemanticsEnabled'] = true;
+
+ return true;
+}
+
+/**
+ * To disable Semantic MediaWiki's operational functionality
+ *
+ * @note This function can be used to temporary disable SMW but it is paramount
+ * that after SMW is re-enabled to run `rebuildData.php` in order for data to
+ * represent a state that mirrors the actual environment (deleted, moved pages
+ * are not tracked when disabled).
+ */
+function disableSemantics() {
+ CompatibilityMode::disableSemantics();
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/HashBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/HashBuilder.php
new file mode 100644
index 00000000..7d5bb100
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/HashBuilder.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace SMW;
+
+use Title;
+
+/**
+ * Utility class to create unified hash keys for a variety of objects
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class HashBuilder {
+
+ /**
+ * @since 2.4
+ *
+ * @param SemanticData $semanticData
+ *
+ * @return string
+ */
+ public static function createFromSemanticData( SemanticData $semanticData ) {
+
+ $hash = [];
+ $hash[] = $semanticData->getSubject()->getSerialization();
+
+ foreach ( $semanticData->getProperties() as $property ) {
+ $hash[] = $property->getKey();
+
+ foreach ( $semanticData->getPropertyValues( $property ) as $di ) {
+ $hash[] = $di->getSerialization();
+ }
+ }
+
+ foreach ( $semanticData->getSubSemanticData() as $data ) {
+ $hash[] = $data->getHash();
+ }
+
+ sort( $hash );
+
+ return md5( implode( '#', $hash ) );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string|array $hashableContent
+ * @param string $prefix
+ *
+ * @return string
+ */
+ public static function createFromContent( $hashableContent, $prefix = '' ) {
+
+ if ( is_string( $hashableContent ) ) {
+ $hashableContent = [ $hashableContent ];
+ }
+
+ return $prefix . md5( json_encode( $hashableContent ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $hashableContent
+ * @param string $prefix
+ *
+ * @return string
+ */
+ public static function createFromArray( array $hashableContent, $prefix = '' ) {
+ return $prefix . md5( json_encode( $hashableContent ) );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ public static function createFromSegments( /* args */ ) {
+ return implode( '#', func_get_args() );
+ }
+
+ /**
+ * @deprecated since 2.4, use Hash::createFromSegments
+ * @since 2.1
+ *
+ * @param string $title
+ * @param string $namespace
+ * @param string $interwiki
+ * @param string $fragment
+ *
+ * @return string
+ */
+ public static function createHashIdFromSegments( $title, $namespace, $interwiki = '', $fragment = '' ) {
+ return self::createFromSegments( $title, $namespace, $interwiki, $fragment );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Title $title
+ *
+ * @return string
+ */
+ public static function getHashIdForTitle( Title $title ) {
+ return self::createFromSegments(
+ $title->getDBKey(),
+ $title->getNamespace(),
+ $title->getInterwiki(),
+ $title->getFragment()
+ );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param DIWikiPage $dataItem
+ *
+ * @return string
+ */
+ public static function getHashIdForDiWikiPage( DIWikiPage $dataItem ) {
+ return self::createFromSegments(
+ $dataItem->getDBKey(),
+ $dataItem->getNamespace(),
+ $dataItem->getInterwiki(),
+ $dataItem->getSubobjectName()
+ );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $hash
+ *
+ * @return Title|null
+ */
+ public static function newTitleFromHash( $hash ) {
+ list( $title, $namespace, $interwiki, $fragement ) = explode( '#', $hash, 4 );
+ return Title::makeTitle( $namespace, $title, $fragement, $interwiki );
+ }
+
+ /**
+ * @note This method does not make additional checks therefore it is assumed
+ * that the input hash is derived or generated from HashBuilder::getSegmentedHashId
+ *
+ * @since 2.1
+ *
+ * @param string
+ *
+ * @return DIWikiPage|null
+ */
+ public static function newDiWikiPageFromHash( $hash ) {
+
+ list( $title, $namespace, $interwiki, $subobjectName ) = explode( '#', $hash, 4 );
+
+ // A leading underscore is an internal SMW convention to describe predefined
+ // properties and as such need to be transformed into a valid representation
+ if ( $title{0} === '_' ) {
+ $title = str_replace( ' ', '_', PropertyRegistry::getInstance()->findPropertyLabelById( $title ) );
+ }
+
+ return new DIWikiPage( $title, $namespace, $interwiki, $subobjectName );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/HierarchyLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/HierarchyLookup.php
new file mode 100644
index 00000000..584fb6f6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/HierarchyLookup.php
@@ -0,0 +1,355 @@
+<?php
+
+namespace SMW;
+
+use InvalidArgumentException;
+use Onoi\Cache\Cache;
+use Psr\Log\LoggerAwareTrait;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class HierarchyLookup {
+
+ use LoggerAwareTrait;
+
+ /**
+ * Persistent cache namespace
+ */
+ const CACHE_NAMESPACE = 'smw:hierarchy';
+
+ /**
+ * Consecutive hierarchy types
+ */
+ const TYPE_PROPERTY = 'type.property';
+ const TYPE_CATEGORY = 'type.category';
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var Cache|null
+ */
+ private $cache;
+
+ /**
+ * @var []
+ */
+ private $inMemoryCache = [];
+
+ /**
+ * @var integer
+ */
+ private $cacheTTL;
+
+ /**
+ * Use 0 to disable the hierarchy lookup
+ *
+ * @var integer
+ */
+ private $subcategoryDepth = 10;
+
+ /**
+ * Use 0 to disable the hierarchy lookup
+ *
+ * @var integer
+ */
+ private $subpropertyDepth = 10;
+
+ /**
+ * @since 2.3
+ *
+ * @param Store $store
+ * @param Cache $cache
+ */
+ public function __construct( Store $store, Cache $cache ) {
+ $this->store = $store;
+ $this->cache = $cache;
+
+ $this->cacheTTL = 60 * 60 * 24 * 7;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ChangePropListener $changePropListener
+ */
+ public function addListenersTo( ChangePropListener $changePropListener ) {
+
+ // @see HierarchyLookup::getConsecutiveHierarchyList
+ //
+ // Remove the global hierarchy cache in the event that some entity was
+ // annotated (or removed) with the `Subproperty of`/ `Subcategory of`
+ // property, and while this purges the entire cache we ensure that the
+ // hierarchy lookup is always correct without loosing too much sleep
+ // over a more fine-grained caching strategy.
+
+ $callback = function( $context ) {
+ $this->cache->delete(
+ smwfCacheKey( self::CACHE_NAMESPACE, [ self::TYPE_PROPERTY, $this->subpropertyDepth ] )
+ );
+ };
+
+ $changePropListener->addListenerCallback( '_SUBP', $callback );
+
+ $callback = function( $context ) {
+ $this->cache->delete(
+ smwfCacheKey( self::CACHE_NAMESPACE, [ self::TYPE_CATEGORY, $this->subcategoryDepth ] )
+ );
+ };
+
+ $changePropListener->addListenerCallback( '_SUBC', $callback );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param integer $subcategoryDepth
+ */
+ public function setSubcategoryDepth( $subcategoryDepth ) {
+ $this->subcategoryDepth = (int)$subcategoryDepth;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param integer $subpropertyDepth
+ */
+ public function setSubpropertyDepth( $subpropertyDepth ) {
+ $this->subpropertyDepth = (int)$subpropertyDepth;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DIProperty $property
+ *
+ * @return boolean
+ */
+ public function hasSubproperty( DIProperty $property ) {
+
+ if ( $this->subpropertyDepth < 1 ) {
+ return false;
+ }
+
+ $result = $this->getConsecutiveHierarchyList(
+ $property
+ );
+
+ return $result !== [];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DIWikiPage $category
+ *
+ * @return boolean
+ */
+ public function hasSubcategory( DIWikiPage $category ) {
+
+ if ( $this->subcategoryDepth < 1 ) {
+ return false;
+ }
+
+ $result = $this->getConsecutiveHierarchyList(
+ $category
+ );
+
+ return $result !== [];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DIProperty $property
+ *
+ * @return DIWikiPage[]|[]
+ */
+ public function findSubpropertyList( DIProperty $property ) {
+
+ if ( $this->subpropertyDepth < 1 ) {
+ return false;
+ }
+
+ return $this->lookup( '_SUBP', $property->getKey(), $property->getDiWikiPage(), new RequestOptions() );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DIWikiPage $category
+ *
+ * @return DIWikiPage[]|[]
+ */
+ public function findSubcategoryList( DIWikiPage $category ) {
+
+ if ( $this->subcategoryDepth < 1 ) {
+ return [];
+ }
+
+ return $this->lookup( '_SUBC', $category->getDBKey(), $category, new RequestOptions() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty|DIWikiPage $id
+ *
+ * @return DIProperty[]|DIWikiPage[]|[]
+ */
+ public function getConsecutiveHierarchyList( $id ) {
+
+ $hierarchyType = null;
+
+ if ( $id instanceof DIProperty ) {
+ $hierarchyType = self::TYPE_PROPERTY;
+ } elseif ( $id instanceof DIWikiPage && $id->getNamespace() === NS_CATEGORY ) {
+ $hierarchyType = self::TYPE_CATEGORY;
+ }
+
+ if ( $hierarchyType === null ) {
+ throw new InvalidArgumentException( 'No matchable hierarchy type, expected a property or category entity.' );
+ }
+
+ // Store elements of the hierarchy tree in one large cache slot
+ // since we are unable to detect if or when a leaf is removed from within
+ // a cached tree unless one stores child and parent in a secondary cache.
+ //
+ // On the assumption that hierarchy data are less frequently changed, using
+ // a "global" cache should be sufficient to avoid constant DB lookups.
+ //
+ // Invalidation of the cache will occur on each _SUBP/_SUBC change event (see
+ // ChangePropListener).
+ $cacheKey = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [
+ $hierarchyType,
+ ( $hierarchyType === self::TYPE_PROPERTY ? $this->subpropertyDepth : $this->subcategoryDepth )
+ ]
+ );
+
+ $hierarchyCache = $this->cache->fetch( $cacheKey );
+ $reqCacheUpdate = false;
+
+ if ( $hierarchyCache === false ) {
+ $hierarchyCache = [];
+ }
+
+ $hierarchyMembers = [];
+ $key = $hierarchyType === self::TYPE_PROPERTY ? $id->getKey() : $id->getDBKey();
+
+ if ( !isset( $hierarchyCache[$key] ) ) {
+ $hierarchyCache[$key] = [];
+
+ if ( $hierarchyType === self::TYPE_PROPERTY ) {
+ $this->findSubproperties( $hierarchyMembers, $id, 1 );
+ } else {
+ $this->findSubcategories( $hierarchyMembers, $id, 1 );
+ }
+
+ $hierarchyList[$key] = $hierarchyMembers;
+
+ // Store only the key to keep the cache size low
+ foreach ( $hierarchyList[$key] as $k ) {
+ if ( $hierarchyType === self::TYPE_PROPERTY ) {
+ $hierarchyCache[$key][] = $k->getKey();
+ } else {
+ $hierarchyCache[$key][] = $k->getDBKey();
+ }
+ }
+
+ $reqCacheUpdate = true;
+ } else {
+ $hierarchyList[$key] = [];
+
+ foreach ( $hierarchyCache[$key] as $k ) {
+ if ( $hierarchyType === self::TYPE_PROPERTY ) {
+ $hierarchyList[$key][] = new DIProperty( $k );
+ } else {
+ $hierarchyList[$key][] = new DIWikiPage( $k, NS_CATEGORY );
+ }
+ }
+ }
+
+ if ( $reqCacheUpdate ) {
+ $this->cache->save( $cacheKey, $hierarchyCache, $this->cacheTTL );
+ }
+
+ return $hierarchyList[$key];
+ }
+
+ private function findSubproperties( &$hierarchyMembers, DIProperty $property, $depth ) {
+
+ if ( $depth++ > $this->subpropertyDepth ) {
+ return;
+ }
+
+ $propertyList = $this->findSubpropertyList(
+ $property
+ );
+
+ if ( $propertyList === null || $propertyList === [] ) {
+ return;
+ }
+
+ foreach ( $propertyList as $property ) {
+ $property = DIProperty::newFromUserLabel(
+ $property->getDBKey()
+ );
+
+ $hierarchyMembers[] = $property;
+ $this->findSubproperties( $hierarchyMembers, $property, $depth );
+ }
+ }
+
+ private function findSubcategories( &$hierarchyMembers, DIWikiPage $category, $depth ) {
+
+ if ( $depth++ > $this->subcategoryDepth ) {
+ return;
+ }
+
+ $categoryList = $this->findSubcategoryList(
+ $category
+ );
+
+ foreach ( $categoryList as $category ) {
+ $hierarchyMembers[] = $category;
+ $this->findSubcategories( $hierarchyMembers, $category, $depth );
+ }
+ }
+
+ private function lookup( $id, $key, DIWikiPage $subject, $requestOptions ) {
+
+ $key = $id . '#' . $key . '#' . md5( $requestOptions->getHash() );
+
+ if ( isset( $this->inMemoryCache[$key] ) ) {
+ return $this->inMemoryCache[$key];
+ }
+
+ $res = $this->store->getPropertySubjects(
+ new DIProperty( $id ),
+ $subject,
+ $requestOptions
+ );
+
+ $this->inMemoryCache[$key] = $res;
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'user',
+ 'id' => $id,
+ 'origin' => $subject
+ ];
+
+ $this->logger->info( "[HierarchyLookup] Lookup: {id}, {origin}", $context );
+
+ return $res;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreator.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreator.php
new file mode 100644
index 00000000..5c910bd4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreator.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace SMW\Importer;
+
+use Onoi\MessageReporter\MessageReporterAware;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+interface ContentCreator extends MessageReporterAware {
+
+ /**
+ * @since 3.0
+ *
+ * @param ImportContents $importContents
+ *
+ * @return boolean
+ */
+ public function canCreateContentsFor( ImportContents $importContents );
+
+ /**
+ * @since 3.0
+ *
+ * @param ImportContents $importContents
+ */
+ public function create( ImportContents $importContents );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/DispatchingContentCreator.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/DispatchingContentCreator.php
new file mode 100644
index 00000000..867c44c7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/DispatchingContentCreator.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace SMW\Importer\ContentCreators;
+
+use Onoi\MessageReporter\MessageReporter;
+use RuntimeException;
+use SMW\Importer\ContentCreator;
+use SMW\Importer\ImportContents;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DispatchingContentCreator implements ContentCreator {
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @var ContentCreator[]
+ */
+ private $contentCreators = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param ContentCreator[]
+ */
+ public function __construct( array $contentCreators ) {
+ $this->contentCreators = $contentCreators;
+ }
+
+ /**
+ * @see MessageReporterAware::setMessageReporter
+ *
+ * @since 3.0
+ *
+ * @param MessageReporter $messageReporter
+ */
+ public function setMessageReporter( MessageReporter $messageReporter ) {
+ $this->messageReporter = $messageReporter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ImportContents $importContents
+ */
+ public function canCreateContentsFor( ImportContents $importContents ) {
+
+ foreach ( $this->contentCreators as $contentCreator ) {
+ if ( $contentCreator->canCreateContentsFor( $importContents ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ImportContents $importContents
+ * @throws RuntimeException
+ */
+ public function create( ImportContents $importContents ) {
+
+ foreach ( $this->contentCreators as $contentCreator ) {
+ if ( $contentCreator->canCreateContentsFor( $importContents ) ) {
+ $contentCreator->setMessageReporter( $this->messageReporter );
+ return $contentCreator->create( $importContents );
+ }
+ }
+
+ throw new RuntimeException( "No dispatchable ContentsCreator is assigned to type " . $importContents->getContentType() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/TextContentCreator.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/TextContentCreator.php
new file mode 100644
index 00000000..81d04045
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/TextContentCreator.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace SMW\Importer\ContentCreators;
+
+use ContentHandler;
+use Onoi\MessageReporter\MessageReporter;
+use SMW\Importer\ContentCreator;
+use SMW\Importer\ImportContents;
+use SMW\MediaWiki\Database;
+use SMW\MediaWiki\PageCreator;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class TextContentCreator implements ContentCreator {
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @var PageCreator
+ */
+ private $pageCreator;
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @since 2.5
+ *
+ * @param PageCreator $pageCreator
+ * @param Database $connection
+ */
+ public function __construct( PageCreator $pageCreator, Database $connection ) {
+ $this->pageCreator = $pageCreator;
+ $this->connection = $connection;
+ }
+
+ /**
+ * @see MessageReporterAware::setMessageReporter
+ *
+ * @since 2.5
+ *
+ * @param MessageReporter $messageReporter
+ */
+ public function setMessageReporter( MessageReporter $messageReporter ) {
+ $this->messageReporter = $messageReporter;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ImportContents $importContents
+ */
+ public function canCreateContentsFor( ImportContents $importContents ) {
+ return $importContents->getContentType() === ImportContents::CONTENT_TEXT;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ImportContents $importContents
+ */
+ public function create( ImportContents $importContents ) {
+
+ if ( !class_exists( 'ContentHandler' ) ) {
+ return $this->messageReporter->reportMessage( "\nContentHandler doesn't exist therefore importing is not possible.\n" );
+ }
+
+ $indent = ' ...';
+ $name = $importContents->getName();
+
+ if ( $name === '' ) {
+ return $this->messageReporter->reportMessage( "$indent no valid page name, abort import." );
+ }
+
+ $title = Title::newFromText(
+ $name,
+ $importContents->getNamespace()
+ );
+
+ if ( $title === null ) {
+ return $this->messageReporter->reportMessage( "$indent $name returned with a null title, abort import." );
+ }
+
+ $prefixedText = $title->getPrefixedText();
+
+ if ( $title->exists() && !$importContents->getOption( 'canReplace' ) && !$importContents->getOption( 'replaceable' ) ) {
+ return $this->messageReporter->reportMessage( "$indent skipping $prefixedText, already exists ...\n" );
+ } elseif( $title->exists() ) {
+ $this->messageReporter->reportMessage( "$indent replacing $prefixedText contents ...\n" );
+ } else {
+ $this->messageReporter->reportMessage( "$indent creating $prefixedText contents ...\n" );
+ }
+
+ // Avoid a possible "Notice: WikiPage::doEditContent: Transaction already
+ // in progress (from DatabaseUpdater::doUpdates), performing implicit
+ // commit ..."
+ $this->connection->onTransactionIdle( function() use ( $title, $importContents ) {
+ $this->doCreateContent( $title, $importContents );
+ } );
+ }
+
+ private function doCreateContent( $title, $importContents ) {
+
+ $page = $this->pageCreator->createPage( $title );
+
+ $content = ContentHandler::makeContent(
+ $this->fetchContents( $importContents ),
+ $title
+ );
+
+ $page->doEditContent(
+ $content,
+ $importContents->getDescription(),
+ EDIT_FORCE_BOT
+ );
+
+ $title->invalidateCache();
+ }
+
+ private function fetchContents( $importContents ) {
+
+ if ( $importContents->getContentsFile() === '' ) {
+ return $importContents->getContents();
+ }
+
+ $contents = file_get_contents( $importContents->getContentsFile() );
+
+ // http://php.net/manual/en/function.file-get-contents.php
+ return mb_convert_encoding(
+ $contents,
+ 'UTF-8',
+ mb_detect_encoding(
+ $contents,
+ 'UTF-8, ISO-8859-1, ISO-8859-2',
+ true
+ )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/XmlContentCreator.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/XmlContentCreator.php
new file mode 100644
index 00000000..5785acc5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentCreators/XmlContentCreator.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace SMW\Importer\ContentCreators;
+
+use Onoi\MessageReporter\MessageReporter;
+use SMW\Importer\ContentCreator;
+use SMW\Importer\ImportContents;
+use SMW\Services\ImporterServiceFactory;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class XmlContentCreator implements ContentCreator {
+
+ /**
+ * @var ImportContentsIterator
+ */
+ private $importerServiceFactory;
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @since 3.0
+ *
+ * @param ImporterServiceFactory $importerServiceFactory
+ */
+ public function __construct( ImporterServiceFactory $importerServiceFactory ) {
+ $this->importerServiceFactory = $importerServiceFactory;
+ }
+
+ /**
+ * @see MessageReporterAware::setMessageReporter
+ *
+ * @since 3.0
+ *
+ * @param MessageReporter $messageReporter
+ */
+ public function setMessageReporter( MessageReporter $messageReporter ) {
+ $this->messageReporter = $messageReporter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ImportContents $importContents
+ */
+ public function canCreateContentsFor( ImportContents $importContents ) {
+ return $importContents->getContentType() === ImportContents::CONTENT_XML;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ImportContents $importContents
+ */
+ public function create( ImportContents $importContents ) {
+
+ $indent = ' ...';
+
+ if ( $importContents->getOption( 'skip' ) === true || $importContents->getContentsFile() === '' ) {
+ return $this->messageReporter->reportMessage( "\n " . $importContents->getDescription() . " was skipped.\n" );
+ }
+
+ $importSource = $this->importerServiceFactory->newImportStreamSource(
+ @fopen( $importContents->getContentsFile(), 'rt' )
+ );
+
+ $importer = $this->importerServiceFactory->newWikiImporter(
+ $importSource
+ );
+
+ $importer->setDebug( false );
+ $importer->setPageOutCallback( [ $this, 'reportPage' ] );
+
+ if ( $importContents->getDescription() !== '' ) {
+ $this->messageReporter->reportMessage( "\n " . $importContents->getDescription() . "\n" );
+ }
+
+ try {
+ $importer->doImport();
+ } catch ( \Exception $e ) {
+ $this->messageReporter->reportMessage( "Failed with " . $e->getMessage() );
+ }
+
+ $this->messageReporter->reportMessage( "$indent done.\n" );
+ }
+
+ /**
+ * @see WikiImporter::handlePage
+ *
+ * @param Title $title
+ * @param ForeignTitle $foreignTitle
+ * @param int $revisionCount
+ * @param int $successCount
+ * @param array $pageInfo
+ */
+ public function reportPage( $title, $foreignTitle, $revisionCount, $successCount, $pageInfo ) {
+
+ $indent = ' ...';
+
+ // Invalid or non-importable title
+ if ( $title === null ) {
+ return;
+ }
+
+ $title->invalidateCache();
+
+ if ( $successCount > 0 ) {
+ $this->messageReporter->reportMessage( "$indent importing " . $title->getPrefixedText() . "\n" );
+ } else {
+ $this->messageReporter->reportMessage( "$indent skipping " . $title->getPrefixedText() . ", no new revision\n" );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentIterator.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentIterator.php
new file mode 100644
index 00000000..c0efb3c1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentIterator.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace SMW\Importer;
+
+use IteratorAggregate;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+interface ContentIterator extends IteratorAggregate {
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getDescription();
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getErrors();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentModeller.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentModeller.php
new file mode 100644
index 00000000..4f667a7f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ContentModeller.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace SMW\Importer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ContentModeller {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $fileDir
+ * @param array $fileContents
+ *
+ * @return ImportContents[]|[]
+ */
+ public function makeContentList( $fileDir, array $fileContents ) {
+
+ $contents = [];
+
+ if ( !isset( $fileContents['import'] ) ) {
+ return $contents;
+ }
+
+ foreach ( $fileContents['import'] as $value ) {
+
+ $importContents = new ImportContents();
+
+ if ( isset( $value['namespace'] ) ) {
+ $importContents->setNamespace(
+ defined( $value['namespace'] ) ? constant( $value['namespace'] ) : 0
+ );
+ }
+
+ if ( isset( $value['page'] ) ) {
+ $importContents->setName( $value['page'] );
+ }
+
+ if ( isset( $value['description'] ) ) {
+ $importContents->setDescription( $value['description'] );
+ } elseif ( isset( $fileContents['description'] ) ) {
+ $importContents->setDescription( $fileContents['description'] );
+ } else {
+ $importContents->setDescription( 'No description' );
+ }
+
+ if ( isset( $fileContents['meta']['version'] ) ) {
+ $importContents->setVersion( $fileContents['meta']['version'] );
+ } else {
+ $importContents->setVersion( 0 );
+ }
+
+ if ( isset( $value['contents']['type'] ) && $value['contents']['type'] === 'xml' ) {
+ $contents[] = $this->newImportContents( $importContents, $fileDir, $value );
+ } else {
+ $contents[] = $this->newImportContents( $importContents, $fileDir, $value );
+ }
+ }
+
+ return $contents;
+ }
+
+ private function newImportContents( $importContents, $fileDir, $value ) {
+
+ $importContents->setContentType( ImportContents::CONTENT_TEXT );
+
+ if ( !isset( $value['contents'] ) || $value['contents'] === '' ) {
+ $importContents->addError( 'Missing, or has empty contents section' );
+ } else {
+ $this->setContents( $importContents, $fileDir, $value['contents'] );
+ }
+
+ if ( isset( $value['options'] ) ) {
+ $importContents->setOptions( $value['options'] );
+ }
+
+ return $importContents;
+ }
+
+ private function setContents( $importContents, $fileDir, $contents ) {
+
+ if ( !is_array( $contents ) || !isset( $contents['importFrom'] ) ) {
+ return $importContents->setContents( $contents );
+ }
+
+ $file = $this->normalizeFile( $fileDir, $contents['importFrom'] );
+
+ if ( !is_readable( $file ) ) {
+ return $importContents->addError( "File: " . $file . " wasn't accessible" );
+ }
+
+ $extension = pathinfo( $file, PATHINFO_EXTENSION );
+
+ if ( isset( $contents['type'] ) && $contents['type'] === 'xml' && $extension !== 'xml' ) {
+ return $importContents->addError( "XML: " . $file . " is not recognized as xml file extension" );
+ }
+
+ if ( $extension === 'xml' ) {
+ $importContents->setContentType( ImportContents::CONTENT_XML );
+ }
+
+ $importContents->setContentsFile( $file );
+ }
+
+ private function normalizeFile( $fileDir, $file ) {
+ return str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, $fileDir . ( $file{0} === '/' ? '' : '/' ) . $file );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/ImportContents.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ImportContents.php
new file mode 100644
index 00000000..bf905e0f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/ImportContents.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace SMW\Importer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ImportContents {
+
+ const CONTENT_TEXT = 'content.text';
+ const CONTENT_XML = 'content.xml';
+
+ /**
+ * @var string
+ */
+ private $version = '';
+
+ /**
+ * @var string
+ */
+ private $description = '';
+
+ /**
+ * @var string
+ */
+ private $name = '';
+
+ /**
+ * @var integer
+ */
+ private $namespace = 0;
+
+ /**
+ * @var string
+ */
+ private $contents = '';
+
+ /**
+ * @var string
+ */
+ private $contentsFile = '';
+
+ /**
+ * @var string
+ */
+ private $contentType = self::CONTENT_TEXT;
+
+ /**
+ * @var string
+ */
+ private $errors = [];
+
+ /**
+ * @var array
+ */
+ private $options = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param string $version
+ */
+ public function setVersion( $version ) {
+ $this->version = intval( $version );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return $this->version;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $description
+ */
+ public function setDescription( $description ) {
+ $this->description = $description;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $contentType
+ */
+ public function setContentType( $contentType ) {
+ $this->contentType = $contentType;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getContentType() {
+ return $this->contentType;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $name
+ */
+ public function setName( $name ) {
+ $this->name = $name;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $namespace
+ */
+ public function setNamespace( $namespace ) {
+ $this->namespace = $namespace;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getNamespace() {
+ return $this->namespace;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $contentsFile
+ */
+ public function setContentsFile( $contentsFile ) {
+ $this->contentsFile = $contentsFile;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getContentsFile() {
+ return $this->contentsFile;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $contents
+ */
+ public function setContents( $contents ) {
+ $this->contents = $contents;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getContents() {
+ return $this->contents;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $error
+ */
+ public function addError( $error ) {
+ $this->errors[] = $error;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $options
+ */
+ public function setOptions( $options ) {
+ $this->options = (array)$options;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getOptions() {
+ return $this->options;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function getOption( $key ) {
+ return isset( $this->options[$key] ) ? $this->options[$key] : false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/Importer.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/Importer.php
new file mode 100644
index 00000000..a799232c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/Importer.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace SMW\Importer;
+
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\MessageReporterAware;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class Importer implements MessageReporterAware {
+
+ /**
+ * @var ContentIterator
+ */
+ private $contentIterator;
+
+ /**
+ * @var ContentCreator
+ */
+ private $contentCreator;
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @var boolean
+ */
+ private $isEnabled = true;
+
+ /**
+ * @var integer|boolean
+ */
+ private $reqVersion = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param ContentIterator $contentIterator
+ * @param ContentCreator $contentCreator
+ */
+ public function __construct( ContentIterator $contentIterator, ContentCreator $contentCreator ) {
+ $this->contentIterator = $contentIterator;
+ $this->contentCreator = $contentCreator;
+ }
+
+ /**
+ * @see MessageReporterAware::setMessageReporter
+ *
+ * @since 2.5
+ *
+ * @param MessageReporter $messageReporter
+ */
+ public function setMessageReporter( MessageReporter $messageReporter ) {
+ $this->messageReporter = $messageReporter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isEnabled
+ */
+ public function isEnabled( $isEnabled ) {
+ $this->isEnabled = $isEnabled;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer|boolean $reqVersion
+ */
+ public function setReqVersion( $reqVersion ) {
+ $this->reqVersion = $reqVersion;
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function doImport() {
+
+ if ( $this->isEnabled === false ) {
+ return $this->messageReporter->reportMessage( "\nSkipping the import process.\n" );
+ }
+
+ if ( $this->reqVersion === false ) {
+ return $this->messageReporter->reportMessage( "\nImport support not enabled, processing completed.\n" );
+ }
+
+ $import = false;
+
+ foreach ( $this->contentIterator as $key => $importContents ) {
+ $this->messageReporter->reportMessage( "\nImport of $key ...\n" );
+
+ foreach ( $importContents as $impContents ) {
+
+ if ( $impContents->getVersion() !== $this->reqVersion ) {
+ $this->messageReporter->reportMessage( " ... version mismatch, abort import for $key\n" );
+ break;
+ }
+
+ $this->doImportContents( $impContents );
+ }
+
+ $this->messageReporter->reportMessage( " ... done.\n" );
+ $import = true;
+ }
+
+ if ( $this->contentIterator->getErrors() !== [] ) {
+ $this->messageReporter->reportMessage(
+ "\n" . 'Import failed on "' . implode( ", ", $this->contentIterator->getErrors() ) . '"'
+ );
+ }
+
+ if ( $import ) {
+ $this->messageReporter->reportMessage( "\nImport processing completed.\n" );
+ }
+ }
+
+ private function doImportContents( ImportContents $importContents ) {
+
+ $indent = ' ...';
+
+ if ( $importContents->getErrors() === [] ) {
+ $this->contentCreator->setMessageReporter( $this->messageReporter );
+ $this->contentCreator->create( $importContents );
+ }
+
+ foreach ( $importContents->getErrors() as $error ) {
+ $this->messageReporter->reportMessage( "$indent " . $error . " ...\n" );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/JsonContentIterator.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/JsonContentIterator.php
new file mode 100644
index 00000000..0fee90aa
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/JsonContentIterator.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace SMW\Importer;
+
+use ArrayIterator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class JsonContentIterator implements ContentIterator {
+
+ /**
+ * @var JsonImportContentsFileDirReader
+ */
+ private $jsonImportContentsFileDirReader;
+
+ /**
+ * @var string
+ */
+ private $description = '';
+
+ /**
+ * @since 2.5
+ *
+ * @param JsonImportContentsFileDirReader $jsonImportContentsFileDirReader
+ */
+ public function __construct( JsonImportContentsFileDirReader $jsonImportContentsFileDirReader ) {
+ $this->jsonImportContentsFileDirReader = $jsonImportContentsFileDirReader;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $description
+ */
+ public function setDescription( $description ) {
+ $this->description = $description;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->jsonImportContentsFileDirReader->getErrors();
+ }
+
+ /**
+ * @see IteratorAggregate::getIterator
+ *
+ * @since 2.5
+ *
+ * @return Iterator
+ */
+ public function getIterator() {
+ return new ArrayIterator( $this->jsonImportContentsFileDirReader->getContentList() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/JsonImportContentsFileDirReader.php b/www/wiki/extensions/SemanticMediaWiki/src/Importer/JsonImportContentsFileDirReader.php
new file mode 100644
index 00000000..b0161969
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/JsonImportContentsFileDirReader.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace SMW\Importer;
+
+use RuntimeException;
+use SMW\Utils\ErrorCodeFormatter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class JsonImportContentsFileDirReader {
+
+ /**
+ * @var ContentModeller
+ */
+ private $contentModeller;
+
+ /**
+ * @var array
+ */
+ private static $contents = [];
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var []
+ */
+ private $importFileDirs = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param ContentModeller $contentModeller
+ * @param array $importFileDirs
+ */
+ public function __construct( ContentModeller $contentModeller, $importFileDirs = [] ) {
+ $this->contentModeller = $contentModeller;
+ $this->importFileDirs = $importFileDirs;
+
+ if ( $this->importFileDirs === [] ) {
+ $this->importFileDirs = $GLOBALS['smwgImportFileDirss'];
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return ImportContents[]
+ */
+ public function getContentList() {
+
+ $contents = [];
+
+ foreach ( $this->importFileDirs as $importFileDir ) {
+
+ try{
+ $files = $this->getFilesFromLocation( $this->normalize( $importFileDir ), 'json' );
+ } catch( RuntimeException $e ) {
+ $this->errors[] = $importFileDir . ' is not accessible.';
+ $files = [];
+ }
+
+ foreach ( $files as $file => $path ) {
+
+ $contentList = $this->contentModeller->makeContentList(
+ $importFileDir,
+ $this->readJSONFile( $path )
+ );
+
+ if ( $contentList === [] ) {
+ continue;
+ }
+
+ $contents[$file] = $contentList;
+ }
+ }
+
+ return $contents;
+ }
+
+ private function readJSONFile( $file ) {
+
+ $contents = json_decode(
+ file_get_contents( $file ),
+ true
+ );
+
+ if ( $contents !== null && json_last_error() === JSON_ERROR_NONE ) {
+ return $contents;
+ }
+
+ throw new RuntimeException( ErrorCodeFormatter::getMessageFromJsonErrorCode( json_last_error() ) );
+ }
+
+ private function normalize( $importFileDir ) {
+
+ if ( $importFileDir === '' ) {
+ return '';
+ }
+
+ $path = str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, $importFileDir );
+
+ if ( is_readable( $path ) ) {
+ return $path;
+ }
+
+ throw new RuntimeException( "Expected an accessible {$path} path" );
+ }
+
+ private function getFilesFromLocation( $path, $extension ) {
+
+ if ( $path === '' ) {
+ return [];
+ }
+
+ $files = [];
+
+ $directoryIterator = new \RecursiveDirectoryIterator( $path );
+
+ foreach ( new \RecursiveIteratorIterator( $directoryIterator ) as $fileInfo ) {
+ if ( strtolower( substr( $fileInfo->getFilename(), -( strlen( $extension ) + 1 ) ) ) === ( '.' . $extension ) ) {
+ $files[$fileInfo->getFilename()] = $fileInfo->getPathname();
+ }
+ }
+
+ return $files;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Importer/README.md b/www/wiki/extensions/SemanticMediaWiki/src/Importer/README.md
new file mode 100644
index 00000000..55db3c03
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Importer/README.md
@@ -0,0 +1,151 @@
+The objective of the `Importer` is to provide a simple mechanism for deploying data structures and support information in a loose yet structured form during the installation (setup) process.
+
+## Import definitions
+
+[`$smwgImportFileDirs`](https://www.semantic-mediawiki.org/wiki/Help:$smwgImportFileDirs) defines import directories from where content can be imported.
+
+Import definitions are defined using a `JSON` format which provides the structural means and is considered easily extendable by end-users.
+
+The import files are sorted and therefore sequentially processed based on the file name. In case where content relies on other content an appropriate naming convention should be followed to ensure required definitions are imported in the expected order.
+
+### Default definitions
+
+Preselected import content is defined in the "default.json" file and includes:
+
+* "Smw import skos"
+* "Smw import owl"
+* "Smw import foaf"
+* "Foaf:knows"
+* "Foaf:name" and
+* "Foaf:homepage"
+
+It should be noted that `default.json` is __not__ expected to be the __authority source__ of content for a wiki and is the reason why the option `canReplace` is set `false` so that pre-existing content with the same name and namespace is not replaced.
+
+### Custom definitions
+
+It is possible to define one or more custom import definitions using [`$smwgImportFileDirs`](https://www.semantic-mediawiki.org/wiki/Help:$smwgImportFileDirs) with a custom location (directory) from where import definitions can be loaded.
+
+<pre>
+$GLOBALS['smwgImportFileDirs']['movie-actor-vocab'] = __DIR__ . '/import/movie-actor';
+</pre>
+
+<pre>
+$GLOBALS['smwgImportFileDirs']['custom-vocab'] = __DIR__ . '/custom';
+</pre>
+
+### Fields
+
+`JSON` schema and fields:
+
+- `description` short description about the purpose of the import (used in the auto summary)
+- `page` the name of a page without a namespace prefix
+- `namespace` literal constant of the namespace of the content (e.g. `NS_MAIN`, `SMW_NS_PROPERTY` ... )
+- `contents` it contains either the raw text or a parameter
+ - `importFrom` link to a file from where the raw text (contains a relative path to the `$smwgImportFileDirs`)
+- `options`
+ - `canReplace` to indicate whether content is being allowed to be replaced during
+ an import or not
+
+The [`$smwgImportReqVersion`](https://www.semantic-mediawiki.org/wiki/Help:$smwgImportReqVersion) stipulates
+the required version for an import and only definitions that match that version are permitted to be imported.
+
+### Examples
+
+#### XML import
+
+It is possible to use MediaWiki's XML format as import source when linked from the
+`importFrom` field (any non MediaWiki XML format will be ignored).
+
+The location for the mentioned `custom.xml` is relative to the selected `$smwgImportFileDirs` directory.
+
+<pre>
+{
+ "description": "Custom import",
+ "import": [
+ {
+ "description" : "Import of custom.xml that contains ...",
+ "contents": {
+ "importFrom": "/xml/custom.xml"
+ }
+ }
+ ],
+ "meta": {
+ "version": "1"
+ }
+}
+</pre>
+
+<pre>
+{
+ "description": "Template import",
+ "import": [
+ {
+ "description" : "Template to ...",
+ "page": "Template_1",
+ "namespace": "NS_TEMPLATE",
+ "contents": "<includeonly>{{{1}}}, {{{2}}}</includeonly>",
+ "options": {
+ "canReplace": false
+ }
+ },
+ {
+ "description" : "Template with ...",
+ "page": "Template_2",
+ "namespace": "NS_TEMPLATE",
+ "contents": {
+ "importFrom": "/templates/template-1.tmpl"
+ },
+ "options": {
+ "canReplace": false
+ }
+ }
+ ],
+ "meta": {
+ "version": "1"
+ }
+}
+</pre>
+
+## Import process
+
+During the setup process, the `Installer` will automatically run and inform
+about the process which will output something similar to:
+
+<pre>
+Import of default.json ...
+ ... replacing MediaWiki:Smw import foaf contents ...
+ ... skipping Property:Foaf:knows, already exists ...
+
+Import processing completed.
+</pre>
+
+If not otherwise specified, content (a.k.a. pages) that pre-exists are going to be skipped by default.
+
+## Technical notes
+
+<pre>
+SMW\Importer
+│ └─ ContentCreators
+│ ├─ DispatchingContentCreator
+│ ├─ XmlContentCreator
+│ └─ TextContentCreator
+│
+├─ ImporterServiceFactory # access to import services
+├─ ContentIterator
+├─ ContentCreator
+├─ JsonContentIterator
+├─ JsonImportContentsFileDirReader
+└─ ContentModeller
+</pre>
+
+- `SMW::SQLStore::Installer::AfterCreateTablesComplete` provides the hook and is the event to execute the import during the setup
+- `ImporterServiceFactory` access to import services
+- `Importer` is responsible for importing contents provided by a `ContentIterator`
+- `ContentIterator` an interface to provide access to individual `ImportContents` instances
+- `JsonContentIterator` implements the `ContentIterator` interface
+- `JsonImportContentsFileDirReader` provides contents of all recursively fetched files from a location (e.g[`$smwgImportFileDirs`](https://www.semantic-mediawiki.org/wiki/Help:$smwgImportFileDirs) setting ) that meets the requirements
+- `ContentModeller` interprets the `JSON` definition and returns a set of `ImportContents` instances
+- `ContentCreator` an interface to specify different creation methods (e.g. text, XML etc.)
+- `DispatchingContentCreator` dispatches to the actual content creation instance based on `ImportContents::getContentType`
+- `XmlContentCreator` support the creation of MediaWiki XML specific content
+- `TextContentCreator` support for raw wikitext
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/InMemoryPoolCache.php b/www/wiki/extensions/SemanticMediaWiki/src/InMemoryPoolCache.php
new file mode 100644
index 00000000..a53b7b98
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/InMemoryPoolCache.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace SMW;
+
+use SMW\Utils\StatsFormatter;
+
+/**
+ * A multipurpose non-persistent static pool cache to keep selected items for
+ * the duration of a request cacheable.
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class InMemoryPoolCache {
+
+ /**
+ * Stats as plain string
+ */
+ const FORMAT_PLAIN = StatsFormatter::FORMAT_PLAIN;
+
+ /**
+ * Stats as JSON output
+ */
+ const FORMAT_JSON = StatsFormatter::FORMAT_JSON;
+
+ /**
+ * Stats as HTML list output
+ */
+ const FORMAT_HTML = StatsFormatter::FORMAT_HTML;
+
+ /**
+ * @var InMemoryPoolCache
+ */
+ private static $instance = null;
+
+ /**
+ * @var CacheFactory
+ */
+ private $cacheFactory = null;
+
+ /**
+ * @var array
+ */
+ private $poolCacheList = [];
+
+ /**
+ * @since 2.3
+ *
+ * @param CacheFactory $cacheFactory
+ */
+ public function __construct( CacheFactory $cacheFactory ) {
+ $this->cacheFactory = $cacheFactory;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return InMemoryPoolCache
+ */
+ public static function getInstance() {
+
+ if ( self::$instance === null ) {
+ self::$instance = new self( ApplicationFactory::getInstance()->newCacheFactory() );
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.3
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $poolCacheName
+ */
+ public function resetPoolCacheById( $poolCacheName = '' ) {
+ foreach ( $this->poolCacheList as $key => $value ) {
+ if ( $key === $poolCacheName || $poolCacheName === '' ) {
+ unset( $this->poolCacheList[$key] );
+ }
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string|null $format
+ *
+ * @return string|array
+ */
+ public function getStats( $format = null ) {
+ return StatsFormatter::format( $this->computeStats(), $format );
+ }
+
+ /**
+ * @deprecated since 2.5, use InMemoryPoolCache::getPoolCacheById
+ * @since 2.3
+ *
+ * @param string $poolCacheName
+ * @param integer $cacheSize
+ *
+ * @return Cache
+ */
+ public function getPoolCacheFor( $poolCacheName, $cacheSize = 500 ) {
+ return $this->getPoolCacheById( $poolCacheName, $cacheSize );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $poolCacheId
+ * @param integer $cacheSize
+ *
+ * @return Cache
+ */
+ public function getPoolCacheById( $poolCacheId, $cacheSize = 500 ) {
+
+ if ( !isset( $this->poolCacheList[$poolCacheId] ) ) {
+ $this->poolCacheList[$poolCacheId] = $this->cacheFactory->newFixedInMemoryCache( $cacheSize );
+ }
+
+ return $this->poolCacheList[$poolCacheId];
+ }
+
+ private function computeStats() {
+
+ ksort( $this->poolCacheList );
+ $stats = [];
+
+ foreach ( $this->poolCacheList as $key => $value ) {
+ $stats[$key] = [];
+
+ $hits = 0;
+ $misses = 0;
+
+ foreach ( $value->getStats() as $k => $v ) {
+ $stats[$key][$k] = $v;
+
+ if ( $k === 'hits' ) {
+ $hits = $v;
+ }
+
+ if ( $k === 'inserts' ) {
+ $misses = $v;
+ }
+
+ if ( $k === 'misses' && $v > 0 ) {
+ $misses = $v;
+ }
+ }
+
+ $hitRatio = $hits > 0 ? round( $hits / ( $hits + $misses ), 4 ) : 0;
+
+ $stats[$key]['hit ratio'] = $hitRatio;
+ $stats[$key]['miss ratio'] = $hitRatio > 0 ? round( 1 - $hitRatio, 4 ) : 0;
+ }
+
+ return $stats;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/IteratorFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/IteratorFactory.php
new file mode 100644
index 00000000..6331352e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/IteratorFactory.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace SMW;
+
+use SMW\Iterators\AppendIterator;
+use SMW\Iterators\ChunkedIterator;
+use SMW\Iterators\CsvFileIterator;
+use SMW\Iterators\MappingIterator;
+use SMW\Iterators\ResultIterator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class IteratorFactory {
+
+ /**
+ * @since 2.5
+ *
+ * @param ResultWrapper|Iterator|array $res
+ *
+ * @return ResultIterator
+ */
+ public function newResultIterator( $res ) {
+ return new ResultIterator( $res );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Iterator/array $iterable
+ * @param callable $callback
+ *
+ * @return MappingIterator
+ */
+ public function newMappingIterator( $iterable, callable $callback ) {
+ return new MappingIterator( $iterable, $callback );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Iterator/array $$iterable
+ * @param integer $chunkSize
+ *
+ * @return ChunkedIterator
+ */
+ public function newChunkedIterator( $iterable, $chunkSize = 500 ) {
+ return new ChunkedIterator( $iterable, $chunkSize );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return AppendIterator
+ */
+ public function newAppendIterator() {
+ return new AppendIterator();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ * @param boolean $parseHeader
+ * @param string $delimiter
+ * @param integer $length
+ *
+ * @return CsvFileIterator
+ */
+ public function newCsvFileIterator( $file, $parseHeader = false, $delimiter = "\t", $length = 8000 ) {
+ return new CsvFileIterator( $file, $parseHeader, $delimiter, $length );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Iterators/AppendIterator.php b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/AppendIterator.php
new file mode 100644
index 00000000..1fcf6cda
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/AppendIterator.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace SMW\Iterators;
+
+use ArrayIterator;
+use Countable;
+use Iterator;
+use RuntimeException;
+use Traversable;
+
+/**
+ * @see Guzzle::AppendIterator
+ * @see https://bugs.php.net/bug.php?id=49104
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ */
+class AppendIterator extends \AppendIterator implements Countable {
+
+ /**
+ * @var integer
+ */
+ private $count = 0;
+
+ /**
+ * @since 3.0
+ *
+ * @param Traversable|array $iterator
+ */
+ public function add( $iterable ) {
+
+ if ( is_array( $iterable ) ) {
+ $iterable = new ArrayIterator( $iterable );
+ }
+
+ if ( !$iterable instanceof Traversable ) {
+ throw new RuntimeException( "AppendIterator expected an Traversable" );
+ }
+
+ $this->append( $iterable );
+ }
+
+ /**
+ * @see Countable::count
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function count() {
+ return $this->count;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function append( Iterator $iterable ) {
+
+ if ( $iterable instanceof Countable ) {
+ $this->count += $iterable->count();
+ }
+
+ $this->getArrayIterator()->append( $iterable );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Iterators/ChunkedIterator.php b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/ChunkedIterator.php
new file mode 100644
index 00000000..1edc5748
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/ChunkedIterator.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace SMW\Iterators;
+
+use ArrayIterator;
+use InvalidArgumentException;
+use IteratorIterator;
+use RuntimeException;
+use Traversable;
+
+/**
+ * @see Guzzle::ChunkedIterator
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ */
+class ChunkedIterator extends IteratorIterator {
+
+ /**
+ * @var integer
+ */
+ private $chunkSize = 0;
+
+ /**
+ * @var array
+ */
+ private $chunk;
+
+ /**
+ * @since 3.0
+ *
+ * @param Traversable|array $iterator
+ * @param integer $chunkSize
+ */
+ public function __construct( $iterable, $chunkSize = 500 ) {
+
+ $chunkSize = (int)$chunkSize;
+
+ if ( is_array( $iterable ) ) {
+ $iterable = new ArrayIterator( $iterable );
+ }
+
+ if ( !$iterable instanceof Traversable ) {
+ throw new RuntimeException( "ChunkedIterator expected an Traversable" );
+ }
+
+ if ( $chunkSize < 0 ) {
+ throw new InvalidArgumentException( "$chunkSize is lower than 0" );
+ }
+
+ parent::__construct( $iterable );
+ $this->chunkSize = $chunkSize;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function rewind() {
+ parent::rewind();
+ $this->next();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function next() {
+ $this->chunk = [];
+
+ for ( $i = 0; $i < $this->chunkSize && parent::valid(); $i++ ) {
+ $this->chunk[] = parent::current();
+ parent::next();
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function current() {
+ return $this->chunk;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function valid() {
+ return (bool) $this->chunk;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Iterators/CsvFileIterator.php b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/CsvFileIterator.php
new file mode 100644
index 00000000..fb8581a5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/CsvFileIterator.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace SMW\Iterators;
+
+use Exception;
+use Iterator;
+use SMW\Exception\FileNotFoundException;
+
+/**
+ * @see http://php.net/manual/en/function.fgetcsv.php
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ */
+class CsvFileIterator implements Iterator {
+
+ /**
+ * @var Resource
+ */
+ private $handle;
+
+ /**
+ * @var boolean
+ */
+ private $parseHeader;
+
+ /**
+ * @var []
+ */
+ private $header = [];
+
+ /**
+ * @var string
+ */
+ private $delimiter;
+
+ /**
+ * @var integer
+ */
+ private $length;
+
+ /**
+ * @var int
+ */
+ private $key = 0;
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ * @param boolean $parseHeader
+ * @param string $delimiter
+ * @param integer $length
+ */
+ public function __construct( $file, $parseHeader = false, $delimiter = ",", $length = 8000 ) {
+
+ try {
+ $this->handle = fopen( $file, "r" );
+ } catch ( Exception $e ) {
+ throw new FileNotFoundException( 'File "'. $file . '" is not accessible.' );
+ }
+
+ $this->parseHeader = $parseHeader;
+ $this->delimiter = $delimiter;
+ $this->length = $length;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function __destruct() {
+ if( is_resource( $this->handle ) ) {
+ fclose( $this->handle );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getHeader() {
+ return $this->header;
+ }
+
+ /**
+ * Resets the file handle
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function rewind() {
+ $this->key = 0;
+ rewind( $this->handle );
+ }
+
+ /**
+ * Returns the current CSV row as a 2 dimensional array
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function current() {
+
+ // First iteration to match the header
+ if ( $this->parseHeader && $this->key == 0 ) {
+ $this->header = fgetcsv( $this->handle, $this->length, $this->delimiter );
+ }
+
+ $currentElement = fgetcsv( $this->handle, $this->length, $this->delimiter );
+ $this->key++;
+
+ return $currentElement;
+ }
+
+ /**
+ * Returns the current row number.
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function key() {
+ return $this->key;
+ }
+
+ /**
+ * Checks if the end of file is reached.
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function next() {
+ return !feof( $this->handle );
+ }
+
+ /**
+ * Checks if the next row is a valid row.
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function valid() {
+
+ if ( $this->next() ) {
+ return true;
+ }
+
+ fclose( $this->handle );
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Iterators/MappingIterator.php b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/MappingIterator.php
new file mode 100644
index 00000000..0aa39888
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/MappingIterator.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace SMW\Iterators;
+
+use ArrayIterator;
+use Countable;
+use Iterator;
+use IteratorIterator;
+use RuntimeException;
+
+/**
+ * This iterator is expected to be called in combination with another iterator
+ * (or traversable/array) in order to apply a mapping on the returned current element
+ * during an iterative (foreach etc.) process.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class MappingIterator extends IteratorIterator implements Countable {
+
+ /**
+ * @var callable
+ */
+ private $callback;
+
+ /**
+ * @var integer
+ */
+ private $count = 1;
+
+ /**
+ * @since 2.5
+ *
+ * @param Iterator|array $iterable
+ * @param callable $callback
+ */
+ public function __construct( $iterable, callable $callback ) {
+
+ if ( is_array( $iterable ) ) {
+ $iterable = new ArrayIterator( $iterable );
+ }
+
+ if ( !$iterable instanceof Iterator ) {
+ throw new RuntimeException( "MappingIterator expected an Iterator" );
+ }
+
+ if ( $iterable instanceof Countable ) {
+ $this->count = $iterable->count();
+ }
+
+ parent::__construct( $iterable );
+ $this->callback = $callback;
+ }
+
+ /**
+ * @see Countable::count
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function count() {
+ return $this->count;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function current() {
+ return call_user_func( $this->callback, parent::current() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Iterators/ResultIterator.php b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/ResultIterator.php
new file mode 100644
index 00000000..31a6b9e5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Iterators/ResultIterator.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace SMW\Iterators;
+
+use ArrayIterator;
+use Countable;
+use Iterator;
+use ResultWrapper;
+use RuntimeException;
+use SeekableIterator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ResultIterator implements Iterator, Countable, SeekableIterator {
+
+ /**
+ * @var ResultWrapper
+ */
+ public $res;
+
+ /**
+ * @var integer
+ */
+ public $position;
+
+ /**
+ * @var mixed
+ */
+ public $current;
+
+ /**
+ * @var boolean
+ */
+ public $numRows = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param Iterator|array $res
+ */
+ public function __construct( $res ) {
+
+ if ( !$res instanceof Iterator && !is_array( $res ) ) {
+ throw new RuntimeException( "Expected an Iterator or array!" );
+ }
+
+ // @see MediaWiki's ResultWrapper
+ if ( $res instanceof Iterator && method_exists( $res , 'numRows' ) ) {
+ $this->numRows = true;
+ }
+
+ if ( is_array( $res ) ) {
+ $res = new ArrayIterator( $res );
+ }
+
+ $this->res = $res;
+ $this->position = 0;
+ $this->setCurrent( $this->res->current() );
+ }
+
+ /**
+ * @see Countable::count
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function count() {
+ return $this->numRows ? $this->res->numRows() : $this->res->count();
+ }
+
+ /**
+ * @see SeekableIterator::seek
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function seek( $position ) {
+ $this->res->seek( $position );
+ $this->setCurrent( $this->res->current() );
+ $this->position = $position;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function current() {
+ return $this->current;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function key() {
+ return $this->position;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function next() {
+ $row = $this->res->next();
+ $this->setCurrent( $row );
+ $this->position++;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function rewind() {
+ $this->res->rewind();
+ $this->position = 0;
+ $this->setCurrent( $this->res->current() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function valid() {
+ return $this->current !== false;
+ }
+
+ protected function setCurrent( $row ) {
+ if ( $row === false || $row === null ) {
+ $this->current = false;
+ } else {
+ $this->current = $row;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Lang/FallbackFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/Lang/FallbackFinder.php
new file mode 100644
index 00000000..4931ebb7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Lang/FallbackFinder.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace SMW\Lang;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class FallbackFinder {
+
+ /**
+ * @var JsonContentsFileReader
+ */
+ private $jsonContentsFileReader;
+
+ /**
+ * @var string
+ */
+ private $canonicalFallbackLanguageCode = 'en';
+
+ /**
+ * @var array
+ */
+ private $fallbackLanguages = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param JsonContentsFileReader $jsonContentsFileReader
+ */
+ public function __construct( JsonContentsFileReader $jsonContentsFileReader ) {
+ $this->jsonContentsFileReader = $jsonContentsFileReader;
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function emptyByLanguageCode( $languageCode ) {
+ unset( $this->fallbackLanguages[strtolower( trim( $languageCode ) )] );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getCanonicalFallbackLanguageCode() {
+ return $this->canonicalFallbackLanguageCode;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $languageCode
+ *
+ * @return string
+ */
+ public function getFallbackLanguageBy( $languageCode = '' ) {
+
+ $languageCode = strtolower( trim( $languageCode ) );
+
+ if ( isset( $this->fallbackLanguages[$languageCode] ) ) {
+ return $this->fallbackLanguages[$languageCode];
+ }
+
+ $index = 'fallback_language';
+
+ // Unknown, use the default
+ if ( $languageCode === '' ) {
+ return $this->canonicalFallbackLanguageCode;
+ }
+
+ try {
+ $contents = $this->jsonContentsFileReader->readByLanguageCode( $languageCode );
+ } catch ( RuntimeException $e ) {
+ $this->fallbackLanguages[$languageCode] = $this->canonicalFallbackLanguageCode;
+ }
+
+ // Get customized fallbackLanguage
+ if ( isset( $contents[$index] ) ) {
+ $this->fallbackLanguages[$languageCode] = $contents[$index];
+ }
+
+ // The ultimate defense line, fallback was not set, or is false or empty
+ // which means use the canonicalFallbackLanguageCode
+ if (
+ !isset( $contents[$index] ) ||
+ $this->fallbackLanguages[$languageCode] === false ||
+ $this->fallbackLanguages[$languageCode] === '' ) {
+ $this->fallbackLanguages[$languageCode] = $this->canonicalFallbackLanguageCode;
+ }
+
+ return $this->fallbackLanguages[$languageCode];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Lang/JsonContentsFileReader.php b/www/wiki/extensions/SemanticMediaWiki/src/Lang/JsonContentsFileReader.php
new file mode 100644
index 00000000..18755f60
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Lang/JsonContentsFileReader.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace SMW\Lang;
+
+use Onoi\Cache\Cache;
+use Onoi\Cache\NullCache;
+use RuntimeException;
+use SMW\Utils\ErrorCodeFormatter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class JsonContentsFileReader {
+
+ /**
+ * @var array
+ */
+ private static $contents = [];
+
+ /**
+ * @var string
+ */
+ private $languageFileDir = '';
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var boolean
+ */
+ private $skipCache = false;
+
+ /**
+ * @var integer
+ */
+ private $ttl = 604800; // 7 * 24 * 3600
+
+ /**
+ * @since 2.5
+ *
+ * @param Cache|null $cache
+ * @param string $languageFileDir
+ */
+ public function __construct( Cache $cache = null, $languageFileDir = '' ) {
+ $this->cache = $cache;
+ $this->languageFileDir = $languageFileDir;
+
+ if ( $this->cache === null ) {
+ $this->cache = new NullCache();
+ }
+
+ if ( $this->languageFileDir === '' ) {
+ $this->languageFileDir = $GLOBALS['smwgExtraneousLanguageFileDir'];
+ }
+ }
+
+ /**
+ * @since 2.5
+ */
+ public static function clear() {
+ self::$contents = [];
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function skipCache() {
+ $this->skipCache = true;
+ }
+
+ /**
+ * @since 1.2.0
+ *
+ * @return integer
+ */
+ public function getFileModificationTime( $languageCode ) {
+ return filemtime( $this->getLanguageFile( $languageCode ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $languageCode
+ * @param boolean $readFromFile
+ *
+ * @return boolean
+ */
+ public function canReadByLanguageCode( $languageCode ) {
+
+ $canReadByLanguageCode = '';
+
+ try {
+ $canReadByLanguageCode = $this->getLanguageFile( $languageCode );
+ } catch ( \Exception $e ) {
+ $canReadByLanguageCode = '';
+ }
+
+ return $canReadByLanguageCode !== '';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $languageCode
+ * @param array $contents
+ */
+ public function writeByLanguageCode( $languageCode, $contents ) {
+
+ $languageCode = strtolower( trim( $languageCode ) );
+
+ file_put_contents(
+ $this->getLanguageFile( $languageCode ),
+ json_encode( $contents, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $languageCode
+ * @param boolean $readFromFile
+ *
+ * @return array
+ * @throws RuntimeException
+ */
+ public function readByLanguageCode( $languageCode, $readFromFile = false ) {
+
+ $languageCode = strtolower( trim( $languageCode ) );
+
+ if ( !$readFromFile && isset( self::$contents[$languageCode] ) ) {
+ return self::$contents[$languageCode];
+ }
+
+ $cacheKey = smwfCacheKey(
+ 'smw:lang',
+ [
+ $languageCode,
+ $this->getFileModificationTime( $languageCode ),
+ $this->ttl
+ ]
+ );
+
+ if ( !$readFromFile && !$this->skipCache && !isset( self::$contents[$languageCode] ) && $this->cache->contains( $cacheKey ) ) {
+ self::$contents[$languageCode] = $this->cache->fetch( $cacheKey );
+ }
+
+ if ( $readFromFile || !isset( self::$contents[$languageCode] ) ) {
+ self::$contents[$languageCode] = $this->readJSONFile( $languageCode, $cacheKey );
+ }
+
+ return self::$contents[$languageCode];
+ }
+
+ protected function readJSONFile( $languageCode, $cacheKey ) {
+
+ $contents = json_decode(
+ file_get_contents( $this->getLanguageFile( $languageCode ) ),
+ true
+ );
+
+ if ( $contents !== null && json_last_error() === JSON_ERROR_NONE ) {
+ $this->cache->save( $cacheKey, $contents, $this->ttl );
+ return $contents;
+ }
+
+ throw new RuntimeException( ErrorCodeFormatter::getMessageFromJsonErrorCode( json_last_error() ) );
+ }
+
+ private function getLanguageFile( $languageCode ) {
+
+ $file = str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, $this->languageFileDir . '/' . $languageCode . '.json' );
+
+ if ( is_readable( $file ) ) {
+ return $file;
+ }
+
+ throw new RuntimeException( "Expected a {$file} file" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Lang/Lang.php b/www/wiki/extensions/SemanticMediaWiki/src/Lang/Lang.php
new file mode 100644
index 00000000..c5ebb4ef
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Lang/Lang.php
@@ -0,0 +1,600 @@
+<?php
+
+namespace SMW\Lang;
+
+/**
+ * This class provides "extraneous" language functions independent from MediaWiki
+ * to handle certain language options in a way required by Semantic MediaWiki and
+ * its registration system.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class Lang {
+
+ /**
+ * @var Lang
+ */
+ private static $instance = null;
+
+ /**
+ * @var LanguageContents
+ */
+ private $languageContents;
+
+ /**
+ * @var string
+ */
+ private $languageCode = 'en';
+
+ /**
+ * @var string
+ */
+ private $canonicalFallbackLanguageCode = 'en';
+
+ /**
+ * @var array
+ */
+ private $propertyIdByLabelMap = [];
+
+ /**
+ * @var array
+ */
+ private $dateFormatsMap = [];
+
+ /**
+ * @var array
+ */
+ private $monthMap = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param LanguageContents $languageContents
+ */
+ public function __construct( LanguageContents $languageContents ) {
+ $this->languageContents = $languageContents;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return Lang
+ */
+ public static function getInstance() {
+
+ if ( self::$instance !== null ) {
+ return self::$instance;
+ }
+
+ // $cache = ApplicationFactory::getInstance()->getCache()
+
+ $jsonContentsFileReader = new JsonContentsFileReader();
+
+ self::$instance = new self(
+ new LanguageContents(
+ $jsonContentsFileReader,
+ new FallbackFinder( $jsonContentsFileReader )
+ )
+ );
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ public function getCode() {
+ return $this->languageCode;
+ }
+
+ /**
+ * @deprecated since 3.0, use Lang::fetch
+ * @since 2.4
+ *
+ * @return string
+ */
+ public function fetchByLanguageCode( $languageCode ) {
+ return $this->fetch( $languageCode );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ public function fetch( $languageCode ) {
+
+ $this->languageCode = strtolower( trim( $languageCode ) );
+
+ if ( !$this->languageContents->isLoaded( $this->languageCode ) ) {
+ $this->languageContents->load( $this->languageCode );
+ }
+
+ $this->canonicalFallbackLanguageCode = $this->languageContents->getCanonicalFallbackLanguageCode();
+
+ return $this;
+ }
+
+ /**
+ * Function that returns an array of namespace identifiers.
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getNamespaces() {
+
+ $namespaces = $this->languageContents->get(
+ 'namespace.labels',
+ $this->languageCode
+ );
+
+ $namespaces += $this->languageContents->get(
+ 'namespace.labels',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ foreach ( $namespaces as $key => $value ) {
+ unset( $namespaces[$key] );
+
+ if ( defined( $key ) ) {
+ $namespaces[constant($key)] = $value;
+ }
+ }
+
+ return $namespaces;
+ }
+
+ /**
+ * Function that returns an array of namespace aliases, if any
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getNamespaceAliases() {
+
+ $namespaceAliases = $this->languageContents->get(
+ 'namespace.aliases',
+ $this->languageCode
+ );
+
+ $namespaceAliases += $this->languageContents->get(
+ 'namespace.aliases',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ foreach ( $namespaceAliases as $alias => $namespace ) {
+ if ( defined( $namespace ) ) {
+ $namespaceAliases[$alias] = constant( $namespace );
+ }
+ }
+
+ return $namespaceAliases;
+ }
+
+ /**
+ * Return all labels that are available as names for built-in datatypes. Those
+ * are the types that users can access via [[has type::...]] (more built-in
+ * types may exist for internal purposes but the user won't need to
+ * know this). The returned array is indexed by (internal) type ids.
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getDatatypeLabels() {
+
+ $datatypeLabels = $this->languageContents->get(
+ 'datatype.labels',
+ $this->languageCode
+ );
+
+ $datatypeLabels += $this->languageContents->get(
+ 'datatype.labels',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ return $datatypeLabels;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $label
+ *
+ * @return string
+ */
+ public function findDatatypeByLabel( $label ) {
+
+ $label = mb_strtolower( $label );
+
+ $datatypeLabels = $this->getDatatypeLabels();
+ $datatypeLabels = array_flip( $datatypeLabels );
+ $datatypeLabels += $this->getDatatypeAliases();
+
+ foreach ( $datatypeLabels as $key => $id ) {
+ if ( mb_strtolower( $key ) === $label ) {
+ return $id;
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getCanonicalDatatypeLabels() {
+
+ $datatypeLabels = $this->languageContents->get(
+ 'datatype.labels',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ $canonicalPropertyLabels = array_flip( $datatypeLabels );
+
+ return $canonicalPropertyLabels;
+ }
+
+ /**
+ * Return an array that maps aliases to internal type ids. All ids used here
+ * should also have a primary label defined in m_DatatypeLabels.
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getDatatypeAliases() {
+
+ $datatypeAliases = $this->languageContents->get(
+ 'datatype.aliases',
+ $this->languageCode
+ );
+
+ $datatypeAliases += $this->languageContents->get(
+ 'datatype.aliases',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ return $datatypeAliases;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getCanonicalPropertyLabels() {
+
+ $canonicalPropertyLabels = $this->languageContents->get(
+ 'property.labels',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ $canonicalPropertyLabels = array_flip( $canonicalPropertyLabels );
+
+ $canonicalPropertyLabels += $this->languageContents->get(
+ 'property.aliases',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ $canonicalPropertyLabels += $this->languageContents->get(
+ 'datatype.aliases',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ return $canonicalPropertyLabels;
+ }
+
+ /**
+ * Function that returns the labels for predefined properties.
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getPropertyLabels() {
+
+ $propertyLabels = $this->languageContents->get(
+ 'property.labels',
+ $this->languageCode
+ );
+
+ $propertyLabels += $this->languageContents->get(
+ 'property.labels',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ return $propertyLabels;
+ }
+
+ /**
+ * Aliases for predefined properties, if any.
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getCanonicalPropertyAliases() {
+
+ $canonicalPropertyAliases = $this->languageContents->get(
+ 'property.aliases',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ // Add standard property lables from the canonical language as
+ // aliases
+ $propertyLabels = $this->languageContents->get(
+ 'property.labels',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ $canonicalPropertyAliases += array_flip( $propertyLabels );
+
+ return $canonicalPropertyAliases;
+ }
+
+ /**
+ * Aliases for predefined properties, if any.
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getPropertyAliases() {
+
+ $propertyAliases = $this->languageContents->get(
+ 'property.aliases',
+ $this->languageCode
+ );
+
+ $propertyLabels = $this->languageContents->get(
+ 'property.labels',
+ $this->languageCode
+ );
+
+ $propertyAliases += array_flip( $propertyLabels );
+
+ return $propertyAliases;
+ }
+
+ /**
+ * @deprecated use getPropertyIdByLabel
+ */
+ protected function getPropertyId( $propertyLabel ) {
+
+ $list += $this->languageContents->get(
+ 'property.aliases',
+ $this->languageCode
+ );
+
+ $list += $this->languageContents->get(
+ 'property.aliases',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ return $list;
+ }
+
+ /**
+ * Function receives property name (for example, `Modificatino date') and
+ * returns a property id (for example, `_MDAT'). Property name may be
+ * localized one. If property name is not recognized, a null value returned.
+ *
+ * @since 2.4
+ *
+ * @return string|null
+ */
+ public function getPropertyIdByLabel( $label ) {
+
+ $this->initPropertyIdByLabelMap( $this->languageCode );
+
+ if ( isset( $this->propertyIdByLabelMap[$this->languageCode]['label'][$label] ) ) {
+ return $this->propertyIdByLabelMap[$this->languageCode]['label'][$label];
+ };
+
+ if ( isset( $this->propertyIdByLabelMap[$this->languageCode]['alias'][$label] ) ) {
+ return $this->propertyIdByLabelMap[$this->languageCode]['alias'][$label];
+ };
+
+ return null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getPropertyLabelList() {
+
+ $this->initPropertyIdByLabelMap( $this->languageCode );
+
+ if ( isset( $this->propertyIdByLabelMap[$this->languageCode] ) ) {
+ return $this->propertyIdByLabelMap[$this->languageCode];
+ }
+
+ return [];
+ }
+
+ /**
+ * Function that returns the preferred date formats
+ *
+ * Preferred interpretations for dates with 1, 2, and 3 components. There
+ * is an array for each case, and the constants define the obvious order
+ * (e.g. SMW_YDM means "first Year, then Day, then Month). Unlisted
+ * combinations will not be accepted at all.
+ *
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getDateFormats() {
+
+ $languageCode = $this->languageCode;
+
+ if ( !isset( $this->dateFormatsMap[$languageCode] ) || $this->dateFormatsMap[$languageCode] === [] ) {
+ $this->dateFormatsMap[$languageCode] = $this->getDateFormatsByLanguageCode( $languageCode );
+ }
+
+ return $this->dateFormatsMap[$languageCode];
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer|null $precision
+ *
+ * @return string
+ */
+ public function getPreferredDateFormatByPrecision( $precision = null ) {
+
+ $dateOutputFormats = $this->languageContents->get(
+ 'date.precision',
+ $this->languageCode
+ );
+
+ foreach ( $dateOutputFormats as $key => $format ) {
+ if ( @constant( $key ) === $precision ) {
+ return $format;
+ }
+ }
+
+ // Fallback
+ return 'd F Y H:i:s';
+ }
+
+ /**
+ * @deprecated use findMonthNumberByLabel
+ */
+ public function findMonth( $label ) {
+ return $this->findMonthNumberByLabel( $label );
+ }
+
+ /**
+ * Function looks up a month and returns the corresponding number.
+ *
+ * @since 2.4
+ *
+ * @param string $label
+ *
+ * @return false|integer
+ */
+ public function findMonthNumberByLabel( $label ) {
+
+ $languageCode = $this->languageCode;
+
+ if ( !isset( $this->months[$languageCode] ) || $this->months[$languageCode] === [] ) {
+ $this->months[$languageCode] = $this->languageContents->get( 'date.months', $languageCode );
+ }
+
+ foreach ( $this->months[$languageCode] as $key => $value ) {
+ if ( strcasecmp( $value[0], $label ) == 0 || strcasecmp( $value[1], $label ) == 0 ) {
+ return $key + 1; // array starts with 0
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @deprecated use getMonthLabelByNumber
+ */
+ public function getMonthLabel( $number ) {
+ return $this->getMonthLabelByNumber( $number );
+ }
+
+ /**
+ * Return the name of the month with the given number.
+ *
+ * @since 2.4
+ *
+ * @param integer $number
+ *
+ * @return array
+ */
+ public function getMonthLabelByNumber( $number ) {
+
+ $languageCode = $this->languageCode;
+ $number = (int)( $number - 1 ); // array starts with 0
+
+ if ( !isset( $this->months[$languageCode] ) || $this->months[$languageCode] === [] ) {
+ $this->months[$languageCode] = $this->languageContents->get( 'date.months', $languageCode );
+ }
+
+ if ( ( ( $number >= 0 ) && ( $number <= 11 ) ) && isset( $this->months[$languageCode][$number]) ) {
+ return $this->months[$languageCode][$number][0]; // Long name
+ }
+
+ return '';
+ }
+
+ private function getDateFormatsByLanguageCode( $languageCode ) {
+
+ $dateformats = [];
+
+ foreach ( $this->languageContents->get( 'date.format', $languageCode ) as $row ) {
+ $internalNumberFormat = [];
+
+ foreach ( $row as $value ) {
+ $internalNumberFormat[] = constant( $value );
+ }
+
+ $dateformats[] = $internalNumberFormat;
+ }
+
+ return $dateformats;
+ }
+
+ private function initPropertyIdByLabelMap( $languageCode ) {
+
+ if ( isset( $this->propertyIdByLabelMap[$languageCode] ) && $this->propertyIdByLabelMap[$languageCode] !== [] ) {
+ return;
+ }
+
+ $this->propertyIdByLabelMap[$languageCode] = [];
+
+ $propertyLabels = $this->languageContents->get(
+ 'property.labels',
+ $languageCode
+ );
+
+ $propertyLabels += $this->languageContents->get(
+ 'datatype.labels',
+ $languageCode
+ );
+
+ foreach ( $propertyLabels as $id => $label ) {
+ $this->propertyIdByLabelMap[$languageCode]['label'][$label] = $id;
+ }
+
+ $propertyAliases = $this->languageContents->get(
+ 'property.aliases',
+ $languageCode
+ );
+
+ $propertyAliases += $this->languageContents->get(
+ 'property.aliases',
+ $this->canonicalFallbackLanguageCode
+ );
+
+ foreach ( $propertyAliases as $label => $id ) {
+ $this->propertyIdByLabelMap[$languageCode]['alias'][$label] = $id;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Lang/LanguageContents.php b/www/wiki/extensions/SemanticMediaWiki/src/Lang/LanguageContents.php
new file mode 100644
index 00000000..be3d4b3d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Lang/LanguageContents.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace SMW\Lang;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class LanguageContents {
+
+ /**
+ * @var JsonContentsFileReader
+ */
+ private $jsonContentsFileReader;
+
+ /**
+ * @var FallbackFinder
+ */
+ private $fallbackFinder;
+
+ /**
+ * @var array
+ */
+ private $contents = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param JsonContentsFileReader $jsonContentsFileReader
+ * @param FallbackFinder $fallbackFinder
+ */
+ public function __construct( JsonContentsFileReader $jsonContentsFileReader, FallbackFinder $fallbackFinder ) {
+ $this->jsonContentsFileReader = $jsonContentsFileReader;
+ $this->fallbackFinder = $fallbackFinder;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getCanonicalFallbackLanguageCode() {
+ return $this->fallbackFinder->getCanonicalFallbackLanguageCode();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $languageCode
+ *
+ * @return boolean
+ */
+ public function isLoaded( $languageCode ) {
+ return isset( $this->contents[$languageCode] ) || array_key_exists( $languageCode, $this->contents );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $languageCode
+ *
+ * @return boolean
+ */
+ public function load( $languageCode ) {
+
+ if ( !$this->isLoaded( $languageCode ) && !$this->jsonContentsFileReader->canReadByLanguageCode( $languageCode ) ) {
+ $languageCode = $this->fallbackFinder->getFallbackLanguageBy( $languageCode );
+ }
+
+ if ( !$this->isLoaded( $languageCode ) ) {
+ $this->contents[$languageCode] = $this->jsonContentsFileReader->readByLanguageCode( $languageCode );
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $id
+ * @param string $languageCode
+ *
+ * @return array|string|false
+ */
+ public function get( $id, $languageCode ) {
+ return $this->matchLanguage( $languageCode, $id );
+ }
+
+ private function matchLanguage( $languageCode, $id ) {
+
+ $canonicalFallbackLanguageCode = $this->fallbackFinder->getCanonicalFallbackLanguageCode();
+
+ if ( !isset( $this->contents[$languageCode] ) || $this->contents[$languageCode] === [] ) {
+ // In case a language has no matching file
+ try {
+ $this->contents[$languageCode] = $this->jsonContentsFileReader->readByLanguageCode( $languageCode );
+ } catch ( RuntimeException $e ) {
+ $this->contents[$languageCode] = [];
+ $languageCode = $canonicalFallbackLanguageCode;
+ }
+ }
+
+ $depth = 1;
+
+ // There is certainly a better (meaning generic) way to do this yet with
+ // only a limited depth, doing a recursive traversal will not yield an
+ // advantage
+ if ( strpos( $id, '.' ) !== false ) {
+ $keys = explode( '.', $id );
+ $depth = count( $keys );
+ }
+
+ if ( $depth == 1 && isset( $this->contents[$languageCode][$id] ) && $this->contents[$languageCode][$id] !== [] ) {
+ return $this->contents[$languageCode][$id];
+ }
+
+ if ( $depth == 2 && isset( $this->contents[$languageCode][$keys[0]][$keys[1]] ) && $this->contents[$languageCode][$keys[0]][$keys[1]] !== [] ) {
+ return $this->contents[$languageCode][$keys[0]][$keys[1]];
+ }
+
+ if ( $depth == 3 && isset( $this->contents[$languageCode][$keys[0]][$keys[1]][$keys[2]] ) && $this->contents[$languageCode][$keys[0]][$keys[1]][$keys[2]] !== [] ) {
+ return $this->contents[$languageCode][$keys[0]][$keys[1]][$keys[2]];
+ }
+
+ if ( $languageCode !== $canonicalFallbackLanguageCode ) {
+ return $this->matchLanguage( $this->fallbackFinder->getFallbackLanguageBy( $languageCode ), $id );
+ }
+
+ return $this->matchCanonicalLanguage( $canonicalFallbackLanguageCode, $id );
+ }
+
+ private function matchCanonicalLanguage( $languageCode, $id ) {
+
+ $depth = 1;
+
+ if ( strpos( $id, '.' ) !== false ) {
+ $keys = explode( '.', $id );
+ $depth = count( $keys );
+ }
+
+ // Last resort before throwing the towel, make sure we really have
+ // something when the default FallbackLanguageCode is used
+ if ( $depth == 1 && !isset( $this->contents[$languageCode][$id] ) ) {
+ $this->contents[$languageCode] = $this->jsonContentsFileReader->readByLanguageCode( $languageCode, true );
+ }
+
+ if ( $depth == 1 && isset( $this->contents[$languageCode][$id] ) ) {
+ return $this->contents[$languageCode][$id];
+ }
+
+ if ( $depth == 2 && !isset( $this->contents[$languageCode][$keys[0]][$keys[1]] ) ) {
+ $this->contents[$languageCode] = $this->jsonContentsFileReader->readByLanguageCode( $languageCode, true );
+ }
+
+ if ( $depth == 2 && isset( $this->contents[$languageCode][$keys[0]][$keys[1]] ) ) {
+ return $this->contents[$languageCode][$keys[0]][$keys[1]];
+ }
+
+ if ( $depth == 3 && !isset( $this->contents[$languageCode][$keys[0]][$keys[1]][$keys[2]] ) ) {
+ $this->contents[$languageCode] = $this->jsonContentsFileReader->readByLanguageCode( $languageCode, true );
+ }
+
+ if ( $depth == 3 && isset( $this->contents[$languageCode][$keys[0]][$keys[1]][$keys[2]] ) ) {
+ return $this->contents[$languageCode][$keys[0]][$keys[1]][$keys[2]];
+ }
+
+ throw new RuntimeException( "Unknown or invalid `{$id}` id for `{$languageCode}`" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Lang/README.md b/www/wiki/extensions/SemanticMediaWiki/src/Lang/README.md
new file mode 100644
index 00000000..2a2c3cd4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Lang/README.md
@@ -0,0 +1,103 @@
+It provides "extraneous" language functions independent of MediaWiki that ate required by Semantic MediaWiki and its registration system.
+
+## JSON format
+
+The location of the content files is determined by the [`$smwgExtraneousLanguageFileDir`](https://www.semantic-mediawiki.org/wiki/Help:$smwgExtraneousLanguageFileDir) setting.
+
+### Field definitions
+
+- `fallback_language`defines a fallback language tag
+- `datatype`
+ - `labels` datatype labels
+ - `aliases` datatype aliases
+- `property`
+ - `labels` predefined property labels
+ - `aliases` predefined property aliases
+- `namespace`
+ - `labels` namespace names
+ - `aliases` namespace aliases
+- `date`
+ - `format` to a define a rule set of how to resolve preferred date formats for dates with 1, 2, and 3 components. It is defined as an array where the constants define the order of the interpretation.
+ - `SMW_MDY` Month-Day-Year
+ - `SMW_DMY` Day-Month-Year
+ - `SMW_YMD` Year-Month-Day
+ - `SMW_YDM` Year-Day-Month
+ - `SMW_MY` Month-Year
+ - `SMW_YM` Year-Month
+ - `SMW_Y` Year
+ - `SMW_YEAR` an entered digit can be a year
+ - `SMW_DAY` an entered digit can be a day
+ - `SMW_MONTH` an entered digit can be a month
+ - `SMW_DAY_MONTH_YEAR` an entered digit can be a day, month or year
+ - `SMW_DAY_YEAR` an entered digit can be either a day or a year
+ - `precision` used to define the rules of formatting for a specific precision:
+ - `SMW_PREC_Y` Year
+ - `SMW_PREC_YMD` Year, Month, and Day
+ - `SMW_PREC_YMDT` Year, Month, Day, and Time
+ - `SMW_PREC_YMDTZ` Year, Month, Day, Time and Timezone
+ - `months` twelve strings naming the months and short strings briefly naming the month
+ - `days` follows ISO-8601 numeric representation, starting with Monday together with the corresponding short name
+- `@...` fields leading with `@` are identified as comment fields
+
+### Example
+
+<pre>
+{
+ "fallback_language": false,
+ "datatype": {
+ "labels":{
+ "_wpg": "Page"
+ },
+ "aliases":{
+ "Page": "_wpg"
+ }
+ },
+ "property": {
+ "labels":{
+ "_TYPE": "Has type"
+ },
+ "aliases": {
+ "Has type": "_TYPE"
+ }
+ },
+ "namespaces": {
+ "labels":{
+ "SMW_NS_PROPERTY": "Property"
+ },
+ "aliases": {
+ "Property": "SMW_NS_PROPERTY"
+ }
+ },
+ "date":{
+ "precision": {
+ "SMW_PREC_YMDTZ": "H:i:s T, j F Y"
+ },
+ "format": [
+ [
+ "SMW_Y"
+ ]
+ ],
+ "months": [
+ [
+ "January",
+ "Jan"
+ ]
+ ]
+ "days":[
+ [
+ "Monday",
+ "Mon"
+ ]
+ ]
+ }
+}
+</pre>
+
+## Technical notes
+
+<pre>
+SMW\Lang
+├─ Lang # interface to the language functions
+├─ JsonContentsFileReader # access the contents of a `JSON` file
+└─ FallbackFinder # resolving a fallback language
+</pre>
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Localizer.php b/www/wiki/extensions/SemanticMediaWiki/src/Localizer.php
new file mode 100644
index 00000000..09be273b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Localizer.php
@@ -0,0 +1,394 @@
+<?php
+
+namespace SMW;
+
+use DateTime;
+use Language;
+use SMW\Lang\Lang;
+use SMW\MediaWiki\LocalTime;
+use Title;
+use User;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class Localizer {
+
+ /**
+ * @var Localizer
+ */
+ private static $instance = null;
+
+ /**
+ * @var Language
+ */
+ private $contentLanguage = null;
+
+ /**
+ * @since 2.1
+ *
+ * @param Language $contentLanguage
+ */
+ public function __construct( Language $contentLanguage ) {
+ $this->contentLanguage = $contentLanguage;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return Localizer
+ */
+ public static function getInstance() {
+
+ if ( self::$instance === null ) {
+ self::$instance = new self( $GLOBALS['wgContLang'] );
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.1
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return Language
+ */
+ public function getContentLanguage() {
+ return $this->contentLanguage;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return Language
+ */
+ public function getUserLanguage() {
+ return $GLOBALS['wgLang'];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param User|null $user
+ *
+ * @return boolean
+ */
+ public function hasLocalTimeOffsetPreference( $user = null ) {
+
+ if ( !$user instanceof User ) {
+ $user = $GLOBALS['wgUser'];
+ }
+
+ return $user->getOption( 'smw-prefs-general-options-time-correction' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DateTime $dateTime
+ * @param User|null $user
+ *
+ * @return DateTime
+ */
+ public function getLocalTime( DateTime $dateTime, $user = null ) {
+
+ if ( !$user instanceof User ) {
+ $user = $GLOBALS['wgUser'];
+ }
+
+ LocalTime::setLocalTimeOffset(
+ $GLOBALS['wgLocalTZoffset']
+ );
+
+ return LocalTime::getLocalizedTime( $dateTime, $user );
+ }
+
+ /**
+ * @note
+ *
+ * 1. If the page content language is available use it as preferred language
+ * (as it is clear that the page content was intended to be in a specific
+ * language)
+ * 2. If no page content language was assigned use the global content
+ * language
+ *
+ * General rules:
+ * - Special pages are in the user language
+ * - Display of values (DV) should use the user language if available otherwise
+ * use the content language as fallback
+ * - Storage of values (DI) should always use the content language
+ *
+ * Notes:
+ * - The page content language is the language in which the content of a page is
+ * written in wikitext
+ *
+ * @since 2.4
+ *
+ * @param DIWikiPage|Title|null $title
+ *
+ * @return Language
+ */
+ public function getPreferredContentLanguage( $title = null ) {
+
+ $language = '';
+
+ if ( $title instanceof DIWikiPage ) {
+ $title = $title->getTitle();
+ }
+
+ // If the page language is different from the global content language
+ // then we assume that an explicit language object was given otherwise
+ // the Title is using the content language as fallback
+ if ( $title instanceof Title ) {
+
+ // Avoid "MWUnknownContentModelException ... " when content model
+ // is not registered
+ try {
+ $language = $title->getPageLanguage();
+ } catch ( \Exception $e ) {
+
+ }
+ }
+
+ return $language instanceof Language ? $language : $this->getContentLanguage();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $languageCode
+ *
+ * @return Language
+ */
+ public function getLanguage( $languageCode = '' ) {
+
+ if ( $languageCode === '' || !$languageCode || $languageCode === null ) {
+ return $this->getContentLanguage();
+ }
+
+ return Language::factory( $languageCode );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Language|string $languageCode
+ *
+ * @return Lang
+ */
+ public function getLang( $language = '' ) {
+
+ $languageCode = $language;
+
+ if ( $language instanceof Language ) {
+ $languageCode = $language->getCode();
+ }
+
+ if ( $languageCode === '' || !$languageCode || $languageCode === null ) {
+ $languageCode = $this->getContentLanguage()->getCode();
+ }
+
+ return Lang::getInstance()->fetch( $languageCode );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param integer $index
+ *
+ * @return string
+ */
+ public function getNamespaceTextById( $index ) {
+ return str_replace( '_', ' ', $this->contentLanguage->getNsText( $index ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $index
+ *
+ * @return string
+ */
+ public function getCanonicalNamespaceTextById( $index ) {
+
+ $canonicalNames = NamespaceManager::getCanonicalNames();
+
+ if ( isset( $canonicalNames[$index] ) ) {
+ return $canonicalNames[$index];
+ }
+
+ return \MWNamespace::getCanonicalName( $index );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $namespaceName
+ *
+ * @return integer|boolean
+ */
+ public function getNamespaceIndexByName( $namespaceName ) {
+ return $this->contentLanguage->getNsIndex( str_replace( ' ', '_', $namespaceName ) );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $languageCode
+ *
+ * @return boolean
+ */
+ public static function isKnownLanguageTag( $languageCode ) {
+
+ $languageCode = mb_strtolower( $languageCode );
+
+ // FIXME 1.19 doesn't know Language::isKnownLanguageTag
+ if ( !method_exists( '\Language', 'isKnownLanguageTag' ) ) {
+ return Language::isValidBuiltInCode( $languageCode );
+ }
+
+ return Language::isKnownLanguageTag( $languageCode );
+ }
+
+ /**
+ * @see IETF language tag / BCP 47 standards
+ *
+ * @since 2.4
+ *
+ * @param string $languageCode
+ *
+ * @return string
+ */
+ public static function asBCP47FormattedLanguageCode( $languageCode ) {
+ if ( !is_callable( [ '\LanguageCode', 'bcp47' ] ) ) {
+ // Backwards compatibility: remove once MW 1.30 is no
+ // longer supported (#3179)
+ return wfBCP47( $languageCode );
+ }
+ return \LanguageCode::bcp47( $languageCode );
+ }
+
+ /**
+ * @deprecated 2.5, use Localizer::getAnnotatedLanguageCodeFrom instead
+ * @since 2.4
+ *
+ * @param string &$value
+ *
+ * @return string|false
+ */
+ public static function getLanguageCodeFrom( &$value ) {
+ return self::getAnnotatedLanguageCodeFrom( $value );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $index
+ * @param string $text
+ *
+ * @return string
+ */
+ public function createTextWithNamespacePrefix( $index, $text ) {
+ return $this->getNamespaceTextById( $index ) . ':' . $text;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $ns
+ * @param string $url
+ *
+ * @return string
+ */
+ public function getCanonicalizedUrlByNamespace( $index, $url ) {
+
+ $namespace = $this->getNamespaceTextById( $index );
+
+ if ( strpos( $url, 'title=' ) !== false ) {
+ return str_replace(
+ [
+ 'title=' . wfUrlencode( $namespace ) . ':',
+ 'title=' . $namespace . ':'
+ ],
+ 'title=' . $this->getCanonicalNamespaceTextById( $index ) .':',
+ $url
+ );
+ }
+
+ return str_replace(
+ [
+ wfUrlencode( '/' . $namespace .':' ),
+ '/' . $namespace .':'
+ ],
+ '/' . $this->getCanonicalNamespaceTextById( $index ) . ':',
+ $url
+ );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string &$value
+ *
+ * @return string|false
+ */
+ public static function getAnnotatedLanguageCodeFrom( &$value ) {
+
+ if ( strpos( $value, '@' ) === false ) {
+ return false;
+ }
+
+ if ( ( $langCode = mb_substr( strrchr( $value, "@" ), 1 ) ) !== '' ) {
+ $value = str_replace( '_', ' ', substr_replace( $value, '', ( mb_strlen( $langCode ) + 1 ) * -1 ) );
+ }
+
+ // Do we want to check here whether isKnownLanguageTag or not?
+ if ( $langCode !== '' && ctype_alpha( str_replace( [ '-' ], '', $langCode ) ) ) {
+ return $langCode;
+ }
+
+ return false;
+ }
+
+ /**
+ * @see Language::convertDoubleWidth
+ *
+ * Convert double-width roman characters to single-width.
+ * range: ff00-ff5f ~= 0020-007f
+ *
+ * @param string $string
+ *
+ * @return string
+ */
+ public static function convertDoubleWidth( $string ) {
+ static $full = null;
+ static $half = null;
+
+ if ( $full === null ) {
+ $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+ // http://php.net/manual/en/function.str-split.php, mb_str_split
+ $length = mb_strlen( $fullWidth, "UTF-8" );
+ $full = [];
+
+ for ( $i = 0; $i < $length; $i += 1 ) {
+ $full[] = mb_substr( $fullWidth, $i, 1, "UTF-8" );
+ }
+
+ $half = str_split( $halfWidth );
+ }
+
+ return str_replace( $full, $half, trim( $string ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/ConceptCacheRebuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/ConceptCacheRebuilder.php
new file mode 100644
index 00000000..c8752e0d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/ConceptCacheRebuilder.php
@@ -0,0 +1,273 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\MessageReporterFactory;
+use SMW\DIConcept;
+use SMW\MediaWiki\TitleLookup;
+use SMW\Settings;
+use SMW\Store;
+use Title;
+
+/**
+ * Is part of the `rebuildConceptCache.php` maintenance script to rebuild
+ * cache entries for selected concepts
+ *
+ * @note This is an internal class and should not be used outside of smw-core
+ *
+ * @license GNU GPL v2+
+ * @since 1.9.2
+ *
+ * @author mwjames
+ */
+class ConceptCacheRebuilder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var Settings
+ */
+ private $settings;
+
+ /**
+ * @var MessageReporter
+ */
+ private $reporter;
+
+ private $concept = null;
+ private $action = null;
+ private $options = [];
+ private $startId = 0;
+ private $endId = 0;
+ private $lines = 0;
+ private $verbose = false;
+
+ /**
+ * @since 1.9.2
+ *
+ * @param Store $store
+ * @param Settings $settings
+ */
+ public function __construct( Store $store, Settings $settings ) {
+ $this->store = $store;
+ $this->settings = $settings;
+ $this->reporter = MessageReporterFactory::getInstance()->newNullMessageReporter();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param MessageReporter $reporter
+ */
+ public function setMessageReporter( MessageReporter $reporter ) {
+ $this->reporter = $reporter;
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @param array $parameters
+ */
+ public function setParameters( array $parameters ) {
+
+ $options = [ 'hard', 'update', 'old', 'quiet', 'status', 'verbose' ];
+
+ foreach ( $options as $option ) {
+ if ( isset( $parameters[$option] ) ) {
+ $this->options[$option] = $parameters[$option];
+ }
+ }
+
+ if ( isset( $parameters['concept'] ) ) {
+ $this->concept = $parameters['concept'];
+ }
+
+ if ( isset( $parameters['s'] ) ) {
+ $this->startId = intval( $parameters['s'] );
+ }
+
+ if ( isset( $parameters['e'] ) ) {
+ $this->endId = intval( $parameters['e'] );
+ }
+
+ $actions = [ 'status', 'create', 'delete' ];
+
+ foreach ( $actions as $action ) {
+ if ( isset( $parameters[$action] ) && $this->action === null ) {
+ $this->action = $action;
+ }
+ }
+
+ $this->verbose = array_key_exists( 'verbose', $parameters );
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @return boolean
+ */
+ public function rebuild() {
+
+ switch ( $this->action ) {
+ case 'status':
+ $this->reportMessage( "\nDisplaying concept cache status information. Use CTRL-C to abort.\n\n" );
+ break;
+ case 'create':
+ $this->reportMessage( "\nCreating/updating concept caches. Use CTRL-C to abort.\n\n" );
+ break;
+ case 'delete':
+ $delay = 5;
+ $this->reportMessage( "\nAbort with CTRL-C in the next $delay seconds ... " );
+
+ if ( !$this->hasOption( 'quiet' ) ) {
+ swfCountDown( $delay );
+ }
+
+ $this->reportMessage( "\nDeleting concept caches.\n\n" );
+ break;
+ default:
+ return false;
+ }
+
+ if ( $this->hasOption( 'hard' ) ) {
+
+ $settings = ' smwgQMaxDepth: ' . $this->settings->get( 'smwgQMaxDepth' );
+ $settings .= ' smwgQMaxSize: ' . $this->settings->get( 'smwgQMaxSize' );
+ $settings .= ' smwgQFeatures: ' . $this->settings->get( 'smwgQFeatures' );
+
+ $this->reportMessage( "Option 'hard' is parameterized by{$settings}\n\n" );
+ }
+
+ $concepts = $this->getConcepts();
+
+ foreach ( $concepts as $concept ) {
+ $this->workOnConcept( $concept );
+ }
+
+ if ( $concepts === [] ) {
+ $this->reportMessage( "No concept available.\n" );
+ } else {
+ $this->reportMessage( "\nDone.\n" );
+ }
+
+ return true;
+ }
+
+ private function workOnConcept( Title $title ) {
+
+ $concept = $this->store->getConceptCacheStatus( $title );
+
+ if ( $this->skipConcept( $title, $concept ) ) {
+ return $this->lines += $this->verbose ? 1 : 0;
+ }
+
+ $result = $this->performAction( $title, $concept );
+
+ if ( $result ) {
+ $this->reportMessage( ' ' . implode( $result, "\n " ) . "\n" );
+ }
+
+ return $this->lines += 1;
+ }
+
+ private function skipConcept( $title, $concept = null ) {
+
+ $skip = false;
+
+ if ( $concept === null ) {
+ $skip = 'page not cacheable (no concept description, maybe a redirect)';
+ } elseif ( ( $this->hasOption( 'update' ) ) && ( $concept->getCacheStatus() !== 'full' ) ) {
+ $skip = 'page not cached yet';
+ } elseif ( ( $this->hasOption( 'old' ) ) && ( $concept->getCacheStatus() === 'full' ) &&
+ ( $concept->getCacheDate() > ( strtotime( 'now' ) - intval( $this->options['old'] ) * 60 ) ) ) {
+ $skip = 'cache is not old yet';
+ } elseif ( ( $this->hasOption( 'hard' ) ) && ( $this->settings->get( 'smwgQMaxSize' ) >= $concept->getSize() ) &&
+ ( $this->settings->get( 'smwgQMaxDepth' ) >= $concept->getDepth() &&
+ ( ( ~( ~( $concept->getQueryFeatures() + 0 ) | $this->settings->get( 'smwgQFeatures' ) ) ) == 0 ) ) ) {
+ $skip = 'concept is not "hard" according to wiki settings';
+ }
+
+ if ( $skip ) {
+ $line = $this->lines !== false ? "($this->lines) " : '';
+ $this->reportMessage( $line . 'Skipping concept "' . $title->getPrefixedText() . "\": $skip\n", $this->verbose );
+ }
+
+ return $skip;
+ }
+
+ private function performAction( Title $title, DIConcept $concept ) {
+ $this->reportMessage( "($this->lines) " );
+
+ if ( $this->action === 'create' ) {
+ $this->reportMessage( 'Creating cache for "' . $title->getPrefixedText() . "\" ...\n" );
+ return $this->store->refreshConceptCache( $title );
+ }
+
+ if ( $this->action === 'delete' ) {
+ $this->reportMessage( 'Deleting cache for "' . $title->getPrefixedText() . "\" ...\n" );
+ return $this->store->deleteConceptCache( $title );
+ }
+
+ $this->reportMessage( 'Status for "' . $title->getPrefixedText() . '": ' );
+
+ if ( $concept->getCacheStatus() === 'full' ) {
+ $this->reportMessage( 'Cache created at ' .
+ $this->getCacheDateInfo( $concept->getCacheDate() ) .
+ "{$concept->getCacheCount()} elements in cache\n"
+ );
+ }
+ else {
+ $this->reportMessage( "Not cached.\n" );
+ }
+ }
+
+ private function getConcepts() {
+
+ if ( $this->concept !== null ) {
+ return [ $this->createConcept() ];
+ }
+
+ return $this->createMultipleConcepts();
+ }
+
+ private function createConcept() {
+ return Title::newFromText( $this->concept, SMW_NS_CONCEPT );
+ }
+
+ private function createMultipleConcepts() {
+
+ $titleLookup = new TitleLookup( $this->store->getConnection( 'mw.db' ) );
+ $titleLookup->setNamespace( SMW_NS_CONCEPT );
+
+ if ( $this->endId == 0 && $this->startId == 0 ) {
+ return $titleLookup->selectAll();
+ }
+
+ $endId = $titleLookup->getMaxId();
+
+ if ( $this->endId > 0 ) {
+ $endId = min( $this->endId, $endId );
+ }
+
+ return $titleLookup->selectByIdRange( $this->startId, $endId );
+ }
+
+ private function hasOption( $key ) {
+ return isset( $this->options[$key] );
+ }
+
+ private function reportMessage( $message, $output = true ) {
+ if ( $output ) {
+ $this->reporter->reportMessage( $message );
+ }
+ }
+
+ private function getCacheDateInfo( $date ) {
+ return date( 'Y-m-d H:i:s', $date ) . ' (' . floor( ( strtotime( 'now' ) - $date ) / 60 ) . ' minutes old), ';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DataRebuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DataRebuilder.php
new file mode 100644
index 00000000..46fc5688
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DataRebuilder.php
@@ -0,0 +1,532 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use Exception;
+use LinkCache;
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\MessageReporterFactory;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\TitleFactory;
+use SMW\Options;
+use SMW\Store;
+use Title;
+
+/**
+ * Is part of the `rebuildData.php` maintenance script to rebuild existing data
+ * for the store
+ *
+ * @note This is an internal class and should not be used outside of smw-core
+ *
+ * @license GNU GPL v2+
+ * @since 1.9.2
+ *
+ * @author mwjames
+ */
+class DataRebuilder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var TitleFactory
+ */
+ private $titleFactory;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var MessageReporter
+ */
+ private $reporter;
+
+ /**
+ * @var DistinctEntityDataRebuilder
+ */
+ private $distinctEntityDataRebuilder;
+
+ /**
+ * @var ExceptionFileLogger
+ */
+ private $exceptionFileLogger;
+
+ /**
+ * @var integer
+ */
+ private $rebuildCount = 0;
+
+ /**
+ * @var integer
+ */
+ private $exceptionCount = 0;
+
+ private $delay = false;
+ private $canWriteToIdFile = false;
+ private $start = 1;
+ private $end = false;
+
+ /**
+ * @var int[]
+ */
+ private $filters = [];
+ private $verbose = false;
+ private $startIdFile = false;
+
+ /**
+ * @since 1.9.2
+ *
+ * @param Store $store
+ * @param TitleFactory $titleFactory
+ */
+ public function __construct( Store $store, TitleFactory $titleFactory ) {
+ $this->store = $store;
+ $this->titleFactory = $titleFactory;
+ $this->reporter = MessageReporterFactory::getInstance()->newNullMessageReporter();
+ $this->distinctEntityDataRebuilder = new DistinctEntityDataRebuilder( $store, $titleFactory );
+ $this->exceptionFileLogger = new ExceptionFileLogger( 'rebuilddata' );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param MessageReporter $reporter
+ */
+ public function setMessageReporter( MessageReporter $reporter ) {
+ $this->reporter = $reporter;
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @param Options $options
+ */
+ public function setOptions( Options $options ) {
+ $this->options = $options;
+
+ if ( $options->has( 'server' ) ) {
+ $GLOBALS['wgServer'] = $options->get( 'server' );
+ }
+
+ if ( $options->has( 'd' ) ) {
+ $this->delay = intval( $options->get( 'd' ) ) * 1000; // convert milliseconds to microseconds
+ }
+
+ if ( $options->has( 's' ) ) {
+ $this->start = max( 1, intval( $options->get( 's' ) ) );
+ } elseif ( $options->has( 'startidfile' ) ) {
+
+ $this->canWriteToIdFile = $this->is_writable( $options->get( 'startidfile' ) );
+ $this->startIdFile = $options->get( 'startidfile' );
+
+ if ( is_readable( $options->get( 'startidfile' ) ) ) {
+ $this->start = max( 1, intval( file_get_contents( $options->get( 'startidfile' ) ) ) );
+ }
+ }
+
+ // Note: this might reasonably be larger than the page count
+ if ( $options->has( 'e' ) ) {
+ $this->end = intval( $options->get( 'e' ) );
+ } elseif ( $options->has( 'n' ) ) {
+ $this->end = $this->start + intval( $options->get( 'n' ) );
+ }
+
+ $this->verbose = $options->has( 'v' );
+ $this->exceptionFileLogger->setOptions( $options );
+
+ $this->setFiltersFromOptions( $options );
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @return boolean
+ */
+ public function rebuild() {
+
+ $this->reportMessage(
+ "\nLong-running scripts may cause memory leaks, if a deteriorating\n" .
+ "rebuild process is detected (after many pages, typically more\n".
+ "than 10000), please abort with CTRL-C and resume this script\n" .
+ "at the last processed ID using the parameter -s. Continue this\n" .
+ "until all pages have been refreshed.\n"
+ );
+
+ $this->reportMessage(
+ "\nThe progress displayed is an estimation and is self-adjusting \n" .
+ "during the maintenance process.\n"
+ );
+
+ $storeName = get_class( $this->store );
+
+ if ( strpos( $storeName, "\\") !== false ) {
+ $storeName = explode("\\", $storeName );
+ $storeName = end( $storeName );
+ }
+
+ $this->reportMessage( "\nRunning for storage: " . $storeName . "\n\n" );
+
+ if ( $this->options->has( 'f' ) ) {
+ $this->performFullDelete();
+ }
+
+ if ( $this->options->has( 'page' ) || $this->options->has( 'query' ) || $this->hasFilters() || $this->options->has( 'redirects' ) ) {
+ return $this->rebuild_selection();
+ }
+
+ return $this->rebuild_all();
+ }
+
+ private function hasFilters() {
+ return $this->filters !== [];
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @return int
+ */
+ public function getRebuildCount() {
+ return $this->rebuildCount;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return int
+ */
+ public function getExceptionCount() {
+ return $this->exceptionCount;
+ }
+
+ private function rebuild_selection() {
+
+ $this->distinctEntityDataRebuilder->setOptions(
+ $this->options
+ );
+
+ $this->distinctEntityDataRebuilder->setMessageReporter(
+ $this->reporter
+ );
+
+ $this->distinctEntityDataRebuilder->setExceptionFileLogger(
+ $this->exceptionFileLogger
+ );
+
+ $this->distinctEntityDataRebuilder->doRebuild();
+
+ $this->rebuildCount = $this->distinctEntityDataRebuilder->getRebuildCount();
+
+ if ( $this->options->has( 'ignore-exceptions' ) && $this->exceptionFileLogger->getExceptionCount() > 0 ) {
+ $count = $this->exceptionFileLogger->getExceptionCount();
+ $this->exceptionFileLogger->doWrite();
+
+ $path_parts = pathinfo(
+ str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, $this->exceptionFileLogger->getExceptionFile() )
+ );
+
+ $this->reportMessage( "\nException log ..." );
+ $this->reportMessage( "\n ... counted $count exceptions" );
+ $this->reportMessage( "\n ... written to ... " . $path_parts['basename'] );
+ $this->reportMessage( "\n ... done.\n" );
+
+ $this->exceptionCount += $count;
+ }
+
+ return true;
+ }
+
+ private function rebuild_all() {
+
+ $this->entityRebuildDispatcher = $this->store->refreshData(
+ $this->start,
+ 1
+ );
+
+ $this->entityRebuildDispatcher->setDispatchRangeLimit( 1 );
+
+ $this->entityRebuildDispatcher->setOptions(
+ [
+ 'shallow-update' => $this->options->safeGet( 'shallow-update', false ),
+ 'force-update' => $this->options->safeGet( 'force-update', false ),
+ 'revision-mode' => $this->options->safeGet( 'revision-mode', false ),
+ 'use-job' => false
+ ]
+ );
+
+ // By default we expect the disposal action to take place whenever the
+ // script is run
+ $this->dispose_outdated();
+
+ // Only expected the disposal action?
+ if ( $this->options->has( 'dispose-outdated' ) ) {
+ return true;
+ }
+
+ $this->reportMessage( "\n" );
+
+ if ( !$this->options->has( 'skip-properties' ) ) {
+ $this->options->set( 'p', true );
+ $this->rebuild_selection();
+ $this->reportMessage( "\n" );
+ }
+
+ $this->store->clear();
+
+ if ( $this->start > 1 && $this->end === false ) {
+ $this->end = $this->entityRebuildDispatcher->getMaxId();
+ }
+
+ $total = $this->end && $this->end - $this->start > 0 ? $this->end - $this->start : $this->entityRebuildDispatcher->getMaxId();
+ $id = $this->start;
+
+ $this->reportMessage(
+ "Rebuilding semantic data ..."
+ );
+
+ $this->reportMessage(
+ "\n ... selecting $this->start to " .
+ ( $this->end ? "$this->end" : $this->entityRebuildDispatcher->getMaxId() ) . " IDs ...\n"
+ );
+
+ $this->rebuildCount = 0;
+ $progress = 0;
+ $estimatedProgress = 0;
+ $skipped_update = 0;
+
+ while ( ( ( !$this->end ) || ( $id <= $this->end ) ) && ( $id > 0 ) ) {
+
+ $current_id = $id;
+
+ // Changes the ID to next target!
+ $this->do_update( $id );
+
+ if ( $this->rebuildCount % 60 === 0 ) {
+ $estimatedProgress = $this->entityRebuildDispatcher->getEstimatedProgress();
+ }
+
+ $progress = round( ( $this->end - $this->start > 0 ? $this->rebuildCount / $total : $estimatedProgress ) * 100 );
+
+ foreach ( $this->entityRebuildDispatcher->getDispatchedEntities() as $value ) {
+
+ if ( isset( $value['skipped'] ) ) {
+ $skipped_update++;
+ continue;
+ }
+
+ $text = $this->getHumanReadableTextFrom( $current_id, $value );
+
+ $this->reportMessage(
+ sprintf( "%-16s%s\n", " ... updating", sprintf( "%-10s%s", $text[0], $text[1] ) ),
+ $this->options->has( 'v' )
+ );
+ }
+
+ if ( !$this->options->has( 'v' ) && $id > 0 ) {
+ $this->reportMessage(
+ "\r". sprintf( "%-50s%s", " ... updating document no.", sprintf( "%s (%1.0f%%)", $current_id, min( 100, $progress ) ) )
+ );
+ }
+ }
+
+ if ( !$this->options->has( 'v' ) ) {
+ $this->reportMessage(
+ "\r". sprintf( "%-50s%s", " ... updating document no.", sprintf( "%s (%1.0f%%)", $current_id, 100 ) )
+ );
+ }
+
+ $this->write_to_file( $id );
+
+ $this->reportMessage( "\n ... $this->rebuildCount IDs checked or refreshed ..." );
+ $this->reportMessage( "\n ... $skipped_update IDs skipped ..." );
+ $this->reportMessage( "\n ... done.\n" );
+
+ if ( $this->options->has( 'ignore-exceptions' ) && $this->exceptionFileLogger->getExceptionCount() > 0 ) {
+ $this->exceptionCount += $this->exceptionFileLogger->getExceptionCount();
+ $this->exceptionFileLogger->doWrite();
+
+ $path_parts = pathinfo(
+ str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, $this->exceptionFileLogger->getExceptionFile() )
+ );
+
+ $this->reportMessage( "\nException log ..." );
+ $this->reportMessage( "\n ... counted $this->exceptionCount exceptions" );
+ $this->reportMessage( "\n ... written to ... " . $path_parts['basename'] );
+ $this->reportMessage( "\n ... done.\n" );
+ }
+
+ return true;
+ }
+
+ private function do_update( &$id ) {
+
+ if ( !$this->options->has( 'ignore-exceptions' ) ) {
+ $this->entityRebuildDispatcher->rebuild( $id );
+ } else {
+
+ try {
+ $this->entityRebuildDispatcher->rebuild( $id );
+ } catch ( Exception $e ) {
+ $this->exceptionFileLogger->recordException( $id, $e );
+ }
+ }
+
+ if ( $this->delay !== false ) {
+ usleep( $this->delay );
+ }
+
+ if ( $this->rebuildCount % 100 === 0 ) { // every 100 pages only
+ LinkCache::singleton()->clear(); // avoid memory leaks
+ }
+
+ $this->rebuildCount++;
+ }
+
+ private function getHumanReadableTextFrom( $id, array $entities ) {
+
+ if ( !$this->options->has( 'v' ) ) {
+ return [ '', ''];
+ }
+
+ // Indicates whether this is a MW page (*) or SMW's object table
+ $text = $id . ( isset( $entities['t'] ) ? '*' : ' ' );
+
+ $entity = end( $entities );
+
+ if ( $entity instanceof \Title ) {
+ return [ $text, '[' . $entity->getPrefixedDBKey() .']' ];
+ }
+
+ if ( $entity instanceof DIWikiPage ) {
+ return [ $text, '[' . $entity->getHash() .']' ];
+ }
+
+ return [ $text, '[' . ( is_string( $entity ) && $entity !== '' ? $entity : 'N/A' ) . ']' ];
+ }
+
+ private function performFullDelete() {
+
+ $this->reportMessage(
+ "Deleting all stored data completely and rebuilding it again later!\n\n" .
+ "Semantic data in the wiki might be incomplete for some time while\n".
+ "this operation runs.\n\n" .
+ "NOTE: It is usually necessary to run this script ONE MORE TIME\n".
+ "after this operation, given that some properties and types are not\n" .
+ "yet stored with the first run.\n\n"
+ );
+
+ if ( $this->options->has( 's' ) || $this->options->has( 'e' ) ) {
+ $this->reportMessage(
+ "WARNING: -s or -e are used, so some pages will not be refreshed at all!\n" .
+ "Data for those pages will only be available again when they have been\n" .
+ "refreshed as well!\n\n"
+ );
+ }
+
+ $obLevel = ob_get_level();
+
+ $this->reportMessage( 'Abort with control-c in the next five seconds ... ' );
+ swfCountDown( 6 );
+
+ $this->reportMessage( "\nDeleting all data ..." );
+
+ $this->reportMessage( "\n ... dropping tables ..." );
+ $this->store->drop( $this->verbose );
+
+ $this->reportMessage( "\n ... creating tables ..." );
+ $this->store->setupStore( $this->verbose );
+
+ $this->reportMessage( "\n ... done.\n" );
+
+ // Be sure to have some buffer, otherwise some PHPs complain
+ while ( ob_get_level() > $obLevel ) {
+ ob_end_flush();
+ }
+
+ $this->reportMessage( "\nAll storage structures have been deleted and recreated.\n\n" );
+
+ return true;
+ }
+
+ private function dispose_outdated() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $entityIdDisposerJob = $applicationFactory->newJobFactory()->newEntityIdDisposerJob(
+ Title::newFromText( __METHOD__ )
+ );
+
+ $outdatedEntitiesResultIterator = $entityIdDisposerJob->newOutdatedEntitiesResultIterator();
+ $matchesCount = $outdatedEntitiesResultIterator->count();
+ $counter = 0;
+
+ $this->reportMessage( "Removing outdated entities ..." );
+
+ if ( $matchesCount > 0 ) {
+ $this->reportMessage( "\n" );
+
+ $chunkedIterator = $applicationFactory->getIteratorFactory()->newChunkedIterator(
+ $outdatedEntitiesResultIterator,
+ 200
+ );
+
+ foreach ( $chunkedIterator as $chunk ) {
+ foreach ( $chunk as $row ) {
+ $counter++;
+ $msg = sprintf( "%s (%1.0f%%)", $row->smw_id, round( $counter / $matchesCount * 100 ) );
+
+ $this->reportMessage(
+ "\r". sprintf( "%-50s%s", " ... cleaning up document no.", $msg )
+ );
+
+ $entityIdDisposerJob->dispose( $row );
+ }
+ }
+
+ $this->reportMessage( "\n ... {$matchesCount} IDs removed ..." );
+ }
+
+ $this->reportMessage( "\n ... done.\n" );
+ }
+
+ private function is_writable( $startIdFile ) {
+
+ if ( !is_writable( file_exists( $startIdFile ) ? $startIdFile : dirname( $startIdFile ) ) ) {
+ die( "Cannot use a startidfile that we can't write to.\n" );
+ }
+
+ return true;
+ }
+
+ private function write_to_file( $id ) {
+ if ( $this->canWriteToIdFile ) {
+ file_put_contents( $this->startIdFile, "$id" );
+ }
+ }
+
+ /**
+ * @param array $options
+ */
+ private function setFiltersFromOptions( Options $options ) {
+ $this->filters = [];
+
+ if ( $options->has( 'categories' ) ) {
+ $this->filters[] = NS_CATEGORY;
+ }
+
+ if ( $options->has( 'p' ) ) {
+ $this->filters[] = SMW_NS_PROPERTY;
+ }
+ }
+
+ private function reportMessage( $message, $output = true ) {
+ if ( $output ) {
+ $this->reporter->reportMessage( $message );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DistinctEntityDataRebuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DistinctEntityDataRebuilder.php
new file mode 100644
index 00000000..a7c64c84
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DistinctEntityDataRebuilder.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use Exception;
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\MessageReporterFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Jobs\UpdateJob;
+use SMW\MediaWiki\TitleFactory;
+use SMW\MediaWiki\TitleLookup;
+use SMW\ApplicationFactory;
+use SMW\Options;
+use SMW\Store;
+use SMWQueryProcessor;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class DistinctEntityDataRebuilder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var TitleFactory
+ */
+ private $titleFactory;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var MessageReporter
+ */
+ private $reporter;
+
+ /**
+ * @var ExceptionFileLogger
+ */
+ private $exceptionFileLogger;
+
+ /**
+ * @var array
+ */
+ private $filters = [];
+
+ /**
+ * @var integer
+ */
+ private $rebuildCount = 0;
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ * @param TitleFactory $titleFactory
+ */
+ public function __construct( Store $store, TitleFactory $titleFactory ) {
+ $this->store = $store;
+ $this->titleFactory = $titleFactory;
+ $this->reporter = MessageReporterFactory::getInstance()->newNullMessageReporter();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param MessageReporter $reporter
+ */
+ public function setOptions( Options $options ) {
+ $this->options = $options;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param MessageReporter $reporter
+ */
+ public function setMessageReporter( MessageReporter $reporter ) {
+ $this->reporter = $reporter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ExceptionFileLogger $exceptionFileLogger
+ */
+ public function setExceptionFileLogger( ExceptionFileLogger $exceptionFileLogger ) {
+ $this->exceptionFileLogger = $exceptionFileLogger;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return int
+ */
+ public function getRebuildCount() {
+ return $this->rebuildCount;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return boolean
+ */
+ public function doRebuild() {
+
+ $type = ( $this->options->has( 'redirects' ) ? 'redirect' : '' ) .
+ ( $this->options->has( 'categories' ) ? 'category' : '' ) .
+ ( $this->options->has( 'query' ) ? 'query (' . $this->options->get( 'query' ) .')' : '' ) .
+ ( $this->options->has( 'p' ) ? 'property' : '' );
+
+ $pages = [];
+ $this->findFilters();
+
+ if ( $this->options->has( 'page' ) ) {
+ $pages = explode( '|', $this->options->get( 'page' ) );
+ }
+
+ $pages = $this->normalize(
+ [
+ $this->getPagesFromQuery(),
+ $pages,
+ $this->getPagesFromFilters(),
+ $this->getRedirectPages()
+ ]
+ );
+
+ $total = count( $pages );
+ $this->reportMessage( "Rebuilding $type pages ...\n" );
+ $this->reportMessage( " ... selecting $total pages ...\n" );
+
+ $jobFactory = ApplicationFactory::getInstance()->newJobFactory();
+
+ foreach ( $pages as $key => $page ) {
+
+ $this->rebuildCount++;
+ $progress = round( ( $this->rebuildCount / $total ) * 100 );
+
+ if ( !$this->options->has( 'v' ) ) {
+ $this->reportMessage(
+ "\r". sprintf( "%-50s%s", " ... updating document no.", sprintf( "%s (%1.0f%%)", $this->rebuildCount, $progress ) )
+ );
+ } else {
+ $this->reportMessage(
+ sprintf( "%-16s%s\n", " ... ($this->rebuildCount/$total $progress)", "Page " . $key ),
+ $this->options->has( 'v' )
+ );
+ }
+
+ $this->doUpdate( $jobFactory, $page );
+ }
+
+ $this->reportMessage( ( $this->options->has( 'v' ) ? "" : "\n" ) . " ... done.\n" );
+
+ return true;
+ }
+
+ private function doUpdate( $jobFactory, $page ) {
+
+ $updatejob = $jobFactory->newUpdateJob(
+ $page,
+ [
+ UpdateJob::FORCED_UPDATE => true,
+ 'shallowUpdate' => $this->options->has( 'shallow-update' )
+ ]
+ );
+
+ if ( !$this->options->has( 'ignore-exceptions' ) ) {
+ return $updatejob->run();
+ }
+
+ try {
+ $updatejob->run();
+ } catch ( Exception $e ) {
+ $this->exceptionFileLogger->recordException( $page->getPrefixedDBkey(), $e );
+ }
+ }
+
+ private function findFilters() {
+ $this->filters = [];
+
+ if ( $this->options->has( 'categories' ) ) {
+ $this->filters[] = NS_CATEGORY;
+ }
+
+ if ( $this->options->has( 'p' ) ) {
+ $this->filters[] = SMW_NS_PROPERTY;
+ }
+ }
+
+ private function hasFilters() {
+ return $this->filters !== [];
+ }
+
+ private function getPagesFromQuery() {
+
+ if ( !$this->options->has( 'query' ) ) {
+ return [];
+ }
+
+ $queryString = $this->options->get( 'query' );
+
+ // get number of pages and fix query limit
+ $query = SMWQueryProcessor::createQuery(
+ $queryString,
+ SMWQueryProcessor::getProcessedParams( [ 'format' => 'count' ] )
+ );
+
+ $result = $this->store->getQueryResult( $query );
+
+ // get pages and add them to the pages explicitly listed in the 'page' parameter
+ $query = SMWQueryProcessor::createQuery(
+ $queryString,
+ SMWQueryProcessor::getProcessedParams( [] )
+ );
+
+ $query->setUnboundLimit( $result instanceof \SMWQueryResult ? $result->getCountValue() : $result );
+
+ return $this->store->getQueryResult( $query )->getResults();
+ }
+
+ private function getPagesFromFilters() {
+
+ $pages = [];
+
+ if ( !$this->hasFilters() ) {
+ return $pages;
+ }
+
+ $titleLookup = new TitleLookup( $this->store->getConnection( 'mw.db' ) );
+
+ foreach ( $this->filters as $namespace ) {
+ $pages = array_merge( $pages, $titleLookup->setNamespace( $namespace )->selectAll() );
+ }
+
+ return $pages;
+ }
+
+ private function getRedirectPages() {
+
+ if ( !$this->options->has( 'redirects' ) ) {
+ return [];
+ }
+
+ $titleLookup = new TitleLookup(
+ $this->store->getConnection( 'mw.db' )
+ );
+
+ return $titleLookup->getRedirectPages();
+ }
+
+ private function normalize( $list ) {
+
+ $titleCache = [];
+ $p = [];
+
+ foreach ( $list as $pages ) {
+ foreach ( $pages as $key => $page ) {
+
+ if ( $page instanceof DIWikiPage ) {
+ $page = $page->getTitle();
+ }
+
+ if ( !$page instanceof Title ) {
+ $page = $this->titleFactory->newFromText( $page );
+ }
+
+ $id = $page->getPrefixedDBkey();
+
+ if ( !isset( $p[$id] ) ) {
+ $p[$id] = $page;
+ }
+ }
+ }
+
+ return $p;
+ }
+
+ private function reportMessage( $message, $output = true ) {
+ if ( $output ) {
+ $this->reporter->reportMessage( $message );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DuplicateEntitiesDisposer.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DuplicateEntitiesDisposer.php
new file mode 100644
index 00000000..33afcb1e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/DuplicateEntitiesDisposer.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use Onoi\Cache\Cache;
+use Onoi\MessageReporter\MessageReporterAwareTrait;
+use SMW\SQLStore\PropertyTableIdReferenceDisposer;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DuplicateEntitiesDisposer {
+
+ use MessageReporterAwareTrait;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var Store
+ */
+ private $cache;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param Cache|null $cache
+ */
+ public function __construct( Store $store, Cache $cache = null ) {
+ $this->store = $store;
+ $this->cache = $cache;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function findDuplicates() {
+ return $this->store->getObjectIds()->findDuplicates();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Iterator|array $duplicates
+ */
+ public function verifyAndDispose( $duplicates ) {
+
+ if ( !$this->is_iterable( $duplicates ) ) {
+ return;
+ }
+
+ $count = count( $duplicates );
+ $this->messageReporter->reportMessage( "Found: $count duplicates\n" );
+
+ if ( $count > 0 ) {
+ $this->doDispose( $duplicates );
+ }
+
+ if ( $this->cache !== null ) {
+ $this->cache->delete( \SMW\MediaWiki\Api\Task::makeCacheKey( 'duplookup' ) );
+ }
+ }
+
+ private function doDispose( $duplicates ) {
+
+ $propertyTableIdReferenceDisposer = new PropertyTableIdReferenceDisposer(
+ $this->store
+ );
+
+ $propertyTableIdReferenceDisposer->setRedirectRemoval( true );
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $log = [
+ 'disposed' => [],
+ 'untouched' => []
+ ];
+
+ $i = 0;
+ foreach ( $duplicates as $duplicate ) {
+ unset( $duplicate['count'] );
+
+ if ( ( $i ) % 60 === 0 ) {
+ $this->messageReporter->reportMessage( "\n" );
+ }
+
+ $this->messageReporter->reportMessage( '.' );
+
+ $res = $connection->select(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_id',
+ ],
+ [
+ 'smw_title'=> $duplicate['smw_title'],
+ 'smw_namespace'=> $duplicate['smw_namespace'],
+ 'smw_iw'=> $duplicate['smw_iw'],
+ 'smw_subobject'=> $duplicate['smw_subobject']
+ ],
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ if ( $propertyTableIdReferenceDisposer->isDisposable( $row->smw_id ) ) {
+ $propertyTableIdReferenceDisposer->cleanUpTableEntriesById( $row->smw_id );
+ $log['disposed'][$row->smw_id] = $duplicate;
+ } else {
+ $log['untouched'][$row->smw_id] = $duplicate;
+ }
+ }
+
+ $i++;
+ }
+
+ $this->messageReporter->reportMessage(
+ "\n\nLog\n\n" . json_encode( $log, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . "\n"
+ );
+ }
+
+ /**
+ * Polyfill for PHP 7.0-
+ *
+ * @see http://php.net/manual/en/function.is-iterable.php
+ *
+ * @since 3.0
+ */
+ private function is_iterable( $obj ) {
+ return is_array( $obj ) || ( is_object( $obj ) && ( $obj instanceof \Traversable ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/ExceptionFileLogger.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/ExceptionFileLogger.php
new file mode 100644
index 00000000..4530e49f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/ExceptionFileLogger.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use Exception;
+use SMW\Options;
+use SMW\Utils\File;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class ExceptionFileLogger {
+
+ /**
+ * @var string
+ */
+ private $namespace;
+
+ /**
+ * @var File
+ */
+ private $file;
+
+ /**
+ * @var string
+ */
+ private $exceptionFile;
+
+ /**
+ * @var integer
+ */
+ private $exceptionCount = 0;
+
+ /**
+ * @var array
+ */
+ private $exceptionLogMessages = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param string $namespace
+ * @param File|null $file
+ */
+ public function __construct( $namespace = 'smw', File $file = null ) {
+ $this->namespace = $namespace;
+ $this->file = $file;
+
+ if ( $this->file === null ) {
+ $this->file = new File();
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Options $options
+ */
+ public function setOptions( Options $options ) {
+
+ $dateTimeUtc = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) );
+ $this->exceptionFile = __DIR__ . "../../../";
+
+ if ( $options->has( 'exception-log' ) ) {
+ $this->exceptionFile = $options->get( 'exception-log' );
+ }
+
+ $this->exceptionFile .= $this->namespace . "-exceptions-" . $dateTimeUtc->format( 'Y-m-d' ) . ".log";
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ public function getExceptionFile() {
+ return realpath( $this->exceptionFile );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return integer
+ */
+ public function getExceptionCount() {
+ return $this->exceptionCount;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $id
+ * @param Exception $exception
+ */
+ public function recordException( $id, Exception $exception ) {
+ $this->exceptionCount++;
+
+ $this->exceptionLogMessages[$id] = [
+ 'msg' => $exception->getMessage(),
+ 'trace' => $exception->getTraceAsString()
+ ];
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function doWrite() {
+
+ foreach ( $this->exceptionLogMessages as $id => $exception ) {
+ $this->put( $id, $exception );
+ }
+
+ $this->exceptionLogMessages = [];
+ $this->exceptionCount = 0;
+ }
+
+ private function put( $id, $exception ) {
+
+ $text = "\n======== EXCEPTION ======\n" .
+ "$id | " . $exception['msg'] . "\n\n" .
+ $exception['trace'] . "\n" .
+ "======== END ======" ."\n";
+
+ $this->file->write( $this->exceptionFile, $text, FILE_APPEND );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceFactory.php
new file mode 100644
index 00000000..3cf4f2c8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceFactory.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use Onoi\MessageReporter\MessageReporterFactory;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\ManualEntryLogger;
+use SMW\SQLStore\PropertyStatisticsStore;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class MaintenanceFactory {
+
+ /**
+ * @since 2.2
+ *
+ * @return MaintenanceHelper
+ */
+ public function newMaintenanceHelper() {
+ return new MaintenanceHelper();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param Callable|null $reporterCallback
+ *
+ * @return DataRebuilder
+ */
+ public function newDataRebuilder( Store $store, $reporterCallback = null ) {
+
+ $messageReporter = $this->newMessageReporter( $reporterCallback );
+
+ $dataRebuilder = new DataRebuilder(
+ $store,
+ ApplicationFactory::getInstance()->newTitleFactory()
+ );
+
+ $dataRebuilder->setMessageReporter(
+ $messageReporter
+ );
+
+ return $dataRebuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param Callable|null $reporterCallback
+ *
+ * @return ConceptCacheRebuilder
+ */
+ public function newConceptCacheRebuilder( Store $store, $reporterCallback = null ) {
+
+ $conceptCacheRebuilder = new ConceptCacheRebuilder(
+ $store,
+ ApplicationFactory::getInstance()->getSettings()
+ );
+
+ $conceptCacheRebuilder->setMessageReporter(
+ $this->newMessageReporter( $reporterCallback )
+ );
+
+ return $conceptCacheRebuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param Callable|null $reporterCallback
+ *
+ * @return PropertyStatisticsRebuilder
+ */
+ public function newPropertyStatisticsRebuilder( Store $store, $reporterCallback = null ) {
+
+ $propertyStatisticsStore = new PropertyStatisticsStore(
+ $store->getConnection( 'mw.db' )
+ );
+
+ $propertyStatisticsRebuilder = new PropertyStatisticsRebuilder(
+ $store,
+ $propertyStatisticsStore
+ );
+
+ $propertyStatisticsRebuilder->setMessageReporter(
+ $this->newMessageReporter( $reporterCallback )
+ );
+
+ return $propertyStatisticsRebuilder;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return RebuildPropertyStatistics
+ */
+ public function newRebuildPropertyStatistics() {
+ return new RebuildPropertyStatistics();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return DuplicateEntitiesDisposer
+ */
+ public function newDuplicateEntitiesDisposer( Store $store, $reporterCallback = null ) {
+
+ $duplicateEntitiesDisposer = new DuplicateEntitiesDisposer(
+ $store,
+ ApplicationFactory::getInstance()->getCache()
+ );
+
+ $duplicateEntitiesDisposer->setMessageReporter(
+ $this->newMessageReporter( $reporterCallback )
+ );
+
+ return $duplicateEntitiesDisposer;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $performer
+ *
+ * @return MaintenanceLogger
+ */
+ public function newMaintenanceLogger( $performer ) {
+
+ $maintenanceLogger = new MaintenanceLogger( $performer, new ManualEntryLogger() );
+ $maintenanceLogger->setMaxNameChars( $GLOBALS['wgMaxNameChars'] );
+
+ return $maintenanceLogger;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return MessageReporter
+ */
+ public function newMessageReporter( $reporterCallback = null ) {
+
+ $messageReporter = MessageReporterFactory::getInstance()->newObservableMessageReporter();
+ $messageReporter->registerReporterCallback( $reporterCallback );
+
+ return $messageReporter;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceHelper.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceHelper.php
new file mode 100644
index 00000000..1b2de4a2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceHelper.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use SMW\ApplicationFactory;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class MaintenanceHelper {
+
+ /**
+ * @var array
+ */
+ private $globals = [];
+
+ /**
+ * @var array
+ */
+ private $runtime = [
+ 'start' => 0,
+ 'memory' => 0
+ ];
+
+ /**
+ * @since 2.2
+ */
+ public function initRuntimeValues() {
+ $this->runtime['start'] = microtime( true );
+ $this->runtime['memory'] = memory_get_peak_usage( false );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getRuntimeValues() {
+
+ $memory = memory_get_peak_usage( false );
+ $time = microtime( true ) - $this->runtime['start'];
+
+ $hTime = round( $time, 2 ) . ' sec';
+ $hTime .= ( $time > 60 ? ' (' . round( $time / 60, 2 ) . ' min)' : '' );
+
+ return [
+ 'time' => $time,
+ 'humanreadable-time' => $hTime,
+ 'memory-before' => $this->runtime['memory'],
+ 'memory-after' => $memory,
+ 'memory-used' => $memory - $this->runtime['memory']
+ ];
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getFormattedRuntimeValues( $indent = '' ) {
+
+ $runtimeValues = $this->getRuntimeValues();
+
+ return "$indent Memory used: " . $runtimeValues['memory-used'] . "\n" .
+ "$indent Time: " . $runtimeValues['humanreadable-time'];
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function setGlobalToValue( $key, $value ) {
+
+ if ( !isset( $GLOBALS[$key] ) ) {
+ return;
+ }
+
+ $this->globals[$key] = $GLOBALS[$key];
+ $GLOBALS[$key] = $value;
+ ApplicationFactory::getInstance()->getSettings()->set( $key, $value );
+ }
+
+ /**
+ * @since 2.2
+ */
+ public function reset() {
+
+ foreach ( $this->globals as $key => $value ) {
+ $GLOBALS[$key] = $value;
+ ApplicationFactory::getInstance()->getSettings()->set( $key, $value );
+ }
+
+ $this->runtime['start'] = 0;
+ $this->runtime['memory'] = 0;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceLogger.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceLogger.php
new file mode 100644
index 00000000..7255fb45
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/MaintenanceLogger.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use RuntimeException;
+use SMW\MediaWiki\ManualEntryLogger;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class MaintenanceLogger {
+
+ /**
+ * @var string
+ */
+ private $performer = '';
+
+ /**
+ * @var ManualEntryLogger
+ */
+ private $manualEntryLogger;
+
+ /**
+ * @var integer
+ */
+ private $maxNameChars = 255;
+
+ /**
+ * @since 2.4
+ *
+ * @param string $performer
+ * @param ManualEntryLogger $manualEntryLogger
+ */
+ public function __construct( $performer, ManualEntryLogger $manualEntryLogger ) {
+ $this->performer = $performer;
+ $this->manualEntryLogger = $manualEntryLogger;
+ $this->manualEntryLogger->registerLoggableEventType( 'maintenance' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $maxNameChars
+ */
+ public function setMaxNameChars( $maxNameChars ) {
+ $this->maxNameChars = $maxNameChars;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $message
+ * @param string $target
+ */
+ public function log( $message, $target = '' ) {
+
+ if ( $target === '' ) {
+ $target = $this->performer;
+ }
+
+ // #1983
+ if ( $this->maxNameChars < strlen( $target ) ) {
+ throw new RuntimeException( 'wgMaxNameChars requires at least ' . strlen( $target ) );
+ }
+
+ $this->manualEntryLogger->log( 'maintenance', $this->performer, $target, $message );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/PropertyStatisticsRebuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/PropertyStatisticsRebuilder.php
new file mode 100644
index 00000000..9ee99fa1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Maintenance/PropertyStatisticsRebuilder.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace SMW\Maintenance;
+
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\MessageReporterFactory;
+use SMW\SQLStore\PropertyStatisticsStore;
+use SMW\Store;
+
+/**
+ * Simple class for rebuilding property usage statistics.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author Nischay Nahata
+ */
+class PropertyStatisticsRebuilder {
+
+ /**
+ * @var Store
+ */
+ private $store = null;
+
+ /**
+ * @var PropertyStatisticsStore
+ */
+ private $propertyStatisticsStore;
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @since 1.9
+ *
+ * @param Store $store
+ * @param PropertyStatisticsStore $propertyStatisticsStore
+ */
+ public function __construct( Store $store, PropertyStatisticsStore $propertyStatisticsStore ) {
+ $this->store = $store;
+ $this->propertyStatisticsStore = $propertyStatisticsStore;
+ $this->messageReporter = MessageReporterFactory::getInstance()->newNullMessageReporter();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param MessageReporter $messageReporter
+ */
+ public function setMessageReporter( MessageReporter $messageReporter ) {
+ $this->messageReporter = $messageReporter;
+ }
+
+ /**
+ * @since 1.9
+ */
+ public function rebuild() {
+ $this->reportMessage( "\nRebulding property statistics (this may take a while) ..." );
+ $table = $this->propertyStatisticsStore->getStatisticsTable();
+
+ $this->reportMessage( "\n ... deleting `$table` content ..." );
+ $this->propertyStatisticsStore->deleteAll();
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $res = $connection->select(
+ \SMWSql3SmwIds::TABLE_NAME,
+ [ 'smw_id', 'smw_title' ],
+ [
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_subobject' => ''
+ ],
+ __METHOD__
+ );
+
+ $propCount = $res->numRows();
+ $this->reportMessage( "\n ... selecting $propCount properties ...\n" );
+
+ $i = 0;
+
+ foreach ( $res as $row ) {
+
+ $i++;
+
+ $this->reportMessage(
+ "\r". sprintf( "%-47s%s", " ... updating", sprintf( "%4.0f%% (%s/%s)", round( ( $i / $propCount ) * 100 ), $i, $propCount ) )
+ );
+
+ $this->propertyStatisticsStore->insertUsageCount(
+ (int)$row->smw_id,
+ $this->getCountFormRow( $row )
+ );
+ }
+
+ $connection->freeResult( $res );
+
+ $this->reportMessage( "\n ... done.\n" );
+ }
+
+ private function getCountFormRow( $row ) {
+
+ $usageCount = 0;
+ $nullCount = 0;
+
+ foreach ( $this->store->getPropertyTables() as $propertyTable ) {
+
+ if ( $propertyTable->isFixedPropertyTable() && $propertyTable->getFixedProperty() !== $row->smw_title ) {
+ // This table cannot store values for this property
+ continue;
+ }
+
+ list( $uCount, $nCount ) = $this->getPropertyTableRowCount(
+ $propertyTable,
+ $row->smw_id
+ );
+
+ $usageCount += $uCount;
+ $nullCount += $nCount;
+ }
+
+ return [ $usageCount, $nullCount ];
+ }
+
+ private function getPropertyTableRowCount( $propertyTable, $pid ) {
+
+ $condition = [];
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ if ( !$propertyTable->isFixedPropertyTable() ) {
+ $condition = [ 'p_id' => $pid ];
+ }
+
+ $tableFields = $this->store->getDataItemHandlerForDIType( $propertyTable->getDiType() )->getTableFields();
+ $tableName = $propertyTable->getName();
+
+ $usageCount = 0;
+ $nullCount = 0;
+
+ // Select all (incl. NULL since for example blob table can have a null
+ // for when only the hash field is used, substract NULL in a second step)
+ $row = $connection->selectRow(
+ $tableName,
+ 'Count(*) as count',
+ $condition,
+ __METHOD__
+ );
+
+ if ( $row !== false ) {
+ $usageCount = $row->count;
+ }
+
+ // Select only those that match NULL for all fields
+ foreach ( $tableFields as $field => $type ) {
+ $condition[] = "$field IS NULL";
+ }
+
+ $nRow = $connection->selectRow(
+ $tableName,
+ 'Count(*) as count',
+ $condition,
+ __METHOD__
+ );
+
+ if ( $nRow !== false ) {
+ $nullCount = $nRow->count;
+ }
+
+ if ( $usageCount > 0 ) {
+ $usageCount = $usageCount - $nullCount;
+ }
+
+ return [ $usageCount, $nullCount ];
+ }
+
+ private function progress( $propCount, $i ) {
+
+ if ( $i % 60 === 0 ) {
+ if ( $i < 1 ) {
+ return "\n";
+ }
+
+ return ' ' . round( ( $i / $propCount ) * 100 ) . ' %' . "\n";
+ }
+
+ return '.';
+ }
+
+ protected function reportMessage( $message ) {
+ $this->messageReporter->reportMessage( $message );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/ApiQueryResultFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/ApiQueryResultFormatter.php
new file mode 100644
index 00000000..684499a3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/ApiQueryResultFormatter.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use InvalidArgumentException;
+use SMW\ProcessingErrorMsgHandler;
+use SMWQueryResult;
+
+/**
+ * This class handles the Api related query result formatting
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class ApiQueryResultFormatter {
+
+ /**
+ * @var Integer|boolean
+ */
+ protected $continueOffset = false;
+
+ /**
+ * @var String
+ */
+ protected $type;
+
+ /**
+ * @var Boolean
+ */
+ protected $isRawMode = false;
+
+ /**
+ * @since 1.9
+ *
+ * @param SMWQueryResult $queryResult
+ */
+ public function __construct( SMWQueryResult $queryResult ) {
+ $this->queryResult = $queryResult;
+ }
+
+ /**
+ * Sets whether the formatter requested raw data and is used in connection
+ * with ApiQueryResultFormatter::setIndexedTagName
+ *
+ * @see ApiResult::getIsRawMode
+ *
+ * @since 1.9
+ *
+ * @param boolean $isRawMode
+ */
+ public function setIsRawMode( $isRawMode ) {
+ $this->isRawMode = $isRawMode;
+ }
+
+ /**
+ * Returns an offset used for continuation support
+ *
+ * @since 1.9
+ *
+ * @return integer
+ */
+ public function getContinueOffset() {
+ return $this->continueOffset;
+ }
+
+ /**
+ * Returns the result type
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * Returns formatted result
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ public function getResult() {
+ return $this->result;
+ }
+
+ /**
+ * Result formatting
+ *
+ * @since 1.9
+ */
+ public function doFormat() {
+
+ if ( $this->queryResult->getErrors() !== [] ) {
+ $this->result = $this->formatErrors(
+ ProcessingErrorMsgHandler::normalizeAndDecodeMessages( $this->queryResult->getErrors() )
+ );
+ } else {
+ $this->result = $this->formatResults( $this->queryResult->toArray() );
+
+ if ( $this->queryResult->hasFurtherResults() ) {
+ $this->continueOffset = $this->result['meta']['count'] + $this->result['meta']['offset'];
+ }
+ }
+ }
+
+ /**
+ * Formatting a result array to support JSON/XML standards
+ *
+ * @since 1.9
+ *
+ * @param array $queryResult
+ *
+ * @return array
+ */
+ protected function formatResults( array $queryResult ) {
+
+ $this->type = 'query';
+ $results = [];
+
+ if ( !$this->isRawMode ) {
+ return $queryResult;
+ }
+
+ foreach ( $queryResult['results'] as $subjectName => $subject ) {
+ $serialized = [];
+
+ foreach ( $subject as $key => $value ) {
+
+ if ( $key === 'printouts' ) {
+ $printouts = [];
+
+ foreach ( $subject['printouts'] as $property => $values ) {
+
+ if ( (array)$values === $values ) {
+ $this->setIndexedTagName( $values, 'value' );
+ $printouts[] = array_merge( [ 'label' => $property ], $values );
+ }
+
+ }
+
+ $serialized['printouts'] = $printouts;
+ $this->setIndexedTagName( $serialized['printouts'], 'property' );
+
+ } else {
+ $serialized[$key] = $value;
+ }
+ }
+
+ $results[] = $serialized;
+ }
+
+ if ( $results !== [] ) {
+ $queryResult['results'] = $results;
+ $this->setIndexedTagName( $queryResult['results'], 'subject' );
+ }
+
+ $this->setIndexedTagName( $queryResult['printrequests'], 'printrequest' );
+ $this->setIndexedTagName( $queryResult['meta'], 'meta' );
+
+ return $queryResult;
+ }
+
+ /**
+ * Formatting an error array in order to support JSON/XML
+ *
+ * @since 1.9
+ *
+ * @param array $errors
+ *
+ * @return array
+ */
+ protected function formatErrors( array $errors ) {
+
+ $this->type = 'error';
+ $result['query'] = $errors;
+
+ $this->setIndexedTagName( $result['query'], 'info' );
+
+ return $result;
+ }
+
+ /**
+ * Add '_element' to an array
+ *
+ * @note Copied from ApiResult::setIndexedTagName to avoid having a
+ * constructor injection in order to be able to access this method
+ *
+ * @see ApiResult::setIndexedTagName
+ *
+ * @since 1.9
+ *
+ * @param array &$arr
+ * @param string $tag
+ */
+ public function setIndexedTagName( &$arr, $tag = null ) {
+
+ if ( !$this->isRawMode ) {
+ return;
+ }
+
+ if ( $arr === null || $tag === null || !is_array( $arr ) || is_array( $tag ) ) {
+ throw new InvalidArgumentException( "{$tag} was incompatible with the requirements" );
+ }
+
+ $arr['_element'] = $tag;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/ApiRequestParameterFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/ApiRequestParameterFormatter.php
new file mode 100644
index 00000000..7dd1165a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/ApiRequestParameterFormatter.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use SMW\DataValueFactory;
+use SMW\Options;
+use SMW\Query\PrintRequest;
+
+/**
+ * This class handles Api related request parameter formatting
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+final class ApiRequestParameterFormatter {
+
+ /**
+ * @var array
+ */
+ protected $requestParameters = [];
+
+ /**
+ * @var ObjectDictionary
+ */
+ protected $results = null;
+
+ /**
+ * @since 1.9
+ *
+ * @param array $requestParameters
+ */
+ public function __construct( array $requestParameters ) {
+ $this->requestParameters = $requestParameters;
+ }
+
+ /**
+ * Return formatted request parameters for the AskApi
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ public function getAskApiParameters() {
+
+ if ( $this->results === null ) {
+ $this->results = isset( $this->requestParameters['query'] ) ? preg_split( "/(?<=[^\|])\|(?=[^\|])/", $this->requestParameters['query'] ) : [];
+ }
+
+ return $this->results;
+ }
+
+ /**
+ * Return formatted request parameters AskArgsApi
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ public function getAskArgsApiParameter( $key ) {
+
+ if ( $this->results === null ) {
+ $this->results = $this->formatAskArgs();
+ }
+
+ return $this->results->get( $key );
+ }
+
+ /**
+ * Return formatted request parameters
+ *
+ * @since 1.9
+ *
+ * @return ObjectDictionary
+ */
+ protected function formatAskArgs() {
+
+ $result = new Options();
+
+ // Set defaults
+ $result->set( 'conditions', [] );
+ $result->set( 'printouts', [] );
+ $result->set( 'parameters', [] );
+
+ if ( isset( $this->requestParameters['parameters'] ) && is_array( $this->requestParameters['parameters'] ) ) {
+ $result->set( 'parameters', $this->formatParameters() );
+ }
+
+ if ( isset( $this->requestParameters['conditions'] ) && is_array( $this->requestParameters['conditions'] ) ) {
+ $result->set( 'conditions', implode( ' ', array_map( 'self::formatConditions', $this->requestParameters['conditions'] ) ) );
+ }
+
+ if ( isset( $this->requestParameters['printouts'] ) && is_array( $this->requestParameters['printouts'] ) ) {
+ $result->set( 'printouts', array_map( 'self::formatPrintouts', $this->requestParameters['printouts'] ) );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Format parameters
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ protected function formatParameters() {
+
+ $parameters = [];
+
+ foreach ( $this->requestParameters['parameters'] as $param ) {
+ $parts = explode( '=', $param, 2 );
+
+ if ( count( $parts ) == 2 ) {
+ $parameters[$parts[0]] = $parts[1];
+ }
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * Format conditions
+ *
+ * @since 1.9
+ *
+ * @param string $condition
+ *
+ * @return string
+ */
+ protected function formatConditions( $condition ) {
+ return "[[$condition]]";
+ }
+
+ /**
+ * Format printout and returns a SMWPrintRequest object
+ *
+ * @since 1.9
+ *
+ * @param string $printout
+ *
+ * @return PrintRequest
+ */
+ protected function formatPrintouts( $printout ) {
+ return new PrintRequest(
+ PrintRequest::PRINT_PROP,
+ $printout,
+ DataValueFactory::getInstance()->newPropertyValueByLabel( $printout )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Ask.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Ask.php
new file mode 100644
index 00000000..b67d64d8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Ask.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use ApiBase;
+use SMWQueryProcessor;
+
+/**
+ * API module to query SMW by providing a query in the ask language.
+ *
+ * @ingroup Api
+ *
+ * @license GNU GPL v2+
+ * @since 1.6.2
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author mwjames
+ */
+class Ask extends Query {
+
+ /**
+ * @see ApiBase::execute
+ */
+ public function execute() {
+
+ $params = $this->extractRequestParams();
+
+ $parameterFormatter = new ApiRequestParameterFormatter( $this->extractRequestParams() );
+ $outputFormat = 'json';
+
+ list( $queryString, $parameters, $printouts ) = SMWQueryProcessor::getComponentsFromFunctionParams( $parameterFormatter->getAskApiParameters(), false );
+
+ $queryResult = $this->getQueryResult( $this->getQuery(
+ $queryString,
+ $printouts,
+ $parameters
+ ) );
+
+ if ( $this->getMain()->getPrinter() instanceof \ApiFormatXml ) {
+ $outputFormat = 'xml';
+ }
+
+ if ( isset( $params['api_version'] ) ) {
+ $queryResult->setSerializerVersion( $params['api_version'] );
+ }
+
+ $this->addQueryResult( $queryResult, $outputFormat );
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getAllowedParams
+ *
+ * @return array
+ */
+ public function getAllowedParams() {
+ return [
+ 'query' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'api_version' => [
+ ApiBase::PARAM_TYPE => [ 2, 3 ],
+ ApiBase::PARAM_DFLT => 2,
+ ApiBase::PARAM_HELP_MSG => 'apihelp-ask-parameter-api-version',
+ ],
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getParamDescription
+ *
+ * @return array
+ */
+ public function getParamDescription() {
+ return [
+ 'query' => 'The query string in ask-language'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getDescription
+ *
+ * @return array
+ */
+ public function getDescription() {
+ return [
+ 'API module to query SMW by providing a query in the ask language.'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getExamples
+ *
+ * @return array
+ */
+ protected function getExamples() {
+ return [
+ 'api.php?action=ask&query=[[Modification%20date::%2B]]|%3FModification%20date|sort%3DModification%20date|order%3Ddesc',
+ 'api.php?action=ask&query=[[Modification%20date::%2B]]|limit%3D5|offset%3D1'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getVersion
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return __CLASS__ . '-' . SMW_VERSION;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/AskArgs.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/AskArgs.php
new file mode 100644
index 00000000..e716694d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/AskArgs.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use ApiBase;
+
+/**
+ * API module to query SMW by providing a query specified as
+ * a list of conditions, printouts and parameters.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6.2
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class AskArgs extends Query {
+
+ /**
+ * @see ApiBase::execute
+ */
+ public function execute() {
+
+ $params = $this->extractRequestParams();
+
+ $parameterFormatter = new ApiRequestParameterFormatter( $this->extractRequestParams() );
+ $outputFormat = 'json';
+
+ $queryResult = $this->getQueryResult( $this->getQuery(
+ $parameterFormatter->getAskArgsApiParameter( 'conditions' ),
+ $parameterFormatter->getAskArgsApiParameter( 'printouts' ),
+ $parameterFormatter->getAskArgsApiParameter( 'parameters' )
+ ) );
+
+ if ( $this->getMain()->getPrinter() instanceof \ApiFormatXml ) {
+ $outputFormat = 'xml';
+ }
+
+ if ( isset( $params['api_version'] ) ) {
+ $queryResult->setSerializerVersion( $params['api_version'] );
+ }
+
+ $this->addQueryResult( $queryResult, $outputFormat );
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getAllowedParams
+ *
+ * @return array
+ */
+ public function getAllowedParams() {
+ return [
+ 'conditions' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'printouts' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '',
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'parameters' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => '',
+ ApiBase::PARAM_ISMULTI => true,
+ ],
+ 'api_version' => [
+ ApiBase::PARAM_TYPE => [ 2, 3 ],
+ ApiBase::PARAM_DFLT => 2,
+ ApiBase::PARAM_HELP_MSG => 'apihelp-ask-parameter-api-version',
+ ],
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getParamDescription
+ *
+ * @return array
+ */
+ public function getParamDescription() {
+ return [
+ 'conditions' => 'The query conditions, i.e. the requirements for a subject to be included',
+ 'printouts' => 'The query printouts, i.e. the properties to show per subject',
+ 'parameters' => 'The query parameters, i.e. all non-condition and non-printout arguments',
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getDescription
+ *
+ * @return array
+ */
+ public function getDescription() {
+ return [
+ 'API module to query SMW by providing a query specified as a list of conditions, printouts and parameters.'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getExamples
+ *
+ * @return array
+ */
+ protected function getExamples() {
+ return [
+ 'api.php?action=askargs&conditions=Modification%20date::%2B&printouts=Modification%20date&parameters=|sort%3DModification%20date|order%3Ddesc',
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getVersion
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return __CLASS__ . '-' . SMW_VERSION;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse.php
new file mode 100644
index 00000000..68d36817
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse.php
@@ -0,0 +1,396 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use ApiBase;
+use SMW\ApplicationFactory;
+use SMW\Exception\RedirectTargetUnresolvableException;
+use SMW\Exception\ParameterNotFoundException;
+use SMW\MediaWiki\Api\Browse\ArticleAugmentor;
+use SMW\MediaWiki\Api\Browse\ArticleLookup;
+use SMW\MediaWiki\Api\Browse\SubjectLookup;
+use SMW\MediaWiki\Api\Browse\CachingLookup;
+use SMW\MediaWiki\Api\Browse\ListAugmentor;
+use SMW\MediaWiki\Api\Browse\ListLookup;
+use SMW\MediaWiki\Api\Browse\PValueLookup;
+use SMW\MediaWiki\Api\Browse\PSubjectLookup;
+
+/**
+ * Module to support selected browse activties including:
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Browse extends ApiBase {
+
+ /**
+ * @see ApiBase::execute
+ */
+ public function execute() {
+
+ $params = $this->extractRequestParams();
+
+ $parameters = json_decode( $params['params'], true );
+ $res = [];
+
+ if ( json_last_error() !== JSON_ERROR_NONE || !is_array( $parameters ) ) {
+
+ // 1.29+
+ if ( method_exists($this, 'dieWithError' ) ) {
+ $this->dieWithError( [ 'smw-api-invalid-parameters', 'JSON: '. json_last_error_msg() ] );
+ } else {
+ $this->dieUsage( 'JSON: '. json_last_error_msg(), 'smw-api-invalid-parameters' );
+ }
+ }
+
+ if ( $params['browse'] === 'category' ) {
+ $res = $this->callListLookup( NS_CATEGORY, $parameters );
+ }
+
+ if ( $params['browse'] === 'property' ) {
+ $res = $this->callListLookup( SMW_NS_PROPERTY, $parameters );
+ }
+
+ if ( $params['browse'] === 'concept' ) {
+ $res = $this->callListLookup( SMW_NS_CONCEPT, $parameters );
+ }
+
+ if ( $params['browse'] === 'pvalue' ) {
+ $res = $this->callPValueLookup( $parameters );
+ }
+
+ if ( $params['browse'] === 'psubject' ) {
+ $res = $this->callPSubjectLookup( $parameters );
+ }
+
+ if ( $params['browse'] === 'subject' ) {
+ $res = $this->callSubjectLookup( $parameters );
+ }
+
+ if ( $params['browse'] === 'page' ) {
+ $res = $this->callPageLookup( $parameters );
+ }
+
+ $result = $this->getResult();
+
+ foreach ( $res as $key => $value ) {
+
+ if ( $key === 'query' && is_array( $value ) ) {
+
+ // For those items that start with _xyz as in _MDAT
+ // https://www.mediawiki.org/wiki/API:JSON_version_2
+ // " ... can indicate that a property beginning with an underscore ..."
+ foreach ( $value as $k => $v ) {
+ if ( $k{0} === '_' ) {
+ $result->addPreserveKeysList( 'query', $k );
+ }
+ }
+ }
+
+ $result->addValue( null, $key, $value );
+ }
+ }
+
+ private function callListLookup( $ns, $parameters ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $cacheUsage = $applicationFactory->getSettings()->get(
+ 'smwgCacheUsage'
+ );
+
+ $cacheTTL = CachingLookup::CACHE_TTL;
+
+ if ( isset( $cacheUsage['api.browse'] ) ) {
+ $cacheTTL = $cacheUsage['api.browse'];
+ }
+
+ $store = $applicationFactory->getStore();
+
+ // We explicitly want the SQLStore here to avoid
+ // "Call to undefined method SMW\SPARQLStore\SPARQLStore::getSQLOptions() ..."
+ // since we don't use those methods anywher else other than the SQLStore
+ if ( !is_a( $store, '\SMW\SQLStore\SQLStore') ) {
+ $store = $applicationFactory->getStore( '\SMW\SQLStore\SQLStore' );
+ }
+
+ $listLookup = new ListLookup(
+ $store,
+ new ListAugmentor( $store )
+ );
+
+ $cachingLookup = new CachingLookup(
+ $applicationFactory->getCache(),
+ $listLookup
+ );
+
+ $cachingLookup->setCacheTTL(
+ $cacheTTL
+ );
+
+ $parameters['ns'] = $ns;
+
+ return $cachingLookup->lookup(
+ $parameters
+ );
+ }
+
+ private function callPValueLookup( $parameters ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $cacheUsage = $applicationFactory->getSettings()->get(
+ 'smwgCacheUsage'
+ );
+
+ $cacheTTL = CachingLookup::CACHE_TTL;
+
+ if ( isset( $cacheUsage['api.browse.pvalue'] ) ) {
+ $cacheTTL = $cacheUsage['api.browse.pvalue'];
+ }
+
+ $store = $applicationFactory->getStore();
+
+ // We explicitly want the SQLStore here to avoid
+ // "Call to undefined method SMW\SPARQLStore\SPARQLStore::getSQLOptions() ..."
+ // since we don't use those methods anywher else other than the SQLStore
+ if ( !is_a( $store, '\SMW\SQLStore\SQLStore') ) {
+ $store = $applicationFactory->getStore( '\SMW\SQLStore\SQLStore' );
+ }
+
+ $listLookup = new PValueLookup(
+ $store
+ );
+
+ $cachingLookup = new CachingLookup(
+ $applicationFactory->getCache(),
+ $listLookup
+ );
+
+ $cachingLookup->setCacheTTL(
+ $cacheTTL
+ );
+
+ return $cachingLookup->lookup(
+ $parameters
+ );
+ }
+
+ private function callPSubjectLookup( $parameters ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $cacheUsage = $applicationFactory->getSettings()->get(
+ 'smwgCacheUsage'
+ );
+
+ $cacheTTL = CachingLookup::CACHE_TTL;
+
+ if ( isset( $cacheUsage['api.browse.psubject'] ) ) {
+ $cacheTTL = $cacheUsage['api.browse.psubject'];
+ }
+
+ $store = $applicationFactory->getStore();
+
+ // We explicitly want the SQLStore here to avoid
+ // "Call to undefined method SMW\SPARQLStore\SPARQLStore::getSQLOptions() ..."
+ // since we don't use those methods anywher else other than the SQLStore
+ if ( !is_a( $store, '\SMW\SQLStore\SQLStore') ) {
+ $store = $applicationFactory->getStore( '\SMW\SQLStore\SQLStore' );
+ }
+
+ $listLookup = new PSubjectLookup(
+ $store
+ );
+
+ $cachingLookup = new CachingLookup(
+ $applicationFactory->getCache(),
+ $listLookup
+ );
+
+ $cachingLookup->setCacheTTL(
+ $cacheTTL
+ );
+
+ return $cachingLookup->lookup(
+ $parameters
+ );
+ }
+
+ private function callPageLookup( $parameters ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $cacheUsage = $applicationFactory->getSettings()->get(
+ 'smwgCacheUsage'
+ );
+
+ $cacheTTL = CachingLookup::CACHE_TTL;
+
+ if ( isset( $cacheUsage['api.browse'] ) ) {
+ $cacheTTL = $cacheUsage['api.browse'];
+ }
+
+ $connection = $applicationFactory->getStore()->getConnection( 'mw.db' );
+
+ $articleLookup = new ArticleLookup(
+ $connection,
+ new ArticleAugmentor(
+ $applicationFactory->create( 'TitleFactory' )
+ )
+ );
+
+ $cachingLookup = new CachingLookup(
+ $applicationFactory->getCache(),
+ $articleLookup
+ );
+
+ $cachingLookup->setCacheTTL(
+ $cacheTTL
+ );
+
+ return $cachingLookup->lookup(
+ $parameters
+ );
+ }
+
+ private function callSubjectLookup( $parameters ) {
+
+ $subjectLookup = new SubjectLookup(
+ ApplicationFactory::getInstance()->getStore()
+ );
+
+ try {
+ $res = $subjectLookup->lookup( $parameters );
+ } catch ( RedirectTargetUnresolvableException $e ) {
+ // 1.29+
+ if ( method_exists( $this, 'dieWithError' ) ) {
+ $this->dieWithError( [ 'smw-redirect-target-unresolvable', $e->getMessage() ] );
+ } else {
+ $this->dieUsage( $e->getMessage(), 'redirect-target-unresolvable' );
+ }
+ } catch ( ParameterNotFoundException $e ) {
+ // 1.29+
+ if ( method_exists( $this, 'dieWithError' ) ) {
+ $this->dieWithError( [ 'smw-parameter-missing', $e->getName() ] );
+ } else {
+ $this->dieUsage( $e->getName(), 'smw-parameter-missing' );
+ }
+ }
+
+ return $res;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getAllowedParams
+ *
+ * @return array
+ */
+ public function getAllowedParams() {
+ return [
+ 'browse' => [
+ ApiBase::PARAM_REQUIRED => true,
+ ApiBase::PARAM_TYPE => [
+
+ // List, browse of properties
+ 'property',
+
+ // List, browse of categories
+ 'category',
+
+ // List, browse of concepts
+ 'concept',
+
+ // List, browse of articles, pages (mediawiki)
+ 'page',
+
+ // Equivalent to Store::getPropertyValues
+ 'pvalue',
+
+ // Equivalent to Store::getPropertySubjects
+ 'psubject',
+
+ // Equivalent to Special:Browse
+ 'subject',
+ ]
+ ],
+ 'params' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getParamDescription
+ *
+ * @return array
+ */
+ public function getParamDescription() {
+ return [
+ 'browse' => 'Specifies the type of browse activity',
+ 'params' => 'JSON encoded parameters containing required and optional fields and depend on the selected browse type'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getDescription
+ *
+ * @return array
+ */
+ public function getDescription() {
+ return [
+ 'API module to support browse activties for different entity types in Semantic MediaWiki.'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getExamples
+ *
+ * @return array
+ */
+ protected function getExamples() {
+ return [
+ 'api.php?action=smwbrowse&browse=property&params={ "limit": 10, "offset": 0, "search": "Date" }',
+ 'api.php?action=smwbrowse&browse=property&params={ "limit": 10, "offset": 0, "search": "Date", "description": true }',
+ 'api.php?action=smwbrowse&browse=property&params={ "limit": 10, "offset": 0, "search": "Date", "description": true, "prefLabel": true }',
+ 'api.php?action=smwbrowse&browse=property&params={ "limit": 10, "offset": 0, "search": "Date", "description": true, "prefLabel": true, "usageCount": true }',
+ 'api.php?action=smwbrowse&browse=pvalue&params={ "limit": 10, "offset": 0, "property" : "Foo", "search": "Bar" }',
+ 'api.php?action=smwbrowse&browse=psubject&params={ "limit": 10, "offset": 0, "property" : "Foo", "value" : "Bar", "search": "foo" }',
+ 'api.php?action=smwbrowse&browse=category&params={ "limit": 10, "offset": 0, "search": "" }',
+ 'api.php?action=smwbrowse&browse=category&params={ "limit": 10, "offset": 0, "search": "Date" }',
+ 'api.php?action=smwbrowse&browse=concept&params={ "limit": 10, "offset": 0, "search": "" }',
+ 'api.php?action=smwbrowse&browse=concept&params={ "limit": 10, "offset": 0, "search": "Date" }',
+ 'api.php?action=smwbrowse&browse=page&params={ "limit": 10, "offset": 0, "search": "Main" }',
+ 'api.php?action=smwbrowse&browse=page&params={ "limit": 10, "offset": 0, "search": "Main", "fullText": true, "fullURL": true }',
+ 'api.php?action=smwbrowse&browse=subject&params={ "subject": "Main page", "ns" :0, "iw": "", "subobject": "" }',
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getVersion
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return __CLASS__ . ':' . SMW_VERSION;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getVersion
+ *
+ * @return string
+ */
+ public function getHelpUrls() {
+ return 'https://www.semantic-mediawiki.org/wiki/Help:API';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ArticleAugmentor.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ArticleAugmentor.php
new file mode 100644
index 00000000..aa58bdeb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ArticleAugmentor.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+use SMW\MediaWiki\TitleFactory;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ArticleAugmentor {
+
+ /**
+ * @var TitleFactory
+ */
+ private $titleFactory;
+
+ /**
+ * @since 3.0
+ *
+ * @param TitleFactory $titleFactory
+ */
+ public function __construct( TitleFactory $titleFactory ) {
+ $this->titleFactory = $titleFactory;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array &$res
+ * @param array $parameters
+ *
+ * @return array
+ */
+ public function augment( array &$res, array $parameters ) {
+
+ if ( !isset( $res['query'] ) && $res['query'] === [] ) {
+ return;
+ }
+
+ if ( isset( $parameters['fullText' ] ) || isset( $parameters['fullURL' ] ) ) {
+
+ foreach ( $res['query'] as $key => &$value ) {
+
+ $title = $this->titleFactory->newFromID( $value['id'] );
+
+ if ( isset( $parameters['fullText' ] ) ) {
+ $value['fullText'] = $title->getFullText();
+ }
+
+ if ( isset( $parameters['fullURL' ] ) ) {
+ $value['fullURL'] = $title->getFullURL();
+ }
+ }
+ }
+
+ // Remove the internal ID, no external consumer should rely on it
+ foreach ( $res['query'] as $key => &$value ) {
+ unset( $value['id'] );
+ }
+
+ return $res;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ArticleLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ArticleLookup.php
new file mode 100644
index 00000000..1a8893e2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ArticleLookup.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+use SMW\Localizer;
+use SMW\MediaWiki\Database;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ArticleLookup extends Lookup {
+
+ const VERSION = 1;
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var ArticleAugmentor
+ */
+ private $articleAugmentor;
+
+ /**
+ * @since 3.0
+ *
+ * @param Database $connection
+ * @param ArticleAugmentor $articleAugmentor
+ */
+ public function __construct( Database $connection, ArticleAugmentor $articleAugmentor ) {
+ $this->connection = $connection;
+ $this->articleAugmentor = $articleAugmentor;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string|integer
+ */
+ public function getVersion() {
+ return 'ArticleLookup:' . self::VERSION;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ *
+ * @return array
+ */
+ public function lookup( array $parameters ) {
+
+ $limit = 50;
+ $offset = 0;
+ $namespace = null;
+
+ if ( isset( $parameters['limit'] ) ) {
+ $limit = (int)$parameters['limit'];
+ }
+
+ if ( isset( $parameters['offset'] ) ) {
+ $offset = (int)$parameters['offset'];
+ }
+
+ if ( isset( $parameters['namespace'] ) ) {
+ $namespace = $parameters['namespace'];
+ }
+
+ if ( isset( $parameters['search'] ) ) {
+ list( $list, $continueOffset ) = $this->search( $limit, $offset, $parameters['search'], $namespace );
+ }
+
+ // Changing this output format requires to set a new version
+ $res = [
+ 'query' => $list,
+ 'query-continue-offset' => $continueOffset,
+ 'version' => self::VERSION,
+ 'meta' => [
+ 'type' => 'article',
+ 'limit' => $limit,
+ 'count' => count( $list )
+ ]
+ ];
+
+ $this->articleAugmentor->augment(
+ $res,
+ $parameters
+ );
+
+ return $res;
+ }
+
+ private function search( $limit, $offset, $search, $namespace = null ) {
+
+ $search = $this->getSearchTerm( $search, $namespace );
+
+ $escapeChar = '`';
+ $list = [];
+
+ $search = str_replace(
+ [ ' ', $escapeChar, '%', '_' ],
+ [ '_', "{$escapeChar}{$escapeChar}", "{$escapeChar}%", "{$escapeChar}_" ],
+ $search
+ );
+
+ $limit = $limit + 1;
+ $conditions = '';
+
+ $fields = [
+ 'page_id',
+ 'page_namespace',
+ 'page_title'
+ ];
+
+ $options = [
+ 'LIMIT' => $limit,
+ 'OFFSET' => $offset,
+ 'ORDER BY' => "page_title,page_namespace"
+ ];
+
+ $conds = [
+ '%' . $search . '%',
+ '%' . ucfirst( $search ) . '%',
+ '%' . strtoupper( $search ) . '%',
+ '%' . strtolower( $search ) . '%'
+ ];
+
+ foreach ( $conds as $s ) {
+ $conditions .= ( $conditions !== '' ? ' OR ' : '' ) . "page_title LIKE ";
+ $conditions .= $this->connection->addQuotes( $s );
+ $conditions .= ' ESCAPE ' . $this->connection->addQuotes( $escapeChar );
+ }
+
+ if ( $namespace !== null ) {
+ $conditions = 'page_namespace=' . $this->connection->addQuotes( $namespace ) . ' AND ('. $conditions. ')';
+ }
+
+ $res = $this->connection->select(
+ [ 'page'],
+ $fields,
+ $conditions,
+ __METHOD__,
+ $options
+ );
+
+ $count = 0;
+ $continueOffset = 0;
+
+ foreach ( $res as $row ) {
+
+ $key = $row->page_title;
+ $count++;
+
+ if ( $count > ( $limit - 1 ) ) {
+ $continueOffset = $offset + $limit;
+ break;
+ }
+
+ $label = str_replace( '_', ' ', $row->page_title );
+
+ $list[$key.'#'.$row->page_namespace] = [
+ // Only keep the ID as internal field which is
+ // removed by the Augmentor
+ 'id' => $row->page_id,
+ 'label' => $label,
+ 'key' => $key,
+ 'ns' => $row->page_namespace
+ ];
+ }
+
+ return [ $list, $continueOffset ];
+ }
+
+ private function getSearchTerm( $search, &$namespace = null ) {
+
+ if ( strpos( $search, ':' ) !== false ) {
+ list( $ns, $term ) = explode( ':', $search );
+
+ if ( ( $namespace = Localizer::getInstance()->getNamespaceIndexByName( $ns ) ) !== false ) {
+ $search = $term;
+ } else {
+ $namespace = null;
+ }
+ }
+
+ return $search;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/CachingLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/CachingLookup.php
new file mode 100644
index 00000000..923ef6b0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/CachingLookup.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+use Onoi\Cache\Cache;
+use SMW\Utils\Timer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class CachingLookup {
+
+ const CACHE_NAMESPACE = 'smw:api:browse';
+ const CACHE_TTL = 3600;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var Lookup
+ */
+ private $lookup;
+
+ /**
+ * @var integer|boolean
+ */
+ private $cacheTTL;
+
+ /**
+ * @since 3.0
+ *
+ * @param Cache $cache
+ * @param Lookup $lookup
+ */
+ public function __construct( Cache $cache, Lookup $lookup ) {
+ $this->cache = $cache;
+ $this->lookup = $lookup;
+ $this->cacheTTL = self::CACHE_TTL;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer|boolean $cacheTTL
+ */
+ public function setCacheTTL( $cacheTTL ) {
+ $this->cacheTTL = $cacheTTL;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ *
+ * @return array
+ */
+ public function lookup( array $parameters ) {
+
+ Timer::start( __METHOD__ );
+
+ $hash = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [
+ $parameters,
+ $this->lookup->getVersion()
+ ]
+ );
+
+ if ( $this->cacheTTL !== false && ( $res = $this->cache->fetch( $hash ) ) !== false ) {
+ $res['meta']['isFromCache'] = true;
+ $res['meta']['queryTime'] = Timer::getElapsedTime( __METHOD__, 5 );
+ return $res;
+ }
+
+ $res = $this->lookup->lookup(
+ $parameters
+ );
+
+ if ( $this->cacheTTL !== false ) {
+ $this->cache->save( $hash, $res, $this->cacheTTL );
+ }
+
+ $res['meta']['isFromCache'] = false;
+ $res['meta']['queryTime'] = Timer::getElapsedTime( __METHOD__, 5 );
+
+ return $res;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ListAugmentor.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ListAugmentor.php
new file mode 100644
index 00000000..e02aa118
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ListAugmentor.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ListAugmentor {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array &$res
+ * @param array $parameters
+ *
+ * @return array
+ */
+ public function augment( array &$res, array $parameters ) {
+
+ if ( !isset( $res['query'] ) && $res['query'] === [] ) {
+ return;
+ }
+
+ $type = null;
+ $lang = 'en';
+
+ if ( isset( $res['meta']['type'] ) ) {
+ $type = $res['meta']['type'];
+ }
+
+ if ( isset( $parameters['lang'] ) ) {
+ $lang = $parameters['lang'];
+ }
+
+ if ( is_string( $lang ) ) {
+ $lang = [ $lang ];
+ }
+
+ if ( $type === 'property' && isset( $parameters['description' ] ) ) {
+ $this->addPropertyDescription( $res, $lang );
+ }
+
+ if ( $type === 'property' && isset( $parameters['prefLabel' ] ) ) {
+ $this->addPreferredPropertyLabel( $res, $lang );
+ }
+
+ if ( $type === 'property' && isset( $parameters['usageCount' ] ) ) {
+ $this->addUsageCount( $res );
+ }
+
+ // Remove the internal ID, no external consumer should rely on it
+ foreach ( $res['query'] as $key => &$value ) {
+ unset( $value['id'] );
+ }
+
+ return $res;
+ }
+
+ private function addUsageCount( &$res ) {
+
+ $list = $res['query'];
+
+ $db = $this->store->getConnection( 'mw.db' );
+
+ foreach ( $list as $key => $value ) {
+
+ $row = $db->selectRow(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ [ 'usage_count' ],
+ [
+ 'p_id' => $value['id']
+ ],
+ __METHOD__
+ );
+
+ $list[$key] = $value + [
+ 'usageCount' => $row->usage_count
+ ];
+ }
+
+ $res['query'] = $list;
+ }
+
+ private function addPreferredPropertyLabel( &$res, array $languageCodes ) {
+
+ $list = $res['query'];
+
+ foreach ( $list as $key => $value ) {
+ $property = new DIProperty( $key );
+ $prefLabel = [];
+
+ foreach ( $languageCodes as $code ) {
+ $prefLabel[$code] = $property->getPreferredLabel( $code );
+ }
+
+ $list[$key] = $value + [
+ 'prefLabel' => $prefLabel
+ ];
+ }
+
+ $res['query'] = $list;
+ }
+
+ private function addPropertyDescription( &$res, array $languageCodes ) {
+
+ $list = $res['query'];
+ $propertySpecificationLookup = ApplicationFactory::getInstance()->getPropertySpecificationLookup();
+
+ foreach ( $list as $key => $value ) {
+ $property = new DIProperty( $key );
+ $description = [];
+
+ foreach ( $languageCodes as $code ) {
+ $description[$code] = $propertySpecificationLookup->getPropertyDescriptionByLanguageCode( $property, $code );
+ }
+
+ $list[$key] = $value + [
+ 'description' => $description
+ ];
+ }
+
+ $res['query'] = $list;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ListLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ListLookup.php
new file mode 100644
index 00000000..690035c2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/ListLookup.php
@@ -0,0 +1,252 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+use Exception;
+use SMW\DIProperty;
+use SMW\RequestOptions;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use SMW\StringCondition;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ListLookup extends Lookup {
+
+ const VERSION = 1;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var ListAugmentor
+ */
+ private $listAugmentor;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store, ListAugmentor $listAugmentor ) {
+ $this->store = $store;
+ $this->listAugmentor = $listAugmentor;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string|integer
+ */
+ public function getVersion() {
+ return 'ListLookup:' . self::VERSION;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ *
+ * @return array
+ */
+ public function lookup( array $parameters ) {
+
+ $requestOptions = $this->newRequestOptions(
+ $parameters
+ );
+
+ $limit = $requestOptions->getLimit();
+ $list = [];
+ $continueOffset = 0;
+
+ // Increase by one to look ahead
+ $requestOptions->setLimit( $limit + 1 );
+ $ns = isset( $parameters['ns'] ) ? $parameters['ns'] : '';
+
+ switch ( $ns ) {
+ case NS_CATEGORY:
+ $type = 'category';
+ break;
+ case SMW_NS_PROPERTY:
+ $type = 'property';
+ break;
+ case SMW_NS_CONCEPT:
+ $type = 'concept';
+ break;
+ default:
+ $type = 'unlisted';
+ break;
+ }
+
+ if ( isset( $parameters['search'] ) ) {
+ list( $res, $continueOffset ) = $this->fetchFromTable( $ns, $requestOptions, $parameters );
+ }
+
+ // Changing this output format requires to set a new version
+ $res = [
+ 'query' => $res,
+ 'query-continue-offset' => $continueOffset,
+ 'version' => self::VERSION,
+ 'meta' => [
+ 'type' => $type,
+ 'limit' => $limit,
+ 'count' => count( $res )
+ ]
+ ];
+
+ $this->listAugmentor->augment(
+ $res,
+ $parameters
+ );
+
+ return $res;
+ }
+
+ private function newRequestOptions( $parameters ) {
+
+ $limit = 50;
+ $offset = 0;
+ $search = '';
+
+ if ( isset( $parameters['limit'] ) ) {
+ $limit = (int)$parameters['limit'];
+ }
+
+ if ( isset( $parameters['offset'] ) ) {
+ $offset = (int)$parameters['offset'];
+ }
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->sort = true;
+ $requestOptions->setLimit( $limit );
+ $requestOptions->setOffset( $offset );
+
+ if ( isset( $parameters['search'] ) && isset( $parameters['strict'] ) ) {
+ $search = $parameters['search'];
+
+ if ( $search !== '' && $search{0} !== '_' ) {
+ $search = str_replace( "_", " ", $search );
+ }
+
+ $requestOptions->addStringCondition(
+ $search,
+ StringCondition::COND_EQ
+ );
+
+ } elseif ( isset( $parameters['search'] ) ) {
+ $search = $parameters['search'];
+
+ if ( $search !== '' && $search{0} !== '_' ) {
+ $search = str_replace( "_", " ", $search );
+ }
+
+ $requestOptions->addStringCondition(
+ $search,
+ StringCondition::STRCOND_MID
+ );
+
+ // Disjunctive condition to allow for auto searches to match foaf OR Foaf
+ $requestOptions->addStringCondition(
+ ucfirst( $search ),
+ StringCondition::STRCOND_MID,
+ true
+ );
+
+ // Allow something like FOO to match the search string `foo`
+ $requestOptions->addStringCondition(
+ strtoupper( $search ),
+ StringCondition::STRCOND_MID,
+ true
+ );
+
+ $requestOptions->addStringCondition(
+ strtolower( $search ),
+ StringCondition::STRCOND_MID,
+ true
+ );
+ }
+
+ return $requestOptions;
+ }
+
+ private function fetchFromTable( $ns, $requestOptions, $parameters ) {
+
+ $limit = $requestOptions->getLimit() - 1;
+ $list = [];
+ $options = [];
+
+ $fields = [
+ 'smw_id',
+ 'smw_title'
+ ];
+
+ // the query needs to do the filtering of internal properties, else LIMIT is wrong
+ if ( isset( $parameters['sort'] ) ) {
+ $options = $this->store->getSQLOptions( $requestOptions, 'smw_sort' );
+ $fields[] = 'smw_sort';
+ }
+
+ $conditions = [
+ 'smw_namespace' => $ns,
+ 'smw_iw' => '',
+ 'smw_subobject' => ''
+ ];
+
+ if ( ( $cond = $this->store->getSQLConditions( $requestOptions, '', 'smw_sortkey', false ) ) !== '' ) {
+ $conditions[] = $cond;
+ $fields[] = 'smw_sortkey';
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $res = $connection->select(
+ $connection->tableName( SQLStore::ID_TABLE ),
+ $fields,
+ $conditions,
+ __METHOD__,
+ $options
+ );
+
+ $count = 0;
+ $continueOffset = 0;
+
+ foreach ( $res as $row ) {
+
+ $key = $row->smw_title;
+ $count++;
+
+ if ( $count > $limit ) {
+ $continueOffset = $requestOptions->getOffset() + $limit;
+ break;
+ }
+
+ if ( $ns === SMW_NS_PROPERTY ) {
+ try {
+ $label = DIProperty::newFromUserLabel( $row->smw_title )->getLabel();
+ } catch( Exception $e ) {
+ continue;
+ }
+
+ } else {
+ $label = str_replace( '_', ' ', $row->smw_title );
+ }
+
+ $list[$key] = [
+ // Only keep the ID as internal field which is
+ // removed by the Augmentor
+ 'id' => $row->smw_id,
+ 'label' => $label,
+ 'key' => $key
+ ];
+ }
+
+ return [ $list, $continueOffset ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/Lookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/Lookup.php
new file mode 100644
index 00000000..c54e413b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/Lookup.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+abstract class Lookup {
+
+ /**
+ * @since 3.0
+ *
+ * @return string|integer
+ */
+ abstract public function getVersion();
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ *
+ * @return array
+ */
+ abstract public function lookup( array $parameters );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/PSubjectLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/PSubjectLookup.php
new file mode 100644
index 00000000..2828755a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/PSubjectLookup.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+use SMW\DIProperty;
+use Exception;
+use SMW\Store;
+use SMW\DIWikiPage;
+use SMW\RequestOptions;
+use SMW\StringCondition;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PSubjectLookup extends Lookup {
+
+ const VERSION = 1;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string|integer
+ */
+ public function getVersion() {
+ return __METHOD__ . self::VERSION;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ *
+ * @return array
+ */
+ public function lookup( array $parameters ) {
+
+ $limit = 20;
+ $offset = 0;
+
+ if ( isset( $parameters['limit'] ) ) {
+ $limit = (int)$parameters['limit'];
+ }
+
+ if ( isset( $parameters['offset'] ) ) {
+ $offset = (int)$parameters['offset'];
+ }
+
+ $list = [];
+ $continueOffset = 0;
+ $property = null;
+ $value = null;
+
+ if ( isset( $parameters['property'] ) ) {
+ $property = $parameters['property'];
+
+ // Get the last which represents the final output
+ // Foo.Bar.Foobar.Baz
+ if ( strpos( $property, '.' ) !== false ) {
+ $chain = explode( '.', $property );
+ $property = array_pop( $chain );
+ }
+ }
+
+ if ( isset( $parameters['value'] ) ) {
+ $value = $parameters['value'];
+ }
+
+ if ( $property === '' || $property === null ) {
+ return [];
+ }
+
+ list( $list, $continueOffset ) = $this->findPropertySubjects(
+ $property,
+ $value,
+ $limit,
+ $offset,
+ $parameters
+ );
+
+ // Changing this output format requires to set a new version
+ $res = [
+ 'query' => $list,
+ 'query-continue-offset' => $continueOffset,
+ 'version' => self::VERSION,
+ 'meta' => [
+ 'type' => 'psubject',
+ 'limit' => $limit,
+ 'count' => count( $list )
+ ]
+ ];
+
+ return $res;
+ }
+
+ private function findPropertySubjects( $property, $value, $limit, $offset, $parameters ) {
+
+ $list = [];
+ $dataItem = null;
+
+ $property = DIProperty::newFromUserLabel( $property );
+
+ if ( $value !== '' && $value !== null ) {
+ $dataItem = DataValueFactory::getInstance()->newDataValueByProperty( $property, $value )->getDataItem();
+ }
+
+ $continueOffset = 0;
+ $count = 0;
+ $requestOptions = $this->newRequestOptions( $parameters );
+
+ $res = $this->store->getPropertySubjects(
+ $property,
+ $dataItem,
+ $requestOptions
+ );
+
+ foreach ( $res as $dataItem ) {
+
+ if ( !$dataItem instanceof DIWikiPage ) {
+ continue;
+ }
+
+ if ( isset( $parameters['title-prefix'] ) && (bool)$parameters['title-prefix'] === false ) {
+ $list[] = $dataItem->getTitle()->getText();
+ } else {
+ $list[] = $dataItem->getTitle()->getPrefixedText();
+ }
+ }
+
+ if ( $this->is_iterable( $res ) ) {
+ $count = count( $res );
+ }
+
+ if ( $count > $limit ) {
+ $continueOffset = $offset + $count;
+ array_pop( $list );
+ }
+
+ return [ $list, $continueOffset ];
+ }
+
+ private function newRequestOptions( $parameters ) {
+
+ $limit = 20;
+ $offset = 0;
+ $search = '';
+
+ if ( isset( $parameters['limit'] ) ) {
+ $limit = (int)$parameters['limit'];
+ }
+
+ if ( isset( $parameters['offset'] ) ) {
+ $offset = (int)$parameters['offset'];
+ }
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->sort = true;
+ $requestOptions->setLimit( $limit + 1 );
+ $requestOptions->setOffset( $offset );
+
+ if ( isset( $parameters['search'] ) && $parameters['search'] !== '' ) {
+ $search = $parameters['search'];
+
+ if ( $search !== '' && $search{0} !== '_' ) {
+ $search = str_replace( "_", " ", $search );
+ }
+
+ $requestOptions->addStringCondition(
+ $search,
+ StringCondition::STRCOND_MID
+ );
+
+ // Disjunctive condition to allow for auto searches to match foaf OR Foaf
+ $requestOptions->addStringCondition(
+ ucfirst( $search ),
+ StringCondition::STRCOND_MID,
+ true
+ );
+
+ // Allow something like FOO to match the search string `foo`
+ $requestOptions->addStringCondition(
+ strtoupper( $search ),
+ StringCondition::STRCOND_MID,
+ true
+ );
+
+ $requestOptions->addStringCondition(
+ strtolower( $search ),
+ StringCondition::STRCOND_MID,
+ true
+ );
+ }
+
+ return $requestOptions;
+ }
+
+ private function is_iterable( $obj ) {
+ return is_array( $obj ) || ( is_object( $obj ) && ( $obj instanceof \Traversable ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/PValueLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/PValueLookup.php
new file mode 100644
index 00000000..bc7eca51
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/PValueLookup.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+use SMW\DataTypeRegistry;
+use SMW\DataValueFactory;
+use SMW\RequestOptions;
+use SMW\DIProperty;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use SMWDataItem as DataItem;
+use SMWDITime as DIime;
+use SMW\SQLStore\Lookup\ProximityPropertyValueLookup;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PValueLookup extends Lookup {
+
+ const VERSION = 1;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string|integer
+ */
+ public function getVersion() {
+ return __METHOD__ . self::VERSION;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ *
+ * @return array
+ */
+ public function lookup( array $parameters ) {
+
+ $limit = 20;
+ $offset = 0;
+
+ if ( isset( $parameters['limit'] ) ) {
+ $limit = (int)$parameters['limit'];
+ }
+
+ if ( isset( $parameters['offset'] ) ) {
+ $offset = (int)$parameters['offset'];
+ }
+
+ $res = [];
+ $continueOffset = 0;
+ $property = null;
+ $sort = false;
+ $count = 0;
+
+ if ( isset( $parameters['property'] ) ) {
+ $property = $parameters['property'];
+
+ // Get the last which represents the final output
+ // Foo.Bar.Foobar.Baz
+ if ( strpos( $property, '.' ) !== false ) {
+ $chain = explode( '.', $property );
+ $property = array_pop( $chain );
+ }
+ }
+
+ if ( $property === '' || $property === null ) {
+ return [];
+ }
+
+ // Generally we don't want to sort results to avoid having the DB to use
+ // temporary tables/filesort when the value pool is very large
+ if ( isset( $parameters['sort'] ) ) {
+ $sort = in_array( strtolower( $parameters['sort'] ), [ 'asc', 'desc' ] ) ? $parameters['sort'] : 'asc';
+ }
+
+ if ( isset( $parameters['search'] ) ) {
+
+ $opts = new RequestOptions();
+ $opts->limit = $limit;
+ $opts->offset = $offset;
+ $opts->sort = $sort;
+
+ $property = DIProperty::newFromUserLabel(
+ $property
+ );
+
+ $proximityPropertyValueLookup = $this->store->service(
+ 'ProximityPropertyValueLookup'
+ );
+
+ $res = $proximityPropertyValueLookup->lookup(
+ $property,
+ $parameters['search'],
+ $opts
+ );
+
+ if ( $this->is_iterable( $res ) ) {
+ $count = count( $res );
+ }
+
+ if ( $count > $limit ) {
+ $continueOffset = $offset + $count;
+ array_pop( $res );
+ }
+ }
+
+ // Changing this output format requires to set a new version
+ $res = [
+ 'query' => $res,
+ 'query-continue-offset' => $continueOffset,
+ 'version' => self::VERSION,
+ 'meta' => [
+ 'type' => 'pvalue',
+ 'limit' => $limit,
+ 'count' => $count
+ ]
+ ];
+
+ return $res;
+ }
+
+ private function is_iterable( $obj ) {
+ return is_array( $obj ) || ( is_object( $obj ) && ( $obj instanceof \Traversable ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/SubjectLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/SubjectLookup.php
new file mode 100644
index 00000000..c995619e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Browse/SubjectLookup.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace SMW\MediaWiki\Api\Browse;
+
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Specials\Browse\HtmlBuilder;
+use SMW\Store;
+use SMW\Exception\RedirectTargetUnresolvableException;
+use SMW\Exception\ParameterNotFoundException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SubjectLookup extends Lookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string|integer
+ */
+ public function getVersion() {
+ return 'SubjectLookup:' . self::VERSION;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ *
+ * @return array
+ */
+ public function lookup( array $parameters ) {
+
+ if ( !isset( $parameters['subject'] ) ) {
+ throw new ParameterNotFoundException( 'subject' );
+ }
+
+ if ( !isset( $parameters['ns'] ) ) {
+ throw new ParameterNotFoundException( 'ns' );
+ }
+
+ if ( !isset( $parameters['iw'] ) ) {
+ $parameters['iw'] = '';
+ }
+
+ if ( !isset( $parameters['subobject'] ) ) {
+ $parameters['subobject'] = '';
+ }
+
+ if ( isset( $parameters['type'] ) && $parameters['type'] === 'html' ) {
+ $data = $this->buildHTML( $parameters );
+ } else {
+ $data = $this->doSerialize( $parameters );
+ }
+
+ // Changing this output format requires to set a new version
+ $res = [
+ 'query' => $data,
+ 'meta' => [
+ 'type' => 'subject'
+ ]
+ ];
+
+ return $res;
+ }
+
+ private function buildHTML( $params ) {
+
+ if ( !isset( $params['options'] ) ) {
+ throw new ParameterNotFoundException( 'options' );
+ }
+
+ $subject = new DIWikiPage(
+ $params['subject'],
+ $params['ns'],
+ $params['iw'],
+ $params['subobject']
+ );
+
+ $htmlBuilder = new HtmlBuilder(
+ $this->store,
+ $subject
+ );
+
+ $htmlBuilder->setOptions(
+ $params['options']
+ );
+
+ return $htmlBuilder->buildHTML();
+ }
+
+ private function doSerialize( $params ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $subobject = isset( $params['subobject'] ) ? $params['subobject'] : '';
+
+ $title = $applicationFactory->newTitleFactory()->newFromText(
+ $params['subject'],
+ $params['ns']
+ );
+
+ $deepRedirectTargetResolver = $applicationFactory->newMwCollaboratorFactory()->newDeepRedirectTargetResolver();
+
+ try {
+ $title = $deepRedirectTargetResolver->findRedirectTargetFor( $title );
+ } catch ( \Exception $e ) {
+ throw new RedirectTargetUnresolvableException( $e->getMessage() );
+ }
+
+ $dataItem = new DIWikiPage(
+ $title->getDBkey(),
+ $title->getNamespace(),
+ $title->getInterwiki(),
+ $subobject
+ );
+
+ $semanticData = $applicationFactory->getStore()->getSemanticData(
+ $dataItem
+ );
+
+ $semanticDataSerializer = $applicationFactory->newSerializerFactory()->newSemanticDataSerializer();
+
+ return $semanticDataSerializer->serialize( $semanticData );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/BrowseByProperty.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/BrowseByProperty.php
new file mode 100644
index 00000000..23c87ed6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/BrowseByProperty.php
@@ -0,0 +1,197 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use ApiBase;
+use SMW\ApplicationFactory;
+use SMW\Localizer;
+use SMW\NamespaceUriFinder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class BrowseByProperty extends ApiBase {
+
+ /**
+ * #2696
+ * @deprecated since 3.0, use the smwbrowse API module
+ */
+ public function isDeprecated() {
+ return true;
+ }
+
+ /**
+ * @see ApiBase::execute
+ */
+ public function execute() {
+
+ $params = $this->extractRequestParams();
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $propertyListByApiRequest = new PropertyListByApiRequest(
+ $applicationFactory->getStore(),
+ $applicationFactory->getPropertySpecificationLookup()
+ );
+
+ $propertyListByApiRequest->setLimit(
+ $params['limit']
+ );
+
+ $propertyListByApiRequest->setListOnly(
+ $params['listonly']
+ );
+
+ if ( ( $lang = $params['lang'] ) === null ) {
+ $lang = Localizer::getInstance()->getUserLanguage()->getCode();
+ }
+
+ $propertyListByApiRequest->setLanguageCode(
+ $lang
+ );
+
+ $propertyListByApiRequest->findPropertyListBy(
+ $params['property']
+ );
+
+ foreach ( $propertyListByApiRequest->getNamespaces() as $ns ) {
+
+ $uri = NamespaceUriFinder::getUri( $ns );
+
+ if ( !$uri ) {
+ continue;
+ }
+
+ $this->getResult()->addValue(
+ null,
+ 'xmlns:' . $ns,
+ $uri
+ );
+ }
+
+ $data = $propertyListByApiRequest->getPropertyList();
+
+ // I'm without words for this utter nonsense introduced here
+ // because property keys can have a underscore _MDAT or for that matter
+ // any other data field can
+ // https://www.mediawiki.org/wiki/API:JSON_version_2
+ // " ... can indicate that a property beginning with an underscore is not metadata using"
+ if ( method_exists( $this->getResult(), 'setPreserveKeysList') ) {
+ $this->getResult()->setPreserveKeysList(
+ $data,
+ array_keys( $data )
+ );
+ }
+
+ $this->getResult()->addValue(
+ null,
+ 'query',
+ $data
+ );
+
+ $this->getResult()->addValue(
+ null,
+ 'version',
+ 2
+ );
+
+ $this->getResult()->addValue(
+ null,
+ 'query-continue-offset',
+ $propertyListByApiRequest->getContinueOffset()
+ );
+
+ $this->getResult()->addValue(
+ null,
+ 'meta',
+ $propertyListByApiRequest->getMeta()
+ );
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getAllowedParams
+ *
+ * @return array
+ */
+ public function getAllowedParams() {
+ return [
+ 'property' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_REQUIRED => false,
+ ],
+ 'limit' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_DFLT => 50,
+ ApiBase::PARAM_REQUIRED => false,
+ ],
+ 'lang' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_REQUIRED => false,
+ ],
+ 'listonly' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_DFLT => false,
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_REQUIRED => false,
+ ]
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getParamDescription
+ *
+ * @return array
+ */
+ public function getParamDescription() {
+ return [
+ 'property' => 'To match a specific property',
+ 'limit' => 'To specify the size of the list request',
+ 'lang' => 'To specify a specific language used for some attributes (description etc.)',
+ 'listonly' => 'To specify that only a property list is returned without further details'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getDescription
+ *
+ * @return array
+ */
+ public function getDescription() {
+ return [
+ 'API module to query a property list or an individual property.'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getExamples
+ *
+ * @return array
+ */
+ public function getExamples() {
+ return [
+ 'api.php?action=browsebyproperty&property=Modification_date',
+ 'api.php?action=browsebyproperty&limit=50',
+ 'api.php?action=browsebyproperty&limit=5&listonly=true',
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getVersion
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return __CLASS__ . '-' . SMW_VERSION;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/BrowseBySubject.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/BrowseBySubject.php
new file mode 100644
index 00000000..1cc4568e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/BrowseBySubject.php
@@ -0,0 +1,238 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use ApiBase;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Specials\Browse\HtmlBuilder;
+
+/**
+ * Browse a subject api module
+ *
+ * @note To browse a particular subobject use the 'subobject' parameter because
+ * MW's WebRequest (responsible for handling request data sent by a browser) will
+ * eliminate any fragments (marked by "#") therefore using something like
+ * '"Lorem_ipsum#Foo' is not going to work but '&subject=Lorem_ipsum&subobject=Foo'
+ * will return results for the selected subobject
+ *
+ * @ingroup Api
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class BrowseBySubject extends ApiBase {
+
+ /**
+ * @deprecated since 3.0, use the smwbrowse API module
+ */
+ public function isDeprecated() {
+ return true;
+ }
+
+ /**
+ * @see ApiBase::execute
+ */
+ public function execute() {
+
+ $params = $this->extractRequestParams();
+
+ if ( isset( $params['type'] ) && $params['type'] === 'html' ) {
+ $data = $this->buildHTML( $params );
+ } else {
+ $data = $this->doSerialize( $params );
+ }
+
+ $this->getResult()->addValue(
+ null,
+ 'query',
+ $data
+ );
+ }
+
+ protected function buildHTML( $params ) {
+
+ $subject = new DIWikiPage(
+ $params['subject'],
+ $params['ns'],
+ $params['iw'],
+ $params['subobject']
+ );
+
+ $htmlBuilder = new HtmlBuilder(
+ ApplicationFactory::getInstance()->getStore(),
+ $subject
+ );
+
+ $htmlBuilder->setOptions(
+ (array)$params['options']
+ );
+
+ return $htmlBuilder->buildHTML();
+ }
+
+ protected function doSerialize( $params ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $title = $applicationFactory->newTitleFactory()->newFromText(
+ $params['subject'],
+ $params['ns']
+ );
+
+ $deepRedirectTargetResolver = $applicationFactory->newMwCollaboratorFactory()->newDeepRedirectTargetResolver();
+
+ try {
+ $title = $deepRedirectTargetResolver->findRedirectTargetFor( $title );
+ } catch ( \Exception $e ) {
+
+ // 1.29+
+ if ( method_exists( $this, 'dieWithError' ) ) {
+ $this->dieWithError( [ 'smw-redirect-target-unresolvable', $e->getMessage() ] );
+ } else {
+ $this->dieUsage( $e->getMessage(), 'redirect-target-unresolvable' );
+ }
+ }
+
+ $dataItem = new DIWikiPage(
+ $title->getDBkey(),
+ $title->getNamespace(),
+ $title->getInterwiki(),
+ $params['subobject']
+ );
+
+ $semanticData = $applicationFactory->getStore()->getSemanticData(
+ $dataItem
+ );
+
+ $semanticDataSerializer = $applicationFactory->newSerializerFactory()->newSemanticDataSerializer();
+
+ return $this->doFormat( $semanticDataSerializer->serialize( $semanticData ) );
+ }
+
+ protected function doFormat( $serialized ) {
+
+ $this->addIndexTags( $serialized );
+
+ if ( isset( $serialized['sobj'] ) ) {
+
+ $this->getResult()->setIndexedTagName( $serialized['sobj'], 'subobject' );
+
+ foreach ( $serialized['sobj'] as $key => &$value ) {
+ $this->addIndexTags( $value );
+ }
+ }
+
+ return $serialized;
+ }
+
+ protected function addIndexTags( &$serialized ) {
+
+ if ( isset( $serialized['data'] ) && is_array( $serialized['data'] ) ) {
+
+ $this->getResult()->setIndexedTagName( $serialized['data'], 'property' );
+
+ foreach ( $serialized['data'] as $key => $value ) {
+ if ( isset( $serialized['data'][$key]['dataitem'] ) && is_array( $serialized['data'][$key]['dataitem'] ) ) {
+ $this->getResult()->setIndexedTagName( $serialized['data'][$key]['dataitem'], 'value' );
+ }
+ }
+ }
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getAllowedParams
+ *
+ * @return array
+ */
+ public function getAllowedParams() {
+ return [
+ 'subject' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ 'ns' => [
+ ApiBase::PARAM_TYPE => 'integer',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_DFLT => 0,
+ ApiBase::PARAM_REQUIRED => false,
+ ],
+ 'iw' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_DFLT => '',
+ ApiBase::PARAM_REQUIRED => false,
+ ],
+ 'subobject' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_DFLT => '',
+ ApiBase::PARAM_REQUIRED => false,
+ ],
+ 'type' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_DFLT => '',
+ ApiBase::PARAM_REQUIRED => false,
+ ],
+ 'options' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_ISMULTI => false,
+ ApiBase::PARAM_DFLT => '',
+ ApiBase::PARAM_REQUIRED => false,
+ ]
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getParamDescription
+ *
+ * @return array
+ */
+ public function getParamDescription() {
+ return [
+ 'subject' => 'The subject to be queried',
+ 'subobject' => 'A particular subobject id for the related subject'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getDescription
+ *
+ * @return array
+ */
+ public function getDescription() {
+ return [
+ 'API module to query a subject.'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getExamples
+ *
+ * @return array
+ */
+ protected function getExamples() {
+ return [
+ 'api.php?action=browsebysubject&subject=Main_Page',
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getVersion
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return __CLASS__ . '-' . SMW_VERSION;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Info.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Info.php
new file mode 100644
index 00000000..5f6e9ac9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Info.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use ApiBase;
+use SMW\ApplicationFactory;
+use SMW\Site;
+
+/**
+ * API module to obtain info about the SMW install, primarily targeted at
+ * usage by the SMW registry.
+ *
+ * @ingroup Api
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class Info extends ApiBase {
+
+ /**
+ * @see ApiBase::execute
+ */
+ public function execute() {
+
+ $params = $this->extractRequestParams();
+ $requestedInfo = $params['info'];
+
+ $map = [];
+ $semanticStats = [];
+
+ if ( in_array( 'propcount', $requestedInfo )
+ || in_array( 'jobcount', $requestedInfo )
+ || in_array( 'errorcount', $requestedInfo )
+ || in_array( 'deletecount', $requestedInfo )
+ || in_array( 'totalpropcount', $requestedInfo )
+ || in_array( 'usedpropcount', $requestedInfo )
+ || in_array( 'proppagecount', $requestedInfo )
+ || in_array( 'querycount', $requestedInfo )
+ || in_array( 'querysize', $requestedInfo )
+ || in_array( 'formatcount', $requestedInfo )
+ || in_array( 'conceptcount', $requestedInfo )
+ || in_array( 'subobjectcount', $requestedInfo )
+ || in_array( 'declaredpropcount', $requestedInfo ) ) {
+
+ $semanticStats = ApplicationFactory::getInstance()->getStore()->getStatistics();
+
+ $map = [
+ 'propcount' => 'PROPUSES',
+ 'errorcount' => 'ERRORUSES',
+ 'deletecount' => 'DELETECOUNT',
+ 'usedpropcount' => 'USEDPROPS',
+ 'totalpropcount' => 'TOTALPROPS',
+ 'declaredpropcount' => 'DECLPROPS',
+ 'proppagecount' => 'OWNPAGE',
+ 'querycount' => 'QUERY',
+ 'querysize' => 'QUERYSIZE',
+ 'conceptcount' => 'CONCEPTS',
+ 'subobjectcount' => 'SUBOBJECTS',
+ ];
+ }
+
+ $this->getResult()->addValue(
+ null,
+ 'info',
+ $this->doMapResultInfoFrom( $map, $requestedInfo, $semanticStats )
+ );
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getAllowedParams
+ *
+ * @return array
+ */
+ public function getAllowedParams() {
+ return [
+ 'info' => [
+ ApiBase::PARAM_DFLT => 'propcount|usedpropcount|declaredpropcount',
+ ApiBase::PARAM_ISMULTI => true,
+ ApiBase::PARAM_TYPE => [
+ 'propcount',
+ 'errorcount',
+ 'deletecount',
+ 'usedpropcount',
+ 'totalpropcount',
+ 'declaredpropcount',
+ 'proppagecount',
+ 'querycount',
+ 'querysize',
+ 'formatcount',
+ 'conceptcount',
+ 'subobjectcount',
+ 'jobcount'
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getParamDescription
+ *
+ * @return array
+ */
+ public function getParamDescription() {
+ return [
+ 'info' => 'The info to provide.'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getDescription
+ *
+ * @return array
+ */
+ public function getDescription() {
+ return [
+ 'API module get info about this SMW install.'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getExamples
+ *
+ * @return array
+ */
+ protected function getExamples() {
+ return [
+ 'api.php?action=smwinfo&info=proppagecount|propcount',
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getVersion
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return __CLASS__ . ': $Id$';
+ }
+
+ private function doMapResultInfoFrom( $map, $requestedInfo, $semanticStats ) {
+
+ $resultInfo = [];
+
+ foreach ( $map as $apiName => $smwName ) {
+ if ( in_array( $apiName, $requestedInfo ) ) {
+ $resultInfo[$apiName] = $semanticStats[$smwName];
+ }
+ }
+
+ if ( in_array( 'formatcount', $requestedInfo ) ) {
+ $resultInfo['formatcount'] = [];
+
+ foreach ( $semanticStats['QUERYFORMATS'] as $name => $count ) {
+ $resultInfo['formatcount'][$name] = $count;
+ }
+ }
+
+ if ( in_array( 'jobcount', $requestedInfo ) ) {
+ $resultInfo['jobcount'] = [];
+ $jobQueue = ApplicationFactory::getInstance()->getJobQueue();
+
+ foreach ( Site::getJobClasses( 'SMW' ) as $type => $class ) {
+ $size = $jobQueue->getQueueSize( $type );
+
+ if ( $size > 0 ) {
+ $resultInfo['jobcount'][$type] = $size;
+ }
+ }
+ }
+
+ return $resultInfo;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/PropertyListByApiRequest.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/PropertyListByApiRequest.php
new file mode 100644
index 00000000..d2eb662d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/PropertyListByApiRequest.php
@@ -0,0 +1,297 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\PropertySpecificationLookup;
+use SMW\RequestOptions;
+use SMW\Store;
+use SMW\StringCondition;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class PropertyListByApiRequest {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertySpecificationLookup
+ */
+ private $propertySpecificationLookup;
+
+ /**
+ * @var RequestOptions
+ */
+ private $requestOptions = null;
+
+ /**
+ * @var array
+ */
+ private $propertyList = [];
+
+ /**
+ * @var array
+ */
+ private $namespaces = [];
+
+ /**
+ * @var array
+ */
+ private $meta = [];
+
+ /**
+ * @var integer
+ */
+ private $limit = 50;
+
+ /**
+ * @var array
+ */
+ private $continueOffset = 1;
+
+ /**
+ * @var string
+ */
+ private $languageCode = '';
+
+ /**
+ * @var boolean
+ */
+ private $listOnly = false;
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ * @param PropertySpecificationLookup $propertySpecificationLookup
+ */
+ public function __construct( Store $store, PropertySpecificationLookup $propertySpecificationLookup ) {
+ $this->store = $store;
+ $this->propertySpecificationLookup = $propertySpecificationLookup;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $limit
+ */
+ public function setLimit( $limit ) {
+ $this->limit = (int)$limit;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $listOnly
+ */
+ public function setListOnly( $listOnly ) {
+ $this->listOnly = (bool)$listOnly;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $languageCode
+ */
+ public function setLanguageCode( $languageCode ) {
+ $this->languageCode = (string)$languageCode;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array
+ */
+ public function getPropertyList() {
+ return $this->propertyList;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array
+ */
+ public function getNamespaces() {
+ return $this->namespaces;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array
+ */
+ public function getMeta() {
+ return $this->meta;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array
+ */
+ public function getContinueOffset() {
+ return $this->continueOffset;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $property
+ *
+ * @return boolean
+ */
+ public function findPropertyListBy( $property = '' ) {
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->sort = true;
+ $requestOptions->limit = $this->limit;
+
+ $isFromCache = false;
+
+ //
+ $this->matchPropertiesToPreferredLabelBy( $property );
+
+ // Increase by one to look ahead
+ $requestOptions->limit++;
+
+ $requestOptions = $this->doModifyRequestOptionsWith(
+ $property,
+ $requestOptions
+ );
+
+ $propertyListLookup = $this->store->getPropertiesSpecial( $requestOptions );
+
+ // Restore original limit
+ $requestOptions->limit--;
+
+ foreach ( $propertyListLookup->fetchList() as $value ) {
+
+ if ( $this->continueOffset > $requestOptions->limit ) {
+ break;
+ }
+
+ $this->addPropertyToList( $value );
+ $this->continueOffset++;
+ }
+
+ $this->continueOffset = $this->continueOffset > $requestOptions->limit ? $requestOptions->limit : 0;
+ $this->namespaces = array_keys( $this->namespaces );
+
+ $this->meta = [
+ 'limit' => $requestOptions->limit,
+ 'count' => count( $this->propertyList ),
+ 'isCached' => $propertyListLookup->isFromCache()
+ ];
+
+ return true;
+ }
+
+ private function doModifyRequestOptionsWith( $property, $requestOptions ) {
+
+ if ( $property === '' ) {
+ return $requestOptions;
+ }
+
+ if ( $property{0} !== '_' ) {
+ $property = str_replace( "_", " ", $property );
+ }
+
+ // Try to match something like _MDAT to find a label and
+ // make the request a success
+ try {
+ $property = DIProperty::newFromUserLabel( $property )->getLabel();
+ } catch ( \Exception $e ) {
+ $property = '';
+ }
+
+ $requestOptions->addStringCondition(
+ $property,
+ StringCondition::STRCOND_MID
+ );
+
+ // Disjunctive condition to allow for auto searches to match foaf OR Foaf
+ $requestOptions->addStringCondition(
+ ucfirst( $property ),
+ StringCondition::STRCOND_MID,
+ true
+ );
+
+ // Allow something like FOO to match the search string `foo`
+ $requestOptions->addStringCondition(
+ strtoupper( $property ),
+ StringCondition::STRCOND_MID,
+ true
+ );
+
+ return $requestOptions;
+ }
+
+ private function addPropertyToList( array $value ) {
+
+ if ( $value === [] || !$value[0] instanceof DIProperty ) {
+ return;
+ }
+
+ $property = $value[0];
+ $key = $property->getKey();
+
+ if ( strpos( $key, ':' ) !== false ) {
+ $this->namespaces[substr( $key, 0, strpos( $key, ':' ) )] = true;
+ }
+
+ $this->propertyList[$key] = [
+ 'label' => $property->getLabel(),
+ 'key' => $property->getKey()
+ ];
+
+ if ( $this->listOnly ) {
+ return;
+ }
+
+ $this->propertyList[$key]['isUserDefined'] = $property->isUserDefined();
+ $this->propertyList[$key]['usageCount'] = $value[1];
+ $this->propertyList[$key]['description'] = $this->findPropertyDescriptionBy( $property );
+ }
+
+ private function findPropertyDescriptionBy( DIProperty $property ) {
+
+ $description = $this->propertySpecificationLookup->getPropertyDescriptionByLanguageCode(
+ $property,
+ $this->languageCode
+ );
+
+ if ( $description === '' || $description === null ) {
+ return $description;
+ }
+
+ return [
+ $this->languageCode => $description
+ ];
+ }
+
+ private function matchPropertiesToPreferredLabelBy( $label ) {
+
+ $propertyLabelFinder = ApplicationFactory::getInstance()->getPropertyLabelFinder();
+
+ // Use the proximity search on a text field
+ $label = '~*' . $label . '*';
+
+ $results = $propertyLabelFinder->findPropertyListFromLabelByLanguageCode(
+ $label,
+ $this->languageCode
+ );
+
+ foreach ( $results as $result ) {
+ $this->addPropertyToList( [ $result, 0 ] );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Query.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Query.php
new file mode 100644
index 00000000..990cd0ee
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Query.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use ApiBase;
+use SMW\ApplicationFactory;
+use SMWQuery;
+use SMWQueryProcessor;
+use SMWQueryResult;
+
+/**
+ * Base for API modules that query SMW
+ *
+ * @ingroup Api
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author mwjames
+ */
+abstract class Query extends ApiBase {
+
+ /**
+ * Returns a query object for the provided query string and list of printouts.
+ *
+ * @since 1.6.2
+ *
+ * @param string $queryString
+ * @param array $printouts
+ * @param array $parameters
+ *
+ * @return SMWQuery
+ */
+ protected function getQuery( $queryString, array $printouts, array $parameters = [] ) {
+
+ SMWQueryProcessor::addThisPrintout( $printouts, $parameters );
+
+ $query = SMWQueryProcessor::createQuery(
+ $queryString,
+ SMWQueryProcessor::getProcessedParams( $parameters, $printouts ),
+ SMWQueryProcessor::SPECIAL_PAGE,
+ '',
+ $printouts
+ );
+
+ $query->setOption( SMWQuery::PROC_CONTEXT, 'API' );
+
+ return $query;
+ }
+
+ /**
+ * Run the actual query and return the result.
+ *
+ * @since 1.6.2
+ *
+ * @param SMWQuery $query
+ *
+ * @return SMWQueryResult
+ */
+ protected function getQueryResult( SMWQuery $query ) {
+ return ApplicationFactory::getInstance()->getStore()->getQueryResult( $query );
+ }
+
+ /**
+ * Add the query result to the API output.
+ *
+ * @since 1.6.2
+ *
+ * @param SMWQueryResult $queryResult
+ */
+ protected function addQueryResult( SMWQueryResult $queryResult, $outputFormat = 'json' ) {
+
+ $result = $this->getResult();
+
+ $resultFormatter = new ApiQueryResultFormatter( $queryResult );
+ $resultFormatter->setIsRawMode( ( strpos( strtolower( $outputFormat ), 'xml' ) !== false ) );
+ $resultFormatter->doFormat();
+
+ if ( $resultFormatter->getContinueOffset() ) {
+ // $result->disableSizeCheck();
+ $result->addValue( null, 'query-continue-offset', $resultFormatter->getContinueOffset() );
+ // $result->enableSizeCheck();
+ }
+
+ $result->addValue(
+ null,
+ $resultFormatter->getType(),
+ $resultFormatter->getResult()
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Task.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Task.php
new file mode 100644
index 00000000..5943d005
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Api/Task.php
@@ -0,0 +1,431 @@
+<?php
+
+namespace SMW\MediaWiki\Api;
+
+use ApiBase;
+use Iterator;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Jobs\UpdateJob;
+use SMW\Enum;
+use SMWQueryProcessor as QueryProcessor;
+use SMWQuery as Query;
+
+/**
+ * Module to support various tasks initiate using the API interface
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Task extends ApiBase {
+
+ const CACHE_NAMESPACE = 'smw:api:task';
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return string
+ */
+ public static function makeCacheKey( $key ) {
+ return smwfCacheKey( self::CACHE_NAMESPACE, [ $key ] );
+ }
+
+ /**
+ * @see ApiBase::execute
+ */
+ public function execute() {
+
+ $params = $this->extractRequestParams();
+
+ $parameters = json_decode(
+ $params['params'],
+ true
+ );
+
+ $results = [];
+
+ if ( $params['task'] === 'update' ) {
+ $results = $this->callUpdateTask( $parameters );
+ }
+
+ if ( $params['task'] === 'check-query' ) {
+ $results = $this->callCheckQueryTask( $parameters );
+ }
+
+ if ( $params['task'] === 'duplookup' ) {
+ $results = $this->callDupLookupTask( $parameters );
+ }
+
+ if ( $params['task'] === 'job' ) {
+ $results = $this->callGenericJobTask( $parameters );
+ }
+
+ if ( $params['task'] === 'run-joblist' ) {
+ $results = $this->callJobListTask( $parameters );
+ }
+
+ $this->getResult()->addValue(
+ null,
+ 'task',
+ $results
+ );
+ }
+
+ private function callDupLookupTask( $parameters ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $cache = $applicationFactory->getCache();
+
+ $cacheUsage = $applicationFactory->getSettings()->get(
+ 'smwgCacheUsage'
+ );
+
+ $cacheTTL = 3600;
+
+ if ( isset( $cacheUsage['api.task'] ) ) {
+ $cacheTTL = $cacheUsage['api.task'];
+ }
+
+ $key = self::makeCacheKey( 'duplookup' );
+
+ // Guard against repeated API calls (or fuzzing)
+ if ( ( $result = $cache->fetch( $key ) ) !== false && $cacheTTL !== false ) {
+ return $result + ['isFromCache' => true ];
+ }
+
+ $rows = $applicationFactory->getStore()->getObjectIds()->findDuplicates();
+
+ // Avoid "Exception caught: Serialization of 'Closure' is not allowedException ..."
+ if ( $rows instanceof Iterator ) {
+ $rows = iterator_to_array( $rows );
+ }
+
+ $result = [
+ 'list' => $rows,
+ 'count' => count( $rows ),
+ 'time' => time()
+ ];
+
+ $cache->save( $key, $result, $cacheTTL );
+
+ return $result;
+ }
+
+ private function callCheckQueryTask( $parameters ) {
+
+ if ( $parameters['subject'] === '' || $parameters['query'] === '' ) {
+ return [ 'done' => false ];
+ }
+
+ $store = ApplicationFactory::getInstance()->getStore();
+
+ $subject = DIWikiPage::doUnserialize(
+ $parameters['subject']
+ );
+
+ foreach ( $parameters['query'] as $hash => $raw_query ) {
+
+ // @see PostProcHandler::addQuery
+ list( $query_hash, $result_hash ) = explode( '#', $hash );
+
+ // Doesn't influence the fingerprint (aka query cache) so just
+ // ignored it
+ $printouts = [];
+ $parameters = $raw_query['parameters'];
+
+ if ( isset( $parameters['sortkeys'] ) ) {
+ $order = [];
+ $sort = [];
+
+ foreach ( $parameters['sortkeys'] as $key => $order_by ) {
+ $order[] = strtolower( $order_by );
+ $sort[] = $key;
+ }
+
+ $parameters['sort'] = implode( ',', $sort );
+ $parameters['order'] = implode( ',', $order );
+ }
+
+ QueryProcessor::addThisPrintout( $printouts, $parameters );
+
+ $query = QueryProcessor::createQuery(
+ $raw_query['conditions'],
+ QueryProcessor::getProcessedParams( $parameters, $printouts ),
+ QueryProcessor::INLINE_QUERY,
+ '',
+ $printouts
+ );
+
+ $query->setLimit(
+ $parameters['limit']
+ );
+
+ $query->setOffset(
+ $parameters['offset']
+ );
+
+ $query->setQueryMode(
+ $parameters['querymode']
+ );
+
+ $query->setContextPage(
+ $subject
+ );
+
+ $query->setOption( Query::PROC_CONTEXT, 'task.api' );
+
+ $res = $store->getQueryResult(
+ $query
+ );
+
+ // If the result_hash from before the post-edit and the result_hash
+ // after the post-edit check are not the same then it means that the
+ // list of entities changed hence send a `reload` command to the
+ // API promise.
+ if ( $result_hash !== $res->getHash( 'quick' ) ) {
+ return [ 'done' => true, 'reload' => true ];
+ }
+ }
+
+ return [ 'done' => true ];
+ }
+
+ private function callGenericJobTask( $params ) {
+
+ $this->checkParameters( $params );
+
+ if ( $params['subject'] === '' ) {
+ return ['done' => false ];
+ }
+
+ $title = DIWikiPage::doUnserialize( $params['subject'] )->getTitle();
+
+ if ( $title === null ) {
+ return ['done' => false ];
+ }
+
+ if ( !isset( $params['job'] ) ) {
+ return ['done' => false ];
+ }
+
+ $parameters = [];
+
+ if ( isset( $params['parameters'] ) ) {
+ $parameters = $params['parameters'];
+ }
+
+ $jobFactory = ApplicationFactory::getInstance()->newJobFactory();
+
+ $job = $jobFactory->newByType(
+ $params['job'],
+ $title,
+ $parameters
+ );
+
+ $job->insert();
+ }
+
+ private function callUpdateTask( $parameters ) {
+
+ $this->checkParameters( $parameters );
+
+ if ( !isset( $parameters['subject'] ) || $parameters['subject'] === '' ) {
+ return [ 'done' => false ];
+ }
+
+ $subject = DIWikiPage::doUnserialize( $parameters['subject'] );
+ $title = $subject->getTitle();
+ $log = [];
+
+ if ( $title === null ) {
+ return ['done' => false ];
+ }
+
+ // Each single update is required to allow for a cascading computation
+ // where one query follows another to ensure that results are updated
+ // according to the value dependency of the referenced annotations that
+ // rely on a computed (#ask) value
+ if ( !isset( $parameters['ref'] ) ) {
+ $parameters['ref'] = [ $subject->getHash() ];
+ }
+
+ $jobFactory = ApplicationFactory::getInstance()->newJobFactory();
+ $isPost = isset( $parameters['post'] ) ? $parameters['post'] : false;
+ $origin = [];
+
+ if ( isset( $parameters['origin'] ) ) {
+ $origin = [ 'origin' => $parameters['origin'] ];
+ }
+
+ foreach ( $parameters['ref'] as $ref ) {
+ $updateJob = $jobFactory->newUpdateJob(
+ $title,
+ [
+ UpdateJob::FORCED_UPDATE => true,
+ Enum::OPT_SUSPEND_PURGE => false,
+ 'ref' => $ref
+ ] + $origin
+ );
+
+ if ( $isPost ) {
+ $updateJob->insert();
+ } else {
+ $updateJob->run();
+ }
+ }
+
+ return [ 'done' => true, 'log' => $log ];
+ }
+
+ private function callJobListTask( $parameters ) {
+
+ $this->checkParameters( $parameters );
+
+ if ( !isset( $parameters['subject'] ) || $parameters['subject'] === '' ) {
+ return [ 'done' => false ];
+ }
+
+ $subject = DIWikiPage::doUnserialize( $parameters['subject'] );
+ $title = $subject->getTitle();
+
+ if ( $title === null ) {
+ return [ 'done' => false ];
+ }
+
+ $jobQueue = ApplicationFactory::getInstance()->getJobQueue();
+ $jobList = [];
+
+ if ( isset( $parameters['jobs'] ) ) {
+ $jobList = $parameters['jobs'];
+ }
+
+ $log = $jobQueue->runFromQueue(
+ $jobList
+ );
+
+ return [ 'done' => true, 'log' => $log ];
+ }
+
+ private function checkParameters( $parameters ) {
+ if ( json_last_error() !== JSON_ERROR_NONE || !is_array( $parameters ) ) {
+
+ // 1.29+
+ if ( method_exists( $this, 'dieWithError' ) ) {
+ $this->dieWithError( [ 'smw-api-invalid-parameters' ] );
+ } else {
+ $this->dieUsageMsg( 'smw-api-invalid-parameters' );
+ }
+ }
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getAllowedParams
+ *
+ * @return array
+ */
+ public function getAllowedParams() {
+ return [
+ 'task' => [
+ ApiBase::PARAM_REQUIRED => true,
+ ApiBase::PARAM_TYPE => [
+
+ // Run update using the updateJob
+ 'update',
+
+ // Run a query check
+ 'check-query',
+
+ // Duplicate lookup support
+ 'duplookup',
+
+ // Insert/run a job
+ 'job',
+
+ // Run jobs from a list directly without the job scheduler
+ 'run-joblist'
+ ]
+ ],
+ 'params' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => false,
+ ],
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getParamDescription
+ *
+ * @return array
+ */
+ public function getParamDescription() {
+ return [
+ 'task' => 'Defines the task type',
+ 'params' => 'JSON encoded parameters that matches the selected type requirement'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getDescription
+ *
+ * @return array
+ */
+ public function getDescription() {
+ return [
+ 'Semantic MediaWiki API module to invoke and execute tasks (for internal use only)'
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::needsToken
+ */
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::mustBePosted
+ */
+ public function mustBePosted() {
+ return true;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::isWriteMode
+ */
+ public function isWriteMode() {
+ return true;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getExamples
+ *
+ * @return array
+ */
+ protected function getExamples() {
+ return [
+ 'api.php?action=smwtask&task=update&params={ "subject": "Foo" }',
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @see ApiBase::getVersion
+ *
+ * @return string
+ */
+ public function getVersion() {
+ return __CLASS__ . ':' . SMW_VERSION;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Collator.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Collator.php
new file mode 100644
index 00000000..5bf10d51
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Collator.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Collation;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Collator {
+
+ /**
+ * @var Collator
+ */
+ private static $instance = [];
+
+ /**
+ * @var Collation
+ */
+ private $collation;
+
+ /**
+ * @var string
+ */
+ private $collationName;
+
+ /**
+ * @private
+ *
+ * @since 3.0
+ *
+ * @param Collation $collation
+ * @param string $collationName
+ */
+ public function __construct( Collation $collation, $collationName = '' ) {
+ $this->collation = $collation;
+ $this->collationName = $collationName;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param srtring $collationName
+ *
+ * @return Collator
+ */
+ public static function singleton( $collationName = '' ) {
+
+ $collationName = $collationName === '' ? $GLOBALS['smwgEntityCollation'] : $collationName;
+
+ if ( !isset( self::$instance[$collationName] ) ) {
+ self::$instance[$collationName] = new self( Collation::factory( $collationName ), $collationName );
+ }
+
+ return self::$instance[$collationName];
+ }
+
+ /**
+ * For any uca-* generated sortkey armor any invalid or unrecognized UTF-8
+ * characters to prevent an invalid XML/UTF output.
+ *
+ * Characters that cannot be expressed are replaced by ? which is surely
+ * inaccurate in comparison to the original uca-* sortkey but it allows to
+ * replicate a near surrogate string to a back-end that requires XML
+ * compliance (triple store).
+ *
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ public function armor( $text, $source = '' ) {
+
+ if ( strpos( $this->collationName, 'uca' ) === false ) {
+ return $text;
+ }
+
+ // $text = mb_convert_encoding( $text, 'UTF-8' );
+
+ // https://magp.ie/2011/01/06/remove-non-utf8-characters-from-string-with-php/
+ // Remove all none utf-8 symbols
+ $text = str_replace( '�', '', htmlspecialchars( $text, ENT_SUBSTITUTE, 'UTF-8' ) );
+
+ // remove non-breaking spaces and other non-standard spaces
+ $text = preg_replace( '~\s+~u', '?', $text );
+
+ // replace controls symbols with "?"
+ $text = preg_replace( '~\p{C}+~u', '?', $text );
+
+ return $text;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ public function getSortKey( $text ) {
+ return $this->collation->getSortKey( $text );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ public function getFirstLetter( $text ) {
+
+ // Add check otherwise the Collation instance returns with a
+ // "Uninitialized string offset: 0"
+ if ( $text === '' ) {
+ return '';
+ }
+
+ return $this->collation->getFirstLetter( $text );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $old
+ * @param string $new
+ *
+ * @return boolean
+ */
+ public function isIdentical( $old, $new ) {
+ return $this->collation->getSortKey( $old ) === $this->collation->getSortKey( $new );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/ConnectionProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/ConnectionProvider.php
new file mode 100644
index 00000000..390cb07d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/ConnectionProvider.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace SMW\MediaWiki\Connection;
+
+use Psr\Log\LoggerAwareTrait;
+use RuntimeException;
+use SMW\Connection\ConnectionProvider as IConnectionProvider;
+use SMW\Connection\ConnectionProviderRef;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class ConnectionProvider implements IConnectionProvider {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var string
+ */
+ private $provider;
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var array
+ */
+ private $localConnectionConf = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param string|null $provider
+ */
+ public function __construct( $provider = null ) {
+ $this->provider = $provider;
+ }
+
+ /**
+ * @see #2532
+ *
+ * @param array $localConnectionConf
+ *
+ * @since 3.0
+ */
+ public function setLocalConnectionConf( array $localConnectionConf ) {
+ $this->localConnectionConf = $localConnectionConf;
+ }
+
+ /**
+ * @see IConnectionProvider::getConnection
+ *
+ * @since 2.1
+ *
+ * @return Database
+ */
+ public function getConnection() {
+
+ if ( $this->connection !== null ) {
+ return $this->connection;
+ }
+
+ // Default configuration
+ $conf = [
+ 'read' => DB_SLAVE,
+ 'write' => DB_MASTER
+ ];
+
+ if ( isset( $this->localConnectionConf[$this->provider] ) ) {
+ $conf = $this->localConnectionConf[$this->provider];
+ }
+
+ return $this->connection = $this->createConnection( $conf );
+ }
+
+ /**
+ * @see IConnectionProvider::releaseConnection
+ *
+ * @since 2.1
+ */
+ public function releaseConnection() {
+
+ if ( $this->connection !== null ) {
+ $this->connection->releaseConnection();
+ }
+
+ $this->connection = null;
+ }
+
+ private function createConnection( $conf ) {
+
+ if ( isset( $conf['callback'] ) && is_callable( $conf['callback'] ) ) {
+ return call_user_func( $conf['callback'] );
+ }
+
+ if ( !isset( $conf['read'] ) || !isset( $conf['write'] ) ) {
+ throw new RuntimeException( "The configuration is incomplete (requires a `read` and `write` identifier)." );
+ }
+
+ $connectionProviders = [];
+
+ $connectionProviders['read'] = new LoadBalancerConnectionProvider(
+ $conf['read']
+ );
+
+ if ( $conf['read'] === $conf['write'] ) {
+ $connectionProviders['write'] = $connectionProviders['read'];
+ } else {
+ $connectionProviders['write'] = new LoadBalancerConnectionProvider(
+ $conf['write']
+ );
+ }
+
+ $transactionProfiler = new TransactionProfiler(
+ \Profiler::instance()->getTransactionProfiler()
+ );
+
+ $transactionProfiler->silenceTransactionProfiler();
+
+ $connection = new Database(
+ new ConnectionProviderRef( $connectionProviders )
+ );
+
+ $connection->setTransactionProfiler(
+ $transactionProfiler
+ );
+
+ // Only required because of SQlite
+ $connection->setDBPrefix( $GLOBALS['wgDBprefix'] );
+
+ $this->logger->info(
+ [
+ 'Connection',
+ '{provider}: {conf}',
+ ],
+ [
+ 'role' => 'developer',
+ 'provider' => $this->provider,
+ 'conf' => [
+ 'read' => $conf['read'] === DB_SLAVE ? 'DB_SLAVE' : 'DB_MASTER',
+ 'write' => $conf['write'] === DB_SLAVE ? 'DB_SLAVE' : 'DB_MASTER',
+ ]
+ ]
+ );
+
+ return $connection;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Database.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Database.php
new file mode 100644
index 00000000..244e0723
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Database.php
@@ -0,0 +1,907 @@
+<?php
+
+namespace SMW\MediaWiki\Connection;
+
+use DBError;
+use Exception;
+use ResultWrapper;
+use RuntimeException;
+use SMW\ApplicationFactory;
+use SMW\Connection\ConnectionProviderRef;
+use UnexpectedValueException;
+
+/**
+ * This adapter class covers MW DB specific operations. Changes to the
+ * interface are likely therefore this class should not be used other than by
+ * SMW itself.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class Database {
+
+ /**
+ * Identifies a request to be executed using an auto commit state
+ *
+ * @note (#1605 "... creating temporary tables in a transaction is not
+ * replication-safe and causes errors in MySQL 5.6. ...")
+ */
+ const AUTO_COMMIT = 'auto.commit';
+
+ /**
+ * @see IDatabase::TRIGGER_ROLLBACK
+ */
+ const TRIGGER_ROLLBACK = 3;
+
+ /**
+ * @var ConnectionProviderRef
+ */
+ private $connectionProviderRef;
+
+ /**
+ * @var ILBFactory
+ */
+ private $loadBalancerFactory;
+
+ /**
+ * @var Database
+ */
+ private $readConnection;
+
+ /**
+ * @var Database
+ */
+ private $writeConnection;
+
+ /**
+ * @var string
+ */
+ private $dbPrefix = '';
+
+ /**
+ * @var TransactionProfiler
+ */
+ private $transactionProfiler;
+
+ /**
+ * @var boolean
+ */
+ private $initConnection = false;
+
+ /**
+ * @var boolean
+ */
+ private $autoCommit = false;
+
+ /**
+ * @var integer
+ */
+ private $insertId = null;
+
+ /**
+ * @since 1.9
+ *
+ * @param ConnectionProviderRef $connectionProviderRef
+ * @param ILBFactory|null $loadBalancerFactory
+ */
+ public function __construct( ConnectionProviderRef $connectionProviderRef, $loadBalancerFactory = null ) {
+ $this->connectionProviderRef = $connectionProviderRef;
+ $this->loadBalancerFactory = $loadBalancerFactory;
+
+ if ( $this->loadBalancerFactory === null ) {
+ $this->loadBalancerFactory = ApplicationFactory::getInstance()->create( 'DBLoadBalancerFactory' );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param TransactionProfiler $transactionProfiler
+ */
+ public function setTransactionProfiler( TransactionProfiler $transactionProfiler ) {
+ $this->transactionProfiler = $transactionProfiler;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $type
+ *
+ * @return boolean
+ */
+ public function releaseConnection() {
+ $this->connectionProviderRef->releaseConnection();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function ping() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Query
+ */
+ public function newQuery() {
+ return new Query( $this );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $type
+ *
+ * @return boolean
+ */
+ public function isType( $type ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->getType() === $type;
+ }
+
+ /**
+ * @see DatabaseBase::getServerInfo
+ *
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getInfo() {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return [ $this->getType() => $this->readConnection->getServerInfo() ];
+ }
+
+ /**
+ * @see DatabaseBase::getType
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ public function getType() {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->getType();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $dbPrefix
+ */
+ public function setDBPrefix( $dbPrefix ) {
+ $this->dbPrefix = $dbPrefix;
+ }
+
+ /**
+ * @see DatabaseBase::tableName
+ *
+ * @since 1.9
+ *
+ * @param string $tableName
+ *
+ * @return string
+ */
+ public function tableName( $tableName ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ if ( $this->getType() === 'sqlite' ) {
+ return $this->dbPrefix . $tableName;
+ }
+
+ return $this->readConnection->tableName( $tableName );
+ }
+
+ /**
+ * @see DatabaseBase::timestamp
+ *
+ * @since 3.0
+ *
+ * @param integer $ts
+ *
+ * @return string
+ */
+ public function timestamp( $ts = 0 ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->timestamp( $ts );
+ }
+
+ /**
+ * @see DatabaseBase::tablePrefix
+ *
+ * @since 3.0
+ *
+ * @param string $prefix
+ *
+ * @return string
+ */
+ public function tablePrefix( $prefix = null ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->tablePrefix( $prefix );
+ }
+
+ /**
+ * @see DatabaseBase::addQuotes
+ *
+ * @since 1.9
+ *
+ * @param string $tableName
+ *
+ * @return string
+ */
+ public function addQuotes( $value ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->addQuotes( $value );
+ }
+
+ /**
+ * @see DatabaseBase::fetchObject
+ *
+ * @since 1.9
+ *
+ * @param ResultWrapper $res
+ *
+ * @return string
+ */
+ public function fetchObject( $res ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->fetchObject( $res );
+ }
+
+ /**
+ * @see DatabaseBase::numRows
+ *
+ * @since 1.9
+ *
+ * @param mixed $results
+ *
+ * @return integer
+ */
+ public function numRows( $results ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->numRows( $results );
+ }
+
+ /**
+ * @see DatabaseBase::freeResult
+ *
+ * @since 1.9
+ *
+ * @param ResultWrapper $res
+ */
+ public function freeResult( $res ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ $this->readConnection->freeResult( $res );
+ }
+
+ /**
+ * @see DatabaseBase::select
+ *
+ * @since 1.9
+ *
+ * @param string $tableName
+ * @param $fields
+ * @param $conditions
+ * @param array $options
+ * @param array $joinConditions
+ *
+ * @return ResultWrapper
+ * @throws UnexpectedValueException
+ */
+ public function select( $tableName, $fields, $conditions = '', $fname, array $options = [], $joinConditions = [] ) {
+
+ $tablePrefix = null;
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ if ( $this->getType() === 'sqlite' ) {
+
+ // MW's SQLite implementation adds an auto prefix to the tableName but
+ // not to the conditions and since ::tableName will handle prefixing
+ // consistently ensure that the select doesn't add an extra prefix
+ $tablePrefix = $this->readConnection->tablePrefix( '' );
+
+ if ( isset( $options['ORDER BY'] ) ) {
+ $options['ORDER BY'] = str_replace( 'RAND', 'RANDOM', $options['ORDER BY'] );
+ }
+ }
+
+ try {
+ $results = $this->readConnection->select(
+ $tableName,
+ $fields,
+ $conditions,
+ $fname,
+ $options,
+ $joinConditions
+ );
+ } catch ( DBError $e ) {
+ throw new RuntimeException ( $e->getMessage() . "\n" . $e->getTraceAsString() );
+ }
+
+ if ( $tablePrefix !== null ) {
+ $this->readConnection->tablePrefix( $tablePrefix );
+ }
+
+ if ( $results instanceof ResultWrapper ) {
+ return $results;
+ }
+
+ throw new UnexpectedValueException (
+ 'Expected a ResultWrapper for ' . "\n" .
+ $tableName . "\n" .
+ $fields . "\n" .
+ $conditions
+ );
+ }
+
+ /**
+ * @see DatabaseBase::query
+ *
+ * @since 1.9
+ *
+ * @param Query|string $sql
+ * @param string $fname
+ * @param boolean $ignoreException
+ *
+ * @return ResultWrapper
+ * @throws RuntimeException
+ */
+ public function query( $sql, $fname = __METHOD__, $ignoreException = false ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ if ( $sql instanceof Query ) {
+ $sql = $sql->build();
+ }
+
+ if ( !$this->isType( 'postgres' ) ) {
+ $sql = str_replace( '@INT', '', $sql );
+ }
+
+ if ( $this->isType( 'postgres' ) ) {
+ $sql = str_replace( '@INT', '::integer', $sql );
+ $sql = str_replace( 'IGNORE', '', $sql );
+ $sql = str_replace( 'DROP TEMPORARY TABLE', 'DROP TABLE IF EXISTS', $sql );
+ $sql = str_replace( 'RAND()', ( strpos( $sql, 'DISTINCT' ) !== false ? '' : 'RANDOM()' ), $sql );
+ }
+
+ if ( $this->isType( 'sqlite' ) ) {
+ $sql = str_replace( 'IGNORE', '', $sql );
+ $sql = str_replace( 'TEMPORARY', 'TEMP', $sql );
+ $sql = str_replace( 'ENGINE=MEMORY', '', $sql );
+ $sql = str_replace( 'DROP TEMP', 'DROP', $sql );
+ $sql = str_replace( 'TRUNCATE TABLE', 'DELETE FROM', $sql );
+ $sql = str_replace( 'RAND', 'RANDOM', $sql );
+ }
+
+ // https://github.com/wikimedia/mediawiki/blob/42d5e6f43a00eb8bedc3532876125f74e3188343/includes/deferred/AutoCommitUpdate.php
+ // https://github.com/wikimedia/mediawiki/blob/f7dad57c64db3eb1296894c2d3ae97b9f7f27c4c/includes/installer/DatabaseInstaller.php#L157
+ if ( $this->autoCommit ) {
+ $autoTrx = $this->writeConnection->getFlag( DBO_TRX );
+ $this->writeConnection->clearFlag( DBO_TRX );
+
+ if ( $autoTrx && $this->writeConnection->trxLevel() ) {
+ $this->writeConnection->commit( __METHOD__ );
+ }
+ }
+
+ try {
+ $exception = null;
+ $results = $this->writeConnection->query(
+ $sql,
+ $fname,
+ $ignoreException
+ );
+ } catch ( Exception $exception ) {
+ }
+
+ if ( $this->autoCommit && $autoTrx ) {
+ $this->writeConnection->setFlag( DBO_TRX );
+ }
+
+ // State is only valid for a single transaction
+ $this->autoCommit = false;
+
+ if ( $exception ) {
+ throw $exception;
+ }
+
+ return $results;
+ }
+
+ /**
+ * @see DatabaseBase::selectRow
+ *
+ * @since 1.9
+ */
+ public function selectRow( $table, $vars, $conds, $fname = __METHOD__, $options = [], $joinConditions = [] ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->selectRow(
+ $table,
+ $vars,
+ $conds,
+ $fname,
+ $options,
+ $joinConditions
+ );
+ }
+
+ /**
+ * @see DatabaseBase::affectedRows
+ *
+ * @since 1.9
+ *
+ * @return int
+ */
+ function affectedRows() {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->affectedRows();
+ }
+
+ /**
+ * @note Method was made protected in 1.28, hence the need
+ * for the DatabaseHelper that copies the functionality.
+ *
+ * @see DatabaseBase::makeSelectOptions
+ *
+ * @since 1.9
+ *
+ * @param array $options
+ *
+ * @return array
+ */
+ public function makeSelectOptions( $options ) {
+ return OptionsBuilder::makeSelectOptions( $this, $options );
+ }
+
+ /**
+ * @see DatabaseBase::nextSequenceValue
+ *
+ * @since 1.9
+ *
+ * @param string $seqName
+ *
+ * @return int|null
+ */
+ public function nextSequenceValue( $seqName ) {
+ $this->insertId = null;
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ if ( !$this->isType( 'postgres' ) ) {
+ return null;
+ }
+
+ // #3101, #2903
+ // MW 1.31+
+ // https://github.com/wikimedia/mediawiki/commit/0a9c55bfd39e22828f2d152ab71789cef3b0897c#diff-278465351b7c14bbcadac82036080e9f
+ $safeseq = str_replace( "'", "''", $seqName );
+ $res = $this->writeConnection->query( "SELECT nextval('$safeseq')" );
+ $row = $this->readConnection->fetchRow( $res );
+
+ return $this->insertId = is_null( $row[0] ) ? null : (int)$row[0];
+ }
+
+ /**
+ * @see DatabaseBase::insertId
+ *
+ * @since 1.9
+ *
+ * @return int
+ */
+ function insertId() {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ if ( $this->insertId !== null ) {
+ return $this->insertId;
+ }
+
+ return (int)$this->writeConnection->insertId();
+ }
+
+ /**
+ * @see DatabaseBase::clearFlag
+ *
+ * @since 2.4
+ */
+ function clearFlag( $flag ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ $this->writeConnection->clearFlag( $flag );
+ }
+
+ /**
+ * @see DatabaseBase::getFlag
+ *
+ * @since 2.4
+ */
+ function getFlag( $flag ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->writeConnection->getFlag( $flag );
+ }
+
+ /**
+ * @see DatabaseBase::setFlag
+ *
+ * @since 2.4
+ */
+ function setFlag( $flag ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ if ( $flag === self::AUTO_COMMIT ) {
+ return $this->autoCommit = true;
+ }
+
+ $this->writeConnection->setFlag( $flag );
+ }
+
+ /**
+ * @see DatabaseBase::insert
+ *
+ * @since 1.9
+ */
+ public function insert( $table, $rows, $fname = __METHOD__, $options = [] ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ $oldSilenced = $this->transactionProfiler->setSilenced(
+ true
+ );
+
+ $res = $this->writeConnection->insert( $table, $rows, $fname, $options );
+
+ $this->transactionProfiler->setSilenced(
+ $oldSilenced
+ );
+
+ return $res;
+ }
+
+ /**
+ * @see DatabaseBase::update
+ *
+ * @since 1.9
+ */
+ function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ $oldSilenced = $this->transactionProfiler->setSilenced(
+ true
+ );
+
+ $res = $this->writeConnection->update( $table, $values, $conds, $fname, $options );
+
+ $this->transactionProfiler->setSilenced(
+ $oldSilenced
+ );
+
+ return $res;
+ }
+
+ /**
+ * @see DatabaseBase::delete
+ *
+ * @since 1.9
+ */
+ public function delete( $table, $conds, $fname = __METHOD__ ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ $oldSilenced = $this->transactionProfiler->setSilenced(
+ true
+ );
+
+ $res = $this->writeConnection->delete( $table, $conds, $fname );
+
+ $this->transactionProfiler->setSilenced(
+ $oldSilenced
+ );
+
+ return $res;
+ }
+
+ /**
+ * @see DatabaseBase::replace
+ *
+ * @since 2.5
+ */
+ public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ $oldSilenced = $this->transactionProfiler->setSilenced(
+ true
+ );
+
+ $res = $this->writeConnection->replace( $table, $uniqueIndexes, $rows, $fname );
+
+ $this->transactionProfiler->setSilenced(
+ $oldSilenced
+ );
+
+ return $res;
+ }
+
+ /**
+ * @see DatabaseBase::makeList
+ *
+ * @since 1.9
+ */
+ public function makeList( $data, $mode ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->writeConnection->makeList( $data, $mode );
+ }
+
+ /**
+ * @see DatabaseBase::tableExists
+ *
+ * @since 1.9
+ *
+ * @param string $table
+ * @param string $fname
+ *
+ * @return bool
+ */
+ public function tableExists( $table, $fname = __METHOD__ ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->tableExists( $table, $fname );
+ }
+
+ /**
+ * @see DatabaseBase::selectField
+ *
+ * @since 1.9.2
+ */
+ public function selectField( $table, $fieldName, $conditions = '', $fname = __METHOD__, $options = [] ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->selectField( $table, $fieldName, $conditions, $fname, $options );
+ }
+
+ /**
+ * @see DatabaseBase::estimateRowCount
+ *
+ * @since 2.1
+ */
+ public function estimateRowCount( $table, $vars = '*', $conditions = '', $fname = __METHOD__, $options = [] ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ return $this->readConnection->estimateRowCount(
+ $table,
+ $vars,
+ $conditions,
+ $fname,
+ $options
+ );
+ }
+
+ /**
+ * @note Only supported with 1.28+
+ * @since 3.0
+ *
+ * @param string $fname Caller name (e.g. __METHOD__)
+ *
+ * @return mixed A value to pass to commitAndWaitForReplication
+ */
+ public function getEmptyTransactionTicket( $fname = __METHOD__ ) {
+
+ $ticket = null;
+
+ if ( !method_exists( $this->loadBalancerFactory, 'getEmptyTransactionTicket' ) ) {
+ return $ticket;
+ }
+
+ // @see LBFactory::getEmptyTransactionTicket
+ // We don't try very hard at this point and will continue without a ticket
+ // if the check fails and hereby avoid a "... does not have outer scope" error
+ if ( !$this->loadBalancerFactory->hasMasterChanges() ) {
+ $ticket = $this->loadBalancerFactory->getEmptyTransactionTicket( $fname );
+ }
+
+ return $ticket;
+ }
+
+ /**
+ * Convenience method for safely running commitMasterChanges/waitForReplication
+ * where it will allow to commit and wait for whena TransactionTicket is
+ * available.
+ *
+ * @note Only supported with 1.28+
+ *
+ * @since 3.0
+ *
+ * @param string $fname Caller name (e.g. __METHOD__)
+ * @param mixed $ticket Result of Database::getEmptyTransactionTicket
+ * @param array $opts Options to waitForReplication
+ */
+ public function commitAndWaitForReplication( $fname, $ticket, array $opts = [] ) {
+
+ if ( !is_int( $ticket ) || !method_exists( $this->loadBalancerFactory, 'commitAndWaitForReplication' ) ) {
+ return;
+ }
+
+ return $this->loadBalancerFactory->commitAndWaitForReplication( $fname, $ticket, $opts );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $fname
+ */
+ public function beginAtomicTransaction( $fname = __METHOD__ ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ // MW 1.23
+ if ( !method_exists( $this->writeConnection, 'startAtomic' ) ) {
+ return null;
+ }
+
+ $this->writeConnection->startAtomic( $fname );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $fname
+ */
+ public function endAtomicTransaction( $fname = __METHOD__ ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ // MW 1.23
+ if ( !method_exists( $this->writeConnection, 'endAtomic' ) ) {
+ return null;
+ }
+
+ $this->writeConnection->endAtomic( $fname );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param callable $callback
+ */
+ public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ if ( method_exists( $this->writeConnection, 'onTransactionResolution' ) && $this->writeConnection->trxLevel() ) {
+ $this->writeConnection->onTransactionResolution( $callback, $fname );
+ }
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param callable $callback
+ */
+ public function onTransactionIdle( $callback ) {
+
+ if ( $this->initConnection === false ) {
+ $this->initConnection();
+ }
+
+ // FIXME For 1.19 it is an unknown method hence execute without idle
+ if ( !method_exists( $this->writeConnection, 'onTransactionIdle' ) ) {
+ return call_user_func( $callback );
+ }
+
+ $this->writeConnection->onTransactionIdle( $callback );
+ }
+
+ private function initConnection() {
+
+ if ( $this->readConnection === null ) {
+ $this->readConnection = $this->connectionProviderRef->getConnection( 'read' );
+ }
+
+ if ( $this->writeConnection === null && $this->connectionProviderRef->hasConnection( 'write' ) ) {
+ $this->writeConnection = $this->connectionProviderRef->getConnection( 'write' );
+ }
+
+ $this->initConnection = true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/LoadBalancerConnectionProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/LoadBalancerConnectionProvider.php
new file mode 100644
index 00000000..a309a75c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/LoadBalancerConnectionProvider.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace SMW\MediaWiki\Connection;
+
+use DatabaseBase;
+use Psr\Log\LoggerAwareTrait;
+use RuntimeException;
+use SMW\Connection\ConnectionProvider as IConnectionProvider;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class LoadBalancerConnectionProvider implements IConnectionProvider {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var DatabaseBase|null
+ */
+ protected $connection = null;
+
+ /**
+ * @var int|null
+ */
+ protected $id = null;
+
+ /**
+ * @var string|array
+ */
+ protected $groups;
+
+ /**
+ * @var string|boolean $wiki
+ */
+ protected $wiki;
+
+ /**
+ * @since 1.9
+ *
+ * @param int $id
+ * @param string|array $groups
+ * @param string|boolean $wiki
+ */
+ public function __construct( $id, $groups = [], $wiki = false ) {
+ $this->id = $id;
+ $this->groups = $groups;
+ $this->wiki = $wiki;
+ }
+
+ /**
+ * @see IConnectionProvider::getConnection
+ *
+ * @since 1.9
+ *
+ * @return DatabaseBase
+ * @throws RuntimeException
+ */
+ public function getConnection() {
+
+ if ( $this->connection === null ) {
+ $this->connection = wfGetLB( $this->wiki )->getConnection( $this->id, $this->groups, $this->wiki );
+ }
+
+ if ( $this->connection instanceof DatabaseBase ) {
+ return $this->connection;
+ }
+
+ throw new RuntimeException( 'Expected a DatabaseBase instance' );
+ }
+
+ /**
+ * @see IConnectionProvider::releaseConnection
+ *
+ * @since 1.9
+ */
+ public function releaseConnection() {
+ if ( $this->wiki !== false && $this->connection !== null ) {
+ wfGetLB( $this->wiki )->reuseConnection( $this->connection );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/OptionsBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/OptionsBuilder.php
new file mode 100644
index 00000000..97d64e7f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/OptionsBuilder.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace SMW\MediaWiki\Connection;
+
+/**
+ * https://phabricator.wikimedia.org/T147550
+ *
+ * The contract of the Database interface has changed in MW 1.28 and introduced
+ * incompatibilities which this class tries to bypass.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class OptionsBuilder {
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function toString( array $options ) {
+
+ $string = '';
+
+ if ( isset( $options['GROUP BY'] ) ) {
+ $string .= ' GROUP BY ' . ( is_array( $options['GROUP BY'] ) ? implode( ',', $options['GROUP BY'] ) : $options['GROUP BY'] );
+ }
+
+ $string .= self::makeOrderBy( $options );
+
+ if ( isset( $options['LIMIT'] ) ) {
+ $string .= ' LIMIT ' . $options['LIMIT'];
+ }
+
+ if ( isset( $options['OFFSET'] ) ) {
+ $string .= ' OFFSET ' . $options['OFFSET'];
+ }
+
+ return $string;
+ }
+
+ /**
+ * @see Database::makeSelectOptions
+ */
+ public static function makeSelectOptions( Database $connection, $options ) {
+ $preLimitTail = $postLimitTail = '';
+ $startOpts = '';
+
+ $noKeyOptions = [];
+
+ foreach ( $options as $key => $option ) {
+ if ( is_numeric( $key ) ) {
+ $noKeyOptions[$option] = true;
+ }
+ }
+
+ $preLimitTail .= self::makeGroupByWithHaving( $connection, $options );
+
+ $preLimitTail .= self::makeOrderBy( $options );
+
+ // if (isset($options['LIMIT'])) {
+ // $tailOpts .= $this->limitResult('', $options['LIMIT'],
+ // isset($options['OFFSET']) ? $options['OFFSET']
+ // : false);
+ // }
+
+ if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
+ $postLimitTail .= ' FOR UPDATE';
+ }
+
+ if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
+ $postLimitTail .= ' LOCK IN SHARE MODE';
+ }
+
+ if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
+ $startOpts .= 'DISTINCT';
+ }
+
+ # Various MySQL extensions
+ if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
+ $startOpts .= ' /*! STRAIGHT_JOIN */';
+ }
+
+ if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
+ $startOpts .= ' HIGH_PRIORITY';
+ }
+
+ if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
+ $startOpts .= ' SQL_BIG_RESULT';
+ }
+
+ if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
+ $startOpts .= ' SQL_BUFFER_RESULT';
+ }
+
+ if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
+ $startOpts .= ' SQL_SMALL_RESULT';
+ }
+
+ if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
+ $startOpts .= ' SQL_CALC_FOUND_ROWS';
+ }
+
+ if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
+ $startOpts .= ' SQL_CACHE';
+ }
+
+ if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
+ $startOpts .= ' SQL_NO_CACHE';
+ }
+
+ $useIndex = '';
+ $ignoreIndex = '';
+
+ return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
+ }
+
+ protected static function makeGroupByWithHaving( $connection, $options ) {
+ $sql = '';
+
+ if ( isset( $options['GROUP BY'] ) ) {
+ $gb = is_array( $options['GROUP BY'] )
+ ? implode( ',', $options['GROUP BY'] )
+ : $options['GROUP BY'];
+ $sql .= ' GROUP BY ' . $gb;
+ }
+
+ if ( isset( $options['HAVING'] ) ) {
+ $having = is_array( $options['HAVING'] )
+ ? $connection->makeList( $options['HAVING'], self::LIST_AND )
+ : $options['HAVING'];
+ $sql .= ' HAVING ' . $having;
+ }
+
+ return $sql;
+ }
+
+ protected static function makeOrderBy( $options ) {
+ if ( isset( $options['ORDER BY'] ) ) {
+ $ob = is_array( $options['ORDER BY'] )
+ ? implode( ',', $options['ORDER BY'] )
+ : $options['ORDER BY'];
+
+ return ' ORDER BY ' . $ob;
+ }
+
+ return '';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Query.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Query.php
new file mode 100644
index 00000000..7b55d457
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Query.php
@@ -0,0 +1,411 @@
+<?php
+
+namespace SMW\MediaWiki\Connection;
+
+use InvalidArgumentException;
+use RuntimeException;
+
+/**
+ * @private
+ *
+ * Convenience class with methods to generate a SQL query statement where value
+ * quotes and name transformations are done automatically.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Query {
+
+ const TYPE_SELECT = 'SELECT';
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var string
+ */
+ protected $type = '';
+
+ /**
+ * @var []
+ */
+ protected $table = '';
+
+ /**
+ * @var []
+ */
+ protected $fields = [];
+
+ /**
+ * @var []
+ */
+ protected $conditions = [];
+
+ /**
+ * @var []
+ */
+ protected $options = [];
+
+ /**
+ * @var []
+ */
+ private $joins = [];
+
+ /**
+ * @var string
+ */
+ public $alias = '';
+
+ /**
+ * @var integer
+ */
+ public $index = 0;
+
+ /**
+ * @var boolean
+ */
+ public $autoCommit = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param Database $connection
+ */
+ public function __construct( Database $connection ) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ * @throws InvalidArgumentException
+ */
+ public function type( $type ) {
+
+ $type = strtoupper( $type );
+
+ if ( !in_array( $type, [ self::TYPE_SELECT ] ) ) {
+ throw new InvalidArgumentException( "$type was not recognized as valid type!" );
+ }
+
+ $this->type = $type;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $fields
+ */
+ public function fields( array $fields ) {
+ $this->fields = $fields;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $field
+ */
+ public function field( ...$field ) {
+ $this->fields[] = $field;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasField( $field = '' ) {
+
+ if ( (string)$field === '' ) {
+ return $this->fields !== [];
+ }
+
+ return strpos( json_encode( $this->fields ), $field ) !== false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasCondition() {
+ return $this->conditions !== [];
+ }
+
+ /**
+ * Register the main table in form of ( 'foo' ) or as ( 'foo', 't1' ).
+ *
+ * @since 3.0
+ *
+ * @param string $table
+ */
+ public function table( ...$table ) {
+ $this->table = $this->connection->tableName( $table[0] ) . ( isset( $table[1] ) ? " AS " . $table[1] : '' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string ...$join
+ */
+ public function join( ...$join ) {
+
+ if ( strpos( $join[0], 'JOIN' ) === false ) {
+ throw new InvalidArgumentException( "A join type is missing!" );
+ }
+
+ // ->join( 'INNNER JOIN', [ Table_Foo => ... ] )
+ if ( is_array( $join[1] ) ) {
+ $joins = [];
+
+ foreach ( $join[1] as $table => $value ) {
+
+ if ( is_string( $table ) ) {
+ $value = $value{0} . $value{1} === 'ON' ? "$value" : "AS $value";
+ $value = $this->connection->tableName( $table ) . " $value";
+ }
+
+ $joins[] = $value;
+ }
+
+ $join[1] = implode( ' ', $joins );
+ }
+
+ $this->joins[] = $join;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $k
+ * @param string $v
+ *
+ * @return string
+ */
+ public function like( $k, $v ) {
+ return "$k LIKE " . $this->connection->addQuotes( $v );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $k
+ * @param string $v
+ *
+ * @return string
+ */
+ public function eq( $k, $v ) {
+ return "$k=" . $this->connection->addQuotes( $v );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $k
+ * @param string $v
+ *
+ * @return string
+ */
+ public function neq( $k, $v ) {
+ return "$k!=" . $this->connection->addQuotes( $v );
+ }
+
+ /**
+ * Supposed to be called `and` but this works only on PHP 7.1+.
+ *
+ * @since 3.0
+ *
+ * @param string $condition
+ *
+ * @return array
+ */
+ public function asAnd( $condition ) {
+ return [ 'AND' => $condition ];
+ }
+
+ /**
+ * Supposed to be called `or` but this works only on PHP 7.1+.
+ *
+ * @since 3.0
+ *
+ * @param string $condition
+ *
+ * @return array
+ */
+ public function asOr( $condition ) {
+ return [ 'OR' => $condition ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|array $condition
+ */
+ public function condition( $condition ) {
+
+ if ( is_string( $condition ) ) {
+ $condition = [ $condition ];
+ }
+
+ $this->conditions[] = $condition;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $options
+ */
+ public function options( array $options ) {
+ $this->options = $options;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function __toString() {
+
+ $params = [
+ 'tables' => $this->table,
+ 'fields' => $this->fields,
+ 'conditions' => $this->conditions,
+ 'joins' => $this->joins,
+ 'options' => $this->options,
+ 'alias' => $this->alias,
+ 'index' => $this->index,
+ 'autocommit' => $this->autoCommit
+ ];
+
+ return json_encode( $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public function build() {
+
+ $statement = $this->sql();
+
+ $this->type = '';
+ $this->table = '';
+ $this->conditions = [];
+ $this->options = [];
+ $this->joins = [];
+ $this->fields = [];
+ $this->alias = '';
+ $this->index = 0;
+
+ return $statement;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $fname
+ *
+ * @return iterable
+ */
+ public function execute( $fname ) {
+ return $this->connection->query( $this, $fname );
+ }
+
+ private function sql() {
+
+ $i = 0;
+ $sql = "";
+ $fields = [];
+
+ if ( $this->type === '' ) {
+ throw new RuntimeException( "Missing a type" );
+ } else {
+ $sql = "$this->type ";
+ }
+
+ if ( isset( $this->options['DISTINCT'] ) ) {
+ if ( is_bool( $this->options['DISTINCT'] ) ) {
+ $sql .= 'DISTINCT ';
+ } else {
+ $sql .= 'DISTINCT ' . $this->options['DISTINCT'] . ' ';
+ }
+ }
+
+ foreach ( $this->fields as $field ) {
+ $fields[] = is_array( $field ) ? implode( ' AS ', $field ) : $field;
+ }
+
+ if ( $fields === [] ) {
+ throw new RuntimeException( "Missing a field" );
+ }
+
+ $sql .= implode( ', ', $fields );
+ $sql .= ' FROM ';
+ $sql .= $this->table;
+
+ foreach ( $this->joins as $join ) {
+ $sql .= ' ' . implode( ' ', $join );
+ }
+
+ $conditions = [];
+
+ foreach ( $this->conditions as $condition ) {
+
+ foreach ( $condition as $exp => $cond ) {
+ if ( $i > 0 && is_int( $exp ) ) {
+ $exp = 'AND';
+ }
+
+ if ( is_array( $cond ) ) {
+ $cond = implode( " $exp ", $cond );
+ }
+
+ if ( $cond !== '' ) {
+
+ if ( $i > 0 && $exp === 'OR' ) {
+ $conditions = [ '(' . implode( ' ', $conditions ) . " OR ($cond))" ];
+ } else {
+ $conditions[] = $i == 0 ? "($cond)" : "$exp ($cond)";
+ }
+ }
+ }
+
+ $i++;
+ }
+
+ if ( $conditions !== [] ) {
+ $sql .= ' WHERE ' . implode( ' ', $conditions );
+ }
+
+ if ( isset( $this->options['GROUP BY'] ) ) {
+ $sql .= " GROUP BY " . $this->options['GROUP BY'];
+
+ if ( isset( $this->options['HAVING'] ) ) {
+ $sql .= " HAVING " . $this->options['HAVING'];
+ }
+ }
+
+ if ( isset( $this->options['ORDER BY'] ) ) {
+ $sql .= " ORDER BY " . $this->options['ORDER BY'];
+ }
+
+ if ( isset( $this->options['LIMIT'] ) ) {
+ $sql .= " LIMIT " . $this->options['LIMIT'];
+ }
+
+ if ( isset( $this->options['OFFSET'] ) ) {
+ $sql .= " OFFSET " . $this->options['OFFSET'];
+ }
+
+ return $sql;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Sequence.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Sequence.php
new file mode 100644
index 00000000..e2b225ca
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/Sequence.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace SMW\MediaWiki\Connection;
+
+use SMW\SQLStore\SQLStore;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Sequence {
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var string
+ */
+ private $tablePrefix;
+
+ /**
+ * @since 3.0
+ */
+ public function __construct( $connection ) {
+
+ if ( !$connection instanceof Database && !$connection instanceof \DatabaseBase ) {
+ throw new RuntimeException( "Invalid connection instance!" );
+ }
+
+ $this->connection = $connection;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function tablePrefix( $tablePrefix = '' ) {
+ $this->tablePrefix = $tablePrefix;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $tableName
+ * @param string $field
+ *
+ * @return string
+ */
+ public static function makeSequence( $table, $field ) {
+ return "{$table}_{$field}_seq";
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $tableName
+ * @param string $field
+ *
+ * @return integer
+ */
+ public function restart( $table, $field ) {
+
+ if ( $this->connection->getType() !== 'postgres' ) {
+ return;
+ }
+
+ if ( $this->tablePrefix !== null ) {
+ $this->connection->tablePrefix( $this->tablePrefix );
+ }
+
+ $seq_num = $this->connection->selectField( $table, "max({$field})", [], __METHOD__ );
+ $seq_num += 1;
+
+ $sequence = self::makeSequence( $table, $field );
+
+ $this->connection->onTransactionIdle( function() use( $sequence, $seq_num ) {
+ $this->connection->query( "ALTER SEQUENCE {$sequence} RESTART WITH {$seq_num}", __METHOD__ );
+ } );
+
+ return $seq_num;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/TransactionProfiler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/TransactionProfiler.php
new file mode 100644
index 00000000..b5e45508
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Connection/TransactionProfiler.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace SMW\MediaWiki\Connection;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TransactionProfiler {
+
+ /**
+ * @var TransactionProfiler
+ */
+ private $transactionProfiler;
+
+ /**
+ * @var boolean
+ */
+ private $silenceTransactionProfiler = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param TransactionProfiler|null $transactionProfiler
+ */
+ public function __construct( $transactionProfiler = null ) {
+
+ // MW 1.28+
+ if ( method_exists( $transactionProfiler, 'setSilenced' ) ) {
+ $this->transactionProfiler = $transactionProfiler;
+ }
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function silenceTransactionProfiler() {
+ $this->silenceTransactionProfiler = true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $state
+ */
+ public function setSilenced( $state ) {
+
+ if ( $this->transactionProfiler === null || $this->silenceTransactionProfiler === false ) {
+ return;
+ }
+
+ // @see https://gerrit.wikimedia.org/r/c/mediawiki/core/+/462130/3/includes/objectcache/SqlBagOStuff.php#836
+ return $this->transactionProfiler->setSilenced( $state );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/DeepRedirectTargetResolver.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/DeepRedirectTargetResolver.php
new file mode 100644
index 00000000..b5b7e3e4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/DeepRedirectTargetResolver.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use RuntimeException;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class DeepRedirectTargetResolver {
+
+ /**
+ * @var PageCreator
+ */
+ private $pageCreator = null;
+
+ /**
+ * Track titles to prevent circular references caused by double redirects
+ * on the same title
+ *
+ * @var array
+ */
+ private $recursiveResolverTracker = [];
+
+ /**
+ * @since 2.1
+ *
+ * @param PageCreator $pageCreator
+ */
+ public function __construct( PageCreator $pageCreator ) {
+ $this->pageCreator = $pageCreator;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Title $title
+ *
+ * @return Title|null
+ * @throws RuntimeException
+ */
+ public function findRedirectTargetFor( Title $title ) {
+ return $this->doResolveRedirectTarget( $title );
+ }
+
+ protected function isValidRedirectTarget( $title ) {
+ return $title instanceof Title && $title->isValidRedirectTarget();
+ }
+
+ protected function isRedirect( $title ) {
+ return $title instanceof Title && $title->isRedirect();
+ }
+
+ private function doResolveRedirectTarget( Title $title ) {
+
+ $this->addToResolverTracker( $title );
+
+ if ( $this->isCircularByKnownRedirectTarget( $title ) ) {
+ throw new RuntimeException( "Circular redirect for {$title->getPrefixedDBkey()} detected." );
+ }
+
+ if ( $this->isRedirect( $title ) ) {
+ $title = $this->pageCreator->createPage( $title )->getRedirectTarget();
+
+ if ( $title instanceof Title ) {
+ $title = $this->doResolveRedirectTarget( $title );
+ }
+ }
+
+ if ( $this->isValidRedirectTarget( $title ) ) {
+ return $title;
+ }
+
+ throw new RuntimeException( "Redirect target is unresolvable" );
+ }
+
+ private function addToResolverTracker( $title ) {
+
+ if ( !isset( $this->recursiveResolverTracker[$title->getPrefixedDBkey()] ) ) {
+ $this->recursiveResolverTracker[$title->getPrefixedDBkey()] = 0;
+ }
+
+ return $this->recursiveResolverTracker[$title->getPrefixedDBkey()]++;
+ }
+
+ private function isCircularByKnownRedirectTarget( $title ) {
+ return isset( $this->recursiveResolverTracker[$title->getPrefixedDBkey()] ) && $this->recursiveResolverTracker[$title->getPrefixedDBkey()] > 1;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/CallableUpdate.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/CallableUpdate.php
new file mode 100644
index 00000000..f63406ff
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/CallableUpdate.php
@@ -0,0 +1,339 @@
+<?php
+
+namespace SMW\MediaWiki\Deferred;
+
+use Closure;
+use DeferrableUpdate;
+use DeferredUpdates;
+use Psr\Log\LoggerAwareTrait;
+use SMW\MediaWiki\Database;
+
+/**
+ * @see MWCallableUpdate
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ */
+class CallableUpdate implements DeferrableUpdate {
+
+ use LoggerAwareTrait;
+
+ /**
+ * Updates that should run before flushing output buffer
+ */
+ const STAGE_PRESEND = 'pre';
+
+ /**
+ * Updates that should run after flushing output buffer
+ */
+ const STAGE_POSTSEND = 'post';
+
+ /**
+ * @var Closure|callable
+ */
+ protected $callback;
+
+ /**
+ * @var boolean
+ */
+ protected $isDeferrableUpdate = true;
+
+ /**
+ * @var boolean
+ */
+ protected $isCommandLineMode = false;
+
+ /**
+ * @var boolean
+ */
+ private $isPending = false;
+
+ /**
+ * @var string
+ */
+ private $origin = '';
+
+ /**
+ * @var array
+ */
+ private static $pendingUpdates = [];
+
+ /**
+ * @var string|null
+ */
+ private $fingerprint = null;
+
+ /**
+ * @var array
+ */
+ private static $queueList = [];
+
+ /**
+ * @var string
+ */
+ private $stage;
+
+ /**
+ * @since 2.4
+ *
+ * @param callable $callback|null
+ * @param Database|null $connection
+ */
+ public function __construct( callable $callback = null ) {
+
+ if ( $callback === null ) {
+ $callback = [ $this, 'emptyCallback' ];
+ }
+
+ $this->callback = $callback;
+ $this->stage = self::STAGE_POSTSEND;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:$wgCommandLineMode
+ * Indicates whether MW is running in command-line mode.
+ *
+ * @since 2.5
+ *
+ * @param boolean $isCommandLineMode
+ */
+ public function isCommandLineMode( $isCommandLineMode ) {
+ $this->isCommandLineMode = $isCommandLineMode;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function asPresend() {
+ $this->stage = self::STAGE_PRESEND;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getStage() {
+ return $this->stage;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param callable $callback
+ */
+ public function setCallback( callable $callback ) {
+ $this->callback = $callback;
+ }
+
+ /**
+ * @deprecated since 3.0, use DeferredCallableUpdate::isDeferrableUpdate
+ * @since 2.4
+ */
+ public function enabledDeferredUpdate( $enabledDeferredUpdate = true ) {
+ $this->isDeferrableUpdate( $enabledDeferredUpdate );
+ }
+
+ /**
+ * @note Unit/Integration tests in MW 1.26- showed ambiguous behaviour when
+ * run in deferred mode because not all MW operations were supporting late
+ * execution.
+ *
+ * @since 3.0
+ */
+ public function isDeferrableUpdate( $isDeferrableUpdate ) {
+ $this->isDeferrableUpdate = (bool)$isDeferrableUpdate;
+ }
+
+ /**
+ * @note If wgCommandLineMode = true (e.g. MW is in CLI mode) then
+ * DeferredUpdates::addUpdate pushes updates directly into execution mode
+ * which may not be desirable for all update processes therefore hold on to it
+ * by using an internal waitableUpdate list and release them at convenience.
+ *
+ * @since 2.4
+ *
+ * @param booloan $isPending
+ */
+ public function markAsPending( $isPending = false ) {
+ $this->isPending = (bool)$isPending;
+ }
+
+ /**
+ * @note Set a fingerprint allowing it to track and detect duplicate update
+ * requests while being unprocessed.
+ *
+ * @since 2.5
+ *
+ * @param string|null $queue
+ */
+ public function setFingerprint( $fingerprint = null ) {
+ $this->fingerprint = md5( $fingerprint );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|null $queue
+ */
+ public function getFingerprint() {
+ return $this->fingerprint;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $origin
+ */
+ public function setOrigin( $origin ) {
+ $this->origin = $origin;
+ }
+
+ /**
+ * @see DeferrableCallback::getOrigin
+ *
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getOrigin() {
+
+ if ( is_string( $this->origin ) ) {
+ $this->origin = [ $this->origin ];
+ }
+
+ return json_encode( $this->origin );
+ }
+
+ /**
+ * @since 2.4
+ */
+ public static function releasePendingUpdates() {
+ foreach ( self::$pendingUpdates as $update ) {
+ DeferredUpdates::addUpdate( $update );
+ }
+
+ self::$pendingUpdates = [];
+ }
+
+ /**
+ * @see DeferrableUpdate::doUpdate
+ *
+ * @since 2.4
+ */
+ public function doUpdate() {
+ call_user_func( $this->callback );
+ unset( self::$queueList[$this->fingerprint] );
+
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Update completed: {origin} (fingerprint:{fingerprint})'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->getOrigin(),
+ 'fingerprint' => $this->fingerprint
+ ]
+ );
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function pushUpdate() {
+
+ if ( $this->fingerprint !== null && isset( self::$queueList[$this->fingerprint] ) ) {
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Push: {origin} (fingerprint: {fingerprint} is already listed, skip)'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->getOrigin(),
+ 'fingerprint' => $this->fingerprint
+ ]
+ );
+ return;
+ }
+
+ self::$queueList[$this->fingerprint] = true;
+
+ if ( $this->isPending && $this->isDeferrableUpdate ) {
+
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Push: {origin} (as pending DeferredCallableUpdate)'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->getOrigin(),
+ 'fingerprint' => $this->fingerprint
+ ]
+ );
+
+ return self::$pendingUpdates[] = $this;
+ }
+
+ if ( !$this->isCommandLineMode && $this->isDeferrableUpdate ) {
+ return $this->addUpdate( $this );
+ }
+
+ $this->doUpdate();
+ }
+
+ protected function addUpdate( $update ) {
+
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Added: {ctx}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'ctx' => json_encode(
+ $this->getLoggableContext(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
+ )
+ ]
+ );
+ $stage = null;
+
+ if ( $update->getStage() === self::STAGE_POSTSEND && defined( 'DeferredUpdates::POSTSEND' ) ) {
+ $stage = DeferredUpdates::POSTSEND;
+ }
+
+ if ( $update->getStage() === self::STAGE_PRESEND && defined( 'DeferredUpdates::PRESEND' ) ) {
+ $stage = DeferredUpdates::PRESEND;
+ }
+
+ DeferredUpdates::addUpdate( $update, $stage );
+ }
+
+ protected function getLoggableContext() {
+ return [
+ 'origin' => $this->origin,
+ 'fingerprint' => $this->fingerprint,
+ 'stage' => $this->stage
+ ];
+ }
+
+ protected function emptyCallback() {
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Empty callback!'
+ ],
+ [
+ 'role' => 'developer',
+ 'method' => __METHOD__
+ ]
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/ChangeTitleUpdate.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/ChangeTitleUpdate.php
new file mode 100644
index 00000000..73603655
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/ChangeTitleUpdate.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace SMW\MediaWiki\Deferred;
+
+use DeferrableUpdate;
+use DeferredUpdates;
+use Title;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Jobs\UpdateJob;
+use SMW\Site;
+use SMW\Enum;
+
+/**
+ * Run a deferred update job for a changed title instance to re-parse the content
+ * of those associated titles and make sure that its content (incl. any
+ * self-reference) is correctly represented.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ChangeTitleUpdate implements DeferrableUpdate {
+
+ /**
+ * @var Title|null
+ */
+ private $oldTitle;
+
+ /**
+ * @var Title|null
+ */
+ private $newTitle;
+
+ /**
+ * @since 3.0
+ *
+ * @param Title|null $oldTitle
+ * @param Title|null $newTitle
+ */
+ public function __construct( Title $oldTitle = null, Title $newTitle = null ) {
+ $this->oldTitle = $oldTitle;
+ $this->newTitle = $newTitle;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title|null $oldTitle
+ * @param Title|null $newTitle
+ */
+ public static function addUpdate( Title $oldTitle = null, Title $newTitle = null ) {
+
+ // Avoid deferring the update on CLI (and the DeferredUpdates::tryOpportunisticExecute)
+ // since we use a Job instance to carry out the change
+ if ( Site::isCommandLineMode() ) {
+ $changeTitleUpdate = new self( $oldTitle, $newTitle );
+ $changeTitleUpdate->doUpdate();
+ } else {
+ DeferredUpdates::addUpdate( new self( $oldTitle, $newTitle ) );
+ }
+ }
+
+ /**
+ * @see DeferrableUpdate::doUpdate
+ *
+ * @since 3.0
+ */
+ public function doUpdate() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $jobFactory = $applicationFactory->newJobFactory();
+
+ $parameters = [
+ UpdateJob::FORCED_UPDATE => true,
+
+ // Run purge job after the change has happened since no post-edit event
+ // will be triggered on a changed/redirect title
+ Enum::PURGE_ASSOC_PARSERCACHE => true,
+
+ 'origin' => 'ChangeTitleUpdate'
+ ];
+
+ if ( $this->oldTitle !== null ) {
+ $jobFactory->newUpdateJob( $this->oldTitle, $parameters )->run();
+ }
+
+ if ( $this->newTitle !== null ) {
+ $jobFactory->newUpdateJob( $this->newTitle, $parameters )->run();
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php
new file mode 100644
index 00000000..d98d6983
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Deferred/TransactionalCallableUpdate.php
@@ -0,0 +1,275 @@
+<?php
+
+namespace SMW\MediaWiki\Deferred;
+
+use Closure;
+use SMW\MediaWiki\Database;
+
+/**
+ * Extends DeferredCallableUpdate to allow handling of transaction related tasks
+ * or isolations to ensure an undisturbed update process before and after
+ * MediaWiki::preOutputCommit.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TransactionalCallableUpdate extends CallableUpdate {
+
+ /**
+ * @var Database|null
+ */
+ private $connection;
+
+ /**
+ * @var boolean
+ */
+ private $onTransactionIdle = false;
+
+ /**
+ * @var int|null
+ */
+ private $transactionTicket = null;
+
+ /**
+ * @var array
+ */
+ private $preCommitableCallbacks = [];
+
+ /**
+ * @var array
+ */
+ private $postCommitableCallbacks = [];
+
+ /**
+ * @var boolean
+ */
+ private $autoCommit = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param callable $callback|null
+ * @param Database|null $connection
+ */
+ public function __construct( callable $callback = null, Database $connection ) {
+ parent::__construct( $callback );
+ $this->connection = $connection;
+ $this->connection->onTransactionResolution( [ $this, 'cancelOnRollback' ], __METHOD__ );
+ }
+
+ /**
+ * @note MW 1.29+ showed transaction collisions (Exception thrown with
+ * an uncommitted database transaction), use 'onTransactionIdle' to isolate
+ * the update execution.
+ *
+ * @since 2.5
+ */
+ public function waitOnTransactionIdle() {
+ $this->onTransactionIdle = !$this->isCommandLineMode;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function runAsAutoCommit() {
+ $this->autoCommit = true;
+ }
+
+ /**
+ * It tries to fetch a transactionTicket to assert whether transaction writes
+ * are active or not and if available will process Database::commitAndWaitForReplication
+ * during DeferredCallableUpdate::doUpdate to safely post commits to the
+ * master.
+ *
+ * @note If the commandLine is active then continue an update without a ticket
+ * to avoid any update lag or possible transaction lock.
+ *
+ * @since 3.0
+ */
+ public function commitWithTransactionTicket() {
+ if ( $this->isCommandLineMode === false && $this->isDeferrableUpdate === true ) {
+ $this->transactionTicket = $this->connection->getEmptyTransactionTicket( $this->getOrigin() );
+ }
+ }
+
+ /**
+ * Attaches a callback pre-execution of the source callback and is scheduled
+ * to be executed before the source callback.
+ *
+ * @since 3.0
+ *
+ * @param string $fname
+ * @param Closure $callback
+ */
+ public function addPreCommitableCallback( $fname, callable $callback ) {
+ if ( is_callable( $callback ) ) {
+ $this->preCommitableCallbacks[$fname] = $callback;
+ }
+ }
+
+ /**
+ * Attaches a callback post execution of the source callback and is scheduled
+ * to be executed after the source callback.
+ *
+ * @since 3.0
+ *
+ * @param string $fname
+ * @param Closure $callback
+ */
+ public function addPostCommitableCallback( $fname, callable $callback ) {
+ if ( is_callable( $callback ) ) {
+ $this->postCommitableCallbacks[$fname] = $callback;
+ }
+ }
+
+ /**
+ * @see DeferrableUpdate::doUpdate
+ *
+ * @since 3.0
+ */
+ public function doUpdate() {
+
+ if ( $this->onTransactionIdle ) {
+ return $this->runOnTransactionIdle();
+ }
+
+ $this->runPreCommitCallbacks();
+
+ $e = null;
+ $autoTrx = null;
+
+ if ( $this->autoCommit ) {
+ $this->logger->info( [ 'DeferrableUpdate', 'Transactional, as auto commit', 'Update' ] );
+ $autoTrx = $this->connection->getFlag( DBO_TRX );
+ $this->connection->clearFlag( DBO_TRX );
+ }
+
+ try {
+ parent::doUpdate();
+ } catch ( \Exception $e ) {
+ }
+
+ if ( $this->autoCommit && $autoTrx ) {
+ $this->connection->setFlag( DBO_TRX );
+ }
+
+ if ( $e ) {
+ throw $e;
+ }
+
+ $this->runPostCommitCallbacks();
+
+ if ( $this->transactionTicket !== null ) {
+ $this->connection->commitAndWaitForReplication( $this->getOrigin(), $this->transactionTicket );
+ }
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function cancelOnRollback( $trigger ) {
+ if ( $trigger === Database::TRIGGER_ROLLBACK ) {
+ $this->callback = [ $this, 'emptyCancelCallback' ];
+ }
+ }
+
+ protected function addUpdate( $update ) {
+
+ if ( $this->onTransactionIdle ) {
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Transactional',
+ 'Added: {origin} (onTransactionIdle)'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->getOrigin()
+ ]
+ );
+
+ return $this->connection->onTransactionIdle( function() use( $update ) {
+ $update->onTransactionIdle = false;
+ parent::addUpdate( $update );
+ } );
+ }
+
+ parent::addUpdate( $update );
+ }
+
+ protected function getLoggableContext() {
+ return parent::getLoggableContext() + [
+ 'transactionTicket' => $this->transactionTicket
+ ];
+ }
+
+ private function runOnTransactionIdle() {
+ $this->connection->onTransactionIdle( function() {
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Transactional',
+ 'Update: {origin} (onTransactionIdle)'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->getOrigin()
+ ]
+ );
+ $this->onTransactionIdle = false;
+ $this->doUpdate();
+ } );
+ }
+
+ private function runPreCommitCallbacks() {
+ foreach ( $this->preCommitableCallbacks as $fname => $preCallback ) {
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Transactional',
+ 'Update: {origin} (pre-commitable callback: {fname})'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->getOrigin(),
+ 'fname' => $fname
+ ]
+ );
+
+ call_user_func( $preCallback, $this->transactionTicket );
+ }
+ }
+
+ private function runPostCommitCallbacks() {
+ foreach ( $this->postCommitableCallbacks as $fname => $postCallback ) {
+ $this->logger->info(
+ [
+ 'DeferrableUpdate',
+ 'Transactional',
+ 'Update: {origin} (post-commitable callback: {fname})'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $this->getOrigin(),
+ 'fname' => $fname
+ ]
+ );
+
+ call_user_func( $postCallback, $this->transactionTicket );
+ }
+ }
+
+ protected function emptyCancelCallback() {
+ $this->logger->info(
+ [ 'DeferrableUpdate', 'cancelOnRollback' ],
+ [ 'role' => 'developer', 'method' => __METHOD__ ]
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/EditInfoProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/EditInfoProvider.php
new file mode 100644
index 00000000..00c2a6ec
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/EditInfoProvider.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Revision;
+use SMW\ParserData;
+use User;
+use WikiPage;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class EditInfoProvider {
+
+ /**
+ * @var WikiPage
+ */
+ private $wikiPage = null;
+
+ /**
+ * @var Revision
+ */
+ private $revision = null;
+
+ /**
+ * @var User
+ */
+ private $user = null;
+
+ /**
+ * @var ParserOutput
+ */
+ private $parserOutput = null;
+
+ /**
+ * @since 1.9
+ *
+ * @param WikiPage $wikiPage
+ * @param Revision $revision
+ * @param User|null $user
+ */
+ public function __construct( WikiPage $wikiPage, Revision $revision, User $user = null ) {
+ $this->wikiPage = $wikiPage;
+ $this->revision = $revision;
+ $this->user = $user;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return ParserOutput|null
+ */
+ public function getOutput() {
+ return $this->parserOutput;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return SemanticData|null
+ */
+ public function fetchSemanticData() {
+
+ $parserOutput = $this->fetchEditInfo()->getOutput();
+
+ if ( $parserOutput === null ) {
+ return null;
+ }
+
+ return $parserOutput->getExtensionData( ParserData::DATA_ID );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return EditInfoProvider
+ */
+ public function fetchEditInfo() {
+
+ $editInfo = $this->hasContentForEditMethod() ? $this->prepareContentForEdit() : $this->prepareTextForEdit();
+
+ $this->parserOutput = isset( $editInfo->output ) ? $editInfo->output : null;
+
+ return $this;
+ }
+
+ /**
+ * FIXME MW 1.21-
+ */
+ protected function hasContentForEditMethod() {
+ return method_exists( 'WikiPage', 'prepareContentForEdit' );
+ }
+
+ private function prepareContentForEdit() {
+
+ if ( !$this->revision instanceof Revision ) {
+ return null;
+ }
+
+ $content = $this->revision->getContent();
+
+ return $this->wikiPage->prepareContentForEdit(
+ $content,
+ null,
+ $this->user,
+ $content->getContentHandler()->getDefaultFormat()
+ );
+ }
+
+ private function prepareTextForEdit() {
+ // keep backwards compatibility with MediaWiki 1.19 by deciding, if the
+ // newer Revision::getContent() method (MW 1.20 and above) or the bc
+ // method Revision::getRawText() is used.
+ if ( method_exists( $this->revision, 'getContent' ) ) {
+ $text = $this->revision->getContent( Revision::RAW );
+ } else {
+ // FIXME: Isn't needed after drop of support for MW 1.19
+ $text = $this->revision->getRawText();
+ }
+ return $this->wikiPage->prepareTextForEdit(
+ $text,
+ null,
+ $this->user
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Exception/ExtendedPermissionsError.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Exception/ExtendedPermissionsError.php
new file mode 100644
index 00000000..9b4fb547
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Exception/ExtendedPermissionsError.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace SMW\MediaWiki\Exception;
+
+use PermissionsError;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ExtendedPermissionsError extends PermissionsError {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function __construct( $permission, $errors = [] ) {
+ parent::__construct( $permission, [] );
+
+ // Push SMW specific messages to appear first, PermissionsError will
+ // generate a list of required permissions
+ array_unshift( $this->errors, $errors );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleDelete.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleDelete.php
new file mode 100644
index 00000000..5d77c30d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleDelete.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\EventHandler;
+use SMW\MediaWiki\Jobs\UpdateDispatcherJob;
+use SMW\SemanticData;
+use SMW\Store;
+use Title;
+use Wikipage;
+
+/**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleDelete
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class ArticleDelete extends HookHandler {
+
+ /**
+ * @var
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param Wikipage $wikiPage
+ *
+ * @return true
+ */
+ public function process( Wikipage $wikiPage ) {
+
+ $deferredCallableUpdate = ApplicationFactory::getInstance()->newDeferredCallableUpdate( function() use( $wikiPage ) {
+ $this->doDelete( $wikiPage->getTitle() );
+ } );
+
+ $deferredCallableUpdate->setOrigin( __METHOD__ );
+ $deferredCallableUpdate->pushUpdate();
+
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ */
+ public function doDelete( Title $title ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $subject = DIWikiPage::newFromTitle( $title );
+
+ $semanticDataSerializer = $applicationFactory->newSerializerFactory()->newSemanticDataSerializer();
+ $jobFactory = $applicationFactory->newJobFactory();
+
+ // Instead of Store::getSemanticData, construct the SemanticData by
+ // attaching only the incoming properties indicating which entities
+ // carry an actual reference to this subject
+ $semanticData = new SemanticData(
+ $subject
+ );
+
+ $properties = $this->store->getInProperties( $subject );
+
+ foreach ( $properties as $property ) {
+ // Avoid doing $propertySubjects = $store->getPropertySubjects( $property, $subject );
+ // as it may produce a too large pool of entities and ultimately
+ // block the delete transaction
+ // Use the subject as dataItem with the UpdateDispatcherJob because
+ // Store::getAllPropertySubjects is only scanning the property
+ $semanticData->addPropertyObjectValue( $property, $subject );
+ }
+
+ $parameters['semanticData'] = $semanticDataSerializer->serialize(
+ $semanticData
+ );
+
+ $parameters['origin'] = 'ArticleDelete';
+
+ // Fetch the ID before the delete process marks it as outdated to help
+ // run a dispatch process on secondary tables
+ $parameters['_id'] = $this->store->getObjectIds()->getId(
+ $subject
+ );
+
+ // Restricted to the available SemanticData
+ $parameters[UpdateDispatcherJob::RESTRICTED_DISPATCH_POOL] = true;
+
+ $updateDispatcherJob = $jobFactory->newUpdateDispatcherJob( $title, $parameters );
+ $updateDispatcherJob->insert();
+
+ $parserCachePurgeJob = $jobFactory->newParserCachePurgeJob(
+ $title,
+ [
+ 'idlist' => [
+ $parameters['_id']
+ ],
+ 'origin' => 'ArticleDelete',
+
+ // Insert will only be done for when the links store is active
+ // otherwise the job wouldn't have any work to do
+ 'is.enabled' => $this->getOption( 'smwgEnabledQueryDependencyLinksStore' )
+ ]
+ );
+
+ $parserCachePurgeJob->insert();
+
+ $this->store->deleteSubject( $title );
+
+ $eventHandler = EventHandler::getInstance();
+ $dispatchContext = $eventHandler->newDispatchContext();
+
+ $dispatchContext->set( 'title', $title );
+ $dispatchContext->set( 'subject', $subject );
+ $dispatchContext->set( 'context', 'ArticleDelete' );
+
+ $eventHandler->getEventDispatcher()->dispatch(
+ 'cached.prefetcher.reset',
+ $dispatchContext
+ );
+
+ // Removes any related update marker
+ $eventHandler->getEventDispatcher()->dispatch(
+ 'cached.update.marker.delete',
+ $dispatchContext
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleFromTitle.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleFromTitle.php
new file mode 100644
index 00000000..495a7cf6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleFromTitle.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Page;
+use SMW\Page\PageFactory;
+use SMW\Store;
+use Title;
+
+/**
+ * Register special classes for displaying semantic content on Property and
+ * Concept pages.
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleFromTitle
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class ArticleFromTitle extends HookHandler {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 2.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param Title &$title
+ * @param Page|null &$page
+ *
+ * @return true
+ */
+ public function process( Title &$title, Page &$page = null ) {
+
+ $ns = $title->getNamespace();
+
+ if ( $ns !== SMW_NS_PROPERTY && $ns !== SMW_NS_CONCEPT ) {
+ return true;
+ }
+
+ $pageFactory = new PageFactory(
+ $this->store
+ );
+
+ $page = $pageFactory->newPageFromTitle(
+ $title
+ );
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleProtectComplete.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleProtectComplete.php
new file mode 100644
index 00000000..bbd9de18
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleProtectComplete.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\EditInfoProvider;
+use SMW\Message;
+use SMW\PropertyAnnotators\EditProtectedPropertyAnnotator;
+use Title;
+
+/**
+ * Occurs after the protect article request has been processed
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleProtectComplete
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ArticleProtectComplete extends HookHandler {
+
+ /**
+ * Whether the update should be restricted or not. Which means that when
+ * no other change is required then categorize the update as restricted
+ * to avoid unnecessary cascading updates.
+ */
+ const RESTRICTED_UPDATE = 'articleprotectcomplete.restricted.update';
+
+ /**
+ * @var Title
+ */
+ private $title;
+
+ /**
+ * @var EditInfoProvider
+ */
+ private $editInfoProvider;
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param EditInfoProvider $editInfoProvider
+ */
+ public function __construct( Title $title, EditInfoProvider $editInfoProvider ) {
+ parent::__construct();
+ $this->title = $title;
+ $this->editInfoProvider = $editInfoProvider;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $protections
+ * @param string $reason
+ */
+ public function process( $protections, $reason ) {
+
+ if ( Message::get( 'smw-edit-protection-auto-update' ) === $reason ) {
+ return $this->log( __METHOD__ . ' No changes required, invoked by own process!' );
+ }
+
+ $this->editInfoProvider->fetchEditInfo();
+
+ $output = $this->editInfoProvider->getOutput();
+
+ if ( $output === null ) {
+ return $this->log( __METHOD__ . ' Missing ParserOutput!' );
+ }
+
+ $parserData = ApplicationFactory::getInstance()->newParserData(
+ $this->title,
+ $output
+ );
+
+ $this->doPrepareData( $protections, $parserData );
+ $parserData->setOrigin( 'ArticleProtectComplete' );
+
+ $parserData->updateStore(
+ true
+ );
+ }
+
+ private function doPrepareData( $protections, $parserData ) {
+
+ $isRestrictedUpdate = true;
+ $isAnnotationBySystem = false;
+
+ $dataItemFactory = ApplicationFactory::getInstance()->getDataItemFactory();
+ $property = $dataItemFactory->newDIProperty( '_EDIP' );
+
+ $dataItems = $parserData->getSemanticData()->getPropertyValues( $property );
+ $dataItem = end( $dataItems );
+
+ if ( $dataItem ) {
+ $isAnnotationBySystem = $dataItem->getOption( EditProtectedPropertyAnnotator::SYSTEM_ANNOTATION );
+ }
+
+ $editProtectionRight = $this->getOption( 'smwgEditProtectionRight', false );
+
+ // No _EDIP annotation but a selected protection matches the
+ // `EditProtectionRight` setting
+ if ( !$dataItem && isset( $protections['edit'] ) && $protections['edit'] === $editProtectionRight ) {
+ $this->log( 'ArticleProtectComplete addProperty `Is edit protected`' );
+
+ $isRestrictedUpdate = false;
+ $parserData->getSemanticData()->addPropertyObjectValue(
+ $property,
+ $dataItemFactory->newDIBoolean( true )
+ );
+ }
+
+ // _EDIP exists and was set by the EditProtectedPropertyAnnotator (which
+ // means that is has been set by the system and is not a "human" added
+ // annotation) but since the selected protection doesn't match the
+ // `EditProtectionRight` setting, remove the annotation
+ if ( $dataItem && $isAnnotationBySystem && isset( $protections['edit'] ) && $protections['edit'] !== $editProtectionRight ) {
+ $this->log( 'ArticleProtectComplete removeProperty `Is edit protected`' );
+
+ $isRestrictedUpdate = false;
+ $parserData->getSemanticData()->removePropertyObjectValue(
+ $property,
+ $dataItemFactory->newDIBoolean( true )
+ );
+ }
+
+ $parserData->getSemanticData()->setOption(
+ self::RESTRICTED_UPDATE,
+ $isRestrictedUpdate
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticlePurge.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticlePurge.php
new file mode 100644
index 00000000..26ecfa8a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticlePurge.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\EventHandler;
+use WikiPage;
+
+/**
+ * A function hook being executed before running "&action=purge"
+ *
+ * A temporary cache entry is created to mark and identify the
+ * Article that has been purged.
+ *
+ * @see http://www.mediawiki.org/wiki/Manual:Hooks/ArticlePurge
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class ArticlePurge {
+
+ /**
+ * @since 1.9
+ *
+ * @return true
+ */
+ public function process( WikiPage &$wikiPage ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $pageId = $wikiPage->getTitle()->getArticleID();
+ $settings = $applicationFactory->getSettings();
+
+ $cache = $applicationFactory->getCache();
+ $cacheFactory = $applicationFactory->newCacheFactory();
+
+ if ( $pageId > 0 ) {
+ $cache->save(
+ $cacheFactory->getPurgeCacheKey( $pageId ),
+ $settings->get( 'smwgAutoRefreshOnPurge' )
+ );
+ }
+
+ $dispatchContext = EventHandler::getInstance()->newDispatchContext();
+ $dispatchContext->set( 'title', $wikiPage->getTitle() );
+ $dispatchContext->set( 'context', 'ArticlePurge' );
+
+ if ( $settings->isFlagSet( 'smwgFactboxFeatures', SMW_FACTBOX_PURGE_REFRESH ) ) {
+ EventHandler::getInstance()->getEventDispatcher()->dispatch(
+ 'factbox.cache.delete',
+ $dispatchContext
+ );
+ }
+
+ if ( $settings->get( 'smwgQueryResultCacheRefreshOnPurge' ) ) {
+
+ $dispatchContext->set( 'ask', $applicationFactory->getStore()->getPropertyValues(
+ DIWikiPage::newFromTitle( $wikiPage->getTitle() ),
+ new DIProperty( '_ASK') )
+ );
+
+ EventHandler::getInstance()->getEventDispatcher()->dispatch(
+ 'cached.prefetcher.reset',
+ $dispatchContext
+ );
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleViewHeader.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleViewHeader.php
new file mode 100644
index 00000000..962adc78
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ArticleViewHeader.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Html;
+use Page;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Jobs\ChangePropagationDispatchJob;
+use SMW\Message;
+use SMW\Store;
+use Title;
+
+/**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleViewHeader
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ArticleViewHeader extends HookHandler {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Page $page
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Page $page
+ * @param boolean &$outputDone
+ * @param boolean &$useParserCache
+ */
+ public function process( Page $page, &$outputDone, &$useParserCache ) {
+
+ $title = $page->getTitle();
+
+ $changePropagationWatchlist = array_flip(
+ $this->getOption( 'smwgChangePropagationWatchlist', [] )
+ );
+
+ // Only act when `_SUBC` is maintained as watchable property
+ if ( isset( $changePropagationWatchlist['_SUBC'] ) && $title->getNamespace() === NS_CATEGORY ) {
+ $useParserCache = $this->updateCategoryTop( $title, $page->getContext()->getOutput() );
+ }
+
+ return true;
+ }
+
+ private function updateCategoryTop( $title, $output ) {
+
+ $message = '';
+
+ $subject = DIWikiPage::newFromTitle(
+ $title
+ );
+
+ $semanticData = $this->store->getSemanticData(
+ $subject
+ );
+
+ if ( $semanticData->hasProperty( new DIProperty( DIProperty::TYPE_CHANGE_PROP ) ) ) {
+ $severity = $this->getOption( 'smwgChangePropagationProtection', true ) ? 'error' : 'warning';
+
+ $message .= $this->message(
+ $severity,
+ [
+ 'smw-category-change-propagation-locked-' . $severity,
+ str_replace( '_', ' ', $subject->getDBKey() )
+ ]
+ );
+ }
+
+ if ( $message === '' && ChangePropagationDispatchJob::hasPendingJobs( $subject ) ) {
+ $message .= $this->message(
+ 'warning',
+ [
+ 'smw-category-change-propagation-pending',
+ ChangePropagationDispatchJob::getPendingJobsCount( $subject )
+ ]
+ );
+ }
+
+ $output->addHTML( $message );
+
+ // No Message means `useParserCache`otherwise refresh the output to
+ // display the latest update
+ return $message === '';
+ }
+
+ private function message( $type, array $message ) {
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => $message[0],
+ 'class' => 'plainlinks ' . ( $type !== '' ? 'smw-callout smw-callout-'. $type : '' )
+ ],
+ Message::get( $message, Message::PARSE, Message::USER_LANGUAGE )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BaseTemplateToolbox.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BaseTemplateToolbox.php
new file mode 100644
index 00000000..57956de6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BaseTemplateToolbox.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\NamespaceExaminer;
+use SMWInfolink as Infolink;
+use SpecialPage;
+use Title;
+
+/**
+ * Hook: Called by BaseTemplate when building the toolbox array and
+ * returning it for the skin to output.
+ *
+ * Add a link to the toolbox to view the properties of the current page in
+ * Special:Browse. The links has the CSS id "t-smwbrowselink" so that it can be
+ * skinned or hidden with all standard mechanisms (also by individual users
+ * with custom CSS).
+ *
+ * @see http://www.mediawiki.org/wiki/Manual:Hooks/BaseTemplateToolbox
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class BaseTemplateToolbox extends HookHandler {
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @since 1.9
+ *
+ * @param NamespaceExaminer $namespaceExaminer
+ */
+ public function __construct( NamespaceExaminer $namespaceExaminer ) {
+ $this->namespaceExaminer = $namespaceExaminer;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param $skinTemplate
+ * @param &$toolbox
+ *
+ * @return boolean
+ */
+ public function process( $skinTemplate, &$toolbox ) {
+
+ $title = $skinTemplate->getSkin()->getTitle();
+
+ if ( $this->canProcess( $title, $skinTemplate ) ) {
+ $this->performUpdate( $title, $toolbox );
+ }
+
+ return true;
+ }
+
+ private function canProcess( Title $title, $skinTemplate ) {
+
+ if ( $title->isSpecialPage() || !$this->namespaceExaminer->isSemanticEnabled( $title->getNamespace() ) ) {
+ return false;
+ }
+
+ if ( !$this->isFlagSet( 'smwgBrowseFeatures', SMW_BROWSE_TLINK ) || !$skinTemplate->data['isarticle'] ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private function performUpdate( $title, &$toolbox ) {
+
+ $link = Infolink::encodeParameters(
+ [
+ $title->getPrefixedDBkey()
+ ],
+ true
+ );
+
+ $toolbox['smw-browse'] = [
+ 'text' => wfMessage( 'smw_browselink' )->text(),
+ 'href' => SpecialPage::getTitleFor( 'Browse', ':' . $link )->getLocalUrl(),
+ 'id' => 't-smwbrowselink',
+ 'rel' => 'search'
+ ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BeforeDisplayNoArticleText.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BeforeDisplayNoArticleText.php
new file mode 100644
index 00000000..66c05ce5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BeforeDisplayNoArticleText.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\DIProperty;
+
+/**
+ * Before displaying noarticletext or noarticletext-nopermission messages
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeDisplayNoArticleText
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class BeforeDisplayNoArticleText {
+
+ /**
+ * @var Page
+ */
+ private $article;
+
+ /**
+ * @since 2.0
+ *
+ * @param Page $article
+ */
+ public function __construct( $article ) {
+ $this->article = $article;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return boolean
+ */
+ public function process() {
+
+ // Avoid having "noarticletext" info being generated for predefined
+ // properties as we are going to display an introductory text
+ if ( $this->article->getTitle()->getNamespace() === SMW_NS_PROPERTY ) {
+ return DIProperty::newFromUserLabel( $this->article->getTitle()->getText() )->isUserDefined();
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BeforePageDisplay.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BeforePageDisplay.php
new file mode 100644
index 00000000..34324af1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/BeforePageDisplay.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use OutputPage;
+use Skin;
+use SpecialPage;
+use Title;
+use SMW\Message;
+use Html;
+
+/**
+ * BeforePageDisplay hook which allows last minute changes to the
+ * output page, e.g. adding of CSS or JavaScript
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class BeforePageDisplay extends HookHandler {
+
+ /**
+ * @since 1.9
+ *
+ * @param OutputPage $outputPage,
+ * @param Skin $skin
+ *
+ * @return boolean
+ */
+ public function process( OutputPage $outputPage, Skin $skin ) {
+
+ $title = $outputPage->getTitle();
+ $user = $outputPage->getUser();
+
+ // MW 1.26 / T107399 / Async RL causes style delay
+ $outputPage->addModuleStyles(
+ [
+ 'ext.smw.style',
+ 'ext.smw.tooltip.styles'
+ ]
+ );
+
+ // Add style resources to avoid unstyled content
+ $outputPage->addModules( 'ext.smw.style' );
+
+ // #2726
+ if ( $user->getOption( 'smw-prefs-general-options-suggester-textinput' ) ) {
+ $outputPage->addModules( 'ext.smw.suggester.textInput' );
+ }
+
+ if ( ( $tasks = $this->getOption( 'installer.incomplete_tasks', [] ) ) !== [] ) {
+ $outputPage->prependHTML( $this->incompleteTasksHTML( $tasks ) );
+ }
+
+ // Add export link to the head
+ if ( $title instanceof Title && !$title->isSpecialPage() ) {
+ $link['rel'] = 'alternate';
+ $link['type'] = 'application/rdf+xml';
+ $link['title'] = $title->getPrefixedText();
+ $link['href'] = SpecialPage::getTitleFor( 'ExportRDF', $title->getPrefixedText() )->getLocalUrl( 'xmlmime=rdf' );
+ $outputPage->addLink( $link );
+ }
+
+ $request = $skin->getContext()->getRequest();
+
+ if ( in_array( $request->getVal( 'action' ), [ 'delete', 'edit', 'protect', 'unprotect', 'diff', 'history' ] ) || $request->getVal( 'diff' ) ) {
+ return true;
+ }
+
+ return true;
+ }
+
+ private function incompleteTasksHTML( array $messages ) {
+
+ $html = '';
+
+ foreach ( $messages as $message ) {
+ $html .= Html::rawElement( 'li', [], Message::get( $message, Message::PARSE ) );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-callout smw-callout-error plainlinks'
+ ],
+ Message::get( 'smw-install-incomplete-intro' ) . "<ul>$html</ul>"
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/EditPageForm.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/EditPageForm.php
new file mode 100644
index 00000000..b5d88f42
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/EditPageForm.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use EditPage;
+use Html;
+use SMW\DIProperty;
+use SMW\Message;
+use SMW\NamespaceExaminer;
+
+/**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPage::showEditForm:initial
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class EditPageForm extends HookHandler {
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @since 2.5
+ *
+ * @param NamespaceExaminer $namespaceExaminer
+ */
+ public function __construct( NamespaceExaminer $namespaceExaminer ) {
+ $this->namespaceExaminer = $namespaceExaminer;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param EditPage $editPage
+ *
+ * @return boolean
+ */
+ public function process( EditPage $editPage ) {
+
+ if ( !$this->getOption( 'smwgEnabledEditPageHelp', false ) || $this->getOption( 'prefs-disable-editpage', false ) ) {
+ return true;
+ }
+
+ $this->updateEditPage( $editPage );
+
+ return true;
+ }
+
+ private function updateEditPage( $editPage ) {
+
+ $msgKey = $this->getMessageKey(
+ $editPage->getTitle()
+ );
+
+ $message = Message::get(
+ $msgKey,
+ Message::PARSE,
+ Message::USER_LANGUAGE
+ );
+
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-editpage-help'
+ ],
+ Html::rawElement(
+ 'p',
+ [
+ 'data-msgKey' => $msgKey
+ ],
+ $message
+ )
+ );
+
+ $editPage->editFormPageTop .= $html;
+ }
+
+ private function getMessageKey( $title ) {
+
+ $text = $title->getText();
+ $namespace = $title->getNamespace();
+
+ if ( $namespace === SMW_NS_PROPERTY ) {
+ if ( DIProperty::newFromUserLabel( $text )->isUserDefined() ) {
+ return 'smw-editpage-property-annotation-enabled';
+ } else {
+ return 'smw-editpage-property-annotation-disabled';
+ }
+ } elseif ( $namespace === SMW_NS_CONCEPT ) {
+ return 'smw-editpage-concept-annotation-enabled';
+ } elseif ( $this->namespaceExaminer->isSemanticEnabled( $namespace ) ) {
+ return 'smw-editpage-annotation-enabled';
+ }
+
+ return 'smw-editpage-annotation-disabled';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ExtensionSchemaUpdates.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ExtensionSchemaUpdates.php
new file mode 100644
index 00000000..02c7ba1c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ExtensionSchemaUpdates.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use DatabaseUpdater;
+use Maintenance;
+use ReflectionProperty;
+use SMW\Options;
+use SMW\SQLStore\Installer;
+
+/**
+ * Schema update to set up the needed database tables
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/LoadExtensionSchemaUpdates
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class ExtensionSchemaUpdates {
+
+ /**
+ * @var DatabaseUpdater
+ */
+ protected $updater = null;
+
+ /**
+ * @since 2.0
+ *
+ * @param DatabaseUpdater $updater = null
+ */
+ public function __construct( DatabaseUpdater $updater = null ) {
+ $this->updater = $updater;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return true
+ */
+ public function process() {
+
+ $verbose = true;
+
+ $options = new Options(
+ [
+ Installer::OPT_SCHEMA_UPDATE => true,
+ Installer::OPT_TABLE_OPTIMIZE => true,
+ Installer::OPT_IMPORT => true,
+ Installer::OPT_SUPPLEMENT_JOBS => true
+ ]
+ );
+
+ if ( $this->hasMaintenanceArg( 'skip-optimize' ) ) {
+ $options->set( Installer::OPT_TABLE_OPTIMIZE, false );
+ }
+
+ // Needs a static caller otherwise the DatabaseUpdater returns with:
+ // "Warning: call_user_func_array() expects parameter 1 to be a
+ // valid callback ..."
+ //
+ // DatabaseUpdater notes "... $callback is the method to call; either a
+ // DatabaseUpdater method name or a callable. Must be serializable (ie.
+ // no anonymous functions allowed). The rest of the parameters (if any)
+ // will be passed to the callback. ..."
+ $this->updater->addExtensionUpdate(
+ [
+ 'SMWStore::setupStore',
+ [
+ 'verbose' => $verbose,
+ 'options' => $options
+ ]
+ ]
+ );
+
+ return true;
+ }
+
+ private function hasMaintenanceArg( $key ) {
+
+ $maintenance = null;
+
+ // We don't have access to the `update.php` internals due to lack
+ // of public methods ... it is far from a clean approach but the only
+ // way to fetch arguments invoked during the execution of `update.php`
+ // Check required due to missing property in MW 1.29-
+ if ( property_exists( $this->updater, 'maintenance' ) ) {
+ $reflectionProperty = new ReflectionProperty( $this->updater, 'maintenance' );
+ $reflectionProperty->setAccessible( true );
+ $maintenance = $reflectionProperty->getValue( $this->updater );
+ }
+
+ if ( $maintenance instanceof Maintenance ) {
+ $reflectionProperty = new ReflectionProperty( $maintenance, 'mOptions' );
+ $reflectionProperty->setAccessible( true );
+ $options = $reflectionProperty->getValue( $maintenance );
+ return isset( $options[$key] );
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ExtensionTypes.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ExtensionTypes.php
new file mode 100644
index 00000000..7b5d46fe
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ExtensionTypes.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+/**
+ * Called when generating the extensions credits, use this to change the tables headers
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ExtensionTypes
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class ExtensionTypes extends HookHandler {
+
+ /**
+ * @since 2.0
+ *
+ * @param array $extensionTypes
+ *
+ * @return boolean
+ */
+ public function process( array &$extensionTypes ) {
+
+ if ( !is_array( $extensionTypes ) ) {
+ $extensionTypes = [];
+ }
+
+ $extensionTypes = array_merge(
+ [ 'semantic' => wfMessage( 'version-semantic' )->text() ],
+ $extensionTypes
+ );
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/FileUpload.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/FileUpload.php
new file mode 100644
index 00000000..97d3bd0b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/FileUpload.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use File;
+use Hooks;
+use ParserOptions;
+use SMW\ApplicationFactory;
+use SMW\Localizer;
+use SMW\NamespaceExaminer;
+use Title;
+use User;
+
+/**
+ * Fires when a local file upload occurs
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/FileUpload
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class FileUpload extends HookHandler {
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @since 1.9
+ *
+ * @param NamespaceExaminer $namespaceExaminer
+ */
+ public function __construct( NamespaceExaminer $namespaceExaminer ) {
+ $this->namespaceExaminer = $namespaceExaminer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param File $file
+ * @param boolean $reUploadStatus
+ *
+ * @return true
+ */
+ public function process( File $file, $reUploadStatus = false ) {
+
+ if ( $this->canProcess( $file->getTitle() ) ) {
+ $this->doProcess( $file, $reUploadStatus );
+ }
+
+ return true;
+ }
+
+ private function canProcess( $title ) {
+ return $title !== null && $this->namespaceExaminer->isSemanticEnabled( $title->getNamespace() );
+ }
+
+ private function doProcess( $file, $reUploadStatus = false ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $filePage = $this->makeFilePage( $file, $reUploadStatus );
+
+ // Avoid WikiPage.php: The supplied ParserOptions are not safe to cache.
+ // Fix the options or set $forceParse = true.
+ $forceParse = true;
+
+ $parserData = $applicationFactory->newParserData(
+ $file->getTitle(),
+ $filePage->getParserOutput( $this->makeCanonicalParserOptions(), null, $forceParse )
+ );
+
+ $pageInfoProvider = $applicationFactory->newMwCollaboratorFactory()->newPageInfoProvider(
+ $filePage
+ );
+
+ $propertyAnnotatorFactory = $applicationFactory->singleton( 'PropertyAnnotatorFactory' );
+
+ $semanticData = $parserData->getSemanticData();
+ $semanticData->setOption( 'is.fileupload', true );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newNullPropertyAnnotator(
+ $semanticData
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newPredefinedPropertyAnnotator(
+ $propertyAnnotator,
+ $pageInfoProvider
+ );
+
+ $propertyAnnotator->addAnnotation();
+
+ // 2.4+
+ Hooks::run( 'SMW::FileUpload::BeforeUpdate', [ $filePage, $semanticData ] );
+
+ $parserData->setOrigin( 'FileUpload' );
+
+ $parserData->pushSemanticDataToParserOutput();
+ $parserData->updateStore( true );
+
+ return true;
+ }
+
+ private function makeFilePage( $file, $reUploadStatus ) {
+
+ $filePage = ApplicationFactory::getInstance()->newPageCreator()->createFilePage(
+ $file->getTitle()
+ );
+
+ $filePage->setFile( $file );
+ $filePage->smwFileReUploadStatus = $reUploadStatus;
+
+ return $filePage;
+ }
+
+ /**
+ * Anonymous user with default preferences and content language
+ */
+ private function makeCanonicalParserOptions() {
+ return ParserOptions::newFromUserAndLang(
+ new User(),
+ Localizer::getInstance()->getContentLanguage()
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/GetPreferences.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/GetPreferences.php
new file mode 100644
index 00000000..e9af0f41
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/GetPreferences.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Hooks;
+use User;
+use Xml;
+
+/**
+ * Hook: GetPreferences adds user preference
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class GetPreferences extends HookHandler {
+
+ /**
+ * @var User
+ */
+ private $user;
+
+ /**
+ * @since 2.0
+ *
+ * @param User $user
+ */
+ public function __construct( User $user ) {
+ $this->user = $user;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param array &$preferences
+ *
+ * @return true
+ */
+ public function process( array &$preferences ) {
+
+ // Intro text
+ $preferences['smw-prefs-intro'] =
+ [
+ 'type' => 'info',
+ 'label' => '&#160;',
+ 'default' => Xml::tags( 'tr', [ 'class' => 'plainlinks' ],
+ Xml::tags( 'td', [ 'colspan' => 2 ],
+ wfMessage( 'smw-prefs-intro-text' )->parseAsBlock() ) ),
+ 'section' => 'smw',
+ 'raw' => 1,
+ 'rawrow' => 1,
+ ];
+
+ // Preference to allow time correction
+ $preferences['smw-prefs-general-options-time-correction'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'smw-prefs-general-options-time-correction',
+ 'section' => 'smw/general-options',
+ ];
+
+ $preferences['smw-prefs-general-options-disable-editpage-info'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'smw-prefs-general-options-disable-editpage-info',
+ 'section' => 'smw/general-options',
+ 'disabled' => !$this->getOption( 'smwgEnabledEditPageHelp', false )
+ ];
+
+ $preferences['smw-prefs-general-options-disable-search-info'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'smw-prefs-general-options-disable-search-info',
+ 'section' => 'smw/general-options',
+ 'disabled' => $this->getOption( 'wgSearchType' ) !== 'SMWSearch'
+ ];
+
+ $preferences['smw-prefs-general-options-jobqueue-watchlist'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'smw-prefs-general-options-jobqueue-watchlist',
+ 'help-message' => 'smw-prefs-help-general-options-jobqueue-watchlist',
+ 'section' => 'smw/general-options',
+ 'disabled' => $this->getOption( 'smwgJobQueueWatchlist', [] ) === []
+ ];
+
+ // Option to enable input assistance
+ $preferences['smw-prefs-general-options-suggester-textinput'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'smw-prefs-general-options-suggester-textinput',
+ 'help-message' => 'smw-prefs-help-general-options-suggester-textinput',
+ 'section' => 'smw/general-options',
+ ];
+
+ // Option to enable tooltip info
+ $preferences['smw-prefs-ask-options-tooltip-display'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'smw-prefs-ask-options-tooltip-display',
+ 'section' => 'smw/ask-options',
+ ];
+
+ Hooks::run( 'SMW::GetPreferences', [ $this->user, &$preferences ] );
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookHandler.php
new file mode 100644
index 00000000..477220f5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookHandler.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Psr\Log\LoggerAwareTrait;
+use Psr\Log\LoggerInterface;
+use SMW\Options;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class HookHandler {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @since 2.5
+ */
+ public function __construct() {
+ $this->options = new Options();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $options
+ */
+ public function setOptions( array $options ) {
+ $this->options = new Options( $options );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getOption( $key, $default = null ) {
+
+ if ( $this->options === null ) {
+ $this->setOptions( [] );
+ }
+
+ return $this->options->safeGet( $key, $default );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $flag
+ *
+ * @return boolean
+ */
+ public function isFlagSet( $key, $flag ) {
+ return $this->options->isFlagSet( $key, $flag );
+ }
+
+ protected function log( $message, $context = [] ) {
+ if ( $this->logger instanceof LoggerInterface ) {
+ $this->logger->info( $message, $context );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookListener.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookListener.php
new file mode 100644
index 00000000..ed8eefcb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookListener.php
@@ -0,0 +1,840 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Parser;
+use ParserHooks\HookRegistrant;
+use SMW\ApplicationFactory;
+use SMW\ParserFunctions\DocumentationParserFunction;
+use SMW\ParserFunctions\InfoParserFunction;
+use SMW\ParserFunctions\SectionTag;
+use SMW\MediaWiki\Search\SearchProfileForm;
+use SMW\SQLStore\Installer;
+use SMW\Site;
+use SMW\Store;
+use SMW\Options;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HookListener {
+
+ /**
+ * @var array
+ */
+ private $vars;
+
+ /**
+ * @var string
+ */
+ private $basePath;
+
+ /**
+ * @since 3.0
+ *
+ * @param array &$vars
+ * @param string $basePath
+ */
+ public function __construct( &$vars = [], $basePath = '' ) {
+ $this->vars = $vars;
+ $this->basePath = $basePath;
+ }
+
+ /**
+ * Hook: ParserAfterTidy to add some final processing to the fully-rendered page output
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ParserAfterTidy
+ */
+ public function onParserAfterTidy( &$parser, &$text ) {
+
+ $parserAfterTidy = new ParserAfterTidy(
+ $parser
+ );
+
+ $parserAfterTidy->isCommandLineMode(
+ Site::isCommandLineMode()
+ );
+
+ // #3341
+ // When running as part of the install don't try to access the DB
+ // or update the Store
+ $parserAfterTidy->isReadOnly(
+ Site::isBlocked()
+ );
+
+ $parserAfterTidy->process( $text );
+
+ return true;
+ }
+
+ /**
+ * Hook: Called by BaseTemplate when building the toolbox array and
+ * returning it for the skin to output.
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/BaseTemplateToolbox
+ */
+ public function onBaseTemplateToolbox( $skinTemplate, &$toolbox ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $baseTemplateToolbox = new BaseTemplateToolbox(
+ $applicationFactory->getNamespaceExaminer()
+ );
+
+ $baseTemplateToolbox->setOptions(
+ [
+ 'smwgBrowseFeatures' => $applicationFactory->getSettings()->get( 'smwgBrowseFeatures' )
+ ]
+ );
+
+ $baseTemplateToolbox->setLogger(
+ $applicationFactory->getMediaWikiLogger()
+ );
+
+ return $baseTemplateToolbox->process( $skinTemplate, $toolbox );
+ }
+
+ /**
+ * Hook: Allows extensions to add text after the page content and article
+ * metadata.
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinAfterContent
+ */
+ public function onSkinAfterContent( &$data, $skin = null ) {
+
+ $skinAfterContent = new SkinAfterContent(
+ $skin
+ );
+
+ return $skinAfterContent->performUpdate( $data );
+ }
+
+ /**
+ * Hook: Called after parse, before the HTML is added to the output
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput
+ */
+ public function onOutputPageParserOutput( &$outputPage, $parserOutput ) {
+
+ $outputPageParserOutput = new OutputPageParserOutput(
+ $outputPage,
+ $parserOutput
+ );
+
+ return $outputPageParserOutput->process();
+ }
+
+ /**
+ * Hook: When checking if the page has been modified since the last visit
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageCheckLastModified
+ */
+ public function onOutputPageCheckLastModified( &$lastModified ) {
+
+ // Required to ensure that ViewAction doesn't bail out with
+ // "ViewAction::show: done 304" and hereby neglects to run the
+ // ArticleViewHeader hook
+
+ // Required on 1.28- for the $outputPage->checkLastModified check
+ // that would otherwise prevent running the ArticleViewHeader hook
+ $lastModified['smw'] = wfTimestamp( TS_MW, time() );
+
+ return true;
+ }
+
+ /**
+ * Hook: Allow an extension to disable file caching on pages
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/IsFileCacheable
+ */
+ public function onIsFileCacheable( &$article ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ if ( !$applicationFactory->getNamespaceExaminer()->isSemanticEnabled( $article->getTitle()->getNamespace() ) ) {
+ return true;
+ }
+
+ // Disallow the file cache to avoid skipping the ArticleViewHeader hook
+ // on Article::tryFileCache
+ return true;
+ }
+
+ /**
+ * Hook: Add changes to the output page, e.g. adding of CSS or JavaScript
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforePageDisplay
+ */
+ public function onBeforePageDisplay( &$outputPage, &$skin ) {
+
+ $beforePageDisplay = new BeforePageDisplay();
+
+ $beforePageDisplay->setOptions(
+ [
+ 'installer.incomplete_tasks' => Installer::incompleteTasks( $GLOBALS )
+ ]
+ );
+
+ return $beforePageDisplay->process( $outputPage, $skin );
+ }
+
+ /**
+ * Hook: Called immediately before returning HTML on the search results page
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialSearchResultsPrepend
+ */
+ public function onSpecialSearchResultsPrepend( $specialSearch, $outputPage, $term ) {
+
+ $user = $outputPage->getUser();
+
+ $specialSearchResultsPrepend = new SpecialSearchResultsPrepend(
+ $specialSearch,
+ $outputPage
+ );
+
+ $specialSearchResultsPrepend->setOptions(
+ [
+ 'prefs-suggester-textinput' => $user->getOption( 'smw-prefs-general-options-suggester-textinput' ),
+ 'prefs-disable-search-info' => $user->getOption( 'smw-prefs-general-options-disable-search-info' )
+ ]
+ );
+
+ return $specialSearchResultsPrepend->process( $term );
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialSearchProfiles
+ */
+ public function onSpecialSearchProfiles( array &$profiles ) {
+
+ SearchProfileForm::addProfile(
+ $GLOBALS['wgSearchType'],
+ $profiles
+ );
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialSearchProfileForm
+ */
+ public function onSpecialSearchProfileForm( $specialSearch, &$form, $profile, $term, $opts ) {
+
+ if ( $profile !== SearchProfileForm::PROFILE_NAME ) {
+ return true;
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $searchProfileForm = new SearchProfileForm(
+ $applicationFactory->getStore(),
+ $specialSearch
+ );
+
+ $searchProfileForm->setSearchableNamespaces(
+ \MediaWiki\MediaWikiServices::getInstance()->getSearchEngineConfig()->searchableNamespaces()
+ );
+
+ $searchProfileForm->getForm( $form, $opts );
+
+ return false;
+ }
+
+ /**
+ * Hook: InternalParseBeforeLinks is used to process the expanded wiki
+ * code after <nowiki>, HTML-comments, and templates have been treated.
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/InternalParseBeforeLinks
+ */
+ public function onInternalParseBeforeLinks( &$parser, &$text, &$stripState ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $internalParseBeforeLinks = new InternalParseBeforeLinks(
+ $parser,
+ $stripState
+ );
+
+ $internalParseBeforeLinks->setOptions(
+ [
+ 'smwgEnabledSpecialPage' => $applicationFactory->getSettings()->get( 'smwgEnabledSpecialPage' )
+ ]
+ );
+
+ return $internalParseBeforeLinks->process( $text );
+ }
+
+ /**
+ * Hook: NewRevisionFromEditComplete called when a revision was inserted
+ * due to an edit
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/NewRevisionFromEditComplete
+ */
+ public function onNewRevisionFromEditComplete( $wikiPage, $revision, $baseId, $user ) {
+
+ $mwCollaboratorFactory = ApplicationFactory::getInstance()->newMwCollaboratorFactory();
+
+ $editInfoProvider = $mwCollaboratorFactory->newEditInfoProvider(
+ $wikiPage,
+ $revision,
+ $user
+ );
+
+ $pageInfoProvider = $mwCollaboratorFactory->newPageInfoProvider(
+ $wikiPage,
+ $revision,
+ $user
+ );
+
+ $newRevisionFromEditComplete = new NewRevisionFromEditComplete(
+ $wikiPage->getTitle(),
+ $editInfoProvider,
+ $pageInfoProvider
+ );
+
+ return $newRevisionFromEditComplete->process();
+ }
+
+ /**
+ * Hook: Occurs after the protect article request has been processed
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleProtectComplete
+ */
+ public function onArticleProtectComplete( &$wikiPage, &$user, $protections, $reason ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $editInfoProvider = $applicationFactory->newMwCollaboratorFactory()->newEditInfoProvider(
+ $wikiPage,
+ $wikiPage->getRevision(),
+ $user
+ );
+
+ $articleProtectComplete = new ArticleProtectComplete(
+ $wikiPage->getTitle(),
+ $editInfoProvider
+ );
+
+ $articleProtectComplete->setOptions(
+ [
+ 'smwgEditProtectionRight' => $applicationFactory->getSettings()->get( 'smwgEditProtectionRight' )
+ ]
+ );
+
+ $articleProtectComplete->setLogger(
+ $applicationFactory->getMediaWikiLogger()
+ );
+
+ $articleProtectComplete->process( $protections, $reason );
+
+ return true;
+ }
+
+ /**
+ * Hook: Occurs when an articleheader is shown
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleViewHeader
+ */
+ public function onArticleViewHeader( &$page, &$outputDone, &$useParserCache ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ $articleViewHeader = new ArticleViewHeader(
+ $applicationFactory->getStore()
+ );
+
+ $articleViewHeader->setOptions(
+ [
+ 'smwgChangePropagationProtection' => $settings->get( 'smwgChangePropagationProtection' ),
+ 'smwgChangePropagationWatchlist' => $settings->get( 'smwgChangePropagationWatchlist' )
+ ]
+ );
+
+ $articleViewHeader->setLogger(
+ $applicationFactory->getMediaWikiLogger()
+ );
+
+ $articleViewHeader->process( $page, $outputDone, $useParserCache );
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/RejectParserCacheValue
+ */
+ public function onRejectParserCacheValue( $value, $wikiPage, $popts ) {
+
+ $queryDependencyLinksStoreFactory = ApplicationFactory::getInstance()->singleton( 'QueryDependencyLinksStoreFactory' );
+
+ $rejectParserCacheValue = new RejectParserCacheValue(
+ $queryDependencyLinksStoreFactory->newDependencyLinksUpdateJournal()
+ );
+
+ // Return false to reject the parser cache
+ // The log will contain something like "[ParserCache] ParserOutput
+ // key valid, but rejected by RejectParserCacheValue hook handler."
+ return $rejectParserCacheValue->process( $wikiPage->getTitle() );
+ }
+
+ /**
+ * Hook: TitleMoveComplete occurs whenever a request to move an article
+ * is completed
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleMoveComplete
+ */
+ public function onTitleMoveComplete( $oldTitle, $newTitle, $user, $oldId, $newId ) {
+
+ $titleMoveComplete = new TitleMoveComplete(
+ $oldTitle,
+ $newTitle,
+ $user,
+ $oldId,
+ $newId
+ );
+
+ return $titleMoveComplete->process();
+ }
+
+ /**
+ * Hook: ArticlePurge executes before running "&action=purge"
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticlePurge
+ */
+ public function onArticlePurge( &$wikiPage ) {
+
+ $articlePurge = new ArticlePurge();
+
+ return $articlePurge->process( $wikiPage );
+ }
+
+ /**
+ * Hook: ArticleDelete occurs whenever the software receives a request
+ * to delete an article
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleDelete
+ */
+ public function onArticleDelete( &$wikiPage, &$user, &$reason, &$error ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $articleDelete = new ArticleDelete(
+ $applicationFactory->getStore()
+ );
+
+ $articleDelete->setLogger(
+ $applicationFactory->getMediaWikiLogger()
+ );
+
+ $articleDelete->setOptions(
+ [
+ 'smwgEnabledQueryDependencyLinksStore' => $applicationFactory->getSettings()->get( 'smwgEnabledQueryDependencyLinksStore' )
+ ]
+ );
+
+ return $articleDelete->process( $wikiPage );
+ }
+
+ /**
+ * Hook: LinksUpdateConstructed called at the end of LinksUpdate() construction
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/LinksUpdateConstructed
+ */
+ public function onLinksUpdateConstructed( $linksUpdate ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $linksUpdateConstructed = new LinksUpdateConstructed(
+ $applicationFactory->getNamespaceExaminer()
+ );
+
+ $linksUpdateConstructed->setLogger(
+ $applicationFactory->getMediaWikiLogger()
+ );
+
+ // #3341
+ // When running as part of the install don't try to access the DB
+ // or update the Store
+ $linksUpdateConstructed->isReadOnly(
+ Site::isBlocked()
+ );
+
+ $linksUpdateConstructed->process( $linksUpdate );
+
+ return true;
+ }
+
+ /**
+ * Hook: Occurs when an articleheader is shown
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ContentHandlerForModelID
+ */
+ public function onContentHandlerForModelID( $modelId, &$contentHandler ) {
+
+ // 'rule-json' being a legacy model, remove with 3.1
+ if ( $modelId === 'rule-json' || $modelId === 'smw/schema' ) {
+ $contentHandler = new \SMW\Schema\Content\ContentHandler();
+ }
+
+ return true;
+ }
+
+ /**
+ * Hook: Add extra statistic at the end of Special:Statistics
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialStatsAddExtra
+ */
+ public function onSpecialStatsAddExtra( &$extraStats ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $specialStatsAddExtra = new SpecialStatsAddExtra(
+ $applicationFactory->getStore()
+ );
+
+ $specialStatsAddExtra->setOptions(
+ [
+ 'smwgSemanticsEnabled' => $applicationFactory->getSettings()->get( 'smwgSemanticsEnabled' )
+ ]
+ );
+
+ return $specialStatsAddExtra->process( $extraStats );
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/FileUpload
+ */
+ public function onFileUpload( $file, $reupload ) {
+
+ $fileUpload = new FileUpload(
+ ApplicationFactory::getInstance()->getNamespaceExaminer()
+ );
+
+ return $fileUpload->process( $file, $reupload );
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderGetConfigVars
+ */
+ public function onResourceLoaderGetConfigVars( &$vars ) {
+
+ $resourceLoaderGetConfigVars = new ResourceLoaderGetConfigVars();
+
+ return $resourceLoaderGetConfigVars->process( $vars );
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/GetPreferences
+ */
+ public function onGetPreferences( $user, &$preferences ) {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $getPreferences = new GetPreferences(
+ $user
+ );
+
+ $getPreferences->setOptions(
+ [
+ 'smwgEnabledEditPageHelp' => $settings->get( 'smwgEnabledEditPageHelp' ),
+ 'wgSearchType' => $GLOBALS['wgSearchType'],
+ 'smwgJobQueueWatchlist' => $settings->get( 'smwgJobQueueWatchlist' )
+ ]
+ );
+
+ $getPreferences->process( $preferences);
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/PersonalUrls
+ */
+ public function onPersonalUrls( array &$personal_urls, $title, $skinTemplate ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $personalUrls = new PersonalUrls(
+ $skinTemplate,
+ $applicationFactory->getJobQueue()
+ );
+
+ $user = $skinTemplate->getUser();
+
+ $personalUrls->setOptions(
+ [
+ 'smwgJobQueueWatchlist' => $applicationFactory->getSettings()->get( 'smwgJobQueueWatchlist' ),
+ 'prefs-jobqueue-watchlist' => $user->getOption( 'smw-prefs-general-options-jobqueue-watchlist' )
+ ]
+ );
+
+ $personalUrls->process( $personal_urls );
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation
+ */
+ public function onSkinTemplateNavigation( &$skinTemplate, &$links ) {
+
+ $skinTemplateNavigation = new SkinTemplateNavigation(
+ $skinTemplate,
+ $links
+ );
+
+ return $skinTemplateNavigation->process();
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/LoadExtensionSchemaUpdates
+ */
+ public function onLoadExtensionSchemaUpdates( $databaseUpdater ) {
+
+ $extensionSchemaUpdates = new ExtensionSchemaUpdates(
+ $databaseUpdater
+ );
+
+ return $extensionSchemaUpdates->process();
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderTestModules
+ */
+ public function onResourceLoaderTestModules( &$testModules, &$resourceLoader ) {
+
+ $resourceLoaderTestModules = new ResourceLoaderTestModules(
+ $resourceLoader,
+ $this->basePath,
+ $this->vars['IP']
+ );
+
+ return $resourceLoaderTestModules->process( $testModules );
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ExtensionTypes
+ */
+ public function onExtensionTypes( &$extTypes ) {
+
+ $extensionTypes = new ExtensionTypes();
+
+ return $extensionTypes->process( $extTypes);
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleIsAlwaysKnown
+ */
+ public function onTitleIsAlwaysKnown( $title, &$result ) {
+
+ $titleIsAlwaysKnown = new TitleIsAlwaysKnown(
+ $title,
+ $result
+ );
+
+ return $titleIsAlwaysKnown->process();
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ArticleFromTitle
+ */
+ public function onArticleFromTitle( &$title, &$article ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $articleFromTitle = new ArticleFromTitle(
+ $applicationFactory->getStore()
+ );
+
+ return $articleFromTitle->process( $title, $article );
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleIsMovable
+ */
+ public function onTitleIsMovable( $title, &$isMovable ) {
+
+ $titleIsMovable = new TitleIsMovable(
+ $title
+ );
+
+ return $titleIsMovable->process( $isMovable );
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeDisplayNoArticleText
+ */
+ public function onBeforeDisplayNoArticleText( $article ) {
+
+ $beforeDisplayNoArticleText = new BeforeDisplayNoArticleText(
+ $article
+ );
+
+ return $beforeDisplayNoArticleText->process();
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/EditPage::showEditForm:initial
+ */
+ public function onEditPageShowEditFormInitial( $editPage, $output ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $user = $output->getUser();
+
+ $editPageForm = new EditPageForm(
+ $applicationFactory->getNamespaceExaminer()
+ );
+
+ $editPageForm->setOptions(
+ [
+ 'smwgEnabledEditPageHelp' => $applicationFactory->getSettings()->get( 'smwgEnabledEditPageHelp' ),
+ 'prefs-disable-editpage' => $user->getOption( 'smw-prefs-general-options-disable-editpage-info' )
+ ]
+ );
+
+ return $editPageForm->process( $editPage );
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleQuickPermissions
+ *
+ * "...Quick permissions are checked first in the Title::checkQuickPermissions
+ * function. Quick permissions are the most basic of permissions needed
+ * to perform an action ..."
+ */
+ public function onTitleQuickPermissions( $title, $user, $action, &$errors, $rigor, $short ) {
+
+ $permissionPthValidator = ApplicationFactory::getInstance()->singleton( 'PermissionPthValidator' );
+
+ $ret = $permissionPthValidator->checkQuickPermission(
+ $title,
+ $user,
+ $action,
+ $errors
+ );
+
+ return $ret;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ParserOptionsRegister (Only 1.30+)
+ */
+ public function onParserOptionsRegister( &$defaults, &$inCacheKey ) {
+
+ // #2509
+ // Register a new options key, used in connection with #ask/#show
+ // where the use of a localTime invalidates the ParserCache to avoid
+ // stalled settings for users with different preferences
+ $defaults['localTime'] = false;
+ $inCacheKey['localTime'] = true;
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ParserFirstCallInit
+ */
+ public function onParserFirstCallInit( &$parser ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $parserFunctionFactory = $applicationFactory->newParserFunctionFactory();
+ $parserFunctionFactory->registerFunctionHandlers( $parser );
+
+ $hookRegistrant = new HookRegistrant( $parser );
+
+ $infoFunctionDefinition = InfoParserFunction::getHookDefinition();
+ $infoFunctionHandler = new InfoParserFunction();
+ $hookRegistrant->registerFunctionHandler( $infoFunctionDefinition, $infoFunctionHandler );
+ $hookRegistrant->registerHookHandler( $infoFunctionDefinition, $infoFunctionHandler );
+
+ $docsFunctionDefinition = DocumentationParserFunction::getHookDefinition();
+ $docsFunctionHandler = new DocumentationParserFunction();
+ $hookRegistrant->registerFunctionHandler( $docsFunctionDefinition, $docsFunctionHandler );
+ $hookRegistrant->registerHookHandler( $docsFunctionDefinition, $docsFunctionHandler );
+
+ /**
+ * Support for <section> ... </section>
+ */
+ SectionTag::register(
+ $parser,
+ $applicationFactory->getSettings()->get( 'smwgSupportSectionTag' )
+ );
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/BlockIpComplete
+ * @provided by MW 1.4
+ *
+ * "... occurs after the request to block (or change block settings of)
+ * an IP or user has been processed ..."
+ */
+ public function onBlockIpComplete( $block, $performer, $priorBlock ) {
+
+ $userChange = new UserChange(
+ ApplicationFactory::getInstance()->getNamespaceExaminer()
+ );
+
+ $userChange->setOrigin( 'BlockIpComplete' );
+ $userChange->process( $block->getTarget() );
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/UnblockUserComplete
+ * @provided by MW 1.29
+ *
+ * "... occurs after the request to unblock an IP or user has been
+ * processed ..."
+ */
+ public function onUnblockUserComplete( $block, $performer ) {
+
+ $userChange = new UserChange(
+ ApplicationFactory::getInstance()->getNamespaceExaminer()
+ );
+
+ $userChange->setOrigin( 'UnblockUserComplete' );
+ $userChange->process( $block->getTarget() );
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/UserGroupsChanged
+ * @provided by MW 1.26
+ *
+ * "... called after user groups are changed ..."
+ */
+ public function onUserGroupsChanged( $user ) {
+
+ $userChange = new UserChange(
+ ApplicationFactory::getInstance()->getNamespaceExaminer()
+ );
+
+ $userChange->setOrigin( 'UserGroupsChanged' );
+ $userChange->process( $user->getName() );
+
+ return true;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SoftwareInfo
+ */
+ public function onSoftwareInfo( &$software ) {
+
+ $store = ApplicationFactory::getInstance()->getStore();
+ $info = $store->getConnection( 'elastic' )->getSoftwareInfo();
+
+ if ( !isset( $software[$info['component']] ) && $info['version'] !== null ) {
+ $software[$info['component']] = $info['version'];
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookRegistry.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookRegistry.php
new file mode 100644
index 00000000..22eb254b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/HookRegistry.php
@@ -0,0 +1,383 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Onoi\HttpRequest\HttpRequestFactory;
+use Parser;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Search\SearchProfileForm;
+use SMW\NamespaceManager;
+use SMW\SemanticData;
+use SMW\Setup;
+use SMW\Site;
+use SMW\SQLStore\QueryDependencyLinksStoreFactory;
+use SMW\SQLStore\QueryEngine\FulltextSearchTableFactory;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class HookRegistry {
+
+ /**
+ * @var array
+ */
+ private $handlers = [];
+
+ /**
+ * @var array
+ */
+ private $globalVars;
+
+ /**
+ * @var string
+ */
+ private $basePath;
+
+ /**
+ * @since 2.1
+ *
+ * @param array &$globalVars
+ * @param string $directory
+ */
+ public function __construct( &$globalVars = [], $directory = '' ) {
+ $this->globalVars =& $globalVars;
+ $this->basePath = $directory;
+ $this->addCallableHandlers( $directory, $globalVars );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array &$vars
+ */
+ public static function initExtension( array &$vars ) {
+
+ $vars['wgContentHandlers'][CONTENT_MODEL_SMW_SCHEMA] = 'SMW\Schema\Content\ContentHandler';
+
+ /**
+ * CanonicalNamespaces initialization
+ *
+ * @note According to T104954 registration via wgExtensionFunctions can be
+ * too late and should happen before that in case RequestContext::getLanguage
+ * invokes Language::getNamespaces before the `wgExtensionFunctions` execution.
+ *
+ * @see https://phabricator.wikimedia.org/T104954#2391291
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/CanonicalNamespaces
+ * @Bug 34383
+ */
+ $vars['wgHooks']['CanonicalNamespaces'][] = function( array &$namespaces ) {
+
+ NamespaceManager::initCanonicalNamespaces(
+ $namespaces
+ );
+
+ return true;
+ };
+
+ /**
+ * To add to or remove pages from the special page list. This array has
+ * the same structure as $wgSpecialPages.
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialPage_initList
+ *
+ * #2813
+ */
+ $vars['wgHooks']['SpecialPage_initList'][] = function( array &$specialPages ) {
+
+ Setup::initSpecialPageList(
+ $specialPages
+ );
+
+ return true;
+ };
+
+ /**
+ * Called when ApiMain has finished initializing its module manager. Can
+ * be used to conditionally register API modules.
+ *
+ * #2813
+ */
+ $vars['wgHooks']['ApiMain::moduleManager'][] = function( $apiModuleManager ) {
+
+ $apiModuleManager->addModules(
+ Setup::getAPIModules(),
+ 'action'
+ );
+
+ return true;
+ };
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $name
+ *
+ * @return boolean
+ */
+ public function isRegistered( $name ) {
+ // return \Hooks::isRegistered( $name );
+ return isset( $this->handlers[$name] );
+ }
+
+ /**
+ * @since 2.3
+ */
+ public function clear() {
+ foreach ( $this->getHandlerList() as $name ) {
+ \Hooks::clear( $name );
+ }
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $name
+ *
+ * @return Callable|false
+ */
+ public function getHandlerFor( $name ) {
+ return isset( $this->handlers[$name] ) ? $this->handlers[$name] : false;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getHandlerList() {
+ return array_keys( $this->handlers );
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function register() {
+ foreach ( $this->handlers as $name => $callback ) {
+ //\Hooks::register( $name, $callback );
+ $this->globalVars['wgHooks'][$name][] = $callback;
+ }
+ }
+
+ private function addCallableHandlers( $basePath, $globalVars ) {
+
+ $hookListener = new HookListener( $this->globalVars, $this->basePath );
+ $elasticFactory = ApplicationFactory::getInstance()->singleton( 'ElasticFactory' );
+
+ $hooks = [
+ 'ParserAfterTidy' => [ $hookListener, 'onParserAfterTidy' ],
+ 'ParserOptionsRegister' => [ $hookListener, 'onParserOptionsRegister' ],
+ 'ParserFirstCallInit' => [ $hookListener, 'onParserFirstCallInit' ],
+ 'InternalParseBeforeLinks' => [ $hookListener, 'onInternalParseBeforeLinks' ],
+ 'RejectParserCacheValue' => [ $hookListener, 'onRejectParserCacheValue' ],
+ 'IsFileCacheable' => [ $hookListener, 'onIsFileCacheable' ],
+
+ 'BaseTemplateToolbox' => [ $hookListener, 'onBaseTemplateToolbox' ],
+ 'SkinAfterContent' => [ $hookListener, 'onSkinAfterContent' ],
+ 'OutputPageParserOutput' => [ $hookListener, 'onOutputPageParserOutput' ],
+ 'OutputPageCheckLastModified' => [ $hookListener, 'onOutputPageCheckLastModified' ],
+ 'BeforePageDisplay' => [ $hookListener, 'onBeforePageDisplay' ],
+ 'BeforeDisplayNoArticleText' => [ $hookListener, 'onBeforeDisplayNoArticleText' ],
+ 'EditPage::showEditForm:initial' => [ $hookListener, 'onEditPageShowEditFormInitial' ],
+
+ 'TitleMoveComplete' => [ $hookListener, 'onTitleMoveComplete' ],
+ 'TitleIsAlwaysKnown' => [ $hookListener, 'onTitleIsAlwaysKnown' ],
+ 'TitleQuickPermissions' => [ $hookListener, 'onTitleQuickPermissions' ],
+ 'TitleIsMovable' => [ $hookListener, 'onTitleIsMovable' ],
+
+ 'ArticlePurge' => [ $hookListener, 'onArticlePurge' ],
+ 'ArticleDelete' => [ $hookListener, 'onArticleDelete' ],
+ 'ArticleFromTitle' => [ $hookListener, 'onArticleFromTitle' ],
+ 'ArticleProtectComplete' => [ $hookListener, 'onArticleProtectComplete' ],
+ 'ArticleViewHeader' => [ $hookListener, 'onArticleViewHeader' ],
+ 'ContentHandlerForModelID' => [ $hookListener, 'onContentHandlerForModelID' ],
+
+ 'NewRevisionFromEditComplete' => [ $hookListener, 'onNewRevisionFromEditComplete' ],
+ 'LinksUpdateConstructed' => [ $hookListener, 'onLinksUpdateConstructed' ],
+ 'FileUpload' => [ $hookListener, 'onFileUpload' ],
+
+ 'ResourceLoaderGetConfigVars' => [ $hookListener, 'onResourceLoaderGetConfigVars' ],
+ 'ResourceLoaderTestModules' => [ $hookListener, 'onResourceLoaderTestModules' ],
+ 'GetPreferences' => [ $hookListener, 'onGetPreferences' ],
+ 'PersonalUrls' => [ $hookListener, 'onPersonalUrls' ],
+ 'SkinTemplateNavigation' => [ $hookListener, 'onSkinTemplateNavigation' ],
+ 'LoadExtensionSchemaUpdates' => [ $hookListener, 'onLoadExtensionSchemaUpdates' ],
+
+ 'ExtensionTypes' => [ $hookListener, 'onExtensionTypes' ],
+ 'SpecialStatsAddExtra' => [ $hookListener, 'onSpecialStatsAddExtra' ],
+ 'SpecialSearchResultsPrepend' => [ $hookListener, 'onSpecialSearchResultsPrepend' ],
+ 'SpecialSearchProfileForm' => [ $hookListener, 'onSpecialSearchProfileForm' ],
+ 'SpecialSearchProfiles' => [ $hookListener, 'onSpecialSearchProfiles' ],
+ 'SoftwareInfo' => [ $hookListener, 'onSoftwareInfo' ],
+
+ 'BlockIpComplete' => [ $hookListener, 'onBlockIpComplete' ],
+ 'UnblockUserComplete' => [ $hookListener, 'onUnblockUserComplete' ],
+ 'UserGroupsChanged' => [ $hookListener, 'onUserGroupsChanged' ],
+
+ 'SMW::SQLStore::EntityReferenceCleanUpComplete' => [ $elasticFactory, 'onEntityReferenceCleanUpComplete' ],
+ 'SMW::Admin::TaskHandlerFactory' => [ $elasticFactory, 'onTaskHandlerFactory' ],
+ ];
+
+ foreach ( $hooks as $hook => $handler ) {
+ $this->handlers[$hook] = is_callable( $handler ) ? $handler : [ $this, $handler ];
+ }
+
+ $this->registerHooksForInternalUse();
+ }
+
+ private function registerHooksForInternalUse() {
+
+ /**
+ * @see https://www.semantic-mediawiki.org/wiki/Hooks#SMW::SQLStore::AfterDataUpdateComplete
+ */
+ $this->handlers['SMW::SQLStore::AfterDataUpdateComplete'] = function ( $store, $semanticData, $changeOp ) {
+
+ // A delete infused change should trigger an immediate update
+ // without having to wait on the job queue
+ $isPrimaryUpdate = $semanticData->getOption( SemanticData::PROC_DELETE, false );
+
+ $queryDependencyLinksStoreFactory = ApplicationFactory::getInstance()->singleton( 'QueryDependencyLinksStoreFactory' );
+
+ $queryDependencyLinksStore = $queryDependencyLinksStoreFactory->newQueryDependencyLinksStore(
+ $store
+ );
+
+ $queryDependencyLinksStore->pruneOutdatedTargetLinks(
+ $changeOp
+ );
+
+ $entityIdListRelevanceDetectionFilter = $queryDependencyLinksStoreFactory->newEntityIdListRelevanceDetectionFilter(
+ $store,
+ $changeOp
+ );
+
+ $queryDependencyLinksStore->isPrimary( $isPrimaryUpdate );
+
+ $queryDependencyLinksStore->pushParserCachePurgeJob(
+ $entityIdListRelevanceDetectionFilter
+ );
+
+ $fulltextSearchTableFactory = new FulltextSearchTableFactory();
+
+ $textChangeUpdater = $fulltextSearchTableFactory->newTextChangeUpdater(
+ $store
+ );
+
+ $textChangeUpdater->isPrimary( $isPrimaryUpdate );
+
+ $textChangeUpdater->pushUpdates(
+ $changeOp
+ );
+
+ return true;
+ };
+
+ /**
+ * @see https://www.semantic-mediawiki.org/wiki/Hooks#SMW::Store::BeforeQueryResultLookupComplete
+ */
+ $this->handlers['SMW::Store::BeforeQueryResultLookupComplete'] = function ( $store, $query, &$result, $queryEngine ) {
+
+ $cachedQueryResultPrefetcher = ApplicationFactory::getInstance()->singleton( 'CachedQueryResultPrefetcher' );
+
+ $cachedQueryResultPrefetcher->setQueryEngine(
+ $queryEngine
+ );
+
+ if ( !$cachedQueryResultPrefetcher->isEnabled() ) {
+ return true;
+ }
+
+ $result = $cachedQueryResultPrefetcher->getQueryResult(
+ $query
+ );
+
+ return false;
+ };
+
+ /**
+ * @see https://www.semantic-mediawiki.org/wiki/Hooks#SMW::Store::AfterQueryResultLookupComplete
+ */
+ $this->handlers['SMW::Store::AfterQueryResultLookupComplete'] = function ( $store, &$result ) {
+
+ $queryDependencyLinksStoreFactory = ApplicationFactory::getInstance()->singleton( 'QueryDependencyLinksStoreFactory' );
+
+ $queryDependencyLinksStore = $queryDependencyLinksStoreFactory->newQueryDependencyLinksStore(
+ $store
+ );
+
+ $queryDependencyLinksStore->updateDependencies( $result );
+
+ ApplicationFactory::getInstance()->singleton( 'CachedQueryResultPrefetcher' )->recordStats();
+
+ $store->getObjectIds()->warmUpCache( $result );
+
+ return true;
+ };
+
+ /**
+ * @see https://www.semantic-mediawiki.org/wiki/Hooks/Browse::AfterIncomingPropertiesLookupComplete
+ */
+ $this->handlers['SMW::Browse::AfterIncomingPropertiesLookupComplete'] = function ( $store, $semanticData, $requestOptions ) {
+
+ $queryDependencyLinksStoreFactory = ApplicationFactory::getInstance()->singleton( 'QueryDependencyLinksStoreFactory' );
+
+ $queryReferenceBacklinks = $queryDependencyLinksStoreFactory->newQueryReferenceBacklinks(
+ $store
+ );
+
+ $queryReferenceBacklinks->addReferenceLinksTo(
+ $semanticData,
+ $requestOptions
+ );
+
+ return true;
+ };
+
+ /**
+ * @see https://www.semantic-mediawiki.org/wiki/Hooks/Browse::BeforeIncomingPropertyValuesFurtherLinkCreate
+ */
+ $this->handlers['SMW::Browse::BeforeIncomingPropertyValuesFurtherLinkCreate'] = function ( $property, $subject, &$html, $store ) {
+
+ $queryDependencyLinksStoreFactory = ApplicationFactory::getInstance()->singleton( 'QueryDependencyLinksStoreFactory' );
+
+ $queryReferenceBacklinks = $queryDependencyLinksStoreFactory->newQueryReferenceBacklinks(
+ $store
+ );
+
+ $doesRequireFurtherLink = $queryReferenceBacklinks->doesRequireFurtherLink(
+ $property,
+ $subject,
+ $html
+ );
+
+ // Return false in order to stop the link creation process to replace the
+ // standard link
+ return $doesRequireFurtherLink;
+ };
+
+ /**
+ * @see https://www.semantic-mediawiki.org/wiki/Hooks#SMW::Store::AfterQueryResultLookupComplete
+ */
+ $this->handlers['SMW::SQLStore::Installer::AfterCreateTablesComplete'] = function ( $tableBuilder, $messageReporter, $options ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $importerServiceFactory = $applicationFactory->create( 'ImporterServiceFactory' );
+
+ $importer = $importerServiceFactory->newImporter(
+ $importerServiceFactory->newJsonContentIterator(
+ $applicationFactory->getSettings()->get( 'smwgImportFileDirs' )
+ )
+ );
+
+ $importer->isEnabled( $options->safeGet( \SMW\SQLStore\Installer::OPT_IMPORT, false ) );
+ $importer->setMessageReporter( $messageReporter );
+ $importer->doImport();
+
+ return true;
+ };
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/InternalParseBeforeLinks.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/InternalParseBeforeLinks.php
new file mode 100644
index 00000000..9994016a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/InternalParseBeforeLinks.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Parser;
+use SMW\ApplicationFactory;
+use SMW\Parser\InTextAnnotationParser;
+use StripState;
+
+/**
+ * Hook: InternalParseBeforeLinks is used to process the expanded wiki
+ * code after <nowiki>, HTML-comments, and templates have been treated.
+ *
+ * This method will be called before an article is displayed or previewed.
+ * For display and preview we strip out the semantic properties and append them
+ * at the end of the article.
+ *
+ * @note MW 1.20+ see InternalParseBeforeSanitize
+ *
+ * @see http://www.mediawiki.org/wiki/Manual:Hooks/InternalParseBeforeLinks
+ *
+ * @ingroup FunctionHook
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class InternalParseBeforeLinks extends HookHandler {
+
+ /**
+ * @var Parser
+ */
+ private $parser;
+
+ /**
+ * @var StripState
+ */
+ private $stripState;
+
+ /**
+ * @since 1.9
+ *
+ * @param Parser $parser
+ * @param StripState $stripState
+ */
+ public function __construct( Parser &$parser, $stripState ) {
+ $this->parser = $parser;
+ $this->stripState = $stripState;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param string $text
+ *
+ * @return true
+ */
+ public function process( &$text ) {
+
+ if ( !$this->canPerformUpdate( $text, $this->parser->getTitle() ) ) {
+ return true;
+ }
+
+ return $this->performUpdate( $text );
+ }
+
+ private function canPerformUpdate( $text, $title ) {
+
+ if ( $this->getRedirectTarget() !== null ) {
+ return true;
+ }
+
+ // #2209, #2370 Allow content to be parsed that contain [[SMW::off]]/[[SMW::on]]
+ // even in case of MediaWiki messages
+ if ( InTextAnnotationParser::hasMarker( $text ) ) {
+ return true;
+ }
+
+ // ParserOptions::getInterfaceMessage is being used to identify whether a
+ // parse was initiated by `Message::parse`
+ if ( $text === '' || $this->parser->getOptions()->getInterfaceMessage() ) {
+ return false;
+ }
+
+ if ( !$title->isSpecialPage() ) {
+ return true;
+ }
+
+ // #2529
+ foreach ( $this->getOption( 'smwgEnabledSpecialPage', [] ) as $specialPage ) {
+ if ( is_string( $specialPage ) && $title->isSpecial( $specialPage ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function performUpdate( &$text ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ /**
+ * @var ParserData $parserData
+ */
+ $parserData = $applicationFactory->newParserData(
+ $this->parser->getTitle(),
+ $this->parser->getOutput()
+ );
+
+ /**
+ * Performs [[link::syntax]] parsing and adding of property annotations
+ * to the ParserOutput
+ *
+ * @var InTextAnnotationParser
+ */
+ $inTextAnnotationParser = $applicationFactory->newInTextAnnotationParser(
+ $parserData
+ );
+
+ $stripMarkerDecoder = $applicationFactory->newMwCollaboratorFactory()->newStripMarkerDecoder(
+ $this->stripState
+ );
+
+ $inTextAnnotationParser->setStripMarkerDecoder(
+ $stripMarkerDecoder
+ );
+
+ $inTextAnnotationParser->setRedirectTarget(
+ $this->getRedirectTarget()
+ );
+
+ $inTextAnnotationParser->parse( $text );
+
+ $parserData->markParserOutput();
+
+ return true;
+ }
+
+ /**
+ * #656 / MW 1.24+
+ */
+ private function getRedirectTarget() {
+
+ if ( method_exists( $this->parser->getOptions(), 'getRedirectTarget' ) ) {
+ return $this->parser->getOptions()->getRedirectTarget();
+ }
+
+ return null;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/LinksUpdateConstructed.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/LinksUpdateConstructed.php
new file mode 100644
index 00000000..194c6916
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/LinksUpdateConstructed.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Hooks;
+use LinksUpdate;
+use SMW\ApplicationFactory;
+use SMW\SemanticData;
+use SMW\NamespaceExaminer;
+use Title;
+
+/**
+ * LinksUpdateConstructed hook is called at the end of LinksUpdate()
+ *
+ * @see http://www.mediawiki.org/wiki/Manual:Hooks/LinksUpdateConstructed
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class LinksUpdateConstructed extends HookHandler {
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @var boolean
+ */
+ private $enabledDeferredUpdate = true;
+
+ /**
+ * @var boolean
+ */
+ private $isReadOnly = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param NamespaceExaminer $namespaceExaminer
+ */
+ public function __construct( NamespaceExaminer $namespaceExaminer ) {
+ $this->namespaceExaminer = $namespaceExaminer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isReadOnly
+ */
+ public function isReadOnly( $isReadOnly ) {
+ $this->isReadOnly = (bool)$isReadOnly;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function disableDeferredUpdate() {
+ $this->enabledDeferredUpdate = false;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param LinksUpdate $linksUpdate
+ *
+ * @return true
+ */
+ public function process( LinksUpdate $linksUpdate ) {
+
+ if ( $this->isReadOnly ) {
+ return false;
+ }
+
+ $title = $linksUpdate->getTitle();
+ $latestRevID = $title->getLatestRevID( Title::GAID_FOR_UPDATE );
+
+ $opts = [ 'defer' => $this->enabledDeferredUpdate ];
+
+ // Allow any third-party extension to suppress the update process
+ if ( \Hooks::run( 'SMW::LinksUpdate::ApprovedUpdate', [ $title, $latestRevID ] ) === false ) {
+ return true;
+ }
+
+ /**
+ * @var ParserData $parserData
+ */
+ $parserData = ApplicationFactory::getInstance()->newParserData(
+ $title,
+ $linksUpdate->getParserOutput()
+ );
+
+ if ( $this->namespaceExaminer->isSemanticEnabled( $title->getNamespace() ) ) {
+ // #347 showed that an external process (e.g. RefreshLinksJob) can inject a
+ // ParserOutput without/cleared SemanticData which forces the Store updater
+ // to create an empty container that will clear all existing data.
+ if ( $parserData->getSemanticData()->isEmpty() ) {
+ $this->updateSemanticData( $parserData, $title, 'empty data' );
+ }
+ }
+
+ // Push updates on properties directly without delay
+ if ( $title->getNamespace() === SMW_NS_PROPERTY ) {
+ $opts['defer'] = false;
+ }
+
+ // Scan the ParserOutput for a possible externally set option
+ if ( $linksUpdate->getParserOutput()->getExtensionData( $parserData::OPT_FORCED_UPDATE ) === true ) {
+ $parserData->setOption( $parserData::OPT_FORCED_UPDATE, true );
+ }
+
+ // Update incurred by a template change and is signaled through
+ // the following condition
+ if ( $linksUpdate->mTemplates !== [] && $linksUpdate->mRecursive === false ) {
+ $parserData->setOption( $parserData::OPT_FORCED_UPDATE, true );
+ }
+
+ $parserData->setOrigin( 'LinksUpdateConstructed' );
+ $parserData->updateStore( $opts );
+
+ // Track the update on per revision because MW 1.29 made the LinksUpdate a
+ // EnqueueableDataUpdate which creates updates as JobSpecification
+ // (refreshLinksPrioritized) and posses a possibility of running an
+ // update more than once for the same RevID
+ $parserData->markUpdate( $latestRevID );
+
+ return true;
+ }
+
+ /**
+ * To ensure that for a Title and its current revision a ParserOutput
+ * object is really meant to be "empty" (e.g. delete action initiated by a
+ * human) the content is re-parsed in order to fetch the newest available data
+ *
+ * @note Parsing is expensive but it is more expensive to loose data or to
+ * expect that an external process adheres the object contract
+ */
+ private function updateSemanticData( &$parserData, $title, $reason = '' ) {
+
+ $this->log(
+ [
+ 'LinksUpdateConstructed',
+ "Required content re-parse due to $reason",
+ $title->getPrefixedDBKey()
+ ]
+ );
+
+ $semanticData = $this->reparseAndFetchSemanticData( $title );
+
+ if ( $semanticData instanceof SemanticData ) {
+ $parserData->setSemanticData( $semanticData );
+ }
+ }
+
+ private function reparseAndFetchSemanticData( $title ) {
+
+ $contentParser = ApplicationFactory::getInstance()->newContentParser( $title );
+ $parserOutput = $contentParser->parse()->getOutput();
+
+ if ( $parserOutput === null ) {
+ return null;
+ }
+
+ if ( method_exists( $parserOutput, 'getExtensionData' ) ) {
+ return $parserOutput->getExtensionData( 'smwdata' );
+ }
+
+ return $parserOutput->mSMWData;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/NewRevisionFromEditComplete.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/NewRevisionFromEditComplete.php
new file mode 100644
index 00000000..b15b3fbc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/NewRevisionFromEditComplete.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use ParserOutput;
+use SMW\ApplicationFactory;
+use SMW\EventHandler;
+use SMW\MediaWiki\EditInfoProvider;
+use SMW\MediaWiki\PageInfoProvider;
+use Title;
+
+/**
+ * Hook: NewRevisionFromEditComplete called when a revision was inserted
+ * due to an edit
+ *
+ * Fetch additional information that is related to the saving that has just happened,
+ * e.g. regarding the last edit date. In runs where this hook is not triggered, the
+ * last DB entry (of MW) will be used to fill such properties.
+ *
+ * Called from LocalFile.php, SpecialImport.php, Article.php, Title.php
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/NewRevisionFromEditComplete
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class NewRevisionFromEditComplete extends HookHandler {
+
+ /**
+ * @var Title
+ */
+ private $title;
+
+ /**
+ * @var EditInfoProvider
+ */
+ private $editInfoProvider;
+
+ /**
+ * @var PageInfoProvider
+ */
+ private $pageInfoProvider;
+
+ /**
+ * @since 1.9
+ *
+ * @param Title $title
+ * @param EditInfoProvider $editInfoProvider
+ * @param PageInfoProvider $pageInfoProvider
+ */
+ public function __construct( Title $title, EditInfoProvider $editInfoProvider, PageInfoProvider $pageInfoProvider ) {
+ parent::__construct();
+ $this->title = $title;
+ $this->editInfoProvider = $editInfoProvider;
+ $this->pageInfoProvider = $pageInfoProvider;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function process() {
+
+ $parserOutput = $this->editInfoProvider->fetchEditInfo()->getOutput();
+ $schema = null;
+
+ if ( !$parserOutput instanceof ParserOutput ) {
+ return true;
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $parserData = $applicationFactory->newParserData(
+ $this->title,
+ $parserOutput
+ );
+
+ if ( $this->title->getNamespace() === SMW_NS_SCHEMA ) {
+ $schemaFactory = $applicationFactory->singleton( 'SchemaFactory' );
+
+ try {
+ $schema = $schemaFactory->newSchema(
+ $this->title->getDBKey(),
+ $this->pageInfoProvider->getNativeData()
+ );
+ } catch( \Exception $e ) {
+ // Do nothing!
+ }
+ }
+
+ $this->addPredefinedPropertyAnnotation(
+ $applicationFactory,
+ $parserData,
+ $schema
+ );
+
+ $dispatchContext = EventHandler::getInstance()->newDispatchContext();
+ $dispatchContext->set( 'title', $this->title );
+ $dispatchContext->set( 'context', 'NewRevisionFromEditComplete' );
+
+ EventHandler::getInstance()->getEventDispatcher()->dispatch(
+ 'cached.prefetcher.reset',
+ $dispatchContext
+ );
+
+ // If the concept was altered make sure to delete the cache
+ if ( $this->title->getNamespace() === SMW_NS_CONCEPT ) {
+ $applicationFactory->getStore()->deleteConceptCache( $this->title );
+ }
+
+ $parserData->pushSemanticDataToParserOutput();
+
+ return true;
+ }
+
+ private function addPredefinedPropertyAnnotation( $applicationFactory, $parserData, $schema = null ) {
+
+ $propertyAnnotatorFactory = $applicationFactory->singleton( 'PropertyAnnotatorFactory' );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newNullPropertyAnnotator(
+ $parserData->getSemanticData()
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newPredefinedPropertyAnnotator(
+ $propertyAnnotator,
+ $this->pageInfoProvider
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newSchemaPropertyAnnotator(
+ $propertyAnnotator,
+ $schema
+ );
+
+ $propertyAnnotator->addAnnotation();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/OutputPageParserOutput.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/OutputPageParserOutput.php
new file mode 100644
index 00000000..f0448c3e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/OutputPageParserOutput.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use OutputPage;
+use ParserOutput;
+use SMW\ApplicationFactory;
+use SMW\Query\QueryRefFinder;
+use Title;
+
+/**
+ * OutputPageParserOutput hook is called after parse, before the HTML is
+ * added to the output
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput
+ *
+ * @note This hook copies SMW's custom data from the given ParserOutput object to
+ * the given OutputPage object, since otherwise it is not possible to access
+ * it later on to build a Factbox.
+ *
+ * @ingroup FunctionHook
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class OutputPageParserOutput {
+
+ /**
+ * @var OutputPage
+ */
+ protected $outputPage = null;
+
+ /**
+ * @var ParserOutput
+ */
+ protected $parserOutput = null;
+
+ /**
+ * @since 1.9
+ *
+ * @param OutputPage $outputPage
+ * @param ParserOutput $parserOutput
+ */
+ public function __construct( OutputPage &$outputPage, ParserOutput $parserOutput ) {
+ $this->outputPage = $outputPage;
+ $this->parserOutput = $parserOutput;
+ }
+
+ /**
+ * @see FunctionHook::process
+ *
+ * @since 1.9
+ *
+ * @return true
+ */
+ public function process() {
+
+ $title = $this->outputPage->getTitle();
+
+ if ( $title->isSpecialPage() ||
+ $title->isRedirect() ||
+ !$this->isSemanticEnabledNamespace( $title ) ) {
+ return true;
+ }
+
+ $request = $this->outputPage->getContext()->getRequest();
+
+ $this->factbox( $request );
+ $this->postProc( $title, $request );
+ }
+
+ private function postProc( $title, $request) {
+
+ if ( in_array( $request->getVal( 'action' ), [ 'delete', 'purge', 'protect', 'unprotect', 'history', 'edit' ] ) ) {
+ return '';
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $postProcHandler = $applicationFactory->create( 'PostProcHandler', $this->parserOutput );
+
+ $html = $postProcHandler->getHtml(
+ $title,
+ $request
+ );
+
+ if ( $html !== '' ) {
+ $this->outputPage->addModules( $postProcHandler->getModules() );
+ $this->outputPage->addHtml( $html );
+ }
+ }
+
+ protected function factbox( $request ) {
+
+ if ( isset( $this->outputPage->mSMWFactboxText ) && $request->getCheck( 'wpPreview' ) ) {
+ return '';
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $cachedFactbox = $applicationFactory->singleton( 'FactboxFactory' )->newCachedFactbox();
+
+ $cachedFactbox->prepareFactboxContent(
+ $this->outputPage,
+ $this->outputPage->getLanguage(),
+ $this->getParserOutput()
+ );
+
+ return true;
+ }
+
+ protected function getParserOutput() {
+
+ if ( $this->outputPage->getContext()->getRequest()->getInt( 'oldid' ) ) {
+
+ $text = $this->parserOutput->getText();
+
+ $parserData = ApplicationFactory::getInstance()->newParserData(
+ $this->outputPage->getTitle(),
+ $this->parserOutput
+ );
+
+ $inTextAnnotationParser = ApplicationFactory::getInstance()->newInTextAnnotationParser( $parserData );
+ $inTextAnnotationParser->parse( $text );
+
+ return $parserData->getOutput();
+ }
+
+ return $this->parserOutput;
+ }
+
+ private function isSemanticEnabledNamespace( Title $title ) {
+ return ApplicationFactory::getInstance()->getNamespaceExaminer()->isSemanticEnabled( $title->getNamespace() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ParserAfterTidy.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ParserAfterTidy.php
new file mode 100644
index 00000000..96ee83d1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ParserAfterTidy.php
@@ -0,0 +1,258 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Parser;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\MediaWiki;
+use SMW\ParserData;
+use SMW\SemanticData;
+
+/**
+ * Hook: ParserAfterTidy to add some final processing to the
+ * fully-rendered page output
+ *
+ * @see http://www.mediawiki.org/wiki/Manual:Hooks/ParserAfterTidy
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class ParserAfterTidy extends HookHandler {
+
+ /**
+ * @var Parser
+ */
+ private $parser;
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @var boolean
+ */
+ private $isCommandLineMode = false;
+
+ /**
+ * @var boolean
+ */
+ private $isReadOnly = false;
+
+ /**
+ * @since 1.9
+ *
+ * @param Parser $parser
+ */
+ public function __construct( Parser &$parser ) {
+ $this->parser = $parser;
+ $this->namespaceExaminer = ApplicationFactory::getInstance()->getNamespaceExaminer();
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:$wgCommandLineMode
+ *
+ * @since 2.5
+ *
+ * @param boolean $isCommandLineMode
+ */
+ public function isCommandLineMode( $isCommandLineMode ) {
+ $this->isCommandLineMode = (bool)$isCommandLineMode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isReadOnly
+ */
+ public function isReadOnly( $isReadOnly ) {
+ $this->isReadOnly = (bool)$isReadOnly;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param string $text
+ *
+ * @return true
+ */
+ public function process( &$text ) {
+
+ if ( $this->canPerformUpdate() ) {
+ $this->performUpdate( $text );
+ }
+
+ return true;
+ }
+
+ private function canPerformUpdate() {
+
+ // #2432 avoid access to the DBLoadBalancer while being in readOnly mode
+ // when for example Title::isProtected is accessed
+ if ( $this->isReadOnly ) {
+ return false;
+ }
+
+ $title = $this->parser->getTitle();
+
+ if ( !$this->namespaceExaminer->isSemanticEnabled( $title->getNamespace() ) ) {
+ return false;
+ }
+
+ // Avoid an update for the SCHEMA NS to ensure errors remain present without
+ // the need the rerun the schema validator again.
+ if ( $title->getNamespace() === SMW_NS_SCHEMA ) {
+ return false;
+ }
+
+ // ParserOptions::getInterfaceMessage is being used to identify whether a
+ // parse was initiated by `Message::parse`
+ if ( $title->isSpecialPage() || $this->parser->getOptions()->getInterfaceMessage() ) {
+ return false;
+ }
+
+ $parserOutput = $this->parser->getOutput();
+
+ if ( $parserOutput->getProperty( 'displaytitle' ) ||
+ $parserOutput->getExtensionData( 'translate-translation-page' ) ||
+ $parserOutput->getCategoryLinks() ) {
+ return true;
+ }
+
+ if ( ParserData::hasSemanticData( $parserOutput ) ||
+ $title->isProtected( 'edit' ) ||
+ $this->parser->getDefaultSort() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function performUpdate( &$text ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $parserData = $applicationFactory->newParserData(
+ $this->parser->getTitle(),
+ $this->parser->getOutput()
+ );
+
+ $semanticData = $parserData->getSemanticData();
+
+ $this->addPropertyAnnotations(
+ $applicationFactory->singleton( 'PropertyAnnotatorFactory' ),
+ $semanticData
+ );
+
+ $parserData->copyToParserOutput();
+ $subject = $semanticData->getSubject();
+
+ // Only carry out a purge where the InTextAnnotationParser have set
+ // an appropriate context reference otherwise it is assumed that the hook
+ // call is part of another non SMW related parse
+ if ( $subject->getContextReference() !== null || $subject->getNamespace() === SMW_NS_SCHEMA ) {
+ $this->checkPurgeRequest( $parserData );
+ }
+ }
+
+ private function addPropertyAnnotations( $propertyAnnotatorFactory, $semanticData ) {
+
+ $parserOutput = $this->parser->getOutput();
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newNullPropertyAnnotator(
+ $semanticData
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newCategoryPropertyAnnotator(
+ $propertyAnnotator,
+ $parserOutput->getCategoryLinks()
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newMandatoryTypePropertyAnnotator(
+ $propertyAnnotator
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newEditProtectedPropertyAnnotator(
+ $propertyAnnotator,
+ $this->parser->getTitle()
+ );
+
+ // Special case! belongs to the EditProtectedPropertyAnnotator instance
+ $propertyAnnotator->addTopIndicatorTo(
+ $parserOutput
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newDisplayTitlePropertyAnnotator(
+ $propertyAnnotator,
+ $parserOutput->getProperty( 'displaytitle' ),
+ $this->parser->getDefaultSort()
+ );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newSortKeyPropertyAnnotator(
+ $propertyAnnotator,
+ $this->parser->getDefaultSort()
+ );
+
+ // #2300
+ $propertyAnnotator = $propertyAnnotatorFactory->newTranslationPropertyAnnotator(
+ $propertyAnnotator,
+ $parserOutput->getExtensionData( 'translate-translation-page' )
+ );
+
+ $propertyAnnotator->addAnnotation();
+ }
+
+ /**
+ * @note Article purge: In case an article was manually purged/moved
+ * the store is updated as well; for all other cases LinksUpdateConstructed
+ * will handle the store update
+ *
+ * @note The purge action is isolated from any other request therefore using
+ * a static variable or any other messaging that is not persistent will not
+ * work hence the reliance on the cache as temporary persistence marker
+ */
+ private function checkPurgeRequest( $parserData ) {
+
+ $cache = ApplicationFactory::getInstance()->getCache();
+ $start = microtime( true );
+
+ $key = ApplicationFactory::getInstance()->getCacheFactory()->getPurgeCacheKey(
+ $this->parser->getTitle()->getArticleID()
+ );
+
+ if( $cache->contains( $key ) && $cache->fetch( $key ) ) {
+ $cache->delete( $key );
+
+ // Avoid a Parser::lock for when a PurgeRequest remains intact
+ // during an update process while being executed from the cmdLine
+ if ( $this->isCommandLineMode ) {
+ return true;
+ }
+
+ $parserData->setOrigin( 'ParserAfterTidy' );
+
+ // Set an explicit timestamp to create a new hash for the property
+ // table change row differ and force a data comparison (this doesn't
+ // change the _MDAT annotation)
+ $parserData->getSemanticData()->setOption(
+ SemanticData::OPT_LAST_MODIFIED,
+ wfTimestamp( TS_UNIX )
+ );
+
+ $parserData->setOption(
+ $parserData::OPT_FORCED_UPDATE,
+ true
+ );
+
+ $parserData->updateStore( true );
+
+ $parserData->addLimitReport(
+ 'pagepurge-storeupdatetime',
+ number_format( ( microtime( true ) - $start ), 3 )
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/PersonalUrls.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/PersonalUrls.php
new file mode 100644
index 00000000..5af209bd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/PersonalUrls.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SkinTemplate;
+use SMW\MediaWiki\JobQueue;
+
+/**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/PersonalUrls
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PersonalUrls extends HookHandler {
+
+ /**
+ * @var SkinTemplate
+ */
+ private $skin;
+
+ /**
+ * @var JobQueue
+ */
+ private $jobQueue;
+
+ /**
+ * @since 3.0
+ *
+ * @param SkinTemplate $skin
+ * @param JobQueue $jobQueue
+ */
+ public function __construct( SkinTemplate $skin, JobQueue $jobQueue ) {
+ $this->skin = $skin;
+ $this->jobQueue = $jobQueue;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array &$personalUrls
+ *
+ * @return true
+ */
+ public function process( array &$personalUrls ) {
+
+ $watchlist = $this->getOption( 'smwgJobQueueWatchlist', [] );
+
+ if ( $this->getOption( 'prefs-jobqueue-watchlist' ) !== null && $watchlist !== [] ) {
+ $this->addJobQueueWatchlist( $watchlist, $personalUrls );
+ }
+
+ return true;
+ }
+
+ private function addJobQueueWatchlist( $watchlist, &$personalUrls ) {
+
+ $queue = [];
+
+ foreach ( $watchlist as $job ) {
+ $size = $this->jobQueue->getQueueSize( $job );
+
+ if ( $size > 0 ) {
+ $queue[$job] = $this->humanReadable( $size );
+ }
+ }
+
+ $out = $this->skin->getOutput();
+ $personalUrl = [];
+
+ $out->addModules( 'ext.smw.personal' );
+ $out->addJsConfigVars( 'smwgJobQueueWatchlist', $queue );
+
+ $personalUrl['smw-jobqueue-watchlist'] = [
+ 'text' => 'ⅉ [ ' . ( $queue === [] ? '0' : implode( ' | ', $queue ) ) . ' ]' ,
+ 'href' => '#',
+ 'class' => 'smw-personal-jobqueue-watchlist is-disabled',
+ 'active' => true
+ ];
+
+ $keys = array_keys( $personalUrls );
+
+ // Insert the link before the watchlist
+ $personalUrls = $this->splice(
+ $personalUrls,
+ $personalUrl,
+ array_search( 'watchlist', $keys )
+ );
+ }
+
+ // https://stackoverflow.com/questions/1783089/array-splice-for-associative-arrays
+ private function splice( $array, $values, $offset ) {
+ return array_slice( $array, 0, $offset, true ) + $values + array_slice( $array, $offset, NULL, true );
+ }
+
+ private function humanReadable( $num, $decimals = 0 ) {
+
+ if ( $num < 1000 ) {
+ $num = number_format( $num );
+ } else if ( $num < 1000000) {
+ $num = number_format( $num / 1000, $decimals ) . 'K';
+ } else {
+ $num = number_format( $num / 1000000, $decimals ) . 'M';
+ }
+
+ return $num;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/README.md b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/README.md
new file mode 100644
index 00000000..0f56b3ad
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/README.md
@@ -0,0 +1,39 @@
+[Hooks][hooks] are so-called event handlers that allow custom code to be executed. Semantic MediaWiki (SMW) uses several of those hooks to enable specific process logic to be integrated with MediaWiki. SMW currently uses the following hooks:
+
+#### ArticlePurge
+ArticlePurge is executed during a manual purge action of an article and depending on available settings is being used to track a page refresh.
+
+#### BeforePageDisplay
+BeforePageDisplay allows last minute changes to the output page and is being used to render a ExportRDF link into each individual article.
+
+#### InternalParseBeforeLinks
+InternalParseBeforeLinks is used to process and expand text content, and in case of SMW it is used to identify and resolve the property annotation syntax ([[link::syntax]]), returning a modified content component and storing annotations within the ParserOutput object.
+
+#### LinksUpdateConstructed
+LinksUpdateConstructed is called at the end of LinksUpdate and is being used to initiate a store update for data that were held by the ParserOutput object.
+
+#### NewRevisionFromEditComplete
+NewRevisionFromEditComplete called when a new revision was inserted due to an edit and used to update the ParserOuput with the latests special property annotation.
+
+#### ParserAfterTidy
+ParserAfterTidy is used to re-introduce content, update base annotations (e.g. special properties, categories etc.) and in case of a manual article purge initiates a store update (LinksUpdateConstructed wouldn't work because it acts only on link changes and therefore would not trigger a LinksUpdateConstructed event).
+
+#### SpecialStatsAddExtra
+SpecialStatsAddExtra is used to add additional statistic being shown at Special:Statistics.
+
+#### SkinAfterContent
+Extend the display with content from the Factbox.
+
+#### OutputPageParserOutput
+Rendering the Factbox and updating the FactboxCache.
+
+#### TitleMoveComplete
+Update the Store after an article has been deleted.
+
+#### ResourceLoaderGetConfigVars
+
+#### GetPreferences
+
+#### SkinTemplateNavigation
+
+[hooks]: https://www.mediawiki.org/wiki/Hooks "Manual:Hooks" \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/RejectParserCacheValue.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/RejectParserCacheValue.php
new file mode 100644
index 00000000..b2b21af2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/RejectParserCacheValue.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\SQLStore\QueryDependency\DependencyLinksUpdateJournal;
+use Title;
+
+/**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/RejectParserCacheValue
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class RejectParserCacheValue extends HookHandler {
+
+ /**
+ * @var DependencyLinksUpdateJournal
+ */
+ private $dependencyLinksUpdateJournal;
+
+ /**
+ * @since 3.0
+ *
+ * @param DependencyLinksUpdateJournal $dependencyLinksUpdateJournal
+ */
+ public function __construct( DependencyLinksUpdateJournal $dependencyLinksUpdateJournal ) {
+ $this->dependencyLinksUpdateJournal = $dependencyLinksUpdateJournal;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ *
+ * @return boolean
+ */
+ public function process( Title $title ) {
+
+ if ( $this->dependencyLinksUpdateJournal->has( $title ) ) {
+ $this->dependencyLinksUpdateJournal->delete( $title );
+ return false;
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderGetConfigVars.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderGetConfigVars.php
new file mode 100644
index 00000000..1117347d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderGetConfigVars.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use MWNamespace;
+use SMW\Localizer;
+
+/**
+ * Hook: ResourceLoaderGetConfigVars called right before
+ * ResourceLoaderStartUpModule::getConfig and exports static configuration
+ * variables to JavaScript. Things that depend on the current
+ * page/request state should use MakeGlobalVariablesScript instead
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderGetConfigVars
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class ResourceLoaderGetConfigVars extends HookHandler {
+
+ /**
+ * @since 1.9
+ *
+ * @param array $vars
+ *
+ * @return boolean
+ */
+ public function process( array &$vars ) {
+
+ $vars['smw-config'] = [
+ 'version' => SMW_VERSION,
+ 'namespaces' => [],
+ 'settings' => [
+ 'smwgQMaxLimit' => $GLOBALS['smwgQMaxLimit'],
+ 'smwgQMaxInlineLimit' => $GLOBALS['smwgQMaxInlineLimit'],
+ ]
+ ];
+
+ $localizer = Localizer::getInstance();
+
+ // Available semantic namespaces
+ foreach ( array_keys( $GLOBALS['smwgNamespacesWithSemanticLinks'] ) as $ns ) {
+ $name = MWNamespace::getCanonicalName( $ns );
+ $vars['smw-config']['settings']['namespace'][$name] = $ns;
+ $vars['smw-config']['namespaces']['canonicalName'][$ns] = $name;
+ $vars['smw-config']['namespaces']['localizedName'][$ns] = $localizer->getNamespaceTextById( $ns );
+ }
+
+ foreach ( array_keys( $GLOBALS['smwgResultFormats'] ) as $format ) {
+ $vars['smw-config']['formats'][$format] = htmlspecialchars( $format );
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderTestModules.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderTestModules.php
new file mode 100644
index 00000000..6fcc5acf
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/ResourceLoaderTestModules.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use ResourceLoader;
+
+/**
+ * Add new JavaScript/QUnit testing modules
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderTestModules
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class ResourceLoaderTestModules extends HookHandler {
+
+ /**
+ * @var ResourceLoader
+ */
+ private $resourceLoader;
+
+ /**
+ * @var string
+ */
+ private $path;
+
+ /**
+ * @var string
+ */
+ private $ip;
+
+ /**
+ * @since 2.0
+ *
+ * @param ResourceLoader $resourceLoader object
+ * @param string $path
+ * @param string $ip
+ */
+ public function __construct( ResourceLoader &$resourceLoader, $path = '', $ip = '' ) {
+ $this->resourceLoader = $resourceLoader;
+ $this->path = $path;
+ $this->ip = $ip;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param array &$testModules
+ *
+ * @return boolean
+ */
+ public function process( array &$testModules ) {
+
+ $testModules['qunit']['ext.smw.tests'] = [
+ 'scripts' => [
+ 'tests/qunit/smw/ext.smw.test.js',
+ 'tests/qunit/smw/util/ext.smw.util.tooltip.test.js',
+
+ // dataItem tests
+ 'tests/qunit/smw/data/ext.smw.dataItem.wikiPage.test.js',
+ 'tests/qunit/smw/data/ext.smw.dataItem.uri.test.js',
+ 'tests/qunit/smw/data/ext.smw.dataItem.time.test.js',
+ 'tests/qunit/smw/data/ext.smw.dataItem.property.test.js',
+ 'tests/qunit/smw/data/ext.smw.dataItem.unknown.test.js',
+ 'tests/qunit/smw/data/ext.smw.dataItem.number.test.js',
+ 'tests/qunit/smw/data/ext.smw.dataItem.text.test.js',
+
+ // dataValues
+ 'tests/qunit/smw/data/ext.smw.dataValue.quantity.test.js',
+
+ // Api / Query
+ 'tests/qunit/smw/data/ext.smw.data.test.js',
+ 'tests/qunit/smw/api/ext.smw.api.test.js',
+ 'tests/qunit/smw/query/ext.smw.query.test.js',
+ ],
+ 'dependencies' => [
+ 'ext.smw',
+ 'ext.smw.tooltip',
+ 'ext.smw.query',
+ 'ext.smw.data',
+ 'ext.smw.api'
+ ],
+ 'position' => 'top',
+ 'localBasePath' => $this->path,
+ 'remoteExtPath' => 'SemanticMediaWiki',
+ ];
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SkinAfterContent.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SkinAfterContent.php
new file mode 100644
index 00000000..a6fd902b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SkinAfterContent.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Skin;
+use SMW\ApplicationFactory;
+
+/**
+ * SkinAfterContent hook to add text after the page content and
+ * article metadata
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinAfterContent
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class SkinAfterContent {
+
+ /**
+ * @var Skin
+ */
+ private $skin = null;
+
+ /**
+ * @since 1.9
+ *
+ * @param Skin|null $skin
+ */
+ public function __construct( Skin $skin = null ) {
+ $this->skin = $skin;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param string &$data
+ *
+ * @return true
+ */
+ public function performUpdate( &$data ) {
+
+ if ( $this->canAddFactbox() ) {
+ $this->addFactboxTo( $data );
+ }
+
+ return true;
+ }
+
+ private function canAddFactbox() {
+
+ if ( !$this->skin instanceof Skin || !ApplicationFactory::getInstance()->getSettings()->get( 'smwgSemanticsEnabled' ) ) {
+ return false;
+ }
+
+ $request = $this->skin->getContext()->getRequest();
+
+ if ( in_array( $request->getVal( 'action' ), [ 'delete', 'purge', 'protect', 'unprotect', 'history' ] ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private function addFactboxTo( &$data ) {
+
+ $cachedFactbox = ApplicationFactory::getInstance()->singleton( 'FactboxFactory' )->newCachedFactbox();
+
+ $data .= $cachedFactbox->retrieveContent(
+ $this->skin->getOutput()
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SkinTemplateNavigation.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SkinTemplateNavigation.php
new file mode 100644
index 00000000..a1a5b109
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SkinTemplateNavigation.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SkinTemplate;
+
+/**
+ * Alter the structured navigation links in SkinTemplates.
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class SkinTemplateNavigation {
+
+ /**
+ * @var SkinTemplate
+ */
+ private $skinTemplate = null;
+
+ /**
+ * @var array
+ */
+ private $links;
+
+ /**
+ * @since 2.0
+ *
+ * @param SkinTemplate $skinTemplate
+ * @param array $links
+ */
+ public function __construct( SkinTemplate &$skinTemplate, array &$links ) {
+ $this->skinTemplate = $skinTemplate;
+ $this->links =& $links;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return true
+ */
+ public function process() {
+
+ if ( $this->skinTemplate->getUser()->isAllowed( 'purge' ) ) {
+ $this->skinTemplate->getOutput()->addModules( 'ext.smw.purge' );
+ $this->links['actions']['purge'] = [
+ 'class' => 'is-disabled',
+ 'text' => $this->skinTemplate->msg( 'smw_purge' )->text(),
+ 'href' => $this->skinTemplate->getTitle()->getLocalUrl( [ 'action' => 'purge' ] )
+ ];
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SpecialSearchResultsPrepend.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SpecialSearchResultsPrepend.php
new file mode 100644
index 00000000..c5831cd6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SpecialSearchResultsPrepend.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use Html;
+use OutputPage;
+use SMW\MediaWiki\Search\Search as SMWSearch;
+use SMW\Message;
+use SMW\Utils\HtmlModal;
+use SpecialSearch;
+
+/**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialSearchResultsPrepend
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SpecialSearchResultsPrepend extends HookHandler {
+
+ /**
+ * @var SpecialSearch
+ */
+ private $specialSearch;
+
+ /**
+ * @var OutputPage
+ */
+ private $outputPage;
+
+ /**
+ * @since 3.0
+ *
+ * @param SpecialSearch $specialSearch
+ * @param OutputPage &$outputPage
+ */
+ public function __construct( SpecialSearch $specialSearch, OutputPage $outputPage ) {
+ $this->specialSearch = $specialSearch;
+ $this->outputPage = $outputPage;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $term
+ *
+ * @return boolean
+ */
+ public function process( $term ) {
+
+ $html = '';
+
+ if ( $this->specialSearch->getSearchEngine() instanceof SMWSearch ) {
+ $this->outputPage->addModuleStyles( [ 'smw.ui.styles', 'smw.special.search.styles' ] );
+ $this->outputPage->addModules( [ 'smw.special.search', 'smw.ui' ] );
+
+ $this->outputPage->addModuleStyles( HtmlModal::getModuleStyles() );
+ $this->outputPage->addModules( HtmlModal::getModules() );
+
+ $html .= HtmlModal::link(
+ '<span class="smw-icon-info" style="margin-left: -5px; padding: 10px 12px 12px 12px;"></span>',
+ [
+ 'data-id' => 'smw-search-cheat-sheet'
+ ]
+ );
+
+ $html .= Message::get(
+ 'smw-search-syntax-support',
+ Message::PARSE,
+ Message::USER_LANGUAGE
+ );
+
+ if ( $this->getOption( 'prefs-suggester-textinput' ) ) {
+ $html .= ' ' . Message::get(
+ 'smw-search-input-assistance',
+ Message::PARSE,
+ Message::USER_LANGUAGE
+ );
+ }
+
+ $html .= HtmlModal::modal(
+ Message::get( 'smw-cheat-sheet', Message::TEXT, Message::USER_LANGUAGE ),
+ $this->search_sheet( $this->getOption( 'prefs-suggester-textinput' ) ),
+ [
+ 'id' => 'smw-search-cheat-sheet',
+ 'class' => 'plainlinks',
+ 'style' => 'display:none;'
+ ]
+ );
+ }
+
+ if ( $html !== '' && !$this->getOption( 'prefs-disable-search-info' ) ) {
+ $this->outputPage->addHtml(
+ "<div class='smw-search-results-prepend plainlinks'>$html</div>"
+ );
+ }
+
+ return true;
+ }
+
+ private function search_sheet( $inputAssistance ) {
+
+ $text = $this->msg( 'smw-search-help-intro' );
+ $text .= $this->section( 'smw-search-input' );
+
+ $text .= $this->msg( 'smw-search-help-structured' );
+ $text .= $this->msg( 'smw-search-help-proximity' );
+
+ if ( $inputAssistance ) {
+ $text .= $this->section( 'smw-ask-input-assistance' );
+ $text .= $this->msg( 'smw-search-help-input-assistance' );
+ }
+
+ $text .= $this->section( 'smw-search-syntax' );
+ $text .= $this->msg( 'smw-search-help-ask' );
+
+ return $text;
+ }
+
+ private function section( $msg, $attributes = [] ) {
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-text-strike',
+ 'style' => 'padding: 5px 0 5px 0;'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'style' => 'font-size: 1.2em; margin-left:0px'
+ ],
+ Message::get( $msg, Message::TEXT, Message::USER_LANGUAGE )
+ )
+ );
+ }
+
+ private function msg( $msg, $html = '', $attributes = [] ) {
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => $msg
+ ] + $attributes,
+ Message::get( $msg, Message::PARSE, Message::USER_LANGUAGE ) . $html
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SpecialStatsAddExtra.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SpecialStatsAddExtra.php
new file mode 100644
index 00000000..42389e28
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/SpecialStatsAddExtra.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\DataTypeRegistry;
+use SMW\Store;
+
+/**
+ * Add extra statistic at the end of Special:Statistics
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/SpecialStatsAddExtra
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class SpecialStatsAddExtra extends HookHandler {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var string[]
+ */
+ private $messageMapper = [
+ 'PROPUSES' => 'smw-statistics-property-instance',
+ 'ERRORUSES' => 'smw-statistics-error-count',
+ 'TOTALPROPS' => 'smw-statistics-property-total',
+ 'USEDPROPS' => 'smw-statistics-property-used',
+ 'OWNPAGE' => 'smw-statistics-property-page',
+ 'DECLPROPS' => 'smw-statistics-property-type',
+ 'DELETECOUNT' => 'smw-statistics-delete-count',
+ 'SUBOBJECTS' => 'smw-statistics-subobject-count',
+ 'QUERY' => 'smw-statistics-query-inline',
+ 'CONCEPTS' => 'smw-statistics-concept-count',
+ ];
+
+ /**
+ * @since 1.9
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param array &$extraStats
+ *
+ * @return true
+ */
+ public function process( array &$extraStats ) {
+
+ if ( !$this->getOption( 'smwgSemanticsEnabled', false ) ) {
+ return true;
+ }
+
+ $this->copyStatistics( $extraStats );
+
+ return true;
+ }
+
+ private function copyStatistics( &$extraStats ) {
+
+ $statistics = $this->store->getStatistics();
+
+ $extraStats['smw-statistics'] = [];
+
+ foreach ( $this->messageMapper as $key => $message ) {
+ if ( isset( $statistics[$key] ) ) {
+ $extraStats['smw-statistics'][$message] = $statistics[$key];
+ }
+ }
+
+ $count = count(
+ DataTypeRegistry::getInstance()->getKnownTypeLabels()
+ );
+
+ $extraStats['smw-statistics']['smw-statistics-datatype-count'] = $count;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleIsAlwaysKnown.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleIsAlwaysKnown.php
new file mode 100644
index 00000000..31a16dfc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleIsAlwaysKnown.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\DIProperty;
+use Title;
+
+/**
+ * Allows overriding default behaviour for determining if a page exists
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleIsAlwaysKnown
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class TitleIsAlwaysKnown {
+
+ /**
+ * @var Title
+ */
+ private $title;
+
+ /**
+ * @var mixed
+ */
+ private $result;
+
+ /**
+ * @since 2.0
+ *
+ * @param Title $title
+ * @param mixed &$result
+ */
+ public function __construct( Title $title, &$result ) {
+ $this->title = $title;
+ $this->result =& $result;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return boolean
+ */
+ public function process() {
+
+ // Two possible ways of going forward:
+ //
+ // The FIRST seen here is to use the hook to override the known status
+ // for predefined properties in order to avoid any edit link
+ // which makes no-sense for predefined properties
+ //
+ // The SECOND approach is to inject SMWWikiPageValue with a setLinkOptions setter
+ // that enables to set the custom options 'known' for each invoked linker during
+ // getShortHTMLText
+ // $linker->link( $this->getTitle(), $caption, $customAttributes, $customQuery, $customOptions )
+ //
+ // @see also HooksTest::testOnTitleIsAlwaysKnown
+
+ if ( $this->title->getNamespace() === SMW_NS_PROPERTY ) {
+ if ( !DIProperty::newFromUserLabel( $this->title->getText() )->isUserDefined() ) {
+ $this->result = true;
+ }
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleIsMovable.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleIsMovable.php
new file mode 100644
index 00000000..3a3670d8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleIsMovable.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\DIProperty;
+use Title;
+
+/**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/TitleIsMovable
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class TitleIsMovable extends HookHandler {
+
+ /**
+ * @var Title
+ */
+ private $title;
+
+ /**
+ * @since 2.1
+ *
+ * @param Title $title
+ */
+ public function __construct( Title $title ) {
+ $this->title = $title;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param boolean &$isMovable
+ *
+ * @return boolean
+ */
+ public function process( &$isMovable ) {
+
+ // We don't allow rule pages to be moved as we cannot track JSON content
+ // as redirects and therefore invalidate any rule assignment without a
+ // possibility to automatically reassign IDs
+ if ( $this->title->getNamespace() === SMW_NS_SCHEMA ) {
+ $isMovable = false;
+ }
+
+ if ( $this->title->getNamespace() !== SMW_NS_PROPERTY ) {
+ return true;
+ }
+
+ // Predefined properties cannot be moved!
+ if ( !DIProperty::newFromUserLabel( $this->title->getText() )->isUserDefined() ) {
+ $isMovable = false;
+ }
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleMoveComplete.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleMoveComplete.php
new file mode 100644
index 00000000..d3858062
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/TitleMoveComplete.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\ApplicationFactory;
+use SMW\EventHandler;
+use SMW\Factbox\FactboxCache;
+
+/**
+ * TitleMoveComplete occurs whenever a request to move an article
+ * is completed
+ *
+ * This method will be called whenever an article is moved so that
+ * semantic properties are moved accordingly.
+ *
+ * @see http://www.mediawiki.org/wiki/Manual:Hooks/TitleMoveComplete
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class TitleMoveComplete {
+
+ /**
+ * @var Title
+ */
+ protected $oldTitle = null;
+
+ /**
+ * @var Title
+ */
+ protected $newTitle = null;
+
+ /**
+ * @var User
+ */
+ protected $user = null;
+
+ /**
+ * @var integer
+ */
+ protected $oldId;
+
+ /**
+ * @var integer
+ */
+ protected $newId;
+
+ /**
+ * @since 1.9
+ *
+ * @param Title $oldTitle old title
+ * @param Title $newTitle: new title
+ * @param Use $user user who did the move
+ * @param $oldId database ID of the page that's been moved
+ * @param $newId database ID of the created redirect
+ */
+ public function __construct( &$oldTitle, &$newTitle, &$user, $oldId, $newId ) {
+ $this->oldTitle = $oldTitle;
+ $this->newTitle = $newTitle;
+ $this->user = $user;
+ $this->oldId = $oldId;
+ $this->newId = $newId;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return true
+ */
+ public function process() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ // Delete all data for a non-enabled target NS
+ if ( !$applicationFactory->getNamespaceExaminer()->isSemanticEnabled( $this->newTitle->getNamespace() ) || $this->newId == 0 ) {
+
+ $applicationFactory->getStore()->deleteSubject(
+ $this->oldTitle
+ );
+
+ } else {
+
+ // Using a different approach since the hook is not triggered
+ // by #REDIRECT which can cause inconsistencies
+ // @see 2.3 / StoreUpdater
+
+ // $applicationFactory->getStore()->changeTitle(
+ // $this->oldTitle,
+ // $this->newTitle,
+ // $this->oldId,
+ // $this->newId
+ // );
+ }
+
+ $eventHandler = EventHandler::getInstance();
+
+ $dispatchContext = $eventHandler->newDispatchContext();
+ $dispatchContext->set( 'title', $this->oldTitle );
+ $dispatchContext->set( 'context', 'ArticleMove' );
+
+ $eventHandler->getEventDispatcher()->dispatch(
+ 'cached.prefetcher.reset',
+ $dispatchContext
+ );
+
+ $dispatchContext = $eventHandler->newDispatchContext();
+ $dispatchContext->set( 'title', $this->newTitle );
+ $dispatchContext->set( 'context', 'ArticleMove' );
+
+ $eventHandler->getEventDispatcher()->dispatch(
+ 'cached.prefetcher.reset',
+ $dispatchContext
+ );
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/UserChange.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/UserChange.php
new file mode 100644
index 00000000..b86a2a80
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Hooks/UserChange.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace SMW\MediaWiki\Hooks;
+
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Jobs\UpdateJob;
+use SMW\NamespaceExaminer;
+use Title;
+use User;
+
+/**
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/BlockIpComplete
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/UnblockUserComplete
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/UserGroupsChanged
+ *
+ * Act on events that happen outside of the normal parser process to ensure that
+ * changes to pre-defined properties related to a user status can be invoked.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class UserChange extends HookHandler {
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @var string
+ */
+ private $origin = '';
+
+ /**
+ * @since 3.0
+ *
+ * @param NamespaceExaminer $namespaceExaminer
+ */
+ public function __construct( NamespaceExaminer $namespaceExaminer ) {
+ $this->namespaceExaminer = $namespaceExaminer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $origin
+ */
+ public function setOrigin( $origin ) {
+ $this->origin = $origin;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param User|string $user
+ */
+ public function process( $user ) {
+
+ if ( !$this->namespaceExaminer->isSemanticEnabled( NS_USER ) ) {
+ return false;
+ }
+
+ if ( $user instanceof User ) {
+ $user = $user->getName();
+ }
+
+ $updateJob = ApplicationFactory::getInstance()->newJobFactory()->newUpdateJob(
+ Title::newFromText( $user, NS_USER ),
+ [
+ UpdateJob::FORCED_UPDATE => true,
+ 'origin' => $this->origin
+ ]
+ );
+
+ $updateJob->insert();
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Job.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Job.php
new file mode 100644
index 00000000..cb289e86
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Job.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Job as MediaWikiJob;
+use JobQueueGroup;
+use SMW\ApplicationFactory;
+use SMW\Site;
+use SMW\Store;
+use Title;
+
+/**
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+abstract class Job extends MediaWikiJob {
+
+ /**
+ * @var boolean
+ */
+ protected $isEnabledJobQueue = true;
+
+ /**
+ * @var JobQueue
+ */
+ protected $jobQueue;
+
+ /**
+ * @var Job
+ */
+ protected $jobs = [];
+
+ /**
+ * @var Store
+ */
+ protected $store = null;
+
+ /**
+ * @since 2.1
+ *
+ * @param Store $store
+ */
+ public function setStore( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * Whether to insert jobs into the JobQueue is enabled or not
+ *
+ * @since 1.9
+ *
+ * @param boolean|true $enableJobQueue
+ *
+ * @return AbstractJob
+ */
+ public function isEnabledJobQueue( $enableJobQueue = true ) {
+ $this->isEnabledJobQueue = (bool)$enableJobQueue;
+ return $this;
+ }
+
+ /**
+ * @note Job::batchInsert was deprecated in MW 1.21
+ * JobQueueGroup::singleton()->push( $job );
+ *
+ * @since 1.9
+ */
+ public function pushToJobQueue() {
+ $this->isEnabledJobQueue ? self::batchInsert( $this->jobs ) : null;
+ }
+
+ /**
+ * @note Job::getType was introduced with MW 1.21
+ *
+ * @return string
+ */
+ public function getType() {
+ return $this->command;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return integer
+ */
+ public function getJobCount() {
+ return count( $this->jobs );
+ }
+
+ /**
+ * @note Job::getTitle() in MW 1.19 does not exist
+ *
+ * @since 1.9
+ *
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param mixed $key
+ *
+ * @return boolean
+ */
+
+ public function hasParameter( $key ) {
+
+ if ( !is_array( $this->params ) ) {
+ return false;
+ }
+
+ return isset( $this->params[$key] ) || array_key_exists( $key, $this->params );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param mixed $key
+ *
+ * @return boolean
+ */
+ public function getParameter( $key, $default = false ) {
+ return $this->hasParameter( $key ) ? $this->params[$key] : $default;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param mixed $key
+ * @param mixed $value
+ */
+ public function setParameter( $key, $value ) {
+ $this->params[$key] = $value;
+ }
+
+ /**
+ * @see https://gerrit.wikimedia.org/r/#/c/162009
+ *
+ * @param self[] $jobs
+ *
+ * @return boolean
+ */
+ public static function batchInsert( $jobs ) {
+ return ApplicationFactory::getInstance()->getJobQueue()->push( $jobs );
+ }
+
+ /**
+ * @see Job::insert
+ */
+ public function insert() {
+ if ( $this->isEnabledJobQueue ) {
+ return self::batchInsert( [ $this ] );
+ }
+ }
+
+ /**
+ * @see JobQueueGroup::lazyPush
+ *
+ * @note Registered jobs are pushed using JobQueueGroup::pushLazyJobs at the
+ * end of MediaWiki::restInPeace
+ *
+ * @since 3.0
+ */
+ public function lazyPush() {
+ if ( $this->isEnabledJobQueue ) {
+ return $this->getJobQueue()->lazyPush( $this );
+ }
+ }
+
+ /**
+ * @see Translate::TTMServerMessageUpdateJob
+ * @since 3.0
+ *
+ * @param integer $delay
+ */
+ public function setDelay( $delay ) {
+
+ $isDelayedJobsEnabled = $this->getJobQueue()->isDelayedJobsEnabled(
+ $this->getType()
+ );
+
+ if ( !$delay || !$isDelayedJobsEnabled ) {
+ return;
+ }
+
+ $oldTime = $this->getReleaseTimestamp();
+ $newTime = time() + $delay;
+
+ if ( $oldTime !== null && $oldTime >= $newTime ) {
+ return;
+ }
+
+ $this->params[ 'jobReleaseTimestamp' ] = $newTime;
+ }
+
+ /**
+ * @see Job::newRootJobParams
+ * @since 3.0
+ */
+ public static function newRootJobParams( $key = '', $title = '' ) {
+
+ if ( $title instanceof Title ) {
+ $title = $title->getPrefixedDBkey();
+ }
+
+ return parent::newRootJobParams( "job:{$key}:root:{$title}" );
+ }
+
+ /**
+ * @see Job::ignoreDuplicates
+ * @since 3.0
+ */
+ public function ignoreDuplicates() {
+
+ if ( isset( $this->params['waitOnCommandLine'] ) ) {
+ return $this->params['waitOnCommandLine'] > 1;
+ }
+
+ return $this->removeDuplicates;
+ }
+
+ /**
+ * Only run the job via commandLine or the cronJob and avoid execution via
+ * Special:RunJobs as it can cause the script to timeout.
+ */
+ public function waitOnCommandLineMode() {
+
+ if ( !$this->hasParameter( 'waitOnCommandLine' ) || Site::isCommandLineMode() ) {
+ return false;
+ }
+
+ if ( $this->hasParameter( 'waitOnCommandLine' ) ) {
+ $this->params['waitOnCommandLine'] = $this->getParameter( 'waitOnCommandLine' ) + 1;
+ } else {
+ $this->params['waitOnCommandLine'] = 1;
+ }
+
+ $job = new static( $this->title, $this->params );
+ $job->insert();
+
+ return true;
+ }
+
+ protected function getJobQueue() {
+
+ if ( $this->jobQueue === null ) {
+ $this->jobQueue = ApplicationFactory::getInstance()->getJobQueue();
+ }
+
+ return $this->jobQueue;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/JobFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/JobFactory.php
new file mode 100644
index 00000000..6fab53e2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/JobFactory.php
@@ -0,0 +1,224 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use SMW\MediaWiki\Jobs\NullJob;
+use SMW\MediaWiki\Jobs\RefreshJob;
+use SMW\MediaWiki\Jobs\UpdateJob;
+use SMW\MediaWiki\Jobs\UpdateDispatcherJob;
+use SMW\MediaWiki\Jobs\ParserCachePurgeJob;
+use SMW\MediaWiki\Jobs\EntityIdDisposerJob;
+use SMW\MediaWiki\Jobs\PropertyStatisticsRebuildJob;
+use SMW\MediaWiki\Jobs\FulltextSearchTableUpdateJob;
+use SMW\MediaWiki\Jobs\FulltextSearchTableRebuildJob;
+use SMW\MediaWiki\Jobs\ChangePropagationDispatchJob;
+use SMW\MediaWiki\Jobs\ChangePropagationUpdateJob;
+use SMW\MediaWiki\Jobs\ChangePropagationClassUpdateJob;
+use RuntimeException;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class JobFactory {
+
+ /**
+ * @since 2.5
+ *
+ * @param array $jobs
+ */
+ public static function batchInsert( array $jobs ) {
+ Job::batchInsert( $jobs );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $type
+ * @param Title|null $title
+ * @param array $parameters
+ *
+ * @return Job
+ * @throws RuntimeException
+ */
+ public function newByType( $type, Title $title = null, array $parameters = [] ) {
+
+ if ( $title === null ) {
+ return new NullJob( null );
+ }
+
+ switch ( $type ) {
+ case 'SMW\RefreshJob':
+ case 'smw.refresh':
+ return $this->newRefreshJob( $title, $parameters );
+ case 'SMW\UpdateJob':
+ case 'smw.update':
+ return $this->newUpdateJob( $title, $parameters );
+ case 'SMW\UpdateDispatcherJob':
+ case 'smw.updateDispatcher':
+ return $this->newUpdateDispatcherJob( $title, $parameters );
+ case 'SMW\ParserCachePurgeJob':
+ case 'smw.parserCachePurge':
+ return $this->newParserCachePurgeJob( $title, $parameters );
+ case 'SMW\EntityIdDisposerJob':
+ case 'smw.entityIdDisposer':
+ return $this->newEntityIdDisposerJob( $title, $parameters );
+ case 'SMW\PropertyStatisticsRebuildJob':
+ case 'smw.propertyStatisticsRebuild':
+ return $this->newPropertyStatisticsRebuildJob( $title, $parameters );
+ case 'SMW\FulltextSearchTableUpdateJob':
+ case 'smw.fulltextSearchTableUpdate':
+ return $this->newFulltextSearchTableUpdateJob( $title, $parameters );
+ case 'SMW\FulltextSearchTableRebuildJob':
+ case 'smw.fulltextSearchTableRebuild':
+ return $this->newFulltextSearchTableRebuildJob( $title, $parameters );
+ case 'SMW\ChangePropagationDispatchJob':
+ case 'smw.changePropagationDispatch':
+ return $this->newChangePropagationDispatchJob( $title, $parameters );
+ case 'SMW\ChangePropagationUpdateJob':
+ case 'smw.changePropagationUpdate':
+ return $this->newChangePropagationUpdateJob( $title, $parameters );
+ case 'SMW\ChangePropagationClassUpdateJob':
+ case 'smw.changePropagationClassUpdate':
+ return $this->newChangePropagationClassUpdateJob( $title, $parameters );
+ }
+
+ throw new RuntimeException( "Unable to match $type to a valid Job type" );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return RefreshJob
+ */
+ public function newRefreshJob( Title $title, array $parameters = [] ) {
+ return new RefreshJob( $title, $parameters );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return UpdateJob
+ */
+ public function newUpdateJob( Title $title, array $parameters = [] ) {
+ return new UpdateJob( $title, $parameters );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return UpdateDispatcherJob
+ */
+ public function newUpdateDispatcherJob( Title $title, array $parameters = [] ) {
+ return new UpdateDispatcherJob( $title, $parameters );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return ParserCachePurgeJob
+ */
+ public function newParserCachePurgeJob( Title $title, array $parameters = [] ) {
+ return new ParserCachePurgeJob( $title, $parameters );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return FulltextSearchTableUpdateJob
+ */
+ public function newFulltextSearchTableUpdateJob( Title $title, array $parameters = [] ) {
+ return new FulltextSearchTableUpdateJob( $title, $parameters );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return EntityIdDisposerJob
+ */
+ public function newEntityIdDisposerJob( Title $title, array $parameters = [] ) {
+ return new EntityIdDisposerJob( $title, $parameters );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return PropertyStatisticsRebuildJob
+ */
+ public function newPropertyStatisticsRebuildJob( Title $title, array $parameters = [] ) {
+ return new PropertyStatisticsRebuildJob( $title, $parameters );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return FulltextSearchTableRebuildJob
+ */
+ public function newFulltextSearchTableRebuildJob( Title $title, array $parameters = [] ) {
+ return new FulltextSearchTableRebuildJob( $title, $parameters );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return ChangePropagationDispatchJob
+ */
+ public function newChangePropagationDispatchJob( Title $title, array $parameters = [] ) {
+ return new ChangePropagationDispatchJob( $title, $parameters );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return ChangePropagationUpdateJob
+ */
+ public function newChangePropagationUpdateJob( Title $title, array $parameters = [] ) {
+ return new ChangePropagationUpdateJob( $title, $parameters );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return ChangePropagationClassUpdateJob
+ */
+ public function newChangePropagationClassUpdateJob( Title $title, array $parameters = [] ) {
+ return new ChangePropagationClassUpdateJob( $title, $parameters );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/JobQueue.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/JobQueue.php
new file mode 100644
index 00000000..2e580459
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/JobQueue.php
@@ -0,0 +1,213 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use JobQueueGroup;
+
+/**
+ * MediaWiki's JobQueue contains mostly final methods making it difficult to use
+ * an instance during tests hence this class provides a reduced interface with
+ * mockable methods.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class JobQueue {
+
+ /**
+ * @var JobQueueGroup
+ */
+ private $jobQueueGroup;
+
+ /**
+ * @var boolean
+ */
+ private $disableCache = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param JobQueueGroup $jobQueueGroup
+ */
+ public function __construct( JobQueueGroup $jobQueueGroup ) {
+ $this->jobQueueGroup = $jobQueueGroup;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $disableCache
+ */
+ public function disableCache( $disableCache = true ) {
+ $this->disableCache = (bool)$disableCache;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return boolean
+ */
+ public function isDelayedJobsEnabled( $type ) {
+ return $this->jobQueueGroup->get( $this->mapLegacyType( $type ) )->delayedJobsEnabled();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $list
+ *
+ * @return []
+ */
+ public function runFromQueue( array $list ) {
+
+ $log = [];
+
+ foreach ( $list as $type => $amount ) {
+
+ if ( $amount == 0 || $amount === false ) {
+ continue;
+ }
+
+ $jobs = array_fill( 0, $amount, $type );
+ $log[$type] = [];
+
+ foreach ( $jobs as $job ) {
+ $j = $this->pop( $job );
+
+ if ( $j === false ) {
+ break;
+ }
+
+ $log[$type][] = $j->getTitle()->getPrefixedDBKey();
+
+ $j->run();
+ $this->ack( $j );
+ }
+ }
+
+ return $log;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return Job|boolean
+ */
+ public function pop( $type ) {
+ return $this->jobQueueGroup->get( $this->mapLegacyType( $type ) )->pop();
+ }
+
+ /**
+ * Acknowledge that a job was completed
+ *
+ * @since 3.0
+ *
+ * @param Job $job
+ */
+ public function ack( \Job $job ) {
+ $this->jobQueueGroup->get( $job->getType() )->ack( $job );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ */
+ public function delete( $type ) {
+
+ $jobQueue = $this->jobQueueGroup->get( $this->mapLegacyType( $type ) );
+ $jobQueue->delete();
+
+ if ( $this->disableCache ) {
+ $jobQueue->flushCaches();
+ $this->disableCache = false;
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Job|Job[] $jobs
+ */
+ public function push( $jobs ) {
+ $this->jobQueueGroup->push( $jobs );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Job|Job[] $jobs
+ */
+ public function lazyPush( $jobs ) {
+
+ if ( !method_exists( $this->jobQueueGroup, 'lazyPush' ) ) {
+ return $this->push( $jobs );
+ }
+
+ $this->jobQueueGroup->lazyPush( $jobs );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getQueueSizes() {
+ return $this->jobQueueGroup->getQueueSizes();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return integer
+ */
+ public function getQueueSize( $type ) {
+
+ $jobQueue = $this->jobQueueGroup->get( $this->mapLegacyType( $type ) );
+
+ if ( $this->disableCache ) {
+ $jobQueue->flushCaches();
+ $this->disableCache = false;
+ }
+
+ return $jobQueue->getSize();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return boolean
+ */
+ public function hasPendingJob( $type ) {
+ return $this->getQueueSize( $type ) > 0;
+ }
+
+ /**
+ * @note FIXME Remove with 3.1
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public static function mapLegacyType( $type ) {
+
+ // Legacy names
+ if ( strpos( $type, 'SMW\\' ) !== false ) {
+ $type = 'smw.' . lcfirst( str_replace( [ 'SMW\\', 'Job' ], '', $type ) );
+ }
+
+ return $type;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationClassUpdateJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationClassUpdateJob.php
new file mode 100644
index 00000000..70e15fdc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationClassUpdateJob.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use Title;
+
+/**
+ * Isolate instance to count update jobs in connection with a category related
+ * update.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ChangePropagationClassUpdateJob extends ChangePropagationUpdateJob {
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [] ) {
+
+ $params = $params + [
+ 'origin' => 'ChangePropagationClassUpdateJob'
+ ];
+
+ parent::__construct( $title, $params, 'smw.changePropagationClassUpdate' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationDispatchJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationDispatchJob.php
new file mode 100644
index 00000000..07b42ff8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationDispatchJob.php
@@ -0,0 +1,424 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\SQLStore\ChangePropagationEntityFinder;
+use SMWExporter as Exporter;
+use Title;
+
+/**
+ * `ChangePropagationDispatchJob` dispatches update jobs via `ChangePropagationUpdateJob`
+ * to allow isolating the execution and count pending jobs without using an extra
+ * tracking mechanism during an update process.
+ *
+ * `ChangePropagationUpdateJob` (and hereby ChangePropagationClassUpdateJob) itself
+ * relies on the `UpdateJob` to initiate the update.
+ *
+ * `ChangePropagationDispatchJob` is responsible for:
+ *
+ * - Select entities that are being connected to a property specification
+ * change
+ * - Once the selection process has been finalized, update the property with the
+ * new specification (which has been locked before this update)
+ *
+ * Due to the possibility that a large list of entities can be connected to a
+ * property and its change, an iterative or recursive processing is not viable
+ * (as the changed specification should be available as soon as possible) therefore
+ * the selection process will move the result of entities to chunked temp files
+ * to avoid having to use a DB connection during the process (has been observed
+ * during tests that would lead to an out-of-memory) to store a list of
+ * entities that require an update.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ChangePropagationDispatchJob extends Job {
+
+ /**
+ * Size of rows stored in a temp file
+ */
+ const CHUNK_SIZE = 1000;
+
+ /**
+ * Temp marker namespace
+ */
+ const CACHE_NAMESPACE = 'smw:chgprop';
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $params
+ */
+ public function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.changePropagationDispatch', $title, $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * Called from PropertyChangePropagationNotifier
+ *
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ * @param array $params
+ *
+ * @return boolean
+ */
+ public static function planAsJob( DIWikiPage $subject, $params = [] ) {
+
+ Exporter::getInstance()->resetCacheBy( $subject );
+ ApplicationFactory::getInstance()->getPropertySpecificationLookup()->resetCacheBy(
+ $subject
+ );
+
+ $changePropagationDispatchJob = new self( $subject->getTitle(), $params );
+ $changePropagationDispatchJob->lazyPush();
+
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ */
+ public static function cleanUp( DIWikiPage $subject ) {
+
+ $namespace = $subject->getNamespace();
+
+ if ( $namespace !== SMW_NS_PROPERTY && $namespace !== NS_CATEGORY ) {
+ return;
+ }
+
+ ApplicationFactory::getInstance()->getCache()->delete(
+ smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ $subject->getHash()
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return boolean
+ */
+ public static function hasPendingJobs( DIWikiPage $subject ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $jobType = 'smw.changePropagationUpdate';
+
+ if ( $subject->getNamespace() === NS_CATEGORY ) {
+ $jobType = 'smw.changePropagationClassUpdate';
+ }
+
+ if ( $applicationFactory->getJobQueue()->hasPendingJob( $jobType ) ) {
+ return true;
+ }
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ $subject->getHash()
+ );
+
+ return $applicationFactory->getCache()->fetch( $key ) > 0;
+ }
+
+ /**
+ * Use as very simple heuristic to count pending jobs for the overall change
+ * propagation. The count will indicate any job related to the change propagation
+ * and does not distinguish by changes to a specific property.
+ *
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return integer
+ */
+ public static function getPendingJobsCount( DIWikiPage $subject ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $jobType = 'smw.changePropagationUpdate';
+
+ if ( $subject->getNamespace() === NS_CATEGORY ) {
+ $jobType = 'smw.changePropagationClassUpdate';
+ }
+
+ $count = $applicationFactory->getJobQueue()->getQueueSize( $jobType );
+
+ // Fallback for when JobQueue::getQueueSize doesn't yet contain the
+ // updated stats
+ if ( $count == 0 && self::hasPendingJobs( $subject ) ) {
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ $subject->getHash()
+ );
+
+ $count = $applicationFactory->getCache()->fetch( $key );
+ }
+
+ return $count;
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 3.0
+ */
+ public function run() {
+
+ $subject = DIWikiPage::newFromTitle( $this->getTitle() );
+
+ if ( $this->hasParameter( 'dataFile' ) ) {
+ return $this->dispatchFromFile( $subject, $this->getParameter( 'dataFile' ) );
+ }
+
+ $this->findAndDispatch();
+
+ return true;
+ }
+
+ private function findAndDispatch() {
+
+ $namespace = $this->getTitle()->getNamespace();
+
+ if ( $namespace !== SMW_NS_PROPERTY && $namespace !== NS_CATEGORY ) {
+ return;
+ }
+
+ $subject = DIWikiPage::newFromTitle( $this->getTitle() );
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $iteratorFactory = $applicationFactory->getIteratorFactory();
+
+ $applicationFactory->getMediaWikiLogger()->info(
+ 'ChangePropagationDispatchJob on ' . $subject->getHash()
+ );
+
+ $changePropagationEntityFinder = new ChangePropagationEntityFinder(
+ $applicationFactory->getStore(),
+ $iteratorFactory
+ );
+
+ $changePropagationEntityFinder->isTypePropagation(
+ $this->getParameter( 'isTypePropagation' )
+ );
+
+ if ( $namespace === SMW_NS_PROPERTY ) {
+ $entity = DIProperty::newFromUserLabel( $this->getTitle()->getText() );
+ } elseif ( $namespace === NS_CATEGORY ) {
+ $entity = $subject;
+ }
+
+ $appendIterator = $changePropagationEntityFinder->findAll(
+ $entity
+ );
+
+ // Refresh the property page once more on the last dispatch
+ $appendIterator->add(
+ [ $subject ]
+ );
+
+ // After relevant subjects has been selected, commit the changes to the
+ // property so that the lock can be removed and any new specification
+ // (type, allows values etc.) are available upon executing individual
+ // jobs.
+ $this->commitSpecificationChangePropagationAsJob(
+ $subject,
+ $appendIterator->count()
+ );
+
+ $chunkedIterator = $iteratorFactory->newChunkedIterator(
+ $appendIterator,
+ self::CHUNK_SIZE
+ );
+
+ $i = 0;
+ $tempFile = $applicationFactory->create( 'TempFile' );
+
+ $file = $tempFile->generate(
+ 'smw_chgprop_',
+ $subject->getHash(),
+ uniqid()
+ );
+
+ foreach ( $chunkedIterator as $chunk ) {
+ $this->pushChangePropagationDispatchJob( $tempFile, $file, $i++, $chunk );
+ }
+ }
+
+ private function pushChangePropagationDispatchJob( $tempFile, $file, $num, $chunk ) {
+
+ $data = [];
+ $file .= "_$num.tmp";
+
+ // Filter any subobject
+ foreach ( $chunk as $val ) {
+ $data[] = ( $val instanceof DIWikiPage ? $val->asBase()->getHash() : $val );
+ }
+
+ // Filter duplicates and write the temp file
+ $tempFile->write(
+ $file,
+ implode( "\n", array_keys( array_flip( $data ) ) )
+ );
+
+ $checkSum = $tempFile->getCheckSum( $file );
+
+ // Use the checkSum as verification method to avoid manipulation of the
+ // contents by third-parties
+ $changePropagationDispatchJob = new ChangePropagationDispatchJob(
+ $this->getTitle(),
+ [
+ 'dataFile' => $file,
+ 'checkSum' => $checkSum
+ ] + self::newRootJobParams(
+ "ChangePropagationDispatchJob:$file:$checkSum"
+ )
+ );
+
+ $changePropagationDispatchJob->lazyPush();
+ }
+
+ private function dispatchFromFile( $subject, $file ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $cache = $applicationFactory->getCache();
+
+ $property = DIProperty::newFromUserLabel(
+ $this->getTitle()->getText()
+ );
+
+ $semanticData = $applicationFactory->getStore()->getSemanticData(
+ $subject
+ );
+
+ $tempFile = $applicationFactory->create( 'TempFile' );
+ $key = smwfCacheKey( self::CACHE_NAMESPACE, $subject->getHash() );
+
+ // SemanticData hasn't been updated, re-enter the cycle to ensure that
+ // the update of the property took place
+ if ( $cache->fetch( $key ) === false ) {
+
+ $cache->save( $key, 1, 60 * 60 * 24 );
+ $params = $this->params;
+
+ $changePropagationDispatchJob = new ChangePropagationDispatchJob(
+ $this->getTitle(),
+ $params
+ );
+
+ $changePropagationDispatchJob->insert();
+
+ $applicationFactory->getMediaWikiLogger()->info(
+ 'ChangePropagationDispatchJob missing update marker, retry on ' . $subject->getHash()
+ );
+
+ return true;
+ }
+
+ $contents = $tempFile->read(
+ $file,
+ $this->getParameter( 'checkSum' )
+ );
+
+ // @see ChangePropagationDispatchJob::pushChangePropagationDispatchJob
+ $dataItems = explode( "\n", $contents );
+
+ $this->scheduleChangePropagationUpdateJobFromList(
+ $dataItems
+ );
+
+ $tempFile->delete( $file );
+
+ return true;
+ }
+
+ private function scheduleChangePropagationUpdateJobFromList( $dataItems ) {
+
+ foreach ( $dataItems as $dataItem ) {
+
+ if ( $dataItem === '' ) {
+ continue;
+ }
+
+ $title = DIWikiPage::doUnserialize( $dataItem )->getTitle();
+
+ $changePropagationUpdateJob = $this->newChangePropagationUpdateJob(
+ $title,
+ [
+ UpdateJob::FORCED_UPDATE => true
+ ]
+ );
+
+ $changePropagationUpdateJob->insert();
+ }
+ }
+
+ private function commitSpecificationChangePropagationAsJob( $subject, $count ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $connection = $applicationFactory->getStore()->getConnection( 'mw.db' );
+ $transactionTicket = $connection->getEmptyTransactionTicket( __METHOD__ );
+
+ $changePropagationUpdateJob = $this->newChangePropagationUpdateJob(
+ $subject->getTitle(),
+ [
+ UpdateJob::CHANGE_PROP => $subject->getSerialization(),
+ UpdateJob::FORCED_UPDATE => true
+ ]
+ );
+
+ $changePropagationUpdateJob->run();
+
+ // Make sure changes are committed before continuing processing
+ $connection->commitAndWaitForReplication( __METHOD__, $transactionTicket );
+
+ // Add temporary update marker
+ // 24h ttl and it is expected that the JobQueue will run within this time
+ // frame so that the JobQueueGroup::getSize can catch up with the update
+ // marker.
+ //
+ // The marker will be removed after running the ChangePropagationUpdateJob
+ // on the same subject.
+ $applicationFactory->getCache()->save(
+ smwfCacheKey( self::CACHE_NAMESPACE, $subject->getHash() ),
+ $count,
+ 60 * 60 * 24
+ );
+
+ $applicationFactory->getPropertySpecificationLookup()->resetCacheBy( $subject );
+
+ // Make sure the cache is reset in case runJobs.php --wait is used to avoid
+ // reusing outdated type assignments
+ $applicationFactory->getStore()->clear();
+ }
+
+ private function newChangePropagationUpdateJob( $title, $parameters ) {
+
+ $namespace = $this->getTitle()->getNamespace();
+ $parameters = $parameters + [ 'origin' => 'ChangePropagationDispatchJob' ];
+
+ if ( $namespace === NS_CATEGORY ) {
+ return new ChangePropagationClassUpdateJob( $title, $parameters );
+ }
+
+ return new ChangePropagationUpdateJob(
+ $title,
+ $parameters
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationUpdateJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationUpdateJob.php
new file mode 100644
index 00000000..33734f10
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ChangePropagationUpdateJob.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use Title;
+use SMW\DIWikiPage;
+
+/**
+ * Make sufficient use of the job table by only tracking remaining jobs without
+ * any detail on an individual update count.
+ *
+ * Use `ChangePropagationUpdateJob` to easily count the jobs and distinguish them
+ * from other `UpdateJob`.
+ *
+ * `JobQueueGroup::singleton()->get( 'SMW\ChangePropagationUpdateJob' )->getSize()`
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ChangePropagationUpdateJob extends Job {
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [], $jobType = null ) {
+
+ if ( $jobType === null ) {
+ $jobType = 'smw.changePropagationUpdate';
+ }
+
+ parent::__construct( $jobType, $title, $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 3.0
+ */
+ public function run() {
+
+ ChangePropagationDispatchJob::cleanUp(
+ DIWikiPage::newFromTitle( $this->getTitle() )
+ );
+
+ $updateJob = new UpdateJob(
+ $this->getTitle(),
+ $this->params + [ 'origin' => 'ChangePropagationUpdateJob' ]
+ );
+
+ $updateJob->run();
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/EntityIdDisposerJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/EntityIdDisposerJob.php
new file mode 100644
index 00000000..ba83499a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/EntityIdDisposerJob.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use Hooks;
+use SMW\ApplicationFactory;
+use SMW\SQLStore\PropertyTableIdReferenceDisposer;
+use SMW\SQLStore\SQLStore;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class EntityIdDisposerJob extends Job {
+
+ /**
+ * Commit chunk size
+ */
+ const CHUNK_SIZE = 200;
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.entityIdDisposer', $title, $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return ResultIterator
+ */
+ public function newOutdatedEntitiesResultIterator() {
+ return $this->newPropertyTableIdReferenceDisposer()->newOutdatedEntitiesResultIterator();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer|stdClass $id
+ */
+ public function dispose( $id ) {
+
+ $propertyTableIdReferenceDisposer = $this->newPropertyTableIdReferenceDisposer();
+
+ if ( is_int( $id ) ) {
+ return $propertyTableIdReferenceDisposer->cleanUpTableEntriesById( $id );
+ }
+
+ $propertyTableIdReferenceDisposer->cleanUpTableEntriesByRow( $id );
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 2.5
+ */
+ public function run() {
+
+ $propertyTableIdReferenceDisposer = $this->newPropertyTableIdReferenceDisposer();
+
+ // MW 1.29+ Avoid transaction collisions during Job execution
+ $propertyTableIdReferenceDisposer->waitOnTransactionIdle();
+
+ if ( $this->hasParameter( 'id' ) ) {
+ $this->dispose( $this->getParameter( 'id' ) );
+ } else {
+ $this->dispose_all( $this->newOutdatedEntitiesResultIterator() );
+ }
+
+ return true;
+ }
+
+ private function dispose_all( $outdatedEntitiesResultIterator ) {
+
+ // Make sure the script is only executed from the command line to avoid
+ // Special:RunJobs to execute a queued job
+ if ( $this->waitOnCommandLineMode() ) {
+ return true;
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $connection = $applicationFactory->getStore()->getConnection( 'mw.db' );
+
+ $chunkedIterator = $applicationFactory->getIteratorFactory()->newChunkedIterator(
+ $outdatedEntitiesResultIterator,
+ self::CHUNK_SIZE
+ );
+
+ foreach ( $chunkedIterator as $chunk ) {
+
+ $transactionTicket = $connection->getEmptyTransactionTicket( __METHOD__ );
+
+ foreach ( $chunk as $row ) {
+ $this->dispose( $row );
+ }
+
+ $connection->commitAndWaitForReplication( __METHOD__, $transactionTicket );
+ }
+ }
+
+ private function newPropertyTableIdReferenceDisposer() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $store = $applicationFactory->getStore();
+
+ // Expect access to the SQL table structure therefore enforce the
+ // SQLStore that provides those methods
+ if ( !is_a( $store, SQLStore::class ) ) {
+ $store = $applicationFactory->getStore( '\SMW\SQLStore\SQLStore' );
+ }
+
+ return new PropertyTableIdReferenceDisposer( $store );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/FulltextSearchTableRebuildJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/FulltextSearchTableRebuildJob.php
new file mode 100644
index 00000000..031280f4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/FulltextSearchTableRebuildJob.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use SMW\ApplicationFactory;
+use SMW\SQLStore\QueryEngine\FulltextSearchTableFactory;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class FulltextSearchTableRebuildJob extends Job {
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.fulltextSearchTableRebuild', $title, $params );
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 2.5
+ */
+ public function run() {
+
+ if ( $this->waitOnCommandLineMode() ) {
+ return true;
+ }
+
+ $fulltextSearchTableFactory = new FulltextSearchTableFactory();
+
+ // Only the SQLStore is supported
+ $searchTableRebuilder = $fulltextSearchTableFactory->newSearchTableRebuilder(
+ ApplicationFactory::getInstance()->getStore( '\SMW\SQLStore\SQLStore' )
+ );
+
+ if ( $this->hasParameter( 'table' ) ) {
+ $searchTableRebuilder->rebuildByTable( $this->getParameter( 'table' ) );
+ } elseif ( $this->hasParameter( 'mode' ) && $this->getParameter( 'mode' ) === 'full' ) {
+ $searchTableRebuilder->rebuild();
+ } else {
+ $searchTableRebuilder->flushTable();
+ $this->createJobsFromTableList( $searchTableRebuilder->getQualifiedTableList() );
+ }
+
+ return true;
+ }
+
+ private function createJobsFromTableList( $tableList ) {
+
+ if ( $tableList === [] ) {
+ return;
+ }
+
+ foreach ( $tableList as $tableName ) {
+ $fulltextSearchTableRebuildJob = new self( $this->getTitle(), [
+ 'table' => $tableName
+ ] );
+
+ $fulltextSearchTableRebuildJob->insert();
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/FulltextSearchTableUpdateJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/FulltextSearchTableUpdateJob.php
new file mode 100644
index 00000000..8dfb4dd8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/FulltextSearchTableUpdateJob.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use Hooks;
+use SMW\ApplicationFactory;
+use SMW\SQLStore\QueryEngine\FulltextSearchTableFactory;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class FulltextSearchTableUpdateJob extends Job {
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.fulltextSearchTableUpdate', $title, $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 2.5
+ */
+ public function run() {
+
+ $fulltextSearchTableFactory = new FulltextSearchTableFactory();
+
+ $textChangeUpdater = $fulltextSearchTableFactory->newTextChangeUpdater(
+ ApplicationFactory::getInstance()->getStore( '\SMW\SQLStore\SQLStore' )
+ );
+
+ $textChangeUpdater->pushUpdatesFromJobParameters(
+ $this->params
+ );
+
+ Hooks::run( 'SMW::Job::AfterFulltextSearchTableUpdateComplete', [ $this ] );
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/NullJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/NullJob.php
new file mode 100644
index 00000000..15ccf610
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/NullJob.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use SMW\ApplicationFactory;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class NullJob extends Job {
+
+ /**
+ * @since 2.5
+ *
+ * @param Title|null $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title = null, $params = [] ) {}
+
+ /**
+ * @see Job::run
+ *
+ * @since 2.5
+ */
+ public function run() {
+ return true;
+ }
+
+ /**
+ * @see Job::insert
+ *
+ * @since 2.5
+ */
+ public function insert() {}
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ParserCachePurgeJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ParserCachePurgeJob.php
new file mode 100644
index 00000000..333c5b45
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/ParserCachePurgeJob.php
@@ -0,0 +1,264 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use Hooks;
+use SMW\ApplicationFactory;
+use SMW\HashBuilder;
+use SMW\RequestOptions;
+use SMW\SQLStore\QueryDependencyLinksStoreFactory;
+use SMW\Utils\Timer;
+use SMW\DIWikiPage;
+use SMWQuery as Query;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class ParserCachePurgeJob extends Job {
+
+ /**
+ * A balanced size that should be carefully monitored in order to not have a
+ * negative impact when running the initial update in online mode.
+ */
+ const CHUNK_SIZE = 300;
+
+ /**
+ * Using DB update execution mode to immediately execute the purge which may
+ * cause a surge in DB inserts.
+ */
+ const EXEC_DB = 'exec.db';
+
+ /**
+ * Using journal update execution mode to pause the execution and temporary
+ * store until an actual page is viewed.
+ */
+ const EXEC_JOURNAL = 'exec.journal';
+
+ /**
+ * @var ApplicationFactory
+ */
+ protected $applicationFactory;
+
+ /**
+ * @var integer
+ */
+ private $limit = self::CHUNK_SIZE;
+
+ /**
+ * @var integer
+ */
+ private $offset = 0;
+
+ /**
+ * @var PageUpdater
+ */
+ protected $pageUpdater;
+
+ /**
+ * @since 2.3
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.parserCachePurge', $title, $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * @see Job::run
+ */
+ public function insert() {
+
+ if (
+ $this->hasParameter( 'is.enabled' ) &&
+ $this->getParameter( 'is.enabled' ) === false ) {
+ return;
+ }
+
+ parent::insert();
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 2.3
+ */
+ public function run() {
+
+ Timer::start( __METHOD__ );
+ $this->applicationFactory = ApplicationFactory::getInstance();
+ $this->pageUpdater = $this->applicationFactory->newPageUpdater();
+
+ $count = 0;
+ $linksCount = 0;
+
+ if ( $this->hasParameter( 'limit' ) ) {
+ $this->limit = $this->getParameter( 'limit' );
+ }
+
+ if ( $this->hasParameter( 'offset' ) ) {
+ $this->offset = $this->getParameter( 'offset' );
+ }
+
+ if ( $this->hasParameter( 'idlist' ) ) {
+ $this->purgeTargetLinksFromList( $this->getParameter( 'idlist' ), $count, $linksCount );
+ }
+
+ if ( $this->getParameter( 'exec.mode' ) !== self::EXEC_JOURNAL ) {
+ $this->pageUpdater->addPage( $this->getTitle() );
+ $this->pageUpdater->setOrigin( __METHOD__ );
+ $this->pageUpdater->doPurgeParserCacheAsPool();
+ }
+
+ Hooks::run( 'SMW::Job::AfterParserCachePurgeComplete', [ $this ] );
+
+ $this->applicationFactory->getMediaWikiLogger()->info(
+ [
+ 'Job',
+ "ParserCachePurgeJob",
+ "List count:{count}",
+ "Links count:{linksCount}",
+ "Limit:{limit}",
+ "Offset:{offset}",
+ "procTime in sec: {procTime}"
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'user',
+ 'procTime' => Timer::getElapsedTime( __METHOD__, 7 ),
+ 'limit' => $this->limit,
+ 'offset' => $this->offset,
+ 'count' => $count,
+ 'linksCount' => $linksCount
+ ]
+ );
+
+ return true;
+ }
+
+ /**
+ * Based on the CHUNK_SIZE, target links are purged in an instant if those
+ * selected entities are < CHUNK_SIZE which should be enough for most
+ * common queries that only share a limited amount of dependencies, yet for
+ * queries that expect a large subject/dependency pool, doing an online update
+ * for all at once is not feasible hence the iterative process of creating
+ * batches that run through the job scheduler.
+ *
+ * @param array|string $idList
+ */
+ private function purgeTargetLinksFromList( $idList, &$listCount, &$linksCount ) {
+
+ if ( is_string( $idList ) && strpos( $idList, '|' ) !== false ) {
+ $idList = explode( '|', $idList );
+ }
+
+ if ( $idList === [] ) {
+ return true;
+ }
+
+ $queryDependencyLinksStoreFactory = $this->applicationFactory->singleton(
+ 'QueryDependencyLinksStoreFactory'
+ );
+
+ $queryDependencyLinksStore = $queryDependencyLinksStoreFactory->newQueryDependencyLinksStore(
+ $this->applicationFactory->getStore()
+ );
+
+ $dependencyLinksUpdateJournal = $queryDependencyLinksStoreFactory->newDependencyLinksUpdateJournal();
+
+ $requestOptions = new RequestOptions();
+
+ // +1 to look ahead
+ $requestOptions->setLimit( $this->limit + 1 );
+ $requestOptions->setOffset( $this->offset );
+ $requestOptions->setOption( 'links.count', 0 );
+
+ $hashList = $queryDependencyLinksStore->findDependencyTargetLinks(
+ $idList,
+ $requestOptions
+ );
+
+ $linksCount = $requestOptions->getOption( 'links.count' );
+
+ // If more results are available then use an iterative increase to fetch
+ // the remaining updates by creating successive jobs
+ if ( $linksCount > $this->limit ) {
+ $job = new self(
+ $this->getTitle(),
+ [
+ 'idlist' => $idList,
+ 'limit' => $this->limit,
+ 'offset' => $this->offset + self::CHUNK_SIZE,
+ 'exec.mode' => $this->getParameter( 'exec.mode' )
+ ]
+ );
+
+ $job->run();
+ }
+
+ if ( $hashList === [] ) {
+ return true;
+ }
+
+ list( $hashList, $queryList ) = $this->splitList( $hashList );
+ $listCount = count( $hashList );
+
+ $cachedQueryResultPrefetcher = $this->applicationFactory->singleton(
+ 'CachedQueryResultPrefetcher'
+ );
+
+ $cachedQueryResultPrefetcher->resetCacheBy(
+ $queryList,
+ 'ParserCachePurgeJob'
+ );
+
+ if ( $this->getParameter( 'exec.mode' ) === self::EXEC_JOURNAL ) {
+ $dependencyLinksUpdateJournal->updateFromList( $hashList, $this->getTitle()->getLatestRevID() );
+ } else{
+ $this->addPagesToUpdater( $hashList );
+ }
+ }
+
+ public function splitList( $hashList ) {
+
+ $targetLinksList = [];
+ $queryList = [];
+
+ foreach ( $hashList as $hash ) {
+
+ if ( $hash instanceof DIWikiPage ) {
+ $hash = $hash->getHash();
+ }
+
+ list( $title, $namespace, $iw, $subobjectname ) = explode( '#', $hash, 4 );
+
+ // QueryResultCache stores queries with they queryID = $subobjectname
+ if ( strpos( $subobjectname, Query::ID_PREFIX ) !== false ) {
+ $queryList[$subobjectname] = true;
+ }
+
+ // We make an assumption (as we avoid to query the DB) about that a
+ // query is bind to its subject by simply removing the subobject
+ // identifier (_QUERY*) and creating the base (or root) subject for
+ // the selected target (embedded query)
+ $targetLinksList[HashBuilder::createHashIdFromSegments( $title, $namespace, $iw )] = true;
+ }
+
+ return [ array_keys( $targetLinksList ), array_keys( $queryList ) ];
+ }
+
+ private function addPagesToUpdater( array $hashList ) {
+ foreach ( $hashList as $hash ) {
+ $this->pageUpdater->addPage(
+ HashBuilder::newTitleFromHash( $hash )
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/PropertyStatisticsRebuildJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/PropertyStatisticsRebuildJob.php
new file mode 100644
index 00000000..f1899c69
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/PropertyStatisticsRebuildJob.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use SMW\ApplicationFactory;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyStatisticsRebuildJob extends Job {
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ */
+ public function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.propertyStatisticsRebuild', $title, $params );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 2.5
+ */
+ public function run() {
+
+ if ( $this->waitOnCommandLineMode() ) {
+ return true;
+ }
+
+ $deferredCallableUpdate = ApplicationFactory::getInstance()->newDeferredTransactionalCallableUpdate(
+ [ $this, 'rebuild' ]
+ );
+
+ $deferredCallableUpdate->setOrigin( __METHOD__ );
+ $deferredCallableUpdate->runAsAutoCommit();
+ $deferredCallableUpdate->pushUpdate();
+
+ return true;
+ }
+
+ public function rebuild() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $maintenanceFactory = $applicationFactory->newMaintenanceFactory();
+
+ // Use a fixed store to avoid issues like "Call to undefined method
+ // SMW\SPARQLStore\SPARQLStore::getDataItemHandlerForDIType" because
+ // the property statistics table and hereby its update is bound to
+ // the SQLStore
+ $propertyStatisticsRebuilder = $maintenanceFactory->newPropertyStatisticsRebuilder(
+ $applicationFactory->getStore( '\SMW\SQLStore\SQLStore' )
+ );
+
+ $propertyStatisticsRebuilder->rebuild();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/RefreshJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/RefreshJob.php
new file mode 100644
index 00000000..eda69170
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/RefreshJob.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use SMW\ApplicationFactory;
+
+/**
+ * RefreshJob iterates over all page ids of the wiki, to perform an update
+ * action for all of them in sequence. This corresponds to the in-wiki version
+ * of the SMW_refreshData.php script for updating the whole wiki.
+ *
+ * @note This class ignores $smwgEnableUpdateJobs and always creates updates.
+ * In fact, it might be needed specifically on wikis that do not use update
+ * jobs in normal operation.
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class RefreshJob extends Job {
+
+ /**
+ * Constructor. The parameters optionally specified in the second
+ * argument of this constructor use the following array keys:
+ *
+ * - 'spos' : (start index, default 1),
+ * - 'prog' : (progress indicator, default 0),
+ * - 'rc' : (number of runs to be done, default 1)
+ *
+ * If more than one run is done, then the first run will restrict to properties
+ * and types. The progress indication refers to the current run, not to the
+ * overall job.
+ *
+ * @param Title $title
+ * @param array $params
+ */
+ public function __construct( $title, $params = [ 'spos' => 1, 'prog' => 0, 'rc' => 1 ] ) {
+ parent::__construct( 'smw.refresh', $title, $params );
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @return boolean
+ */
+ public function run() {
+
+ if ( $this->hasParameter( 'spos' ) ) {
+ $this->refreshData( $this->getParameter( 'spos' ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * Report the estimated progress status of this job as a number between
+ * 0 and 1 (0% to 100%). The progress refers to the state before
+ * processing this job.
+ *
+ * @return double
+ */
+ public function getProgress() {
+
+ $prog = $this->hasParameter( 'prog' ) ? $this->getParameter( 'prog' ) : 0;
+ $run = $this->hasParameter( 'run' ) ? $this->getParameter( 'run' ): 1;
+ $rc = $this->hasParameter( 'rc' ) ? $this->getParameter( 'rc' ) : 1;
+
+ return round( ( $run - 1 + $prog ) / $rc, 1 );
+ }
+
+ /**
+ * @param $spos start index
+ */
+ protected function refreshData( $spos ) {
+
+ $run = $this->hasParameter( 'run' ) ? $this->getParameter( 'run' ) : 1;
+
+ $entityRebuildDispatcher = ApplicationFactory::getInstance()->getStore()->refreshData(
+ $spos,
+ 20,
+ $this->getNamespace( $run )
+ );
+
+ $entityRebuildDispatcher->rebuild( $spos );
+ $prog = $entityRebuildDispatcher->getEstimatedProgress();
+
+ if ( $spos > 0 ) {
+
+ $this->createNextJob( [
+ 'spos' => $spos,
+ 'prog' => $prog,
+ 'rc' => $this->getParameter( 'rc' ),
+ 'run' => $run
+ ] );
+
+ } elseif ( $this->hasParameter( 'rc' ) && $this->getParameter( 'rc' ) > $run ) { // do another run from the beginning
+
+ $this->createNextJob( [
+ 'spos' => 1,
+ 'prog' => 0,
+ 'rc' => $this->getParameter( 'rc' ),
+ 'run' => $run + 1
+ ] );
+
+ }
+
+ return true;
+ }
+
+ protected function createNextJob( array $parameters ) {
+
+ $job = new self(
+ $this->getTitle(),
+ $parameters
+ );
+
+ $job->isEnabledJobQueue( $this->isEnabledJobQueue );
+ $job->insert();
+ }
+
+ protected function getNamespace( $run ) {
+
+ if ( !$this->hasParameter( 'rc' ) ) {
+ return false;
+ }
+
+ return ( ( $this->getParameter( 'rc' ) > 1 ) && ( $run == 1 ) ) ? [ SMW_NS_PROPERTY ] : false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/UpdateDispatcherJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/UpdateDispatcherJob.php
new file mode 100644
index 00000000..8022830f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/UpdateDispatcherJob.php
@@ -0,0 +1,368 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use Hooks;
+use SMW\MediaWiki\Job;
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\DataTypeRegistry;
+use SMW\RequestOptions;
+use SMW\Enum;
+use SMW\Exception\DataItemDeserializationException;
+use SMWDataItem as DataItem;
+use Title;
+
+/**
+ * Dispatcher to find and create individual UpdateJob instances for a specific
+ * subject and its linked entities.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class UpdateDispatcherJob extends Job {
+
+ /**
+ * Restrict dispatch process to an available pool of data
+ */
+ const RESTRICTED_DISPATCH_POOL = 'restricted.disp.pool';
+
+ /**
+ * Parameter for the secondary run to contain a list of update jobs to be
+ * inserted at once.
+ */
+ const JOB_LIST = 'job-list';
+
+ /**
+ * Size of chunks used when invoking the secondary dispatch run
+ */
+ const CHUNK_SIZE = 500;
+
+ /**
+ * @since 1.9
+ *
+ * @param Title $title
+ * @param array $params job parameters
+ * @param integer $id job id
+ */
+ public function __construct( Title $title, $params = [], $id = 0 ) {
+ parent::__construct( 'smw.updateDispatcher', $title, $params, $id );
+ $this->removeDuplicates = true;
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function run() {
+
+ $this->initServices();
+
+ /**
+ * Retrieved a job list (most likely from a secondary dispatch run) and
+ * push each list entry into the job queue to spread the work independently
+ * from the actual dispatch process.
+ */
+ if ( $this->hasParameter( self::JOB_LIST ) ) {
+ return $this->push_jobs_from_list( $this->getParameter( self::JOB_LIST ) );
+ }
+
+ /**
+ * Using an entity ID to initiate some work (which if send from the DELETE
+ * will have no valid ID_TABLE reference by the time this job is run) on
+ * some secondary tables.
+ */
+ if ( $this->hasParameter( '_id' ) ) {
+ $this->dispatch_by_id( $this->getParameter( '_id' ) );
+ }
+
+ if ( $this->getTitle()->getNamespace() === SMW_NS_PROPERTY ) {
+ $this->dispatchUpdateForProperty(
+ DIProperty::newFromUserLabel( $this->getTitle()->getText() )
+ );
+
+ $this->jobs[] = DIWikiPage::newFromTitle( $this->getTitle() )->getHash();
+ } else {
+ $this->dispatchUpdateForSubject(
+ DIWikiPage::newFromTitle( $this->getTitle() )
+ );
+ }
+
+ /**
+ * Create a secondary run by pushing collected jobs into a chunked queue
+ */
+ if ( $this->jobs !== [] ) {
+ $this->create_secondary_dispatch_run( $this->jobs );
+ }
+
+ Hooks::run( 'SMW::Job::AfterUpdateDispatcherJobComplete', [ $this ] );
+
+ return true;
+ }
+
+ private function initServices() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $this->setStore( $applicationFactory->getStore() );
+
+ $this->serializerFactory = $applicationFactory->newSerializerFactory();
+
+ $this->isEnabledJobQueue(
+ $applicationFactory->getSettings()->get( 'smwgEnableUpdateJobs' )
+ );
+ }
+
+ private function dispatch_by_id( $id ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $queryDependencyLinksStoreFactory = $applicationFactory->singleton( 'QueryDependencyLinksStoreFactory' );
+
+ $queryDependencyLinksStore = $queryDependencyLinksStoreFactory->newQueryDependencyLinksStore(
+ $applicationFactory->getStore()
+ );
+
+ $count = $queryDependencyLinksStore->countDependencies(
+ $id
+ );
+
+ if ( $count === 0 ) {
+ return;
+ }
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->setLimit(
+ $count
+ );
+
+ $dependencyTargetLinks = $queryDependencyLinksStore->findDependencyTargetLinks(
+ [ $id ],
+ $requestOptions
+ );
+
+ foreach ( $dependencyTargetLinks as $targetLink ) {
+ list( $title, $namespace, $iw, $subobjectname ) = explode( '#', $targetLink, 4 );
+
+ // @see DIWikiPage::doUnserialize
+ if ( !isset( $this->jobs[( $title . '#' . $namespace . '#' . $iw . '#' )] ) ) {
+ $this->jobs[( $title . '#' . $namespace . '#' . $iw . '#' )] = true;
+ }
+ }
+ }
+
+ private function create_secondary_dispatch_run( $jobs ) {
+
+ $origin = $this->getTitle()->getPrefixedText();
+
+ foreach ( array_chunk( $jobs, self::CHUNK_SIZE, true ) as $jobList ) {
+ $job = new self(
+ Title::newFromText( 'UpdateDispatcher/SecondaryRun/' . md5( json_encode( $jobList ) ) ),
+ [
+ self::JOB_LIST => $jobList,
+ 'origin' => $origin,
+
+ // We expect entities to exists that are send through the
+ // dispatch to avoid creating "dead" ids on non existing (or
+ // already deleted) entities
+ 'check_exists' => true
+ ]
+ );
+
+ $job->insert();
+ }
+ }
+
+ private function dispatchUpdateForSubject( DIWikiPage $subject ) {
+
+ if ( $this->getParameter( self::RESTRICTED_DISPATCH_POOL ) !== true ) {
+ $this->addUpdateJobsForProperties(
+ $this->store->getProperties( $subject )
+ );
+
+ $this->addUpdateJobsForProperties(
+ $this->store->getInProperties( $subject )
+ );
+ }
+
+ $this->addUpdateJobsFromDeserializedSemanticData();
+ }
+
+ private function dispatchUpdateForProperty( DIProperty $property ) {
+ $this->addUpdateJobsForProperties( [ $property ] );
+ $this->addUpdateJobsForSubjectsThatContainTypeError();
+ $this->addUpdateJobsFromDeserializedSemanticData();
+ }
+
+ private function addUpdateJobsForProperties( array $properties ) {
+ foreach ( $properties as $property ) {
+
+ if ( !$property->isUserDefined() ) {
+ continue;
+ }
+
+ // Before doing some work, make sure to only use page type properties
+ // as a means to generate a resource (job) action
+ $type = DataTypeRegistry::getInstance()->getDataItemByType(
+ $property->findPropertyTypeId()
+ );
+
+ if ( $type !== DataItem::TYPE_WIKIPAGE ) {
+ continue;
+ }
+
+ $requestOptions = new RequestOptions();
+
+ // No need for a warmup since we want to keep the iterator for as
+ // long as possible to only access one item at a time
+ $requestOptions->setOption( Enum::SUSPEND_CACHE_WARMUP, true );
+
+ // If we have an ID then use it to restrict the range of mactches
+ // against that object reference (aka `o_id`). Of course, in case of
+ // a delete action it is required that the disposer job (that removes
+ // all pending references from any active table for that reference)
+ // is called only after the job queue has been cleared otherwise
+ // the `o_id` can no longer be a matchable ID.
+ if ( $this->hasParameter( '_id' ) ) {
+ $requestOptions->addExtraCondition( [ 'o_id' => $this->getParameter( '_id' ) ] );
+ }
+
+ // Best effort to find all entities to a selected property
+ $subjects = $this->store->getAllPropertySubjects( $property, $requestOptions );
+
+ $this->add_job(
+ $this->apply_filter( $property, $subjects )
+ );
+ }
+ }
+
+ private function apply_filter( $property, $subjects ) {
+
+ // If the an ID was provided it already restricted the list of references
+ // hence avoid any further work
+ if ( $this->hasParameter( '_id' ) ) {
+ return $subjects;
+ }
+
+ if ( $this->getParameter( self::RESTRICTED_DISPATCH_POOL ) !== true ) {
+ return $subjects;
+ }
+
+ $list = [];
+
+ // Identify the source as base for a comparison
+ $source = DIWikiPage::newFromTitle( $this->getTitle() );
+
+ foreach ( $subjects as $subject ) {
+
+ // #3322
+ // Investigate which subjects have an actual connection to the
+ // subject
+ $dataItems = $this->store->getPropertyValues( $subject, $property );
+
+ foreach ( $dataItems as $dataItem ) {
+ // Make a judgment based on a literal comparison for the
+ // values assigned and the now deleted entity
+ if ( $dataItem instanceof DIWikiPage && $dataItem->equals( $source ) ) {
+ $list[] = $subject;
+ }
+ }
+ }
+
+ return $list;
+ }
+
+ private function addUpdateJobsForSubjectsThatContainTypeError() {
+
+ $subjects = $this->store->getPropertySubjects(
+ new DIProperty( DIProperty::TYPE_ERROR ),
+ DIWikiPage::newFromTitle( $this->getTitle() )
+ );
+
+ $this->add_job(
+ $subjects
+ );
+ }
+
+ private function addUpdateJobsFromDeserializedSemanticData() {
+
+ if ( !$this->hasParameter( 'semanticData' ) ) {
+ return;
+ }
+
+ $semanticData = $this->serializerFactory->newSemanticDataDeserializer()->deserialize(
+ $this->getParameter( 'semanticData' )
+ );
+
+ $this->addUpdateJobsForProperties(
+ $semanticData->getProperties()
+ );
+ }
+
+ private function add_job( $subjects = [] ) {
+
+ foreach ( $subjects as $subject ) {
+
+ // Not trying to get the title here as it is waste of resources
+ // as makeTitleSafe is expensive for large lists
+ // $title = $subject->getTitle();
+
+ if ( !$subject instanceof DIWikiPage ) {
+ continue;
+ }
+
+ // Do not use the full subject as hash as we don't care about subobjects
+ // since the root subject is enough to update all related subobjects
+ // The format is the same as expected by DIWikiPage::doUnserialize
+ $hash = $subject->getDBKey() . '#' . $subject->getNamespace() . '#' . $subject->getInterwiki() . '#';
+
+ if ( !isset( $this->jobs[$hash] ) ) {
+ $this->jobs[$hash] = true;
+ }
+ }
+ }
+
+ private function push_jobs_from_list( array $subjects ) {
+
+ $check_exists = $this->getParameter( 'check_exists', false );
+
+ $parameters = [
+ UpdateJob::FORCED_UPDATE => true,
+ 'origin' => $this->getParameter( 'origin', 'UpdateDispatcherJob' )
+ ];
+
+ // We expect non-duplicate subjects in the list and therefore deserialize
+ // without any extra validation
+ foreach ( $subjects as $key => $subject ) {
+
+ if ( is_string( $key ) ) {
+ $subject = $key;
+ }
+
+ try {
+ $subject = DIWikiPage::doUnserialize( $subject );
+ } catch( DataItemDeserializationException $e ) {
+ continue;
+ }
+
+ if ( $check_exists && !$this->store->getObjectIds()->exists( $subject ) ) {
+ continue;
+ }
+
+ if ( ( $title = $subject->getTitle() ) === null ) {
+ continue;
+ }
+
+ $this->jobs[] = new UpdateJob( $title, $parameters );
+ }
+
+ $this->pushToJobQueue();
+
+ return true;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/UpdateJob.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/UpdateJob.php
new file mode 100644
index 00000000..6fbf269d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Jobs/UpdateJob.php
@@ -0,0 +1,322 @@
+<?php
+
+namespace SMW\MediaWiki\Jobs;
+
+use SMW\MediaWiki\Job;
+use LinkCache;
+use ParserOutput;
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Enum;
+use SMW\EventHandler;
+use Title;
+
+/**
+ * UpdateJob is responsible for the asynchronous update of semantic data
+ * using MediaWiki's JobQueue infrastructure.
+ *
+ * Update jobs are created if, when saving an article,
+ * it is detected that the content of other pages must be re-parsed as well (e.g.
+ * due to some type change).
+ *
+ * @note This job does not update the page display or parser cache, so in general
+ * it might happen that part of the wiki page still displays old data (e.g.
+ * formatting in-page values based on a datatype thathas since been changed), whereas
+ * the Factbox and query/browsing interfaces might already show the updated records.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Daniel M. Herzig
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class UpdateJob extends Job {
+
+ /**
+ * Enforces an update independent of the update marker status
+ */
+ const FORCED_UPDATE = 'forcedUpdate';
+
+ /**
+ * Indicates the use of the _CHGPRO property as base for the SemanticData
+ */
+ const CHANGE_PROP = 'changeProp';
+
+ /**
+ * Indicates the use of the semanticData parameter
+ */
+ const SEMANTIC_DATA = 'semanticData';
+
+ /**
+ * @var ApplicationFactory
+ */
+ private $applicationFactory = null;
+
+ /**
+ * @since 1.9
+ *
+ * @param Title $title
+ * @param array $params
+ */
+ function __construct( Title $title, $params = [] ) {
+ parent::__construct( 'smw.update', $title, $params );
+ $this->removeDuplicates = true;
+
+ $this->isEnabledJobQueue(
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgEnableUpdateJobs' )
+ );
+ }
+
+ /**
+ * @see Job::run
+ *
+ * @return boolean
+ */
+ public function run() {
+
+ // #2199 ("Invalid or virtual namespace -1 given")
+ if ( $this->getTitle()->isSpecialPage() ) {
+ return true;
+ }
+
+ LinkCache::singleton()->clear();
+
+ $this->applicationFactory = ApplicationFactory::getInstance();
+
+ if ( !$this->hasParameter( self::FORCED_UPDATE ) && $this->matchesLastModified( $this->getTitle() ) ) {
+ return true;
+ }
+
+ if ( $this->getTitle()->exists() ) {
+ return $this->doUpdate();
+ }
+
+ $this->applicationFactory->getStore()->clearData(
+ DIWikiPage::newFromTitle( $this->getTitle() )
+ );
+
+ return true;
+ }
+
+ private function matchesLastModified( $title ) {
+
+ if ( !$this->getParameter( 'shallowUpdate' ) ) {
+ return false;
+ }
+
+ $lastModified = $this->getLastModifiedTimestamp(
+ DIWikiPage::newFromTitle( $title )
+ );
+
+ if ( $lastModified !== \WikiPage::factory( $title )->getTimestamp() ) {
+ return false;
+ }
+
+ $pageUpdater = $this->applicationFactory->newPageUpdater();
+ $pageUpdater->addPage( $title );
+ $pageUpdater->waitOnTransactionIdle();
+ $pageUpdater->doPurgeParserCache();
+
+ return true;
+ }
+
+ private function doUpdate() {
+
+ // ChangePropagationJob
+ if ( $this->hasParameter( self::CHANGE_PROP ) ) {
+ return $this->change_propagation( $this->getParameter( self::CHANGE_PROP ) );
+ }
+
+ if ( $this->hasParameter( self::SEMANTIC_DATA ) ) {
+ return $this->set_data( $this->getParameter( self::SEMANTIC_DATA ) );
+ }
+
+ return $this->parse_content();
+ }
+
+ private function change_propagation( $dataItem ) {
+
+ $this->setParameter( 'updateType', 'ChangePropagation' );
+ $subject = DIWikiPage::doUnserialize( $dataItem );
+
+ // Read the _CHGPRO property and fetch the serialized
+ // SemanticData object
+ $pv = $this->applicationFactory->getStore()->getPropertyValues(
+ $subject,
+ new DIProperty( DIProperty::TYPE_CHANGE_PROP )
+ );
+
+ if ( $pv === [] ) {
+ return;
+ }
+
+ // PropertySpecificationChangeNotifier encodes the serialized content
+ // using the JSON format
+ $semanticData = json_decode( end( $pv )->getString(), true );
+
+ $this->set_data(
+ $semanticData
+ );
+ }
+
+ private function set_data( $semanticData ) {
+
+ $this->setParameter( 'updateType', 'SemanticData' );
+
+ $semanticData = $this->applicationFactory->newSerializerFactory()->newSemanticDataDeserializer()->deserialize(
+ $semanticData
+ );
+
+ $semanticData->removeProperty(
+ new DIProperty( DIProperty::TYPE_CHANGE_PROP )
+ );
+
+ $parserData = $this->applicationFactory->newParserData(
+ $this->getTitle(),
+ new ParserOutput()
+ );
+
+ $parserData->setSemanticData( $semanticData );
+
+ $parserData->setOption(
+ Enum::OPT_SUSPEND_PURGE,
+ false
+ );
+
+ return $this->updateStore( $parserData );
+ }
+
+ private function parse_content() {
+
+ $this->setParameter( 'updateType', 'ContentParse' );
+
+ $contentParser = $this->applicationFactory->newContentParser( $this->getTitle() );
+ $contentParser->parse();
+
+ if ( !( $contentParser->getOutput() instanceof ParserOutput ) ) {
+ $this->setLastError( $contentParser->getErrors() );
+ return false;
+ }
+
+ $parserData = $this->applicationFactory->newParserData(
+ $this->getTitle(),
+ $contentParser->getOutput()
+ );
+
+ // Suspend the purge as any preceding parse process most likely has
+ // invalidated the cache for a selected subject
+ $parserData->setOption(
+ Enum::OPT_SUSPEND_PURGE,
+ true
+ );
+
+ return $this->updateStore( $parserData );
+ }
+
+ private function updateStore( $parserData ) {
+
+ $this->applicationFactory->getMediaWikiLogger()->info(
+ [
+ 'Job',
+ 'UpdateJob',
+ '{title}',
+ 'Type: {updateType}',
+ 'Origin: {origin}',
+ 'isForcedUpdate: {forcedUpdate}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'user',
+ 'title' => $this->getTitle()->getPrefixedDBKey(),
+ 'origin' => $this->getParameter( 'origin', 'N/A' ),
+ 'updateType' => $this->getParameter( 'updateType' ),
+ 'forcedUpdate' => $this->getParameter( self::FORCED_UPDATE )
+ ]
+ );
+
+ $eventHandler = EventHandler::getInstance();
+
+ $dispatchContext = $eventHandler->newDispatchContext();
+ $dispatchContext->set( 'title', $this->getTitle() );
+
+ $eventHandler->getEventDispatcher()->dispatch(
+ 'factbox.cache.delete',
+ $dispatchContext
+ );
+
+ $eventHandler->getEventDispatcher()->dispatch(
+ 'cached.propertyvalues.prefetcher.reset',
+ $dispatchContext
+ );
+
+ // TODO
+ // Rebuild the factbox
+
+ $origin[] = 'UpdateJob';
+
+ if ( $this->hasParameter( 'origin' ) ) {
+ $origin[] = $this->getParameter( 'origin' );
+ }
+
+ if ( $this->hasParameter( 'ref' ) ) {
+ $origin[] = $this->getParameter( 'ref' );
+ }
+
+ $parserData->setOrigin( $origin );
+
+ $parserData->setOption(
+ Enum::OPT_SUSPEND_PURGE,
+ $this->getParameter( Enum::OPT_SUSPEND_PURGE )
+ );
+
+ $parserData->setOption(
+ $parserData::OPT_FORCED_UPDATE,
+ $this->getParameter( self::FORCED_UPDATE )
+ );
+
+ $parserData->setOption(
+ $parserData::OPT_CHANGE_PROP_UPDATE,
+ $this->getParameter( self::CHANGE_PROP )
+ );
+
+ $parserData->getSemanticData()->setOption(
+ \SMW\SemanticData::OPT_LAST_MODIFIED,
+ wfTimestamp( TS_UNIX )
+ );
+
+ $parserData->setOption(
+ $parserData::OPT_CREATE_UPDATE_JOB,
+ false
+ );
+
+ $parserData->getSemanticData()->setOption(
+ Enum::PURGE_ASSOC_PARSERCACHE,
+ (bool)$this->getParameter( Enum::PURGE_ASSOC_PARSERCACHE )
+ );
+
+ $parserData->updateStore();
+
+ return true;
+ }
+
+ /**
+ * Convenience method to find last modified MW timestamp for a subject that
+ * has been added using the storage-engine.
+ */
+ private function getLastModifiedTimestamp( DIWikiPage $wikiPage ) {
+
+ $dataItems = $this->applicationFactory->getStore()->getPropertyValues(
+ $wikiPage,
+ new DIProperty( '_MDAT' )
+ );
+
+ if ( $dataItems !== [] ) {
+ return end( $dataItems )->getMwTimestamp( TS_MW );
+ }
+
+ return 0;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/LocalTime.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/LocalTime.php
new file mode 100644
index 00000000..86070a80
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/LocalTime.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use DateInterval;
+use DateTime;
+use DateTimeZone;
+use User;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class LocalTime {
+
+ /**
+ * @see $GLOBALS['wgLocalTZoffset']
+ * @var integer
+ */
+ private static $localTimeOffset = 0;
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $localTimeOffset
+ */
+ public static function setLocalTimeOffset( $localTimeOffset ) {
+ self::$localTimeOffset = $localTimeOffset;
+ }
+
+ /**
+ * @see Language::userAdjust
+ *
+ * Language::userAdjust cannot be used as entirely relies on the timestamp
+ * premises making < 1970 return invalid results hence we copy the relevant
+ * part on work with the DateInterval instead.
+ *
+ * @since 3.0
+ *
+ * @param DateTime $dateTime
+ * @param User|null $user
+ *
+ * @return DateTime
+ */
+ public static function getLocalizedTime( DateTime $dateTime, User $user = null ) {
+
+ $tz = $user instanceof User ? $user->getOption( 'timecorrection' ) : false;
+ $data = explode( '|', $tz, 3 );
+
+ // DateTime is mutable, keep track of possible changes
+ $dateTime->hasLocalTimeCorrection = false;
+
+ if ( $data[0] == 'ZoneInfo' ) {
+ try {
+ $userTZ = new DateTimeZone( $data[2] );
+ $dateTime->setTimezone( $userTZ );
+ $dateTime->hasLocalTimeCorrection = true;
+ return $dateTime;
+ } catch ( \Exception $e ) {
+ // Unrecognized timezone, default to 'Offset' with the stored offset.
+ $data[0] = 'Offset';
+ }
+ }
+
+ if ( $data[0] == 'System' || $tz == '' ) {
+ # Global offset in minutes.
+ $minDiff = self::$localTimeOffset;
+ } elseif ( $data[0] == 'Offset' ) {
+ $minDiff = intval( $data[1] );
+ } else {
+ $data = explode( ':', $tz );
+ if ( count( $data ) == 2 ) {
+ $data[0] = intval( $data[0] );
+ $data[1] = intval( $data[1] );
+ $minDiff = abs( $data[0] ) * 60 + $data[1];
+ if ( $data[0] < 0 ) {
+ $minDiff = -$minDiff;
+ }
+ } else {
+ $minDiff = intval( $data[0] ) * 60;
+ }
+ }
+
+ # No difference ?
+ if ( 0 == $minDiff ) {
+ return $dateTime;
+ }
+
+ $dateInterval = new DateInterval( "PT" . abs( $minDiff ) . "M" );
+
+ if ( $minDiff > 0 ) {
+ $dateTime->add( $dateInterval );
+ } else {
+ $dateTime->sub( $dateInterval );
+ }
+
+ $dateTime->hasLocalTimeCorrection = true;
+
+ return $dateTime;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MagicWordsFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MagicWordsFinder.php
new file mode 100644
index 00000000..56733321
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MagicWordsFinder.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use MagicWord;
+use ParserOutput;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class MagicWordsFinder {
+
+ /**
+ * @var ParserOutput
+ */
+ private $parserOutput = null;
+
+ /**
+ * @since 2.0
+ *
+ * @param ParserOutput|null $parserOutput
+ */
+ public function __construct( ParserOutput $parserOutput = null ) {
+ $this->parserOutput = $parserOutput;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param ParserOutput $parserOutput
+ *
+ * @return self
+ */
+ public function setOutput( ParserOutput $parserOutput ) {
+ $this->parserOutput = $parserOutput;
+ return $this;
+ }
+
+ /**
+ * Find the magic word and have it removed from the text
+ *
+ * @since 2.0
+ *
+ * @param $magicWord
+ * @param &$text
+ *
+ * @return string
+ */
+ public function findMagicWordInText( $magicWord, &$text ) {
+
+ $mw = MagicWord::get( $magicWord );
+
+ if ( $mw->matchAndRemove( $text ) ) {
+ return $magicWord;
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param array $words
+ */
+ public function pushMagicWordsToParserOutput( array $words ) {
+
+ $this->parserOutput->setTimestamp( wfTimestampNow() );
+
+ // Filter empty lines
+ $words = array_values( array_filter( $words ) );
+
+ if ( $this->hasExtensionData() && $words !== [] ) {
+ return $this->parserOutput->setExtensionData( 'smwmagicwords', $words );
+ }
+
+ return $this->parserOutput->mSMWMagicWords = $words;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return array
+ */
+ public function getMagicWords() {
+
+ if ( $this->hasExtensionData() ) {
+ return $this->parserOutput->getExtensionData( 'smwmagicwords' );
+ }
+
+ if ( isset( $this->parserOutput->mSMWMagicWords ) ) {
+ return $this->parserOutput->mSMWMagicWords;
+ }
+
+ return [];
+ }
+
+ /**
+ * FIXME Remove when MW 1.21 becomes mandatory
+ */
+ protected function hasExtensionData() {
+ return method_exists( $this->parserOutput, 'getExtensionData' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/ManualEntryLogger.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/ManualEntryLogger.php
new file mode 100644
index 00000000..a8e92649
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/ManualEntryLogger.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use LogEntry;
+use ManualLogEntry;
+use Title;
+use User;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class ManualEntryLogger {
+
+ /**
+ * @var logEntry
+ */
+ private $logEntry = null;
+
+ /**
+ * @var array
+ */
+ private $eventTypes = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param LogEntry|null $logEntry
+ */
+ public function __construct( LogEntry $logEntry = null ) {
+ $this->logEntry = $logEntry;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $eventTypes
+ */
+ public function registerLoggableEventType( $eventType ) {
+ $this->eventTypes[$eventType] = true;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $type
+ * @param string $performer
+ * @param string $target
+ * @param string $comment
+ *
+ * @return integer|null
+ */
+ public function log( $type, $performer, $target, $comment ) {
+
+ if ( !isset( $this->eventTypes[$type] ) || !$this->eventTypes[$type] ) {
+ return null;
+ }
+
+ $logEntry = $this->newManualLogEntryForType( $type );
+ $logEntry->setTarget( Title::newFromText( $target ) );
+
+ if ( is_string( $performer) ) {
+ $performer = User::newFromName( $performer );
+ }
+
+ $logEntry->setPerformer( $performer );
+ $logEntry->setParameters( [] );
+ $logEntry->setComment( $comment );
+
+ return $logEntry->insert();
+ }
+
+ protected function newManualLogEntryForType( $type ) {
+
+ if ( $this->logEntry !== null ) {
+ return $this->logEntry;
+ }
+
+ return new ManualLogEntry( 'smw', $type );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MediaWikiNsContentReader.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MediaWikiNsContentReader.php
new file mode 100644
index 00000000..3b2c74cb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MediaWikiNsContentReader.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Revision;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class MediaWikiNsContentReader {
+
+ /**
+ * @var boolean
+ */
+ private $skipMessageCache = false;
+
+ /**
+ * @since 2.3
+ */
+ public function skipMessageCache() {
+ $this->skipMessageCache = true;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $name
+ *
+ * @return string
+ */
+ public function read( $name ) {
+
+ $content = '';
+
+ if ( !$this->skipMessageCache && wfMessage( $name )->exists() ) {
+ $content = wfMessage( $name )->inContentLanguage()->text();
+ }
+
+ if ( $content === '' ) {
+ $content = $this->readFromDatabase( $name );
+ }
+
+ return $content;
+ }
+
+ private function readFromDatabase( $name ) {
+
+ $title = Title::makeTitleSafe( NS_MEDIAWIKI, ucfirst( $name ) );
+
+ if ( $title === null ) {
+ return '';
+ }
+
+ // Revision::READ_LATEST is not specified in MW 1.19
+ $revisionReadFlag = defined( 'Revision::READ_LATEST' ) ? Revision::READ_LATEST : 0;
+
+ $revision = Revision::newFromTitle( $title, false, $revisionReadFlag );
+
+ if ( $revision === null ) {
+ return '';
+ }
+
+ if ( class_exists( 'WikitextContent' ) ) {
+ return $revision->getContent()->getNativeData();
+ }
+
+ if ( method_exists( $revision, 'getContent') ) {
+ return $revision->getContent( Revision::RAW );
+ }
+
+ return $revision->getRawText();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MessageBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MessageBuilder.php
new file mode 100644
index 00000000..5c25d48c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MessageBuilder.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use IContextSource;
+use Language;
+use Message;
+use RuntimeException;
+use Title;
+
+/**
+ * Convenience class to build language dependent messages and special text
+ * components and decrease depdencency on the Language object with SMW's code
+ * base
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class MessageBuilder {
+
+ /**
+ * @var Language
+ */
+ private $language = null;
+
+ /**
+ * @since 2.1
+ *
+ * @param Language|null $language
+ */
+ public function __construct( Language $language = null ) {
+ $this->language = $language;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Language $language
+ *
+ * @return MessageBuilder
+ */
+ public function setLanguage( Language $language ) {
+ $this->language = $language;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param IContextSource $context
+ *
+ * @return MessageBuilder
+ */
+ public function setLanguageFromContext( IContextSource $context ) {
+ $this->language = $context->getLanguage();
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param mixed $number
+ * @param boolean $useForSpecialNumbers set to true for numbers like dates
+ *
+ * @return string
+ */
+ public function formatNumberToText( $number, $useForSpecialNumbers = false ) {
+ return $this->getLanguage()->formatNum( $number, $useForSpecialNumbers );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param array $list
+ *
+ * @return string
+ */
+ public function listToCommaSeparatedText( array $list ) {
+ return $this->getLanguage()->listToText( $list );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Title $title,
+ * @param integer $offset,
+ * @param integer $offset,
+ * @param array $query,
+ * @param boolean|null $isAtTheEnd
+ *
+ * @return string
+ */
+ public function prevNextToText( Title $title, $limit, $offset, array $query, $isAtTheEnd ) {
+ return $this->getLanguage()->viewPrevNext( $title, $offset, $limit, $query, $isAtTheEnd );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $key
+ *
+ * @return Message
+ */
+ public function getMessage( $key ) {
+
+ $params = func_get_args();
+ array_shift( $params );
+
+ if ( isset( $params[0] ) && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+
+ $message = new Message( $key, $params );
+
+ return $message->inLanguage( $this->getLanguage() )->title( $GLOBALS['wgTitle'] );
+ }
+
+ private function getLanguage() {
+
+ if ( $this->language instanceof Language ) {
+ return $this->language;
+ }
+
+ throw new RuntimeException( 'Expected a valid language object' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MwCollaboratorFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MwCollaboratorFactory.php
new file mode 100644
index 00000000..1c0d0a22
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/MwCollaboratorFactory.php
@@ -0,0 +1,229 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Language;
+use Parser;
+use Revision;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Connection\LoadBalancerConnectionProvider;
+use SMW\MediaWiki\Connection\ConnectionProvider;
+use SMW\MediaWiki\Renderer\HtmlColumnListRenderer;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\MediaWiki\Renderer\HtmlTableRenderer;
+use SMW\MediaWiki\Renderer\HtmlTemplateRenderer;
+use SMW\MediaWiki\Renderer\WikitextTemplateRenderer;
+use StripState;
+use Title;
+use User;
+use WikiPage;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class MwCollaboratorFactory {
+
+ /**
+ * @var ApplicationFactory
+ */
+ private $applicationFactory;
+
+ /**
+ * @since 2.1
+ *
+ * @param ApplicationFactory $applicationFactory
+ */
+ public function __construct( ApplicationFactory $applicationFactory ) {
+ $this->applicationFactory = $applicationFactory;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Language|null $language
+ *
+ * @return MessageBuilder
+ */
+ public function newMessageBuilder( Language $language = null ) {
+ return new MessageBuilder( $language );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return MagicWordsFinder
+ */
+ public function newMagicWordsFinder() {
+ return new MagicWordsFinder();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return RedirectTargetFinder
+ */
+ public function newRedirectTargetFinder() {
+ return new RedirectTargetFinder();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return DeepRedirectTargetResolver
+ */
+ public function newDeepRedirectTargetResolver() {
+ return new DeepRedirectTargetResolver( $this->applicationFactory->newPageCreator() );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Title $title
+ * @param Language|null $language
+ *
+ * @return HtmlFormRenderer
+ */
+ public function newHtmlFormRenderer( Title $title, Language $language = null ) {
+
+ if ( $language === null ) {
+ $language = $title->getPageLanguage();
+ }
+
+ $messageBuilder = $this->newMessageBuilder( $language );
+
+ return new HtmlFormRenderer( $title, $messageBuilder );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return HtmlTableRenderer
+ */
+ public function newHtmlTableRenderer() {
+ return new HtmlTableRenderer();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return HtmlColumnListRenderer
+ */
+ public function newHtmlColumnListRenderer() {
+ return new HtmlColumnListRenderer();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return LoadBalancerConnectionProvider
+ */
+ public function newLoadBalancerConnectionProvider( $connectionType ) {
+ return new LoadBalancerConnectionProvider( $connectionType );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string|null $provider
+ *
+ * @return ConnectionProvider
+ */
+ public function newConnectionProvider( $provider = null ) {
+
+ $connectionProvider = new ConnectionProvider(
+ $provider
+ );
+
+ $connectionProvider->setLocalConnectionConf(
+ $this->applicationFactory->getSettings()->get( 'smwgLocalConnectionConf' )
+ );
+
+ $connectionProvider->setLogger(
+ $this->applicationFactory->getMediaWikiLogger()
+ );
+
+ return $connectionProvider;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param WikiPage $wkiPage
+ * @param Revision|null $revision
+ * @param User|null $user
+ *
+ * @return PageInfoProvider
+ */
+ public function newPageInfoProvider( WikiPage $wkiPage, Revision $revision = null, User $user = null ) {
+ return new PageInfoProvider( $wkiPage, $revision, $user );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param WikiPage $wkiPage
+ * @param Revision $revision
+ * @param User|null $user
+ *
+ * @return EditInfoProvider
+ */
+ public function newEditInfoProvider( WikiPage $wkiPage, Revision $revision, User $user = null ) {
+ return new EditInfoProvider( $wkiPage, $revision, $user );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return WikitextTemplateRenderer
+ */
+ public function newWikitextTemplateRenderer() {
+ return new WikitextTemplateRenderer();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Parser $parser
+ *
+ * @return HtmlTemplateRenderer
+ */
+ public function newHtmlTemplateRenderer( Parser $parser ) {
+ return new HtmlTemplateRenderer(
+ $this->newWikitextTemplateRenderer(),
+ $parser
+ );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return MediaWikiNsContentReader
+ */
+ public function newMediaWikiNsContentReader() {
+ return new MediaWikiNsContentReader();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param StripState $stripState
+ *
+ * @return StripMarkerDecoder
+ */
+ public function newStripMarkerDecoder( StripState $stripState ) {
+
+ $stripMarkerDecoder = new StripMarkerDecoder(
+ $stripState
+ );
+
+ $stripMarkerDecoder->isSupported(
+ $this->applicationFactory->getSettings()->isFlagSet( 'smwgParserFeatures', SMW_PARSER_UNSTRIP )
+ );
+
+ return $stripMarkerDecoder;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageCreator.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageCreator.php
new file mode 100644
index 00000000..9a3331e7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageCreator.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Title;
+use WikiFilePage;
+use WikiPage;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class PageCreator {
+
+ /**
+ * @since 2.0
+ *
+ * @param Title $title
+ *
+ * @return WikiPage
+ */
+ public function createPage( Title $title ) {
+ return WikiPage::factory( $title );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param Title $title
+ *
+ * @return WikiFilePage
+ */
+ public function createFilePage( Title $title ) {
+ return new WikiFilePage( $title );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageInfoProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageInfoProvider.php
new file mode 100644
index 00000000..4fe69a5b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageInfoProvider.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Revision;
+use SMW\PageInfo;
+use User;
+use WikiPage;
+
+/**
+ * Provide access to MediaWiki objects relevant for the predefined property
+ * annotation process
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class PageInfoProvider implements PageInfo {
+
+ /**
+ * @var WikiPage
+ */
+ private $wikiPage = null;
+
+ /**
+ * @var Revision
+ */
+ private $revision = null;
+
+ /**
+ * @var User
+ */
+ private $user = null;
+
+ /**
+ * @since 1.9
+ *
+ * @param WikiPage $wikiPage
+ * @param Revision|null $revision
+ * @param User|null $user
+ */
+ public function __construct( WikiPage $wikiPage, Revision $revision = null, User $user = null ) {
+ $this->wikiPage = $wikiPage;
+ $this->revision = $revision;
+ $this->user = $user;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return integer
+ */
+ public function getModificationDate() {
+ return $this->wikiPage->getTimestamp();
+ }
+
+ /**
+ * @note getFirstRevision() is expensive as it initiates a read on the
+ * revision table which is not cached
+ *
+ * @since 1.9
+ *
+ * @return integer
+ */
+ public function getCreationDate() {
+ return $this->wikiPage->getTitle()->getFirstRevision()->getTimestamp();
+ }
+
+ /**
+ * @note Using isNewPage() is expensive due to access to the database
+ *
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function isNewPage() {
+
+ if ( $this->isFilePage() ) {
+ return isset( $this->wikiPage->smwFileReUploadStatus ) ? !$this->wikiPage->smwFileReUploadStatus : false;
+ }
+
+ if ( $this->revision ) {
+ return $this->revision->getParentId() === null;
+ }
+
+ return $this->wikiPage->getRevision()->getParentId() === null;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return Title
+ */
+ public function getLastEditor() {
+ return $this->user ? $this->user->getUserPage() : null;
+ }
+
+ /**
+ * @since 1.9.1
+ *
+ * @return boolean
+ */
+ public function isFilePage() {
+ return $this->wikiPage instanceof \WikiFilePage;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return text
+ */
+ public function getNativeData() {
+
+ if ( $this->wikiPage->getContent() === null ) {
+ return '';
+ }
+
+ $content = $this->wikiPage->getContent();
+
+ if ( $content instanceof \SMW\Schema\Content\Content ) {
+ return $content->toJson();
+ }
+
+ return $content->getNativeData();
+ }
+
+ /**
+ * @since 1.9.1
+ *
+ * @return string|null
+ */
+ public function getMediaType() {
+
+ if ( $this->isFilePage() === false ) {
+ return null;
+ }
+
+ return $this->wikiPage->getFile()->getMediaType();
+ }
+
+ /**
+ * @since 1.9.1
+ *
+ * @return string|null
+ */
+ public function getMimeType() {
+
+ if ( $this->isFilePage() === false ) {
+ return null;
+ }
+
+ return $this->wikiPage->getFile()->getMimeType();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageUpdater.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageUpdater.php
new file mode 100644
index 00000000..c65727ff
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/PageUpdater.php
@@ -0,0 +1,357 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use DeferrableUpdate;
+use DeferredpendingUpdates;
+use Psr\Log\LoggerAwareTrait;
+use SMW\MediaWiki\Deferred\TransactionalCallableUpdate;
+use SMW\Utils\Timer;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class PageUpdater implements DeferrableUpdate {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var TransactionalCallableUpdate
+ */
+ private $transactionalCallableUpdate;
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var Title[]
+ */
+ private $titles = [];
+
+ /**
+ * @var string
+ */
+ private $origin = '';
+
+ /**
+ * @var string|null
+ */
+ private $fingerprint = null;
+
+ /**
+ * @var boolean
+ */
+ private $isHtmlCacheUpdate = true;
+
+ /**
+ * @var boolean
+ */
+ private $onTransactionIdle = false;
+
+ /**
+ * @var boolean
+ */
+ private $asPoolPurge = false;
+
+ /**
+ * @var boolean
+ */
+ private $isPending = false;
+
+ /**
+ * @var array
+ */
+ private $pendingUpdates = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param Database|null $connection
+ * @param TransactionalCallableUpdate|null $transactionalCallableUpdate
+ */
+ public function __construct( Database $connection = null, TransactionalCallableUpdate $transactionalCallableUpdate = null ) {
+ $this->connection = $connection;
+ $this->transactionalCallableUpdate = $transactionalCallableUpdate;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $origin
+ */
+ public function setOrigin( $origin ) {
+ $this->origin = $origin;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|null $fingerprint
+ */
+ public function setFingerprint( $fingerprint = null ) {
+ $this->fingerprint = $fingerprint;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isHtmlCacheUpdate
+ */
+ public function isHtmlCacheUpdate( $isHtmlCacheUpdate ) {
+ $this->isHtmlCacheUpdate = $isHtmlCacheUpdate;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param booloan $isPending
+ */
+ public function markAsPending() {
+ $this->isPending = true;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Title|null $title
+ */
+ public function addPage( Title $title = null ) {
+
+ if ( $title === null ) {
+ return;
+ }
+
+ $this->titles[$title->getDBKey()] = $title;
+ }
+
+ /**
+ * @note MW 1.29+ runs Title::invalidateCache in AutoCommitUpdate which has
+ * been shown to cause transaction issues when executed while a transaction
+ * hasn't finished therefore use 'onTransactionIdle' to isolate the
+ * execution.
+ *
+ * @since 2.5
+ */
+ public function waitOnTransactionIdle() {
+ $this->onTransactionIdle = true;
+ }
+
+ /**
+ * Controls the purge to use a direct DB access to make changes to avoid
+ * racing conditions for a large number of title entities.
+ *
+ * @since 3.0
+ */
+ public function doPurgeParserCacheAsPool() {
+ if ( $this->connection !== null ) {
+ $this->connection->onTransactionIdle( function() {
+ $this->doPoolPurge();
+ } );
+ } else {
+ $this->doPoolPurge();
+ }
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function clear() {
+ $this->titles = [];
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return boolean
+ */
+ public function canUpdate() {
+ return !wfReadOnly();
+ }
+
+ /**
+ * Push pendingUpdates to be either deferred or direct executable, pending
+ * the setting invoked by PageUPdater::markAsPending.
+ *
+ * @since 3.0
+ */
+ public function pushUpdate() {
+
+ if ( $this->transactionalCallableUpdate === null ) {
+ return $this->log( __METHOD__ . ' it is not possible to push updates as DeferredTransactionalUpdate)' );
+ }
+
+ $this->transactionalCallableUpdate->setCallback( function(){
+ $this->doUpdate();
+ } );
+
+ if ( $this->onTransactionIdle ) {
+ $this->transactionalCallableUpdate->waitOnTransactionIdle();
+ }
+ if ( $this->isPending ) {
+ $this->transactionalCallableUpdate->markAsPending();
+ }
+
+ $this->transactionalCallableUpdate->setFingerprint(
+ $this->fingerprint
+ );
+
+ $this->transactionalCallableUpdate->setOrigin( [
+ __METHOD__,
+ $this->origin
+ ] );
+
+ $this->transactionalCallableUpdate->pushUpdate();
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function doUpdate() {
+ $this->isPending = false;
+ $this->onTransactionIdle = false;
+
+ foreach ( array_keys( $this->pendingUpdates ) as $update ) {
+ call_user_func( [ $this, $update ] );
+ }
+
+ $this->pendingUpdates = [];
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function doPurgeParserCache() {
+
+ $method = __METHOD__;
+
+ if ( $this->isPending || $this->onTransactionIdle ) {
+ return $this->pendingUpdates['doPurgeParserCache'] = true;
+ }
+
+ foreach ( $this->titles as $title ) {
+ $title->invalidateCache();
+ }
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function doPurgeHtmlCache() {
+
+ if ( $this->isHtmlCacheUpdate === false ) {
+ return;
+ }
+
+ if ( $this->isPending || $this->onTransactionIdle ) {
+ return $this->pendingUpdates['doPurgeHtmlCache'] = true;
+ }
+
+ $method = __METHOD__;
+
+ // Calls HTMLCacheUpdate, HTMLCacheUpdateJob including HTMLFileCache,
+ // CdnCacheUpdate
+ foreach ( $this->titles as $title ) {
+ $title->touchLinks();
+ }
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function doPurgeWebCache() {
+
+ $method = __METHOD__;
+
+ if ( $this->isPending || $this->onTransactionIdle ) {
+ return $this->pendingUpdates['doPurgeWebCache'] = true;
+ }
+
+ foreach ( $this->titles as $title ) {
+ $title->purgeSquid();
+ }
+ }
+
+ /**
+ * Copied from PurgeJobUtils to avoid the AutoCommitUpdate from
+ * Title::invalidateCache introduced with MW 1.28/1.29 on a large update pool
+ */
+ private function doPoolPurge() {
+
+ Timer::start( __METHOD__ );
+
+ // #3413
+ $byNamespace = [];
+
+ foreach ( $this->titles as $title ) {
+ $namespace = $title->getNamespace();
+ $pagename = $title->getDBkey();
+ $byNamespace[$namespace][] = $pagename;
+ }
+
+ $conds = [];
+
+ foreach ( $byNamespace as $namespaces => $pagenames ) {
+
+ $cond = [
+ 'page_namespace' => $namespaces,
+ 'page_title' => $pagenames,
+ ];
+
+ $conds[] = $this->connection->makeList( $cond, LIST_AND );
+ }
+
+ $titleConds = $this->connection->makeList( $conds, LIST_OR );
+
+ // Required due to postgres and "Error: 22007 ERROR: invalid input
+ // syntax for type timestamp with time zone: "20170408113703""
+ $now = $this->connection->timestamp();
+ $res = $this->connection->select(
+ 'page',
+ 'page_id',
+ [
+ $titleConds,
+ 'page_touched < ' . $this->connection->addQuotes( $now )
+ ],
+ __METHOD__
+ );
+
+ if ( $res === false ) {
+ return;
+ }
+
+ $ids = [];
+
+ foreach ( $res as $row ) {
+ $ids[] = $row->page_id;
+ }
+
+ if ( $ids === [] ) {
+ return;
+ }
+
+ $this->connection->update(
+ 'page',
+ [ 'page_touched' => $now ],
+ [
+ 'page_id' => $ids,
+ 'page_touched < ' . $this->connection->addQuotes( $now )
+ ],
+ __METHOD__
+ );
+
+ $context = [
+ 'method' => __METHOD__,
+ 'procTime' => Timer::getElapsedTime( __METHOD__, 7 ),
+ 'role' => 'developer'
+ ];
+
+ $this->logger->info( 'Page update, pool update (procTime in sec: {procTime})', $context );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/RedirectTargetFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/RedirectTargetFinder.php
new file mode 100644
index 00000000..38b61896
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/RedirectTargetFinder.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use ContentHandler;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class RedirectTargetFinder {
+
+ /**
+ * @var Title|null
+ */
+ private $redirectTarget = null;
+
+ /**
+ * @since 2.0
+ *
+ * @param string $text
+ *
+ * @return Title|null
+ */
+ public function findRedirectTargetFromText( $text ) {
+
+ if ( $this->redirectTarget === null ) {
+ $this->redirectTarget = $this->findFromText( $text );
+ }
+
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Title|null
+ */
+ public function setRedirectTarget( Title $redirectTarget = null ) {
+ $this->redirectTarget = $redirectTarget;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return Title|null
+ */
+ public function getRedirectTarget() {
+ return $this->redirectTarget;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return boolean
+ */
+ public function hasRedirectTarget() {
+ return $this->redirectTarget instanceof Title;
+ }
+
+ private function findFromText( $text ) {
+
+ if ( $this->hasContentHandler() ) {
+ return ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT )->getRedirectTarget();
+ }
+
+ return Title::newFromRedirect( $text );
+ }
+
+ protected function hasContentHandler() {
+ return defined( 'CONTENT_MODEL_WIKITEXT' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlColumnListRenderer.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlColumnListRenderer.php
new file mode 100644
index 00000000..8f8a85f4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlColumnListRenderer.php
@@ -0,0 +1,296 @@
+<?php
+
+namespace SMW\MediaWiki\Renderer;
+
+use Html;
+
+/**
+ * Simple list formatter to transform an indexed array (e.g. array( 'F' => array( 'Foo', 'Bar' ) )
+ * into a column divided list.
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class HtmlColumnListRenderer {
+
+ /**
+ * @var integer
+ */
+ private $numberOfColumns = 1;
+
+ /**
+ * @var array
+ */
+ private $contentsByIndex = [];
+
+ /**
+ * @var array
+ */
+ private $itemAttributes = [];
+
+ /**
+ * @var integer
+ */
+ private $numRows = 0;
+
+ /**
+ * @var integer
+ */
+ private $numberOfResults = 0;
+
+ /**
+ * @var integer
+ */
+ private $rowsPerColumn = 0;
+
+ /**
+ * @var integer
+ */
+ private $columnWidth = 0;
+
+ /**
+ * @var string
+ */
+ private $listType = 'ul';
+
+ /**
+ * @var string
+ */
+ private $olType = '';
+
+ /**
+ * @var string
+ */
+ private $columnListClass = 'smw-columnlist-container';
+
+ /**
+ * @var string
+ */
+ private $columnClass = 'smw-column';
+
+ /**
+ * @var boolean
+ */
+ private $isRTL = false;
+
+ /**
+ * @since 2.2
+ *
+ * @param string $columnListClass
+ *
+ * @return HtmlColumnListRenderer
+ */
+ public function setColumnListClass( $columnListClass ) {
+ $this->columnListClass = htmlspecialchars( $columnListClass );
+ return $this;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $columnListClass
+ *
+ * @return HtmlColumnListRenderer
+ */
+ public function setColumnClass( $columnClass ) {
+ $this->columnClass = htmlspecialchars( $columnClass );
+ return $this;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param boolean $isRTL
+ */
+ public function setColumnRTLDirectionalityState( $isRTL ) {
+ $this->isRTL = (bool)$isRTL;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param integer $numberOfColumns
+ *
+ * @return HtmlColumnListRenderer
+ */
+ public function setNumberOfColumns( $numberOfColumns ) {
+ $this->numberOfColumns = $numberOfColumns;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $listType
+ *
+ * @return HtmlColumnListRenderer
+ */
+ public function setListType( $listType, $olType = '' ) {
+
+ if ( in_array( $listType, [ 'ul', 'ol' ] ) ) {
+ $this->listType = $listType;
+ }
+
+ if ( $this->listType === 'ol' && in_array( $olType, [ '1', 'a', 'A', 'i', 'I' ] ) ) {
+ $this->olType = $olType;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Allows to define attributes for a item such as:
+ *
+ * [md5( $itemContent )] = [
+ * 'id' => 'Foo'
+ * ]
+ *
+ * @since 3.0
+ *
+ * @param array $itemAttributes
+ */
+ public function setItemAttributes( array $itemAttributes ) {
+ $this->itemAttributes = $itemAttributes;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string[] $contentsByNoIndex
+ *
+ * @return HtmlColumnListRenderer
+ */
+ public function addContentsByNoIndex( array $contentsByNoIndex ) {
+
+ $contentsByEmptyIndex[''] = [];
+
+ foreach ( $contentsByNoIndex as $value ) {
+ $contentsByEmptyIndex[''][] = $value;
+ }
+
+ return $this->addContentsByIndex( $contentsByEmptyIndex );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string[] $contentsByIndex
+ *
+ * @return HtmlColumnListRenderer
+ */
+ public function addContentsByIndex( array $contentsByIndex ) {
+ $this->contentsByIndex = $contentsByIndex;
+ $this->numberOfResults = count( $this->contentsByIndex, COUNT_RECURSIVE ) - count( $this->contentsByIndex );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return string
+ */
+ public function getHtml() {
+
+ $result = '';
+ $usedColumnCloser = false;
+ $this->numRows = 0;
+
+ // Class to determine whether we want responsive columns width
+ if ( strpos( $this->columnClass, 'responsive' ) !== false ) {
+ $this->columnWidth = 100;
+ $this->numberOfColumns = 1;
+ } else {
+ $this->columnWidth = floor( 100 / $this->numberOfColumns );
+ }
+
+ $this->rowsPerColumn = ceil( $this->numberOfResults / $this->numberOfColumns );
+ $listContinuesAbbrev = wfMessage( 'listingcontinuesabbrev' )->text();
+
+ foreach ( $this->contentsByIndex as $key => $resultItems ) {
+
+ if ( $resultItems === [] ) {
+ continue;
+ }
+
+ $result .= $this->makeList(
+ $key,
+ $listContinuesAbbrev,
+ $resultItems,
+ $usedColumnCloser
+ );
+ }
+
+ if ( !$usedColumnCloser ) {
+ $result .= "</{$this->listType}></div> <!-- end column -->";
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => $this->columnListClass,
+ 'dir' => $this->isRTL ? 'rtl' : 'ltr'
+ ],
+ $result . "\n" . '<br style="clear: both;"/>'
+ );
+ }
+
+ private function makeList( $key, $listContinuesAbbrev, $items, &$usedColumnCloser ) {
+
+ $result = '';
+ $previousKey = "";
+ $dir = $this->isRTL ? 'rtl' : 'ltr';
+
+ foreach ( $items as $item ) {
+
+ $attributes = [];
+
+ if ( $this->itemAttributes !== [] ) {
+ $hash = md5( $item );
+
+ if ( isset( $this->itemAttributes[$hash] ) ) {
+ $attributes = $this->itemAttributes[$hash];
+ }
+ }
+
+ if ( $this->numRows % $this->rowsPerColumn == 0 ) {
+ $result .= "<div class=\"$this->columnClass\" style=\"width:$this->columnWidth%;\" dir=\"$dir\">";
+
+ $numRowsInColumn = $this->numRows + 1;
+ $type = $this->olType !== '' ? " type={$this->olType}" : '';
+
+ if ( $key == $previousKey ) {
+ // @codingStandardsIgnoreStart phpcs, ignore --sniffs=Generic.Files.LineLength.MaxExceeded
+ $result .= $key !== '' ? Html::element( 'div', [ 'class' => 'smw-column-header' ], "$key $listContinuesAbbrev" ) : '';
+ $result .= "<{$this->listType}$type start={$numRowsInColumn}>";
+ // @codingStandardsIgnoreEnd
+ }
+ }
+
+ // if we're at a new first letter, end
+ // the last list and start a new one
+ if ( $key != $previousKey ) {
+ $result .= $this->numRows % $this->rowsPerColumn > 0 ? "</{$this->listType}>" : '';
+ $result .= ( $key !== '' ? Html::element( 'div', [ 'class' => 'smw-column-header' ], $key ) : '' ) . "<{$this->listType}>";
+ }
+
+ $previousKey = $key;
+ $result .= Html::rawElement( 'li', $attributes, $item );
+ $usedColumnCloser = false;
+
+ if ( ( $this->numRows + 1 ) % $this->rowsPerColumn == 0 && ( $this->numRows + 1 ) < $this->numberOfResults ) {
+ $result .= "</{$this->listType}></div> <!-- end column -->";
+ $usedColumnCloser = true;
+ }
+
+ $this->numRows++;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlFormRenderer.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlFormRenderer.php
new file mode 100644
index 00000000..23bf19a4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlFormRenderer.php
@@ -0,0 +1,513 @@
+<?php
+
+namespace SMW\MediaWiki\Renderer;
+
+use Html;
+use SMW\MediaWiki\MessageBuilder;
+use Title;
+use Xml;
+
+/**
+ * Convenience class to build a html form by using a fluid interface
+ *
+ * @par Example:
+ * @code
+ * $htmlFormRenderer = new HtmlFormRenderer( $this->title, new MessageBuilder() );
+ * $htmlFormRenderer
+ * ->setName( 'Foo' )
+ * ->setParameter( 'foo', 'someValue' )
+ * ->addPaging( 10, 0, 5 )
+ * ->addHorizontalRule()
+ * ->addInputField( 'BarLabel', 'bar', 'someValue' )
+ * ->addSubmitButton()
+ * ->getForm();
+ * @endcode
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class HtmlFormRenderer {
+
+ /**
+ * @var Title
+ */
+ private $title = null;
+
+ /**
+ * @var MessageBuilder
+ */
+ private $messageBuilder = null;
+
+ /**
+ * @var array
+ */
+ private $queryParameters = [];
+
+ /**
+ * @var string
+ */
+ private $name ='';
+
+ /**
+ * @var string|boolean
+ */
+ private $method = false;
+
+ /**
+ * @var string|boolean
+ */
+ private $useFieldset = false;
+
+ /**
+ * @var string|boolean
+ */
+ private $actionUrl = false;
+
+ /**
+ * @var string[]
+ */
+ private $content = [];
+
+ /**
+ * @var string
+ */
+ private $defaultPrefix = 'smw-form';
+
+ /**
+ * @since 2.1
+ *
+ * @param Title $title
+ * @param MessageBuilder $messageBuilder
+ */
+ public function __construct( Title $title, MessageBuilder $messageBuilder ) {
+ $this->title = $title;
+ $this->messageBuilder = $messageBuilder;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return HtmlFormRenderer
+ */
+ public function clear() {
+ $this->queryParameters = [];
+ $this->content = [];
+ $this->name = '';
+ $this->method = false;
+ $this->useFieldset = false;
+ $this->actionUrl = false;
+
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return MessageBuilder
+ */
+ public function getMessageBuilder() {
+ return $this->messageBuilder;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $name
+ *
+ * @return HtmlFormRenderer
+ */
+ public function setName( $name ) {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $actionUrl
+ *
+ * @return HtmlFormRenderer
+ */
+ public function setActionUrl( $actionUrl ) {
+ $this->actionUrl = $actionUrl;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return HtmlFormRenderer
+ */
+ public function withFieldset() {
+ $this->useFieldset = true;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $method
+ *
+ * @return HtmlFormRenderer
+ */
+ public function setMethod( $method ) {
+ $this->method = strtolower( $method );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $key
+ * @param string $value
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addQueryParameter( $key, $value ) {
+ $this->queryParameters[$key] = $value;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return array
+ */
+ public function getQueryParameter() {
+ return $this->queryParameters;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $description
+ * @param array $attributes
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addParagraph( $text, $attributes = [] ) {
+
+ if ( $attributes === [] ) {
+ $attributes = [ 'class' => $this->defaultPrefix . '-paragraph' ];
+ }
+
+ $this->content[] = Xml::tags( 'p', $attributes, $text );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param array $attributes
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addHorizontalRule( $attributes = [] ) {
+
+ if ( $attributes === [] ) {
+ $attributes = [ 'class' => $this->defaultPrefix . '-horizontalrule' ];
+ }
+
+ $this->content[] = Xml::tags( 'hr', $attributes, '' );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param $level
+ * @param $text
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addHeader( $level, $text ) {
+
+ $level = strtolower( $level );
+ $level = in_array( $level, [ 'h2', 'h3', 'h4' ] ) ? $level : 'h2';
+
+ $this->content[] = Html::element( $level, [], $text );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addLineBreak() {
+ $this->content[] = Html::element( 'br', [], '' );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addNonBreakingSpace() {
+ $this->content[] = '&nbsp;';
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string|null $text
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addSubmitButton( $text, $attributes = [] ) {
+ $this->content[] = Xml::submitButton( $text, $attributes );
+ return $this;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $element
+ * @param array $attributes
+ *
+ * @return HtmlFormRenderer
+ */
+ public function openElement( $element = 'div', array $attributes = [] ) {
+ $this->content[] = Html::openElement( $element, $attributes );
+ return $this;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $element
+ * @param array $attributes
+ *
+ * @return HtmlFormRenderer
+ */
+ public function closeElement( $element = 'div', array $attributes = [] ) {
+ $this->content[] = Html::closeElement( $element, $attributes );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $label
+ * @param string $name
+ * @param string $value
+ * @param string|null $id
+ * @param integer $length
+ * @param array $attributes
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addInputField( $label, $name, $value, $id = null, $size = 20, array $attributes = [] ) {
+
+ if ( $id === null ) {
+ $id = $name;
+ }
+
+ $this->addQueryParameter( $name, $value );
+
+ if ( !isset( $attributes['class'] ) ) {
+ $attributes['class'] = $this->defaultPrefix . '-input';
+ }
+
+ $label = Xml::label( $label, $id, [] );
+ $input = Xml::input( $name, $size, $value, [ 'id' => $id ] + $attributes );
+
+ $this->content[] = $label . '&#160;' . $input;
+
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $inputName
+ * @param string $inputValue
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addHiddenField( $inputName, $inputValue ) {
+
+ $this->addQueryParameter( $inputName, $inputValue );
+
+ $this->content[] = Html::hidden( $inputName, $inputValue );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $label
+ * @param string $inputName
+ * @param string $inputValue
+ * @param array $options
+ * @param string|null $id
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addOptionSelectList( $label, $inputName, $inputValue, $options, $id = null ) {
+
+ if ( $id === null ) {
+ $id = $inputName;
+ }
+
+ $this->addQueryParameter( $inputName, $inputValue );
+
+ ksort( $options );
+
+ $html = '';
+ $optionsHtml = [];
+
+ foreach ( $options as $internalId => $name ) {
+ $optionsHtml[] = Html::element(
+ 'option', [
+ // 'disabled' => false,
+ 'value' => $internalId,
+ 'selected' => $internalId == $inputValue,
+ ], $name
+ );
+ }
+
+ $html .= Html::element( 'label', [ 'for' => $id ], $label ) . '&#160;';
+
+ $html .= Html::openElement(
+ 'select',
+ [
+ 'name' => $inputName,
+ 'id' => $id,
+ 'class' => $this->defaultPrefix . '-select' ] ) . "\n" .
+ implode( "\n", $optionsHtml ) . "\n" .
+ Html::closeElement( 'select' );
+
+ $this->content[] = $html;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $label
+ * @param string $inputName
+ * @param string $inputValue
+ * @param boolean $isChecked
+ * @param string|null $id
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addCheckbox( $label, $inputName, $inputValue, $isChecked = false, $id = null, $attributes = [] ) {
+
+ if ( $id === null ) {
+ $id = $inputName;
+ }
+
+ $this->addQueryParameter( $inputName, $inputValue );
+
+ $html = Xml::checkLabel(
+ $label,
+ $inputName,
+ $id,
+ $isChecked,
+ [
+ 'id' => $id,
+ 'class' => $this->defaultPrefix . '-checkbox',
+ 'value' => $inputValue
+ ] + ( $isChecked ? [ 'checked' => 'checked' ] : [] )
+ );
+
+ $this->content[] = Html::rawElement( 'span', $attributes, $html );
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @note Encapsulate as closure to ensure that the build contains all query
+ * parameters that are necessary to build the paging links
+ *
+ * @param integer $limit
+ * @param integer $offset
+ * @param integer $count
+ * @param integer|null $messageCount
+ *
+ * @return HtmlFormRenderer
+ */
+ public function addPaging( $limit, $offset, $count, $messageCount = null ) {
+
+ $title = $this->title;
+
+ $this->content[] = function( $instance ) use ( $title, $limit, $offset, $count, $messageCount ) {
+
+ if ( $messageCount === null ) {
+ $messageCount = ( $count > $limit ? $count - 1 : $count );
+ }
+
+ $resultCount = $instance->getMessageBuilder()
+ ->getMessage( 'showingresults' )
+ ->numParams( $messageCount, $offset + 1 )
+ ->parse();
+
+ $paging = $instance->getMessageBuilder()->prevNextToText(
+ $title,
+ $limit,
+ $offset,
+ $instance->getQueryParameter(),
+ $count < $limit
+ );
+
+ return Xml::tags( 'p', [], $resultCount ) . Xml::tags( 'p', [], $paging );
+ };
+
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return string
+ */
+ public function getForm() {
+
+ $content = '';
+
+ foreach ( $this->content as $value ) {
+ $content .= is_callable( $value ) ? $value( $this ) : $value;
+ }
+
+ if ( $this->useFieldset ) {
+ $content = Xml::fieldset(
+ $this->messageBuilder->getMessage( $this->name )->text(),
+ $content,
+ [
+ 'id' => $this->defaultPrefix . "-fieldset-{$this->name}"
+ ]
+ );
+ }
+
+ $form = Xml::tags( 'form', [
+ 'id' => $this->defaultPrefix . "-{$this->name}",
+ 'name' => $this->name,
+ 'method' => in_array( $this->method, [ 'get', 'post' ] ) ? $this->method : 'get',
+ 'action' => htmlspecialchars( $this->actionUrl ? $this->actionUrl : $GLOBALS['wgScript'] )
+ ], Html::hidden(
+ 'title',
+ strtok( $this->title->getPrefixedText(), '/' )
+ ) . $content );
+
+ $this->clear();
+
+ return $form;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function renderForm() {
+ return $this->getForm();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlTableRenderer.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlTableRenderer.php
new file mode 100644
index 00000000..00c9d4ba
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlTableRenderer.php
@@ -0,0 +1,290 @@
+<?php
+
+namespace SMW\MediaWiki\Renderer;
+
+use Html;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class HtmlTableRenderer {
+
+ /**
+ * @var array
+ */
+ private $headerItems = [];
+
+ /**
+ * @var array
+ */
+ private $tableRows = [];
+
+ /**
+ * @var array
+ */
+ private $rawRows = [];
+
+ /**
+ * @var array
+ */
+ private $tableHeaders = [];
+
+ /**
+ * @var array
+ */
+ private $rawHeaders = [];
+
+ /**
+ * @var array
+ */
+ private $tableCells = [];
+
+ /**
+ * @var array
+ */
+ private $transpose = false;
+
+ /**
+ * @par Example:
+ * @code
+ * $tableBuilder = new TableBuilder();
+ *
+ * $tableBuilder
+ * ->addHeader( 'Foo' )
+ * ->addHeader( 'Bar' )
+ * ->addCell( 'Lula' )
+ * ->addCell( 'Lala' )
+ * ->addRow();
+ *
+ * $tableBuilder->getHtml()
+ * @endcode
+ *
+ * @since 1.9
+ *
+ * @param boolean $htmlContext
+ */
+ public function __construct( $htmlContext = false ) {
+ $this->htmlContext = $htmlContext;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param boolean $htmlContext
+ */
+ public function setHtmlContext( $htmlContext ) {
+ $this->htmlContext = $htmlContext;
+ return $this;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param boolean $transpose
+ *
+ * @return TableBuilder
+ */
+ public function transpose( $transpose = true ) {
+ $this->transpose = $transpose;
+ return $this;
+ }
+
+ /**
+ * Adds an arbitrary header item to an internal array
+ *
+ * @since 1.9
+ *
+ * @param string $element
+ * @param string $content
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public function addHeaderItem( $element, $content = '', $attributes = [] ) {
+ $this->headerItems[] = Html::rawElement( $element, $attributes, $content );
+ }
+
+ /**
+ * Returns concatenated header items
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ public function getHeaderItems() {
+ return implode( '', $this->headerItems );
+ }
+
+ /**
+ * Collects and adds table cells
+ *
+ * @since 1.9
+ *
+ * @param string $content
+ * @param array $attributes
+ *
+ * @return TableBuilder
+ */
+ public function addCell( $content = '', $attributes = [] ) {
+ if ( $content !== '' ) {
+ $this->tableCells[] = $this->createCell( $content, $attributes );
+ }
+ return $this;
+ }
+
+ /**
+ * Collects and adds table headers
+ *
+ * @since 1.9
+ *
+ * @param string $content
+ * @param array $attributes
+ *
+ * @return TableBuilder
+ */
+ public function addHeader( $content = '', $attributes = [] ) {
+ if ( $content !== '' ) {
+ $this->rawHeaders[] = [ 'content' => $content, 'attributes' => $attributes ];
+ }
+ return $this;
+ }
+
+ /**
+ * Build a row from invoked cells, copy them into a new associated array
+ * and delete those cells as they are now part of a row
+ *
+ * @par Example:
+ * @code
+ * ...
+ * $TableBuilder->addCell( 'Lula' )->addCell( 'Lala' )->addRow()
+ * ...
+ * @endcode
+ *
+ * @since 1.9
+ *
+ * @param array $attributes
+ *
+ * @return TableBuilder
+ */
+ public function addRow( $attributes = [] ) {
+ if ( $this->tableCells !== [] ) {
+ $this->rawRows[] = [ 'cells' => $this->tableCells, 'attributes' => $attributes ];
+ $this->tableCells = [];
+ }
+ return $this;
+ }
+
+ /**
+ * Returns a table
+ *
+ * @since 1.9
+ *
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public function getHtml( $attributes = [] ) {
+
+ $table = $this->transpose ? $this->buildTransposedTable() : $this->buildStandardTable();
+
+ if ( $this->transpose ) {
+ $attributes['data-transpose'] = true;
+ }
+
+ if ( $table !== '' ) {
+ return Html::rawElement( 'table', $attributes, $table );
+ }
+
+ return '';
+ }
+
+ private function createRow( $content = '', $attributes = [] ) {
+ $alternate = count( $this->tableRows ) % 2 == 0 ? 'row-odd' : 'row-even';
+
+ if ( isset( $attributes['class'] ) ) {
+ $attributes['class'] = $attributes['class'] . ' ' . $alternate;
+ } else {
+ $attributes['class'] = $alternate;
+ }
+
+ return Html::rawElement( 'tr', $attributes, $content );
+ }
+
+ private function createCell( $content = '', $attributes = [] ) {
+ return Html::rawElement( 'td', $attributes, $content );
+ }
+
+ private function createHeader( $content = '', $attributes = [] ) {
+ return Html::rawElement( 'th', $attributes, $content );
+ }
+
+ private function doConcatenatedHeader() {
+
+ if ( $this->htmlContext ) {
+ return Html::rawElement( 'thead', [], implode( '', $this->tableHeaders ) );
+ }
+
+ return implode( '', $this->tableHeaders );
+ }
+
+ private function doConcatenatedRows() {
+
+ if ( $this->htmlContext ) {
+ return Html::rawElement( 'tbody', [], implode( '', $this->tableRows ) );
+ }
+
+ return implode( '', $this->tableRows );
+ }
+
+ private function buildStandardTable() {
+ $this->tableHeaders = [];
+ $this->tableRows = [];
+
+ foreach( $this->rawHeaders as $i => $header ) {
+ $this->tableHeaders[] = $this->createHeader( $header['content'], $header['attributes'] );
+ }
+
+ foreach( $this->rawRows as $row ) {
+ $this->tableRows[] = $this->createRow( implode( '', $row['cells'] ), $row['attributes'] );
+ }
+
+ return $this->doConcatenatedHeader() . $this->doConcatenatedRows();
+ }
+
+ private function buildTransposedTable() {
+ $this->tableRows = [];
+
+ foreach( $this->rawHeaders as $hIndex => $header ) {
+ $cells = [];
+ $headerItem = $this->createHeader( $header['content'], $header['attributes'] );
+
+ foreach( $this->rawRows as $rIndex => $row ) {
+ $cells[] = $this->getTransposedCell( $hIndex, $row );
+ }
+
+ // Collect new rows
+ $this->tableRows[] = $this->createRow( $headerItem . implode( '', $cells ) );
+ }
+
+ return $this->doConcatenatedHeader() . $this->doConcatenatedRows();
+ }
+
+ private function getTransposedCell( $index, $row ) {
+
+ if ( isset( $row['cells'][$index] ) ) {
+ return $row['cells'][$index];
+ }
+
+ $attributes = [];
+
+ if ( isset( $row['attributes']['class'] ) && $row['attributes']['class'] === 'smwfooter' ) {
+ $attributes = [ 'class' => 'footer-cell' ];
+ }
+
+ return $this->createCell( '', $attributes );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlTemplateRenderer.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlTemplateRenderer.php
new file mode 100644
index 00000000..1f4c273b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/HtmlTemplateRenderer.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace SMW\MediaWiki\Renderer;
+
+use Parser;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class HtmlTemplateRenderer {
+
+ /**
+ * @var WikitextTemplateRenderer
+ */
+ private $wikitextTemplateRenderer;
+
+ /**
+ * @var Parser
+ */
+ private $parser;
+
+ /**
+ * @since 2.2
+ *
+ * @param WikitextTemplateRenderer $wikitextTemplateRenderer
+ * @param Parser $parser
+ */
+ public function __construct( WikitextTemplateRenderer $wikitextTemplateRenderer, Parser $parser ) {
+ $this->wikitextTemplateRenderer = $wikitextTemplateRenderer;
+ $this->parser = $parser;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $field
+ * @param mixed $value
+ */
+ public function addField( $field, $value ) {
+ $this->wikitextTemplateRenderer->addField( $field, $value );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $templateName
+ */
+ public function packFieldsForTemplate( $templateName ) {
+ $this->wikitextTemplateRenderer->packFieldsForTemplate( $templateName );
+ }
+
+ /**
+ * @since since 2.2
+ *
+ * @return string
+ */
+ public function render() {
+
+ $wikiText = $this->wikitextTemplateRenderer->render();
+
+ if ( $wikiText === '' ) {
+ return '';
+ }
+
+ return $this->parser->recursiveTagParse( $wikiText );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/WikitextTemplateRenderer.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/WikitextTemplateRenderer.php
new file mode 100644
index 00000000..3c6dfbcc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Renderer/WikitextTemplateRenderer.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace SMW\MediaWiki\Renderer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class WikitextTemplateRenderer {
+
+ /**
+ * @var array
+ */
+ private $fields = [];
+
+ /**
+ * @var string
+ */
+ private $template = '';
+
+ /**
+ * @since 2.2
+ *
+ * @param string $field
+ * @param mixed $value
+ */
+ public function addField( $field, $value ) {
+ $this->fields[$field] = $value;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $templateName
+ */
+ public function packFieldsForTemplate( $templateName ) {
+
+ $this->template .= '{{'. $templateName;
+
+ foreach ( $this->fields as $key => $value ) {
+ $this->template .= "\n|$key=$value";
+ }
+
+ $this->template .= '}}';
+ $this->fields = [];
+ }
+
+ /**
+ * @since since 2.2
+ *
+ * @return string
+ */
+ public function render() {
+ $wikiText = $this->template;
+ $this->template = '';
+ $this->fields = [];
+ return $wikiText;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/CustomForm.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/CustomForm.php
new file mode 100644
index 00000000..84bac0cd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/CustomForm.php
@@ -0,0 +1,189 @@
+<?php
+
+namespace SMW\MediaWiki\Search\Form;
+
+use Html;
+use SMW\DIProperty;
+use Title;
+use WebRequest;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class CustomForm {
+
+ /**
+ * @var WebRequest
+ */
+ private $request;
+
+ /**
+ * @var Field
+ */
+ private $field;
+
+ /**
+ * @var boolean
+ */
+ private $isActiveForm = false;
+
+ /**
+ * @var []
+ */
+ private $parameters = [];
+
+ /**
+ * @var []
+ */
+ private $fieldCounter = [];
+
+ /**
+ * @var []
+ */
+ private $html5TypeMap = [
+ '_txt' => 'text',
+ '_uri' => 'url',
+ '_dat' => 'date',
+ '_tel' => 'tel',
+ '_ema' => 'email',
+ '_num' => 'number'
+ ];
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest $request
+ */
+ public function __construct( WebRequest $request ) {
+ $this->request = $request;
+ $this->field = new Field();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getParameters() {
+ return $this->parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isActiveForm
+ */
+ public function isActiveForm( $isActiveForm ) {
+ $this->isActiveForm = (bool)$isActiveForm;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $definition
+ */
+ public function makeFields( $definition ) {
+
+ $fields = [];
+ $this->parameters = [];
+ $nameList = [];
+
+ foreach ( $definition as $property ) {
+ $options = [];
+
+ // Simple list, or does a property have some options?
+ if ( is_array( $property ) ) {
+ foreach ( $property as $p => $options ) {
+ $property = $p;
+ }
+ }
+
+ // Transforms (Foo bar -> foobar), better URL query conformity
+ $name = FormsBuilder::toLowerCase( $property );
+ $value = '';
+
+ // Field with the same name should only appear once in a form
+ if ( isset( $nameList[$name] ) ) {
+ continue;
+ }
+
+ // Each form definition may contain properties that are also defined
+ // in other forms therefore count its member position so that only
+ // values for the active form are fetched. The counter is used as
+ // positioning for the value array index.
+ if ( !isset( $this->fieldCounter[$name] ) ) {
+ $this->fieldCounter[$name] = 0;
+ } else {
+ $this->fieldCounter[$name]++;
+ }
+
+ // Find request related value for the active form
+ if ( $this->isActiveForm ) {
+ $vals = $this->request->getArray( $name );
+
+ $i = $this->fieldCounter[$name];
+ $value = isset( $vals[$i] ) ? $vals[$i] : $vals[0];
+ $this->parameters[$name] = $value;
+ }
+
+ $nameList[$name] = true;
+ $fields[] = $this->makeField( $name, $property, $value, $options );
+ }
+
+ return implode( '', $fields );
+ }
+
+ private function makeField( $name, $property, $value, $options ) {
+
+ $display = $this->isActiveForm ? 'inline-block' : 'none';
+ $options = !is_array( $options ) ? [] : $options;
+
+ if ( !isset( $options['placeholder'] ) ) {
+ $options['placeholder'] = "$property ...";
+ }
+
+ if ( !isset( $options['class'] ) ) {
+ $options['class'] = "";
+ }
+
+ if ( isset( $options['autocomplete'] ) && $options['autocomplete'] ) {
+ $options['class'] .= " smw-propertyvalue-input autocomplete-arrow";
+ }
+
+ if ( isset( $options['type'] ) ) {
+ $type = $options['type'];
+ } else {
+ $typeID = DIProperty::newFromUserLabel( $property )->findPropertyTypeID();
+ $type = 'text';
+
+ if ( isset( $this->html5TypeMap[$typeID] ) ) {
+ $type = $this->html5TypeMap[$typeID];
+ }
+ }
+
+ // Numeric names are not useful and may have been caused by an invalid
+ // JSON array/object definition
+ if ( is_numeric( $name ) ) {
+ $options[] = 'disabled';
+ }
+
+ $attributes = [
+ 'name' => $name,
+ 'value' => $value,
+ 'type' => $type,
+ 'display' => $display,
+ 'placeholder' => $options['placeholder'],
+ 'data-property' => $property,
+ 'title' => $property,
+ 'multifield' => true,
+ ] + $options;
+
+ return $this->field->create( 'input', $attributes );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/Field.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/Field.php
new file mode 100644
index 00000000..cf521f7f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/Field.php
@@ -0,0 +1,242 @@
+<?php
+
+namespace SMW\MediaWiki\Search\Form;
+
+use Html;
+use SMW\Highlighter;
+use SMW\Message;
+use Title;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Field {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public function create( $type, $attributes = [] ) {
+
+ $attributes['class'] = "smw-$type" . ( isset( $attributes['class'] ) ? ' ' . $attributes['class'] : '' );
+
+ if ( isset( $attributes['tooltip'] ) ) {
+ $attributes['tooltip'] = $this->tooltip( $attributes );
+ $attributes['class'] .= " smw-$type-tooltip";
+ }
+
+ if ( $type === 'input' ) {
+ return $this->input( $attributes );
+ }
+
+ if ( $type === 'select' ) {
+ return $this->select( $attributes );
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public function tooltip( $attributes = [] ) {
+
+ $highlighter = Highlighter::factory( Highlighter::TYPE_NOTE );
+ $msg = '';
+
+ // Simple text, or is it message-key?
+ if ( isset( $attributes['tooltip'] ) && Message::exists( $attributes['tooltip'] ) ) {
+ $msg = Message::get( $attributes['tooltip'], Message::PARSE, Message::USER_LANGUAGE );
+ } elseif ( isset( $attributes['tooltip'] ) ) {
+ $msg = $attributes['tooltip'];
+ }
+
+ $highlighter->setContent(
+ [
+ 'content' => $msg,
+ 'style' => 'margin-left:10px;vertical-align:-1px;'
+ ]
+ );
+
+ return $highlighter->getHtml();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public function select( $attributes = [] ) {
+
+ $list = [];
+ $html = [];
+ $selected = false;
+
+ if ( isset( $attributes['list'] ) ) {
+ $list = $attributes['list'];
+ unset( $attributes['list'] );
+ }
+
+ if ( isset( $attributes['selected'] ) ) {
+ $selected = $attributes['selected'];
+ unset( $attributes['selected'] );
+ }
+
+ foreach ( $list as $key => $value ) {
+
+ $opt = '';
+ $val = $value;
+
+ if ( is_array( $value ) ) {
+ $val = $value[0];
+ $opt = ' ' . $value[1];
+ }
+
+ if ( $selected === $key ) {
+ $opt = ' selected';
+ }
+
+ $html[] = "<option value='$key'$opt>$val</option>";
+ }
+
+ $style = '';
+ $name = '';
+ $label = '';
+ $class = '';
+
+ if ( isset( $attributes['class'] ) ) {
+ $class = $attributes['class'];
+ unset( $attributes['class'] );
+ }
+
+ if ( isset( $attributes['name'] ) ) {
+ $name = $attributes['name'];
+ }
+
+ if ( isset( $attributes['style'] ) ) {
+ $style .= $attributes['style'];
+ unset( $attributes['style'] );
+ }
+
+ if ( isset( $attributes['display'] ) ) {
+ $style = 'display:' . $attributes['display'] . ';';
+ unset( $attributes['display'] );
+ }
+
+ if ( isset( $attributes['multifield'] ) ) {
+ $name = $attributes['name'] . "[]";
+ unset( $attributes['multifield'] );
+ }
+
+ if ( isset( $attributes['label'] ) ) {
+ $label = "<label for='$name'>" . $attributes['label'] . "</label>";
+ unset( $attributes['label'] );
+ }
+
+ return $label . Html::rawElement(
+ 'select',
+ [
+ 'class' => $class,
+ 'name' => $name,
+ ] + ( $style !== '' ? [ 'style' => $style ] : [] ) + $attributes,
+ implode( '', $html )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public function input( $attributes = [] ) {
+
+ $class = isset( $attributes['class'] ) ? $attributes['class'] : '';
+ $type = 'text';
+ $tooltip = '';
+ $required = false;
+ $placeholder = '';
+ $value = '';
+ $style = '';
+ $name = '';
+
+ if ( isset( $attributes['style'] ) ) {
+ $style .= $attributes['style'];
+ unset( $attributes['style'] );
+ }
+
+ if ( isset( $attributes['display'] ) ) {
+ $style = 'display:' . $attributes['display'] . ';';
+ unset( $attributes['display'] );
+ }
+
+ if ( isset( $attributes['name'] ) ) {
+ $name = $attributes['name'];
+ unset( $attributes['name'] );
+ }
+
+ if ( $name !== '' && isset( $attributes['multifield'] ) ) {
+ $name .= "[]";
+ unset( $attributes['multifield'] );
+ }
+
+ if ( isset( $attributes['required'] ) ) {
+ $required = (bool) $attributes['required'];
+ unset( $attributes['required'] );
+ }
+
+ if ( isset( $attributes['placeholder'] ) ) {
+ $placeholder = $attributes['placeholder'];
+ }
+
+ if ( isset( $attributes['tooltip'] ) ) {
+ $tooltip = $attributes['tooltip'];
+ unset( $attributes['tooltip'] );
+ }
+
+ if ( isset( $attributes['type'] ) ) {
+ $type = $attributes['type'];
+ }
+
+ if ( isset( $attributes['value'] ) ) {
+ $value = $attributes['value'];
+ }
+
+ $attr = [
+ 'class' => $class,
+ 'name' => $name,
+ 'type' => $type,
+ 'value' => $value,
+ 'placeholder' => $placeholder,
+ 'data-required' => $required
+ ] + $attributes;
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-input-field',
+ ] + ( $style !== '' ? [ 'style' => $style ] : [] ),
+ Html::rawElement(
+ 'input',
+ $attr
+ ) . $tooltip
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsBuilder.php
new file mode 100644
index 00000000..35ea47d6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsBuilder.php
@@ -0,0 +1,358 @@
+<?php
+
+namespace SMW\MediaWiki\Search\Form;
+
+use Html;
+use RuntimeException;
+use SMW\Message;
+use Title;
+use WebRequest;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FormsBuilder {
+
+ /**
+ * @var WebRequest
+ */
+ private $request;
+
+ /**
+ * @var FormsFactory
+ */
+ private $formsFactory;
+
+ /**
+ * @var OpenForm
+ */
+ private $openForm;
+
+ /**
+ * @var CustomForm
+ */
+ private $customForm;
+
+ /**
+ * @var string
+ */
+ private $defaultForm = '';
+
+ /**
+ * @var []
+ */
+ private $formList = [];
+
+ /**
+ * @var []
+ */
+ private $preselectNsList = [];
+
+ /**
+ * @var []
+ */
+ private $hiddenNsList = [];
+
+ /**
+ * @var []
+ */
+ private $parameters = [];
+
+ /**
+ * @var []
+ */
+ private $termPrefixes = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest $request
+ * @param FormsFactory $formsFactory
+ */
+ public function __construct( WebRequest $request, FormsFactory $formsFactory ) {
+ $this->request = $request;
+ $this->formsFactory = $formsFactory;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getParameters() {
+ return $this->parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $form
+ *
+ * @return string
+ */
+ public static function toLowerCase( $key ) {
+ return strtolower( str_replace( [ ' ' ], [ '' ], $key ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getTermPrefixes() {
+ return $this->termPrefixes;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getHiddenNsList() {
+ return $this->hiddenNsList;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getPreselectNsList() {
+
+ $activeForm = $this->request->getVal( 'smw-form', $this->defaultForm );
+
+ if ( $activeForm === null ) {
+ return [];
+ }
+
+ $activeForm = self::toLowerCase( $activeForm );
+
+ if ( isset( $this->preselectNsList[$activeForm] )) {
+ return $this->preselectNsList[$activeForm];
+ }
+
+ return [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function buildFormList() {
+
+ $list = [];
+ $name = '';
+ $value = '';
+
+ foreach ( $this->formList as $k => $options ) {
+
+ if ( $k === '' ) {
+ continue;
+ }
+
+ if ( $options['selected'] ) {
+ $name = $options['name'];
+ $value = $k;
+ }
+
+ $list[] = [ 'id' => $k, 'name' => $options['name'], 'desc' => $options['name'] ];
+ }
+
+ return Html::rawElement(
+ 'button',
+ [
+ 'type' => 'button',
+ 'id' => 'smw-search-forms',
+ 'class' => 'smw-selectmenu-button is-disabled',
+ 'title' => Message::get( 'smw-search-profile-extended-section-form', Message::TEXT, Message::USER_LANGUAGE ),
+ 'name' => 'smw-form',
+ 'value' => $value,
+ 'data-list' => json_encode( $list ),
+ 'data-nslist' => json_encode( $this->preselectNsList )
+ ],
+ $name === '' ? 'Form' : $name
+ ) . Html::rawElement(
+ 'input',
+ [
+ 'type' => 'hidden',
+ 'name' => 'smw-form',
+ 'value' => $value,
+ ]
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $data
+ *
+ * @return string
+ */
+ public function buildForm( array $data ) {
+
+ if ( !isset( $data['forms'] ) ) {
+ throw new RuntimeException( "Missing forms definition" );
+ }
+
+ if ( isset( $data['default_form'] ) ) {
+ $this->defaultForm = self::toLowerCase( $data['default_form'] );
+ }
+
+ if ( isset( $data['term_parser']['prefix'] ) ) {
+ $this->termPrefixes = $data['term_parser']['prefix'];
+ }
+
+ $activeForm = $this->request->getVal( 'smw-form', $this->defaultForm );
+
+ $divider = "<div class='divider' style='display:none;'></div>";
+
+ if ( $activeForm !== null && $activeForm !== '' ) {
+ $divider = "<div class='divider'></div>";
+ }
+
+ $this->formList = [];
+ $this->preselectNsList = [];
+ $this->parameters = [];
+
+ if ( $activeForm === null || $activeForm === '' ) {
+ $forms = [ '' => '' ] + $data['forms'];
+ } else {
+ $forms = $data['forms'];
+ }
+
+ $formDefinitions = [];
+
+ if ( $this->openForm === null ) {
+ $this->openForm = $this->formsFactory->newOpenForm( $this->request );
+ }
+
+ if ( $this->customForm === null ) {
+ $this->customForm = $this->formsFactory->newCustomForm( $this->request );
+ }
+
+ ksort( $forms );
+
+ foreach ( $forms as $name => $definition ) {
+ $formDefinitions[] = $this->form_fields( $data, $activeForm, $name, $definition );
+ }
+
+ if ( isset( $data['namespaces']['preselect'] ) && is_array( $data['namespaces']['preselect'] ) ) {
+ $this->preselect_namespaces( $data['namespaces']['preselect'] );
+ }
+
+ if ( isset( $data['namespaces']['hidden'] ) && is_array( ) ) {
+ $this->hidden_namespaces( $data['namespaces']['hidden'] );
+ }
+
+ if ( isset( $data['namespaces']['hide'] ) && is_array( $data['namespaces']['hide'] ) ) {
+ $this->hidden_namespaces( $data['namespaces']['hide'] );
+ }
+
+ return $divider . Html::rawElement(
+ 'div',
+ [
+ 'id' => 'smw-form-definitions',
+ 'class' => 'is-disabled'
+ ],
+ implode( '', $formDefinitions )
+ );
+ }
+
+ private function form_fields( $data, $activeForm, $name, $definition ) {
+
+ // Short form, URL query conform
+ $s = self::toLowerCase( $name );
+ $this->formList[$s] = [ 'name' => $name, 'selected' => $activeForm === $s ];
+
+ if ( !is_array( $definition ) ) {
+ return;
+ }
+
+ $description = '';
+ $isActiveForm = $s === $activeForm;
+
+ if ( isset( $data['descriptions'] ) ) {
+ $description = $this->findDescription( $data['descriptions'], $name, $isActiveForm );
+ }
+
+ if ( $s === 'open' ) {
+ $this->openForm->isActiveForm( $isActiveForm );
+ $fields = $this->openForm->makeFields();
+ $this->parameters = array_merge( $this->parameters, $this->openForm->getParameters() );
+ } else {
+ $this->customForm->isActiveForm( $isActiveForm );
+ $fields = $this->customForm->makeFields( $definition );
+ $this->parameters = array_merge( $this->parameters, $this->customForm->getParameters() );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => "smw-form-{$s}",
+ 'class' => 'smw-fields'
+ ],
+ $description . $fields
+ );
+ }
+
+ private function preselect_namespaces( $preselect ) {
+ foreach ( $preselect as $k => $values ) {
+ $k = self::toLowerCase( $k );
+ $this->preselectNsList[$k] = [];
+
+ foreach ( $values as $ns ) {
+ if ( is_string( $ns ) && defined( $ns ) ) {
+ $this->preselectNsList[$k][] = constant( $ns );
+ }
+
+ if ( is_numeric( $ns ) ) {
+ $this->preselectNsList[$k][] = $ns;
+ }
+ }
+ }
+ }
+
+ private function hidden_namespaces( $hidden ) {
+ foreach ( $hidden as $ns ) {
+ if ( is_string( $ns ) && defined( $ns ) ) {
+ $this->hiddenNsList[] = constant( $ns );
+ }
+
+ if ( is_numeric( $ns ) ) {
+ $this->hiddenNsList[] = $ns;
+ }
+ }
+ }
+
+ private function findDescription( $descriptions, $name, $isActiveForm ) {
+
+ if ( !isset( $descriptions[$name] ) ) {
+ return '';
+ }
+
+ $display = $isActiveForm ? 'inline-block' : 'none';
+
+ // Simple text, or is it message-key?
+ if ( Message::exists( $descriptions[$name] ) ) {
+ $description = Message::get( $descriptions[$name], Message::PARSE, Message::USER_LANGUAGE );
+ } else{
+ $description = $descriptions[$name];
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-form-description',
+ 'style' => "display:$display;"
+ ],
+ $description
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsFactory.php
new file mode 100644
index 00000000..f9c50f08
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsFactory.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace SMW\MediaWiki\Search\Form;
+
+use WebRequest;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FormsFactory {
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest $request
+ *
+ * @return OpenForm
+ */
+ public function newOpenForm( WebRequest $request ) {
+ return new OpenForm( $request );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest $request
+ *
+ * @return CustomForm
+ */
+ public function newCustomForm( WebRequest $request ) {
+ return new CustomForm( $request );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest $request
+ *
+ * @return SortForm
+ */
+ public function newSortForm( WebRequest $request ) {
+ return new SortForm( $request );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return NamespaceForm
+ */
+ public function newNamespaceForm() {
+ return new NamespaceForm();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsFinder.php
new file mode 100644
index 00000000..6eda37e5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/FormsFinder.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace SMW\MediaWiki\Search\Form;
+
+use SMW\DIProperty;
+use SMW\MediaWiki\Search\SearchProfileForm;
+use SMW\RequestOptions;
+use SMW\Store;
+use SMWDIBlob as DIBlob;
+use Title;
+use WikiPage;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FormsFinder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getFormDefinitions() {
+
+ $data = [];
+ $requestOptions = new RequestOptions();
+ $requestOptions->setOption( 'DISTINCT', false );
+
+ $subjects = $this->store->getPropertySubjects(
+ new DIProperty( '_SCHEMA_TYPE' ),
+ new DIBlob( SearchProfileForm::SCHEMA_TYPE ),
+ $requestOptions
+ );
+
+ foreach ( $subjects as $subject ) {
+
+ if ( ( $nativeData = $this->getNativeData( $subject->getTitle() ) ) === '' ) {
+ continue;
+ }
+
+ $d = json_decode( $nativeData, true );
+
+ if ( json_last_error() !== JSON_ERROR_NONE ) {
+ continue;
+ }
+
+ $data = array_merge_recursive( $data, $d );
+ }
+
+ return $data;
+ }
+
+ protected function getNativeData( $title ) {
+
+ if ( $title === null ) {
+ return '';
+ }
+
+ $content = WikiPage::factory( $title )->getContent();
+
+ if ( $content === null ) {
+ return '';
+ }
+
+ return $content->getNativeData();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/NamespaceForm.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/NamespaceForm.php
new file mode 100644
index 00000000..571dda8a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/NamespaceForm.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace SMW\MediaWiki\Search\Form;
+
+use Html;
+use MWNamespace;
+use SMW\Message;
+use SpecialSearch;
+use Xml;
+
+/**
+ * @note Copied from SearchFormWidget::powerSearchBox, #3126 contains the reason
+ * why we need to copy the code!
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class NamespaceForm {
+
+ /**
+ * @var []
+ */
+ private $activeNamespaces = [];
+
+ /**
+ * @var []
+ */
+ private $hiddenNamespaces = [];
+
+ /**
+ * @var []
+ */
+ private $searchableNamespaces = [];
+
+ /**
+ * @var null|string
+ */
+ private $token;
+
+ /**
+ * @var null|string
+ */
+ private $hideList = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param array $activeNamespaces
+ */
+ public function setActiveNamespaces( array $activeNamespaces ) {
+ $this->activeNamespaces = $activeNamespaces;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $hideList
+ */
+ public function setHideList( $hideList ) {
+ $this->hideList = (bool)$hideList;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $hiddenNamespaces
+ */
+ public function setHiddenNamespaces( array $hiddenNamespaces ) {
+ $this->hiddenNamespaces = $hiddenNamespaces;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $searchableNamespaces
+ */
+ public function setSearchableNamespaces( array $searchableNamespaces ) {
+ $this->searchableNamespaces = $searchableNamespaces;
+ }
+
+ /**
+ * @see SearchFormWidget
+ *
+ * @since 3.0
+ *
+ * @param SpecialSearch $specialSearch
+ */
+ public function checkNamespaceEditToken( SpecialSearch $specialSearch ) {
+
+ $user = $specialSearch->getUser();
+
+ if ( !$user->isLoggedIn() ) {
+ return;
+ }
+
+ $this->token = $user->getEditToken( 'searchnamespace', $specialSearch->getRequest() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function makeFields() {
+ global $wgContLang;
+
+ $divider = "<div class='divider'></div>";
+ $rows = [];
+ $tableRows = [];
+
+ $hiddenNamespaces = array_flip( $this->hiddenNamespaces );
+
+ foreach ( $this->searchableNamespaces as $namespace => $name ) {
+ $subject = MWNamespace::getSubject( $namespace );
+
+ if ( MWNamespace::isTalk( $namespace ) ) {
+ // continue;
+ }
+
+ if ( isset( $hiddenNamespaces[$namespace] ) ) {
+ continue;
+ }
+
+ if ( !isset( $rows[$subject] ) ) {
+ $rows[$subject] = "";
+ }
+
+ $name = $wgContLang->getConverter()->convertNamespace( $namespace );
+
+ if ( $name === '' ) {
+ $name = Message::get( 'blanknamespace', Message::TEXT, Message::USER_LANGUAGE );
+ }
+
+ $isChecked = in_array( $namespace, $this->activeNamespaces );
+
+ $rows[$subject] .= Html::rawElement(
+ 'td',
+ [],
+ Xml::checkLabel( $name, "ns{$namespace}", "mw-search-ns{$namespace}", $isChecked )
+ );
+ }
+
+ // Lays out namespaces in multiple floating two-column tables so they'll
+ // be arranged nicely while still accomodating diferent screen widths
+ foreach ( $rows as $row ) {
+ $tableRows[] = "<tr>{$row}</tr>";
+ }
+
+ $namespaceTables = [];
+ $display = $this->hideList ? 'none' : 'block';
+
+ foreach ( array_chunk( $tableRows, 4 ) as $chunk ) {
+ $namespaceTables[] = implode( '', $chunk );
+ }
+
+ $showSections = [
+ 'namespaceTables' => "<table>" . implode( '</table><table>', $namespaceTables ) . '</table>',
+ ];
+
+ // Stuff to feed SpecialSearch::saveNamespaces()
+ $remember = '';
+
+ if ( $this->token ) {
+ $remember = $divider . Xml::checkLabel(
+ Message::get( 'powersearch-remember', Message::TEXT, Message::USER_LANGUAGE ),
+ 'nsRemember',
+ 'mw-search-powersearch-remember',
+ false,
+ // The token goes here rather than in a hidden field so it
+ // is only sent when necessary (not every form submission)
+ [ 'value' => $this->token ]
+ );
+ }
+
+ return "<fieldset id='mw-searchoptions'>" .
+ "<legend>" . Message::get( 'powersearch-legend', Message::ESCAPED, Message::USER_LANGUAGE ) . '</legend>' .
+ "<h4>" . Message::get( 'powersearch-ns', Message::PARSE, Message::USER_LANGUAGE ) . '</h4>' .
+ // populated by js if available
+ "<div id='smw-search-togglensview'></div>" .
+ "<div id='mw-search-togglebox'></div>" .
+ "<div id='mw-search-ns' style='display:$display'>" . $divider .
+ implode(
+ $divider,
+ $showSections
+ ) .
+ $remember . "</div>" .
+ "</fieldset>";
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/OpenForm.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/OpenForm.php
new file mode 100644
index 00000000..10a14f2b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/OpenForm.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace SMW\MediaWiki\Search\Form;
+
+use Html;
+use Title;
+use WebRequest;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class OpenForm {
+
+ /**
+ * @var WebRequest
+ */
+ private $request;
+
+ /**
+ * @var Field
+ */
+ private $field;
+
+ /**
+ * @var boolean
+ */
+ private $isActiveForm = false;
+
+ /**
+ * @var []
+ */
+ private $parameters = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest $request
+ */
+ public function __construct( WebRequest $request ) {
+ $this->request = $request;
+ $this->field = new Field();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getParameters() {
+ return $this->parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isActiveForm
+ */
+ public function isActiveForm( $isActiveForm ) {
+ $this->isActiveForm = (bool)$isActiveForm;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $definition
+ */
+ public function makeFields( $definition = [] ) {
+
+ $this->parameters = [];
+
+ $group = '';
+ $properties = [];
+ $values = [];
+ $op = [];
+
+ if ( $this->isActiveForm ) {
+ $properties = $this->request->getArray( 'property', [] );
+ $values = $this->request->getArray( 'pvalue', [] );
+ $op = $this->request->getArray( 'op', [] );
+ }
+
+ $this->parameters = [
+ 'property' => [],
+ 'pvalue' => [],
+ 'op' => []
+ ];
+
+ foreach ( $properties as $i => $property ) {
+
+ if ( $property === '' ) {
+ continue;
+ }
+
+ $this->parameters['property'][] = $property;
+ $this->parameters['pvalue'][] = $values[$i];
+ $this->parameters['op'][] = $op[$i];
+
+ $group .= $this->makeFieldGroup( $property, $values[$i], $op[$i] );
+ }
+
+ // At least one empty group
+ $group .= $this->makeFieldGroup( '', '', '' );
+
+ return $group;
+ }
+
+ private function makeFieldGroup( $property, $value, $op ) {
+
+ $display = $this->isActiveForm ? 'inline-block' : 'none';
+
+ $attributes = [
+ 'multifield' => true,
+ 'display' => $display,
+ 'name' => 'property',
+ 'value' => $property,
+ 'data-autocomplete-indicator' => true,
+ 'placeholder' => 'Property ...',
+ 'class' => 'smw-property-input autocomplete-arrow'
+ ];
+
+ $prop = $this->field->create( 'input', $attributes );
+
+ $attributes = [
+ 'multifield' => true,
+ 'display' => $display,
+ 'name' => 'pvalue',
+ 'value' => $value,
+ 'data-autocomplete-indicator' => true,
+ 'data-property' => $property,
+ 'placeholder' => 'Value ...',
+ 'class' => 'smw-propertyvalue-input autocomplete-arrow'
+ ];
+
+ if ( $value === '' ) {
+ $attributes['class'] .= ' is-disabled';
+ }
+
+ $pvalue = $this->field->create( 'input', $attributes );
+ $disabled = $property === '' && $value === '' ? 'disabled' : '';
+
+ $list = [
+ '' => '',
+ 'OR' => 'OR',
+ ' ' => [ '————', 'disabled' ],
+ 'del' => [ 'del', $disabled ]
+ ];
+
+ $attributes = [
+ 'list' => $list,
+ 'selected' => $op,
+ 'multifield' => true,
+ 'name' => 'op',
+ 'display' => $display,
+ 'class' => 'smw-select-field'
+ ];
+
+ $select = $this->field->create( 'select', $attributes );
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-input-group'
+ ],
+ $prop . $pvalue . $select
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/SortForm.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/SortForm.php
new file mode 100644
index 00000000..820cc1e6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Form/SortForm.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace SMW\MediaWiki\Search\Form;
+
+use Html;
+use WebRequest;
+use SMW\Message;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SortForm {
+
+ /**
+ * @var WebRequest
+ */
+ private $request;
+
+ /**
+ * @var Field
+ */
+ private $field;
+
+ /**
+ * @var []
+ */
+ private $parameters = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest $request
+ */
+ public function __construct( WebRequest $request ) {
+ $this->request = $request;
+ $this->field = new Field();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getParameters() {
+ return $this->parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $features
+ */
+ public function makeFields( $features = [] ) {
+
+ $default = isset( $features['best'] ) && $features['best'] ? 'best' : 'title';
+ $sort = $this->request->getVal( 'sort', $default );
+
+ $this->parameters['sort'] = $sort;
+
+ $list = [];
+ $name = '';
+
+ foreach ( $this->sortList( $features ) as $key => $value ) {
+
+ if ( $key === $sort ) {
+ $name = $value;
+ }
+
+ $list[] = [ 'id' => $key, 'name' => $value, 'desc' => $value ];
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-search-sort'
+ ],
+ Html::rawElement(
+ 'button',
+ [
+ 'type' => 'button',
+ 'id' => 'smw-search-sort',
+ 'class' => 'smw-selectmenu-button is-disabled',
+ 'name' => 'sort',
+ 'value' => $sort,
+ 'data-list' => json_encode( $list ),
+ 'title' => Message::get( 'smw-search-profile-extended-section-sort', Message::TEXT, Message::USER_LANGUAGE ),
+ ],
+ $sort === '' ? 'Sort' : $name
+ ) . Html::rawElement(
+ 'input',
+ [
+ 'type' => 'hidden',
+ 'name' => 'sort',
+ 'value' => $sort,
+ ]
+ )
+ );
+ }
+
+ private function sortList( $features ) {
+
+ $list = [];
+
+ if ( isset( $features['best'] ) && $features['best'] ) {
+ $list['best'] = Message::get( 'smw-search-profile-sort-best', Message::TEXT, Message::USER_LANGUAGE );
+
+ $list += [
+ 'recent' => Message::get( 'smw-search-profile-sort-recent', Message::TEXT, Message::USER_LANGUAGE ),
+ 'title' => Message::get( 'smw-search-profile-sort-title', Message::TEXT, Message::USER_LANGUAGE )
+ ];
+
+ } else{
+ $list = [
+ 'title' => Message::get( 'smw-search-profile-sort-title', Message::TEXT, Message::USER_LANGUAGE ),
+ 'recent' => Message::get( 'smw-search-profile-sort-recent', Message::TEXT, Message::USER_LANGUAGE )
+ ];
+ }
+
+ return $list;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/QueryBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/QueryBuilder.php
new file mode 100644
index 00000000..34b2a238
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/QueryBuilder.php
@@ -0,0 +1,290 @@
+<?php
+
+namespace SMW\MediaWiki\Search;
+
+use SMW\MediaWiki\Search\Form\FormsBuilder;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\NamespaceDescription;
+use SMW\Query\Parser\TermParser;
+use SMW\Store;
+use SMWQuery as Query;
+use SMWQueryProcessor as QueryProcessor;
+use Title;
+use WebRequest;
+use WikiPage;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class QueryBuilder {
+
+ /**
+ * @var WebRequest
+ */
+ private $request;
+
+ /**
+ * @var array
+ */
+ private $data = [];
+
+ /**
+ * @var array
+ */
+ private $queryCache = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest|null $request
+ * @param array|null $data
+ */
+ public function __construct( WebRequest $request = null, array $data = [] ) {
+ $this->request = $request;
+ $this->data = $data;
+
+ if ( $this->request === null ) {
+ $this->request = $GLOBALS['wgRequest'];
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $term
+ *
+ * @return Query|null
+ */
+ public function getQuery( $term ) {
+
+ if ( !is_string( $term ) || trim( $term ) === '' ) {
+ return null;
+ }
+
+ if ( !array_key_exists( $term, $this->queryCache ) ) {
+
+ $params = QueryProcessor::getProcessedParams( [] );
+ $query = QueryProcessor::createQuery( $term, $params );
+
+ $description = $query->getDescription();
+
+ if ( $description === null || is_a( $description, 'SMWThingDescription' ) ) {
+ $query = null;
+ }
+
+ $this->queryCache[$term] = $query;
+ }
+
+ return $this->queryCache[$term];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ * @param array $searchableNamespaces
+ */
+ public function addNamespaceCondition( Query $query = null, $searchableNamespaces = [] ) {
+
+ if ( $query === null ) {
+ return;
+ }
+
+ $namespaces = [];
+
+ foreach ( $searchableNamespaces as $ns => $name ) {
+ if ( $this->request->getCheck( 'ns' . $ns ) ) {
+ $namespaces[] = $ns;
+ }
+ }
+
+ $namespacesDisjunction = new Disjunction(
+ array_map( function ( $ns ) {
+ return new NamespaceDescription( $ns );
+ }, $namespaces )
+ );
+
+ $description = new Conjunction( [ $query->getDescription(), $namespacesDisjunction ] );
+ $query->setDescription( $description );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ */
+ public function addSort( Query $query = null ) {
+
+ if ( $query === null ) {
+ return;
+ }
+
+ // @see SortForm
+ $sort = $this->request->getVal( 'sort' );
+
+ if ( $sort === 'recent' ) {
+ $query->setSortKeys( [ '_MDAT' => 'desc' ] );
+ } elseif ( $sort === 'title' ) {
+ $query->setSortKeys( [ '' => 'asc' ] );
+ } else {
+ // Sort by score/relevance if it is supported otherwise the default
+ // by title sort will be used instead.
+ $query->setOption( Query::SCORE_SORT, 'desc' );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getQueryString( Store $store, $term ) {
+
+ // Special invisible char which is set by the JS component to allow to
+ // push a forms submit through the SearchEngine without an actual "search
+ // term" to avoid being blocked on an empty request which only contains
+ // structured searches.
+ $term = rtrim( $term, " " );
+ $prefix_map = [];
+
+ if ( $this->data === [] ) {
+ $data = SearchProfileForm::getFormDefinitions( $store );
+ } else {
+ $data = $this->data;
+ }
+
+ if ( isset( $data['term_parser']['prefix'] ) && $data['term_parser']['prefix'] ) {
+ $prefix_map = (array)$data['term_parser']['prefix'];
+ }
+
+ $termParser = new TermParser( $prefix_map );
+ $term = $termParser->parse( $term );
+
+ $form = $this->request->getVal( 'smw-form' );
+
+ if ( ( $data = $this->fetchFieldValues( $form, $data ) ) === [] && trim( $term ) ) {
+ return $term;
+ }
+
+ $queryString = '';
+ $lastOr = '';
+
+ foreach ( $data as $key => $values ) {
+
+ if ( !is_array( $values ) ) {
+ continue;
+ }
+
+ foreach ( $values as $k => $value ) {
+
+ if ( !isset( $value[0] ) || $value[0] === '' ) {
+ continue;
+ }
+
+ $val = $value[0];
+ $op = strtolower( $value[1] ) === 'or' ? ' OR ' : '';
+
+ $queryString .= "[[$key::$val]]$op";
+ }
+ }
+
+ // Remove last OR to ensure <q></q> has no open OR expression
+ if ( substr( $queryString, -3 ) === 'OR ' ) {
+ $lastOr = $term !== '' ? 'OR' : '';
+ $queryString = substr( $queryString, 0, -3 );
+ }
+
+ if ( $queryString === '' ) {
+ return $term;
+ }
+
+ return "<q>$queryString</q> $lastOr $term";
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $form
+ * @param array $data
+ *
+ * @return []
+ */
+ public function fetchFieldValues( $form, array $data ) {
+
+ $fieldValues = [];
+
+ if ( !isset( $data['forms'] ) ) {
+ return [];
+ }
+
+ if ( $form === 'open' ) {
+ $properties = $this->request->getArray( 'property' );
+ $pvalues = $this->request->getArray( 'pvalue' );
+ $op = $this->request->getArray( 'op' );
+
+ foreach ( $properties as $i => $property ) {
+
+ if ( !isset( $fieldValues[$property] ) ) {
+ $fieldValues[$property] = [];
+ }
+
+ $fieldValues[$property][] = [ $pvalues[$i], $op[$i] ];
+ }
+
+ return $fieldValues;
+ }
+
+ $fieldsCounter = [];
+
+ foreach ( $data['forms'] as $key => $value ) {
+
+ // @see FormsBuilder
+ $k = FormsBuilder::toLowerCase( $key );
+
+ foreach ( $value as $property ) {
+
+ if ( is_array( $property ) ) {
+ foreach ( $property as $p => $options ) {
+ $property = $p;
+ }
+ }
+
+ $name = FormsBuilder::toLowerCase( $property );
+
+ if ( !isset( $fieldsCounter[$name] ) ) {
+ $fieldsCounter[$name] = 0;
+ } else {
+ $fieldsCounter[$name]++;
+ }
+
+ if ( $form !== $k ) {
+ continue;
+ }
+
+ $vals = $this->request->getArray(
+ FormsBuilder::toLowerCase( $property )
+ );
+
+ if ( !isset( $vals[$fieldsCounter[$name]] ) ) {
+ continue;
+ }
+
+ $val = $vals[$fieldsCounter[$name]];
+
+ // Conditions from custom forms are conjunctive
+ $fieldValues[$property][] = [ $val, 'and' ];
+ }
+ }
+
+ return $fieldValues;
+ }
+
+
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/README.md b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/README.md
new file mode 100644
index 00000000..93dadca8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/README.md
@@ -0,0 +1,161 @@
+# SMWSearch
+
+[SMWSearch](https://www.semantic-mediawiki.org/wiki/Help:SMWSearch) is the `SearchEngine` interface to provide classes and functions to integrate Semantic MediaWiki with `Special:Search`.
+
+It adds support for using #ask queries in the search input field and provides an extended search profile where user defined forms can empower users to find and match entities using property and value input fields.
+
+## Extended search profile
+
+![image](https://user-images.githubusercontent.com/1245473/43684698-7748fd76-9894-11e8-971f-3125892dc9ed.png)
+
+### Defining forms and form fields
+
+Form definitions are expected be created in the [rule namespace](https://www.semantic-mediawiki.org/wiki/Help:Rule) with the [`SEARCH_FORM_DEFINITION_RULE`](https://www.semantic-mediawiki.org/wiki/Help:Rule/Type/SEARCH_FORM_DEFINITION_RULE) as mandatory type to declare field constraints and validation checkpoints (see the `$smwgRuleTypes` setting) required by the type.
+
+Definitions are structured using the JSON format and in the following example represent:
+
+- `"type"` requires `SEARCH_FORM_DEFINITION_RULE`
+- `"forms"` defines a collection of forms
+ - `"Books and journals"` as title of a form
+ - `"Has title"` is a simple input field without any constraints
+ - `"Publication type"` is a input field with additional attributes
+
+<pre>
+{
+ "type": "SEARCH_FORM_DEFINITION_RULE",
+ "forms": {
+ "Books and journals": [
+ "Has title",
+ "Has author",
+ "Has year",
+ {
+ "Publication type": {
+ "autocomplete": true,
+ "tooltip": "Some context to be shown ...",
+ "required": true
+ }
+ },
+ {
+ "Publisher": {
+ "autocomplete": true
+ "tooltip": "message-can-be-a-msg-key"
+ }
+ }
+ ],
+ "Media and files": [ ]
+ }
+}
+</pre>
+
+| Attributes | Values | Description |
+|--------------|-----------------|-----------------------------------|
+| autocomplete | true, false | whether the field should add an autocomplete function or not |
+| tooltip | text or msg key | shows a tooltip with either a text or retrieves information from a message key |
+| placeholder | text | shown instead of the property name |
+| required | true, false | whether the field input is required before submitting or not |
+| type | HTML5 | preselect a specific type field |
+
+`default_form` can define a default form that is displayed when no other form was preselected.
+
+<pre>
+{
+ "type": "SEARCH_FORM_DEFINITION_RULE",
+ "default_form": "Books and journals",
+ ...
+}
+</pre>
+
+### Term parser
+
+The `term_parser` prefix can be used to shorten the input cycle and summarize frequent properties so that a user can write:
+ - `(in:foobar || phrase:foo bar) lang:fr` instead of
+ - `<q>[[in:foobar]] || [[phrase:foo bar]]</q><q>[[Language code::fr]] OR [[Document language::fr]] OR [[File attachment.Content language::fr]] OR [[Has interlanguage link.Page content language::fr]]</q>`
+
+<pre>
+{
+ "type": "SEARCH_FORM_DEFINITION_RULE",
+ "term_parser": {
+ "prefix": {
+ "lang": [
+ "Language code",
+ "Document language",
+ "File attachment.Content language",
+ "Has interlanguage link.Page content language"
+ ]
+ }
+ }
+}
+</pre>
+
+Prefixes are only applicable (and usable as means the shorten the search term) from within the extended search form.
+
+### Namespaces
+
+- ` "namespaces"`
+ - `default_hide` hides the namespace box by default on the extended profile form
+ - `"hide"` identify namespaces that should be hidden from appearing in any SMW related form
+ - `"preselect"` assign a pre-selection of namespaces to a specific form
+ - `"Books and journals"` specific form the pre-selection should be enacted
+
+<pre>
+{
+ "type": "SEARCH_FORM_DEFINITION_RULE",
+ "namespaces": {
+ "default_hide": true,
+ "hide": [
+ "NS_PROJECT",
+ "NS_PROJECT_TALK"
+ ],
+ "preselect": {
+ "Books and journals": [
+ "NS_CUSTOM_BOOKS",
+ "NS_FILE"
+ ],
+ "Media and files": [
+ "NS_FILE"
+ ]
+ }
+ }
+}
+</pre>
+
+
+### Descriptions
+
+Describes a form and is shown at the top of the form fields to inform users about the intent of
+the form.
+
+<pre>
+{
+ "descriptions": {
+ "Books and journals": "Short description to be shown on top of a selected form"
+ }
+}
+</pre>
+
+## Technical notes
+
+### Search engine
+
+Classes that provide an interface to support MW's `SearchEngine` by transforming a search term into a SMW equivalent expression of query elements.
+
+<pre>
+SMW\MediaWiki\Search
+┃
+┠━ QueryBuilder
+┠━ Search # Implements the `SearchEngine`
+┠━ SearchResult # Individual result representation
+┕━ SearchResultSet # Contains a set of results return from the `QueryEngine`
+</pre>
+
+### Search profile and form builder
+
+Classes that provide an additional search form to support structured searches in `Special:Search` with the help of the [`SpecialSearchProfileForm`](https://www.mediawiki.org/wiki/Manual:Hooks/SpecialSearchProfileForm) hook.
+
+<pre>
+SMW\MediaWiki\Search
+┃ ┃
+┃ ┕━ Form # Classes to generate a HTML from a JSON definition
+┃
+┕━ SearchProfileForm # Interface to the `SpecialSearchProfileForm` hook
+</pre>
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Search.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Search.php
new file mode 100644
index 00000000..5e18e7eb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/Search.php
@@ -0,0 +1,474 @@
+<?php
+
+namespace SMW\MediaWiki\Search;
+
+use Content;
+use DatabaseBase;
+use RuntimeException;
+use SearchEngine;
+use SMW\ApplicationFactory;
+use SMWQuery;
+use SMWQueryResult as QueryResult;
+use Title;
+
+/**
+ * Search engine that will try to find wiki pages by interpreting the search
+ * term as an SMW query.
+ *
+ * If successful, the pages according to the query will be returned.
+ * If not it falls back to the default search engine.
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Stephan Gambke
+ */
+class Search extends SearchEngine {
+
+ private $fallbackSearch = null;
+
+ private $database = null;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var QueryBuilder
+ */
+ private $queryBuilder;
+
+ /**
+ * @var string
+ */
+ private $queryString = '';
+
+ /**
+ * @var InfoLink
+ */
+ private $queryLink;
+
+ /**
+ * @see SearchEngine::getValidSorts
+ *
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getValidSorts() {
+ return [
+
+ // SemanticMediaWiki supported
+ 'title', 'recent', 'best',
+
+ // MediaWiki default
+ 'relevance'
+ ];
+ }
+
+ /**
+ * @param null|SearchEngine $fallbackSearch
+ */
+ public function setFallbackSearchEngine( SearchEngine $fallbackSearch = null ) {
+ $this->fallbackSearch = $fallbackSearch;
+ }
+
+ /**
+ * @param $type
+ */
+ private function assertValidFallbackSearchEngineType( $type ) {
+
+ if ( !class_exists( $type ) ) {
+ throw new RuntimeException( "$type does not exist." );
+ }
+
+ if ( $type === 'SMWSearch' ) {
+ throw new RuntimeException( 'SMWSearch is not a valid fallback search engine type.' );
+ }
+
+ if ( $type !== 'SearchEngine' && !is_subclass_of( $type, 'SearchEngine' ) ) {
+ throw new RuntimeException( "$type is not a valid fallback search engine type." );
+ }
+ }
+
+ /**
+ * @return SearchEngine
+ */
+ public function getFallbackSearchEngine() {
+
+ if ( $this->fallbackSearch === null ) {
+
+ $type = ApplicationFactory::getInstance()->getSettings()->get( 'smwgFallbackSearchType' );
+
+ $dbr = $this->getDB();
+
+ if ( $type === null ) {
+ $type = ApplicationFactory::getInstance()->create( 'DefaultSearchEngineTypeForDB', $dbr );
+ }
+
+ $this->assertValidFallbackSearchEngineType( $type );
+
+ $this->fallbackSearch = new $type( $dbr );
+ }
+
+ return $this->fallbackSearch;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getQueryString() {
+ return $this->queryString;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getQueryLink() {
+ return $this->queryLink;
+ }
+
+ /**
+ * @param DatabaseBase $connection
+ */
+ public function setDB( DatabaseBase $connection ) {
+ $this->database = $connection;
+ $this->fallbackSearch = null;
+ }
+
+ /**
+ * @return \IDatabase
+ */
+ public function getDB() {
+
+ if ( $this->database === null ) {
+ $this->database = ApplicationFactory::getInstance()->getLoadBalancer()->getConnection( defined( 'DB_REPLICA' ) ? DB_REPLICA : DB_SLAVE );
+ }
+
+ return $this->database;
+ }
+
+ /**
+ * @param String $term
+ *
+ * @return SMWQuery | null
+ */
+ private function getSearchQuery( $term ) {
+
+ if ( $this->queryBuilder === null ) {
+ $this->queryBuilder = new QueryBuilder();
+ }
+
+ $this->queryString = $this->queryBuilder->getQueryString(
+ ApplicationFactory::getInstance()->getStore(),
+ $term
+ );
+
+ $query = $this->queryBuilder->getQuery(
+ $this->queryString
+ );
+
+ $this->queryBuilder->addSort( $query );
+
+ $this->queryBuilder->addNamespaceCondition(
+ $query,
+ $this->searchableNamespaces()
+ );
+
+ return $query;
+ }
+
+ private function searchFallbackSearchEngine( $term, $fulltext ) {
+
+ $f = $this->getFallbackSearchEngine();
+ $f->prefix = $this->prefix;
+ $f->namespaces = $this->namespaces;
+
+ $term = $f->transformSearchTerm( $term );
+ $term = $f->replacePrefixes( $term );
+
+ return $fulltext ? $f->searchText( $term ) : $f->searchTitle( $term );
+ }
+
+ /**
+ * Perform a title-only search query and return a result set.
+ *
+ * This method will try to find wiki pages by interpreting the search term as an SMW query.
+ *
+ * If successful, the pages according to the query will be returned.
+ * If not, it falls back to the default search engine.
+ *
+ * @param string $term Raw search term
+ *
+ * @return SearchResultSet|null
+ */
+ public function searchTitle( $term ) {
+
+ if ( $this->getSearchQuery( $term ) !== null ) {
+ return null;
+ }
+
+ return $this->searchFallbackSearchEngine( $term, false );
+ }
+
+ /**
+ * Perform a full text search query and return a result set.
+ * If title searches are not supported or disabled, return null.
+ *
+ * @param string $term Raw search term
+ *
+ * @return SearchResultSet|\Status|null
+ */
+ public function searchText( $term ) {
+
+ if ( $this->getSearchQuery( $term ) !== null ) {
+ return $this->newSearchResultSet( $term );
+ }
+
+ return $this->searchFallbackSearchEngine( $term, true );
+ }
+
+ /**
+ * @see SearchEngine::completionSearchBackend
+ *
+ * Perform a completion search.
+ *
+ * @param string $search
+ *
+ * @return SearchSuggestionSet
+ */
+ protected function completionSearchBackend( $search ) {
+
+ $searchResultSet = null;
+
+ // Avoid MW's auto formatting of title entities
+ if ( $search !== '' ) {
+ $search{0} = strtolower( $search{0} );
+ }
+
+ $searchEngine = $this->getFallbackSearchEngine();
+
+ if ( !$this->hasPrefixAndMinLenForCompletionSearch( $search, 3 ) ) {
+ return $searchEngine->completionSearch( $search );
+ }
+
+ if ( $this->getSearchQuery( $search ) !== null ) {
+ $searchResultSet = $this->newSearchResultSet( $search, false, false );
+ }
+
+ if ( $searchResultSet instanceof SearchResultSet ) {
+ return $searchResultSet->newSearchSuggestionSet();
+ }
+
+ return $searchEngine->completionSearch( $search );
+ }
+
+ private function hasPrefixAndMinLenForCompletionSearch( $term, $minLen ) {
+
+ // Only act on when `in:foo`, `has:SomeProperty`, or `phrase:some text`
+ // is actively used as prefix
+
+ if ( strpos( $term, 'in:' ) !== false && mb_strlen( $term ) >= ( 3 + $minLen ) ) {
+ return true;
+ }
+
+ if ( strpos( $term, 'has:' ) !== false && mb_strlen( $term ) >= ( 4 + $minLen ) ) {
+ return true;
+ }
+
+ if ( strpos( $term, 'phrase:' ) !== false && mb_strlen( $term ) >= ( 7 + $minLen ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function newSearchResultSet( $term, $count = true, $highlight = true ) {
+
+ $query = $this->getSearchQuery( $term );
+
+ if ( $query === null ) {
+ return null;
+ }
+
+ $query->setOffset( $this->offset );
+ $query->setLimit( $this->limit, false );
+ $this->queryString = $query->getQueryString();
+
+ $store = ApplicationFactory::getInstance()->getStore();
+ $query->clearErrors();
+ $query->setOption( 'highlight.fragment', $highlight );
+
+ $result = $store->getQueryResult( $query );
+ $this->errors = $query->getErrors();
+ $this->queryLink = $result->getQueryLink();
+ $this->queryLink->setParameter( $this->offset, 'offset' );
+ $this->queryLink->setParameter( $this->limit, 'limit' );
+
+ if ( $count ) {
+ $query->querymode = SMWQuery::MODE_COUNT;
+ $query->setOffset( 0 );
+
+ $queryResult = $store->getQueryResult( $query );
+ $count = $queryResult instanceof QueryResult ? $queryResult->getCountValue() : $queryResult;
+ } else {
+ $count = 0;
+ }
+
+ return new SearchResultSet( $result, $count );
+ }
+
+ /**
+ * @param string $feature
+ *
+ * @return bool
+ */
+ public function supports( $feature ) {
+ return $this->getFallbackSearchEngine()->supports( $feature );
+ }
+
+ /**
+ * May performs database-specific conversions on text to be used for
+ * searching or updating search index.
+ *
+ * @param string $string String to process
+ *
+ * @return string
+ */
+ public function normalizeText( $string ) {
+ return $this->getFallbackSearchEngine()->normalizeText( $string );
+ }
+
+ public function getTextFromContent( Title $t, Content $c = null ) {
+ return $this->getFallbackSearchEngine()->getTextFromContent( $t, $c );
+ }
+
+ public function textAlreadyUpdatedForIndex() {
+ return $this->getFallbackSearchEngine()->textAlreadyUpdatedForIndex();
+ }
+
+ /**
+ * Create or update the search index record for the given page.
+ * Title and text should be pre-processed.
+ *
+ * @param int $id
+ * @param string $title
+ * @param string $text
+ */
+ public function update( $id, $title, $text ) {
+ $this->getFallbackSearchEngine()->update( $id, $title, $text );
+ }
+
+ /**
+ * Update a search index record's title only.
+ * Title should be pre-processed.
+ *
+ * @param int $id
+ * @param string $title
+ */
+ public function updateTitle( $id, $title ) {
+ $this->getFallbackSearchEngine()->updateTitle( $id, $title );
+ }
+
+ /**
+ * Delete an indexed page
+ * Title should be pre-processed.
+ *
+ * @param int $id Page id that was deleted
+ * @param string $title Title of page that was deleted
+ */
+ public function delete( $id, $title ) {
+ $this->getFallbackSearchEngine()->delete( $id, $title );
+ }
+
+ public function setFeatureData( $feature, $data ) {
+ parent::setFeatureData( $feature, $data );
+ $this->getFallbackSearchEngine()->setFeatureData( $feature, $data );
+ }
+
+ /**
+ * @param String $feature
+ *
+ * @return array|null
+ */
+ public function getFeatureData( $feature ) {
+
+ if ( array_key_exists( $feature, $this->features ) ) {
+ return $this->features[$feature];
+ }
+
+ return null;
+ }
+
+ /**
+ * SMW queries do not have prefixes. Returns query as is.
+ *
+ * @param string $query
+ *
+ * @return string
+ */
+ public function replacePrefixes( $query ) {
+ return $query;
+ }
+
+ /**
+ * No Transformation needed. Returns term as is.
+ * @param $term
+ * @return mixed
+ */
+ public function transformSearchTerm( $term ) {
+ return $term;
+ }
+
+ /**
+ * @return int
+ */
+ public function getLimit() {
+ return $this->limit;
+ }
+
+ /**
+ * @return int
+ */
+ public function getOffset() {
+ return $this->offset;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function getShowSuggestion() {
+ return $this->showSuggestion;
+ }
+
+ public function setLimitOffset( $limit, $offset = 0 ) {
+ parent::setLimitOffset( $limit, $offset );
+ $this->getFallbackSearchEngine()->setLimitOffset( $limit, $offset );
+ }
+
+ public function setNamespaces( $namespaces ) {
+ parent::setNamespaces( $namespaces );
+ $this->getFallbackSearchEngine()->setNamespaces( $namespaces );
+ }
+
+ public function setShowSuggestion( $showSuggestion ) {
+ parent::setShowSuggestion( $showSuggestion );
+ $this->getFallbackSearchEngine()->setShowSuggestion( $showSuggestion );
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchProfileForm.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchProfileForm.php
new file mode 100644
index 00000000..1f6ebce9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchProfileForm.php
@@ -0,0 +1,433 @@
+<?php
+
+namespace SMW\MediaWiki\Search;
+
+use Html;
+use MWNamespace;
+use SMW;
+use SMW\MediaWiki\Search\Form\FormsBuilder;
+use SMW\MediaWiki\Search\Form\FormsFactory;
+use SMW\MediaWiki\Search\Form\FormsFinder;
+use SMW\ProcessingErrorMsgHandler;
+use SMW\Utils\HtmlModal;
+use SMW\Store;
+use SMW\Message;
+use SpecialSearch;
+use Title;
+use WikiPage;
+use Xml;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SearchProfileForm {
+
+ const PROFILE_NAME = 'smw';
+
+ /**
+ * Page that hosts the form/forms definition
+ */
+ const SCHEMA_TYPE = 'SEARCH_FORM_SCHEMA';
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var SpecialSearch
+ */
+ private $specialSearch;
+
+ /**
+ * @var FormsFactory
+ */
+ private $formsFactory;
+
+ /**
+ * @var []
+ */
+ private $searchableNamespaces = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param SpecialSearch $specialSearch
+ */
+ public function __construct( Store $store, SpecialSearch $specialSearch ) {
+ $this->store = $store;
+ $this->specialSearch = $specialSearch;
+ $this->formsFactory = new FormsFactory();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ * @param array &$profiles
+ */
+ public static function addProfile( $type, array &$profiles ) {
+
+ if ( $type !== 'SMWSearch' ) {
+ return;
+ }
+
+ $profiles[self::PROFILE_NAME] = [
+ 'message' => 'smw-search-profile',
+ 'tooltip' => 'smw-search-profile-tooltip',
+ 'namespaces' => \SearchEngine::defaultNamespaces()
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ *
+ * @return array
+ */
+ public static function getFormDefinitions( Store $store ) {
+
+ static $data = null;
+
+ if ( $data !== null ) {
+ return $data;
+ }
+
+ $formsFinder = new FormsFinder( $store );
+ $data = $formsFinder->getFormDefinitions();
+
+ return $data;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $searchableNamespaces
+ */
+ public function setSearchableNamespaces( array $searchableNamespaces ) {
+ $this->searchableNamespaces = $searchableNamespaces;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string &$form
+ * @param array $opts
+ */
+ public function getForm( &$form, array $opts = [] ) {
+
+ $hidden = '';
+ $html = '';
+
+ $context = $this->specialSearch->getContext();
+ $request = $context->getRequest();
+
+ foreach ( $opts as $key => $value ) {
+ $hidden .= Html::hidden( $key, $value );
+ }
+
+ $outputPage = $context->getOutput();
+
+ $outputPage->addModuleStyles( [ 'smw.ui.styles', 'smw.special.search.styles' ] );
+ $outputPage->addModules(
+ [
+ 'smw.ui',
+ 'smw.special.search',
+ 'ext.smw.tooltip',
+ 'ext.smw.autocomplete.property'
+ ]
+ );
+
+ // Set active form
+ $this->specialSearch->setExtraParam( 'smw-form', $request->getVal( 'smw-form' ) );
+
+ $searchEngine = $this->specialSearch->getSearchEngine();
+
+ if ( ( $queryLink = $searchEngine->getQueryLink() ) instanceof \SMWInfolink ) {
+ $queryLink->setCaption( $this->msg( 'smw-search-profile-link-caption-query', Message::TEXT ) );
+ $queryLink->setLinkAttributes(
+ [
+ 'title' => 'Special:Ask'
+ ]
+ );
+ }
+
+ list( $searchForms, $formList, $termPrefixes, $preselectNamespaces, $hiddenNamespaces ) = $this->buildSearchForms(
+ $request
+ );
+
+ $sortForm = $this->buildSortForm( $request );
+
+ $namespaceForm = $this->buildNamespaceForm(
+ $request,
+ $searchEngine,
+ $preselectNamespaces,
+ $hiddenNamespaces,
+ $hidden
+ );
+
+ $options = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-search-options'
+ ],
+ Html::rawElement(
+ 'div',
+ [
+ 'style' => 'color: #586069;position: relative;display: inline-block; padding-top: 5px; padding-bottom: 2px;'
+ ],
+ ''
+ ) . $sortForm . $formList . HtmlModal::link(
+ '<span class="smw-icon-info"></span>',
+ [
+ 'data-id' => 'smw-search-profile-extended-cheat-sheet'
+ ]
+ )
+ );
+
+ $errors = $this->findErrors( $searchEngine );
+
+ $modal = HtmlModal::modal(
+ Message::get( 'smw-cheat-sheet', Message::TEXT, Message::USER_LANGUAGE ),
+ $this->profile_sheet( $searchEngine->getQueryString(), $queryLink, $termPrefixes ),
+ [
+ 'id' => 'smw-search-profile-extended-cheat-sheet',
+ 'class' => 'plainlinks',
+ 'style' => 'display:none;'
+ ]
+ );
+
+ $form .= Html::rawElement(
+ 'fieldset',
+ [
+ 'id' => 'smw-searchoptions'
+ ],
+ $hidden . $errors . $modal . $options . $searchForms
+ );
+
+ // Different fieldset therefore it is used as last element
+ $form .= $namespaceForm;
+ }
+
+ private function buildNamespaceForm( $request, $searchEngine, $preselectNamespaces, $hiddenNamespaces, &$hidden ) {
+
+ $activeNamespaces = array_merge( $this->specialSearch->getNamespaces(), $preselectNamespaces );
+ $default = false;
+
+ $data = $this->getFormDefinitions( $this->store );
+
+ foreach ( $this->searchableNamespaces as $ns => $name ) {
+
+ if ( $request->getCheck( 'ns' . $ns ) ) {
+ $activeNamespaces[] = $ns;
+ $this->specialSearch->setExtraParam( 'ns' . $ns, true );
+ }
+ }
+
+ if ( $searchEngine !== null ) {
+ $searchEngine->setNamespaces( $activeNamespaces );
+ }
+
+ // Contains the copied Advanced namespace form
+ $namespaceForm = $this->formsFactory->newNamespaceForm();
+
+ $namespaceForm->setActiveNamespaces(
+ $activeNamespaces
+ );
+
+ $namespaceForm->setHiddenNamespaces(
+ $hiddenNamespaces
+ );
+
+ if ( isset( $data['namespaces']['default_hide'] ) ) {
+ $default = $data['namespaces']['default_hide'];
+ }
+
+ $namespaceForm->setHideList(
+ $request->getVal( 'ns-list', $default )
+ );
+
+ $namespaceForm->setSearchableNamespaces(
+ $this->searchableNamespaces
+ );
+
+ $namespaceForm->checkNamespaceEditToken(
+ $this->specialSearch
+ );
+
+ // Carry over the status (hide/show) of the ns section during a search
+ // request so we don't have to set a cookie while still being able to
+ // retain its status on whether the users has the NS hidden or not.
+ $hidden .= Html::hidden( 'ns-list', $request->getVal( 'ns-list', $default ) );
+
+ return $namespaceForm->makeFields();
+ }
+
+ private function buildSearchForms( $request ) {
+
+ $data = $this->getFormDefinitions( $this->store );
+
+ if ( $data === [] ) {
+ return [ '', '', [], [], [] ];
+ }
+
+ $formsBuilder = new FormsBuilder( $request, $this->formsFactory );
+
+ $form = $formsBuilder->buildForm( $data );
+ $parameters = $formsBuilder->getParameters();
+
+ // Set parameters so that any link to a (... 20, 50 ...) list carries
+ // those parameters, using them as hidden elements is not sufficient
+ foreach ( $parameters as $key => $value ) {
+ $this->specialSearch->setExtraParam( $key, $value );
+ }
+
+ $formList = $formsBuilder->buildFormList();
+
+ return [
+ $form,
+ $formList,
+ $formsBuilder->getTermPrefixes(),
+ $formsBuilder->getPreselectNsList(),
+ $formsBuilder->getHiddenNsList()
+ ];
+ }
+
+ private function findErrors( $searchEngine ) {
+
+ if ( ( $errors = $searchEngine->getErrors() ) === [] ) {
+ return '';
+ }
+
+ $divider = "<div class='divider'></div>";
+
+ $list = ProcessingErrorMsgHandler::normalizeAndDecodeMessages(
+ $errors
+ );
+
+ return Html::rawElement(
+ 'ul',
+ [
+ 'class' => 'smw-errors',
+ 'style' => 'color:#b32424;'
+ ],
+ '<li>' . implode( '</li><li>', $list ) . '</li>'
+ ) . $divider;
+ }
+
+ private function buildSortForm( $request ) {
+
+ $sortForm = $this->formsFactory->newSortForm( $request );
+
+ // TODO this information should come from the store and not being
+ // derived from a class! How should such characteristic be represented?
+ $features = [
+ 'best' => is_a( $this->store, "SMWElasticStore" )
+ ];
+
+ $form = $sortForm->makeFields( $features );
+ $parameters = $sortForm->getParameters();
+
+ foreach ( $parameters as $key => $value ) {
+ $this->specialSearch->setExtraParam( $key, $value );
+ }
+
+ return $form;
+ }
+
+ private function profile_sheet( $query, $queryLink, $termPrefixes ) {
+
+ $text = Message::get( 'smw-search-profile-extended-help-intro', Message::PARSE, Message::USER_LANGUAGE );
+
+ $link = $queryLink !== null ? $queryLink->getHtml() : '';
+
+ if ( $link !== '' ) {
+ $text .= $this->section( 'smw-search-profile-extended-section-query' );
+ $text .= $this->msg( [ 'smw-search-profile-extended-help-query', trim( $query ) ] ) . '&nbsp;';
+ $text .= $this->msg( [ 'smw-search-profile-extended-help-query-link', $link ], Message::TEXT );
+ }
+
+ $text .= $this->section( 'smw-search-profile-extended-section-search-syntax' );
+ $text .= $this->msg( 'smw-search-profile-extended-help-search-syntax', Message::TEXT );
+
+ $syntax = $this->msg( 'smw-search-profile-extended-help-search-syntax-simplified-in' );
+ $syntax .= $this->msg( 'smw-search-profile-extended-help-search-syntax-simplified-phrase' );
+ $syntax .= $this->msg( 'smw-search-profile-extended-help-search-syntax-simplified-has' );
+ $syntax .= $this->msg( 'smw-search-profile-extended-help-search-syntax-simplified-not' );
+
+ if ( $termPrefixes !== [] ) {
+ $prefixes = '';
+
+ foreach ( array_keys( $termPrefixes ) as $pref ) {
+ $prefixes .= ( $prefixes === '' ? '' : ', ' ) . "<code>$pref:</code>";
+ }
+
+ $syntax .= $this->msg( [ 'smw-search-profile-extended-help-search-syntax-prefix', $prefixes ] );
+ }
+
+ $syntax .= $this->msg( [ 'smw-search-profile-extended-help-search-syntax-reserved', "'&&', 'AND', '||', 'OR', '(', ')', '[[', ']]'" ] );
+
+ $text .= Html::rawElement( 'div', [ 'id' => 'smw-search-synatx-list' ],
+ $syntax
+ );
+
+ $text .= Html::rawElement( 'p', [] ,
+ $this->msg( 'smw-search-profile-extended-help-search-syntax-note' )
+ );
+
+ $text .= $this->section( 'smw-search-profile-extended-section-sort' );
+ $text .= $this->msg( 'smw-search-profile-extended-help-sort' );
+ $sort = $this->msg( 'smw-search-profile-extended-help-sort-title' );
+ $sort .= $this->msg( 'smw-search-profile-extended-help-sort-recent' );
+
+ if ( is_a( $this->store, "SMWElasticStore" ) ) {
+ $sort .= $this->msg( 'smw-search-profile-extended-help-sort-best' );
+ }
+
+ $text .= Html::rawElement( 'div', [ 'id' => 'smw-search-sort-list' ],
+ $sort
+ );
+
+ $formLink = Html::element(
+ 'a',
+ [
+ 'href' => Title::newFromText( 'Special:SearchByProperty/Schema type/' . self::SCHEMA_TYPE )->getFullUrl()
+ ],
+ $this->msg( 'smw-search-profile-extended-help-find-forms' )
+ );
+
+ $text .= $this->section( 'smw-search-profile-extended-section-form' );
+ $text .= $this->msg( [ 'smw-search-profile-extended-help-form', $formLink ], Message::TEXT );
+ $text .= $this->section( 'smw-search-profile-extended-section-namespace' );
+ $text .= $this->msg( 'smw-search-profile-extended-help-namespace' );
+
+ return $text;
+ }
+
+ private function section( $msg, $attributes = [] ) {
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-text-strike',
+ 'style' => 'padding: 5px 0 5px 0;'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'style' => 'font-size: 1.2em; margin-left:0px'
+ ],
+ Message::get( $msg, Message::TEXT, Message::USER_LANGUAGE )
+ )
+ );
+ }
+
+ private function msg( $msg, $type = Message::PARSE, $lang = Message::USER_LANGUAGE ) {
+ return Message::get( $msg, $type, $lang );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchResult.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchResult.php
new file mode 100644
index 00000000..28f650d7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchResult.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace SMW\MediaWiki\Search;
+
+use SMW\DataValueFactory;
+use SMW\DIWikiPage;
+
+/**
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SearchResult extends \SearchResult {
+
+ /**
+ * @var boolean
+ */
+ private $hasHighlight = false;
+
+ /**
+ * @see SearchResult::getTextSnippet
+ */
+ function getTextSnippet( $terms ) {
+
+ if ( $this->hasHighlight ) {
+ return str_replace( [ '<em>', '</em>' ], [ "<span class='searchmatch'>", '</span>' ], $this->mText );
+ }
+
+ return parent::getTextSnippet( $terms );
+ }
+
+ /**
+ * @see SearchResult::getSectionTitle
+ */
+ function getSectionTitle() {
+
+ if ( !isset( $this->mTitle ) || $this->mTitle->getFragment() === '' ) {
+ return null;
+ }
+
+ return $this->mTitle;
+ }
+
+ /**
+ * Set a text excerpt retrieved from a different back-end.
+ *
+ * @param string $text|null
+ * @param boolean $hasHighlight
+ */
+ public function setExcerpt( $text = null, $hasHighlight = false ) {
+ $this->mText = $text;
+ $this->hasHighlight = $hasHighlight;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getExcerpt() {
+ return $this->mText;
+ }
+
+ /**
+ * @see SearchResult::getTitleSnippet
+ */
+ public function getTitleSnippet() {
+
+ if ( !isset( $this->mTitle ) ) {
+ return '';
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ DIWikiPage::newFromTitle( $this->mTitle )
+ );
+
+ // Will return the DISPLAYTITLE, if available
+ return $dataValue->getPreferredCaption();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchResultSet.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchResultSet.php
new file mode 100644
index 00000000..6843891b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Search/SearchResultSet.php
@@ -0,0 +1,204 @@
+<?php
+
+namespace SMW\MediaWiki\Search;
+
+use SMW\DIWikiPage;
+use SMW\Utils\CharExaminer;
+
+/**
+ * @ingroup SMW
+ *
+ * @licence GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Stephan Gambke
+ */
+class SearchResultSet extends \SearchResultSet {
+
+ /**
+ * @var DIWikiPage[]|[]
+ */
+ private $pages;
+
+ /**
+ * @var QueryToken
+ */
+ private $queryToken;
+
+ /**
+ * @var Excerpts
+ */
+ private $excerpts;
+
+ private $count = null;
+
+ public function __construct( \SMWQueryResult $result, $count = null ) {
+ $this->pages = $result->getResults();
+ $this->queryToken = $result->getQuery()->getQueryToken();
+ $this->excerpts = $result->getExcerpts();
+ $this->count = $count;
+ }
+
+ /**
+ * Return number of rows included in this result set.
+ *
+ * @return int|void
+ */
+ public function numRows() {
+ return count( $this->pages );
+ }
+
+ /**
+ * Return true if results are included in this result set.
+ *
+ * @return bool
+ */
+ public function hasResults() {
+ return $this->numRows() > 0;
+ }
+
+ /**
+ * Fetches next search result, or false.
+ *
+ * @return SearchResult
+ */
+ public function next() {
+
+ $page = current( $this->pages );
+ $searchResult = false;
+
+ if ( $page instanceof DIWikiPage ) {
+ $searchResult = SearchResult::newFromTitle( $page->getTitle() );
+ }
+
+ // Attempt to use excerpts available from a different back-end
+ if ( $searchResult && $this->excerpts !== null ) {
+ if ( ( $excerpt = $this->excerpts->getExcerpt( $page ) ) !== false ) {
+ $searchResult->setExcerpt( $excerpt, $this->excerpts->hasHighlight() );
+ }
+ }
+
+ next( $this->pages );
+
+ return $searchResult;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return SearchSuggestionSet
+ */
+ public function newSearchSuggestionSet() {
+
+ $suggestions = [];
+ $hasMoreResults = false;
+ $score = count( $this->pages );
+
+ foreach ( $this->pages as $page ) {
+ if ( ( $title = $page->getTitle() ) && $title->exists() ) {
+ $suggestions[] = \SearchSuggestion::fromTitle( $score--, $title );
+ }
+ }
+
+ return new \SearchSuggestionSet( $suggestions, $hasMoreResults );
+ }
+
+ /**
+ * @see SearchResultSet::extractResults
+ *
+ * @since 3.0
+ */
+ public function extractResults() {
+
+ // #3204
+ // https://github.com/wikimedia/mediawiki/commit/720fdfa7901cbba93b5695ed5f00f982272ced27
+ //
+ // MW 1.32+:
+ // - Remove SearchResultSet::next, SearchResultSet::numRows
+ // - Move QueryResult::getResults, QueryResult::getExcerpts into this
+ // method to avoid constructor work
+
+ if ( $this->pages === [] ) {
+ return $this->results = [];
+ }
+
+ foreach ( $this->pages as $page ) {
+
+ if ( $page instanceof DIWikiPage ) {
+ $searchResult = SearchResult::newFromTitle( $page->getTitle() );
+ }
+
+ // Attempt to use excerpts available from a different back-end
+ if ( $searchResult && $this->excerpts !== null ) {
+ if ( ( $excerpt = $this->excerpts->getExcerpt( $page ) ) !== false ) {
+ $searchResult->setExcerpt( $excerpt, $this->excerpts->hasHighlight() );
+ }
+ }
+
+ $this->results[] = $searchResult;
+ }
+
+ return $this->results;
+ }
+
+ /**
+ * Returns true, so Special:Search won't offer the user a link to a create
+ * a page named by the search string because the name would contain the
+ * search syntax, i.e. the SMW query.
+ *
+ * @return bool
+ */
+ public function searchContainedSyntax() {
+ return true;
+ }
+
+ public function getTotalHits() {
+ return $this->count;
+ }
+
+ /**
+ * Return an array of regular expression fragments for matching
+ * the search terms as parsed by the engine in a text extract.
+ *
+ * This is a temporary hack for MW versions that can not cope
+ * with no search term being returned (<1.24).
+ *
+ * @deprecated remove once min supported MW version has \SearchHighlighter::highlightNone()
+ *
+ * @return string[]
+ */
+ public function termMatches() {
+
+ if ( ( $tokens = $this->getTokens() ) !== [] ) {
+ return $tokens;
+ }
+
+ if ( method_exists( '\SearchHighlighter', 'highlightNone' ) ) {
+ return [];
+ }
+
+ // Will cause the highlighter to match every line start, thus returning the first few lines of found pages.
+ return [ '^' ];
+ }
+
+ private function getTokens() {
+
+ $tokens = [];
+
+ if ( $this->queryToken === null ) {
+ return $tokens;
+ }
+
+ // Use tokens gathered from a query context [[in:Foo]] (~~*Foo*), a filter context
+ // such as [[Category:Foo]] is not considered eligible to provide a
+ // token.
+ foreach ( $this->queryToken->getTokens() as $key => $value ) {
+ // Avoid add \b boundary checks for CJK where whitespace is not used
+ // as word break
+ $tokens[] = CharExaminer::isCJK( $key ) ? "$key" : "\b$key\b";
+ }
+
+ return $tokens;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/CacheStatisticsListTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/CacheStatisticsListTaskHandler.php
new file mode 100644
index 00000000..b5b4a0e7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/CacheStatisticsListTaskHandler.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\Message;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class CacheStatisticsListTaskHandler extends TaskHandler {
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @since 3.0
+ *
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( OutputFormatter $outputFormatter ) {
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SUPPLEMENT;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'stats/cache';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-operational-statistics-cache-title' ),
+ [ 'action' => 'stats/cache' ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-operational-statistics-cache-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $this->outputFormatter->setPageTitle(
+ $this->msg( 'smw-admin-supplementary-operational-statistics-cache-title' )
+ );
+
+ $this->outputFormatter->addParentLink(
+ [ 'action' => 'stats' ],
+ 'smw-admin-supplementary-operational-statistics-title'
+ );
+
+ $this->outputQueryCacheStatistics();
+ }
+
+ private function outputQueryCacheStatistics() {
+
+ $this->outputFormatter->addHTML(
+ Html::element( 'h2', [], $this->msg( 'smw-admin-statistics-querycache-title' ) )
+ );
+
+ $cachedQueryResultPrefetcher = ApplicationFactory::getInstance()->singleton( 'CachedQueryResultPrefetcher' );
+
+ if ( !$cachedQueryResultPrefetcher->isEnabled() ) {
+ $msg = $this->msg(
+ [ 'smw-admin-statistics-querycache-disabled' ],
+ Message::PARSE
+ );
+
+ return $this->outputFormatter->addHTML(
+ Html::rawElement( 'p', [], $msg )
+ );
+ }
+
+ $msg = $this->msg(
+ [ 'smw-admin-statistics-querycache-explain' ],
+ Message::PARSE
+ );
+
+ $this->outputFormatter->addHTML(
+ Html::rawElement( 'p', [], $msg )
+ );
+
+ $this->outputFormatter->addAsPreformattedText(
+ $this->outputFormatter->encodeAsJson( $cachedQueryResultPrefetcher->getStats() )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/ConfigurationListTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/ConfigurationListTaskHandler.php
new file mode 100644
index 00000000..d0321697
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/ConfigurationListTaskHandler.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\Message;
+use SMW\NamespaceManager;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ConfigurationListTaskHandler extends TaskHandler {
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @since 2.5
+ *
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( OutputFormatter $outputFormatter ) {
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SUPPLEMENT;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'settings';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-settings-title' ),
+ [
+ 'action' => 'settings'
+ ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-settings-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $this->outputFormatter->setPageTitle(
+ $this->msg( 'smw-admin-supplementary-settings-title' )
+ );
+
+ $this->outputFormatter->addParentLink(
+ [ 'tab' => 'supplement' ]
+ );
+
+ $this->outputFormatter->addHtml(
+ Html::rawElement(
+ 'p',
+ [
+ 'class' => 'plainlinks'
+ ],
+ $this->msg( 'smw-admin-settings-docu', Message::PARSE )
+ )
+ );
+
+ $options = ApplicationFactory::getInstance()->getSettings()->toArray();
+
+ $this->outputFormatter->addAsPreformattedText(
+ str_replace( '\\\\', '\\', $this->outputFormatter->encodeAsJson( $this->cleanPath( $options ) ) )
+ );
+
+ $this->outputFormatter->addAsPreformattedText(
+ $this->outputFormatter->encodeAsJson(
+ [
+ 'canonicalNames' => NamespaceManager::getCanonicalNames()
+ ]
+ )
+ );
+ }
+
+ private function cleanPath( array &$options ) {
+
+ foreach ( $options as $key => &$value ) {
+ if ( is_array( $value ) ) {
+ $this->cleanPath( $value );
+ }
+
+ if ( is_string( $value ) && strpos( $value , 'SemanticMediaWiki/') !== false ) {
+ $value = preg_replace('/[\s\S]+?SemanticMediaWiki/', '.../SemanticMediaWiki', $value );
+ }
+ }
+
+ return $options;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DataRefreshJobTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DataRefreshJobTaskHandler.php
new file mode 100644
index 00000000..82d059cf
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DataRefreshJobTaskHandler.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use Title;
+use WebRequest;
+use SMW\MediaWiki\Job;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DataRefreshJobTaskHandler extends TaskHandler {
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @var null|Job
+ */
+ private $refreshjob = null;
+
+ /**
+ * @since 2.5
+ *
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( HtmlFormRenderer $htmlFormRenderer, OutputFormatter $outputFormatter ) {
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_DATAREPAIR;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'refreshstore';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $this->htmlFormRenderer
+ ->addHeader( 'h4', $this->msg( 'smw_smwadmin_datarefresh' ) )
+ ->addParagraph( $this->msg( 'smw_smwadmin_datarefreshdocu' ) );
+
+ if ( !$this->isEnabledFeature( SMW_ADM_REFRESH ) ) {
+ $this->htmlFormRenderer->addParagraph( $this->msg( 'smw-admin-feature-disabled' ) );
+ } elseif ( $this->getRefreshJob() !== null ) {
+ var_dump('expression');
+ $this->htmlFormRenderer
+ ->setMethod( 'post' )
+ ->addHiddenField( 'action', 'refreshstore' )
+ ->addParagraph( $this->msg( 'smw_smwadmin_datarefreshprogress' ) )
+ ->addParagraph( $this->getProgressBar( $this->getRefreshJob()->getProgress() ) )
+ ->addLineBreak()
+ ->addSubmitButton(
+ $this->msg( 'smw_smwadmin_datarefreshstop' ),
+ [
+ 'class' => ''
+ ]
+ )
+ ->addCheckbox(
+ $this->msg( 'smw_smwadmin_datarefreshstopconfirm' ),
+ 'rfsure',
+ 'stop'
+ );
+ } elseif ( $this->getRefreshJob() === null ) {
+ $this->htmlFormRenderer
+ ->setMethod( 'post' )
+ ->addHiddenField( 'action', 'refreshstore' )
+ ->addHiddenField( 'rfsure', 'yes' )
+ ->addSubmitButton(
+ $this->msg( 'smw_smwadmin_datarefreshbutton' ),
+ [
+ 'class' => ''
+ ]
+ );
+ }
+
+ return Html::rawElement( 'div', [], $this->htmlFormRenderer->getForm() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ if ( !$this->isEnabledFeature( SMW_ADM_REFRESH ) ) {
+ return '';
+ }
+
+ $sure = $webRequest->getText( 'rfsure' );
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ if ( $sure == 'yes' ) {
+ $refreshjob = $this->getRefreshJob();
+
+ if ( $refreshjob === null ) { // careful, there might be race conditions here
+
+ $newjob = $applicationFactory->newJobFactory()->newByType(
+ 'SMW\RefreshJob',
+ \SpecialPage::getTitleFor( 'SMWAdmin' ),
+ [ 'spos' => 1, 'prog' => 0, 'rc' => 2 ]
+ );
+
+ $newjob->insert();
+ }
+
+ } elseif ( $sure == 'stop' ) {
+ $jobQueue = $applicationFactory->getJobQueue();
+ $jobQueue->disableCache();
+ $jobQueue->delete( 'SMW\RefreshJob' );
+ }
+
+ $this->outputFormatter->redirectToRootPage( '', [ 'tab' => 'rebuild' ] );
+ }
+
+ private function getProgressBar( $prog ) {
+ return Html::rawElement(
+ 'div',
+ [ 'style' => 'float: left; background: #DDDDDD; border: 1px solid grey; width: 300px;' ],
+ Html::rawElement( 'div', [ 'style' => 'background: #AAF; width: ' . round( $prog * 300 ) . 'px; height: 20px; ' ], '' )
+ ) . '&#160;' . round( $prog * 100, 4 ) . '%';
+ }
+
+ private function getRefreshJob() {
+
+ if ( !$this->isEnabledFeature( SMW_ADM_REFRESH ) ) {
+ return null;
+ }
+
+ if ( $this->refreshjob !== null ) {
+ return $this->refreshjob;
+ }
+
+ $jobQueue = ApplicationFactory::getInstance()->getJobQueue();
+
+ if ( !$jobQueue->hasPendingJob( 'SMW\RefreshJob' ) ) {
+ return null;
+ }
+
+ // Pop and acknowledge the job to fetch progress details
+ // from itself
+ $refreshJob = $jobQueue->pop( 'SMW\RefreshJob' );
+
+ if ( $refreshJob instanceof Job ) {
+ $refreshJob->run();
+ $jobQueue->ack( $refreshJob );
+ $this->refreshjob = $refreshJob;
+ }
+
+ return $this->refreshjob;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DeprecationNoticeTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DeprecationNoticeTaskHandler.php
new file mode 100644
index 00000000..7842b9c4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DeprecationNoticeTaskHandler.php
@@ -0,0 +1,281 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\Message;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DeprecationNoticeTaskHandler extends TaskHandler {
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @var array
+ */
+ private $deprecationNoticeList = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param OutputFormatter $outputFormatter
+ * @param array $deprecationNoticeList
+ */
+ public function __construct( OutputFormatter $outputFormatter, array $deprecationNoticeList = [] ) {
+ $this->outputFormatter = $outputFormatter;
+ $this->deprecationNoticeList = $deprecationNoticeList;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_DEPRECATION;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $html = '';
+
+ // Push `smw` to the top
+ uksort( $this->deprecationNoticeList, function( $a, $b ) {
+ return $b === 'smw';
+ } );
+
+ foreach ( $this->deprecationNoticeList as $section => $deprecationNoticeList ) {
+ $html .= $this->buildSection( $section, $deprecationNoticeList );
+ }
+
+ if ( $html === '' ) {
+ return '';
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-admin-deprecation'
+ ],
+ Html::rawElement(
+ 'p',
+ [
+ 'class' => 'plainlinks'
+ ],
+ $this->msg( 'smw-admin-deprecation-notice-docu' )
+ ) . $html
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {}
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {}
+
+ private function buildSection( $section, $deprecationNoticeList ) {
+
+ $noticeConfigList = [];
+ $replacementConfigList = [];
+ $removedConfigList = [];
+ $html = '';
+
+ if ( isset( $deprecationNoticeList['notice'] ) ) {
+ $noticeConfigList = $deprecationNoticeList['notice'];
+ }
+
+ if ( isset( $deprecationNoticeList['replacement'] ) ) {
+ $replacementConfigList = $deprecationNoticeList['replacement'];
+ }
+
+ if ( isset( $deprecationNoticeList['removal'] ) ) {
+ $removedConfigList = $deprecationNoticeList['removal'];
+ }
+
+ $sectionList = $this->build_list(
+ $section,
+ $noticeConfigList,
+ $replacementConfigList,
+ $removedConfigList
+ );
+
+ if ( $sectionList === [] ) {
+ return '';
+ }
+
+ if ( $section !== 'smw' ) {
+ $html .= Html::rawElement(
+ 'h2',
+ [
+ 'class' => "$section-admin-deprecation-notice-section"
+ ],
+ $this->msg( "$section-admin-deprecation-notice-section" )
+ );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => "$section-admin-deprecation-section"
+ ],
+ $html . implode( '', $sectionList )
+ );
+ }
+
+ private function build_list( $section, $noticeConfigList, $replacementConfigList, $removedConfigList ) {
+
+ $noticeList = [];
+ $list = [];
+
+ // Replacements
+ foreach ( $replacementConfigList as $setting => $value ) {
+ if ( $setting === 'options' ) {
+ $list[] = $this->createListItems( "$section-admin-deprecation-notice-config-replacement", $value );
+ } elseif ( isset( $GLOBALS[$setting] ) ) {
+ $list[] = $this->createListItem( [ "$section-admin-deprecation-notice-config-replacement", '$' . $setting, '$' . $value ] );
+ }
+ }
+
+ if ( $list !== [] && ( $mList = $this->mergeList( "$section-admin-deprecation-notice-title-replacement", $section, $list ) ) !== null ) {
+ $noticeList[] = $mList;
+ }
+
+ // Changes
+ foreach ( $noticeConfigList as $setting => $value ) {
+ if ( $setting === 'options' ) {
+ $list[] = $this->createListItems( "$section-admin-deprecation-notice-config-notice", $value );
+ } elseif ( isset( $GLOBALS[$setting] ) ) {
+ $list[] = $this->createListItem( [ "$section-admin-deprecation-notice-config-notice", '$' . $setting, $value ] );
+ }
+ }
+
+ if ( $list !== [] && ( $mList = $this->mergeList( "$section-admin-deprecation-notice-title-notice", $section, $list ) ) !== null ) {
+ $noticeList[] = $mList;
+ }
+
+ // Removals
+ foreach ( $removedConfigList as $setting => $msg ) {
+ if ( isset( $GLOBALS[$setting] ) ) {
+ $list[] = $this->createListItem( [ "$section-admin-deprecation-notice-config-removal", '$' . $setting, $msg ] );
+ }
+ }
+
+ if ( $list !== [] && ( $mList = $this->mergeList( "$section-admin-deprecation-notice-title-removal", $section, $list ) ) !== null ) {
+ $noticeList[] = $mList;
+ }
+
+ return $noticeList;
+ }
+
+ private function mergeList( $title, $section, &$list ) {
+
+ if ( $list === [] || ( $items = implode( '', $list ) ) === '' ) {
+ return;
+ }
+
+ $html = Html::rawElement(
+ 'h3',
+ [],
+ $this->msg( $title )
+ ) . Html::rawElement(
+ 'p',
+ [
+ 'class' => "$section-admin-deprecation-notice-section-explanation",
+ 'style' => 'margin-bottom:10px;'
+ ],
+ $this->msg( $title . '-explanation' )
+ ) . Html::rawElement(
+ 'ul',
+ [
+ 'style' => 'margin-bottom:10px;'
+ ],
+ $items
+ );
+
+ $list = [];
+
+ return $html;
+ }
+
+ private function createListItem( $message ) {
+ return Html::rawElement( 'li', [], $this->msg( $message, Message::PARSE ) );
+ }
+
+ private function createListItems( $message, $values ) {
+
+ $list = [];
+
+ if ( !is_array( $values ) ) {
+ return '';
+ }
+
+ foreach ( $values as $setting => $options ) {
+
+ if ( !is_array( $options ) ) {
+ continue;
+ }
+
+ $opt = [];
+
+ foreach ( $options as $option => $v ) {
+ if ( $this->hasOption( $setting, $option ) ) {
+ $opt[] = $this->createListItem(
+ [
+ $message . '-option-list',
+ $option,
+ $v
+ ]
+ );
+ }
+ }
+
+ if ( $opt !== [] ) {
+ $list[] = $this->createListItem(
+ [
+ $message . '-option',
+ '$' . $setting,
+ count( $opt )
+ ]
+ ) . '<ul>' . implode( '', $opt ) . '</ul>';
+ }
+ }
+
+ return implode( '', $list );
+ }
+
+ private function hasOption( $setting, $option ) {
+ return isset( $GLOBALS[$setting][$option] ) || ( is_array( $GLOBALS[$setting] ) && array_search( $option, $GLOBALS[$setting] ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DisposeJobTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DisposeJobTaskHandler.php
new file mode 100644
index 00000000..3b0df798
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DisposeJobTaskHandler.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\Message;
+use Title;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DisposeJobTaskHandler extends TaskHandler {
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @var null|Job
+ */
+ private $refreshjob = null;
+
+ /**
+ * @var boolean
+ */
+ public $isApiTask = true;
+
+ /**
+ * @since 2.5
+ *
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( HtmlFormRenderer $htmlFormRenderer, OutputFormatter $outputFormatter ) {
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_DATAREPAIR;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isApiTask() {
+ return $this->isApiTask;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'dispose';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $subject = DIWikiPage::newFromTitle( \SpecialPage::getTitleFor( 'SMWAdmin' ) );
+
+ // smw-admin-outdateddisposal
+ $this->htmlFormRenderer
+ ->addHeader( 'h4', $this->msg( 'smw-admin-outdateddisposal-title' ) )
+ ->addParagraph(
+ $this->msg( 'smw-admin-outdateddisposal-intro', Message::PARSE ),
+ [
+ 'id' => 'smw-admin-outdated-disposal',
+ 'class' => 'plainlinks'
+ ]
+ );
+
+ if ( $this->isEnabledFeature( SMW_ADM_DISPOSAL ) && !$this->hasPendingJob() ) {
+ $this->htmlFormRenderer
+ ->setMethod( 'post' )
+ ->addHiddenField( 'action', 'dispose' )
+ ->addSubmitButton(
+ $this->msg( 'smw-admin-outdateddisposal-button' ),
+ [
+ 'class' => $this->isApiTask() ? 'smw-admin-api-job-task' : '',
+ 'data-job' => 'SMW\EntityIdDisposerJob',
+ 'data-subject' => $subject->getHash()
+ ]
+ );
+ } elseif ( $this->isEnabledFeature( SMW_ADM_DISPOSAL ) ) {
+ $this->htmlFormRenderer->addParagraph(
+ Html::element(
+ 'span',
+ [
+ 'class' => 'smw-admin-circle-orange'
+ ]
+ ) . Html::element(
+ 'span',
+ [
+ 'style' => 'font-style:italic; margin-left:25px;'
+ ],
+ $this->msg( 'smw-admin-outdateddisposal-active' )
+ ),
+ [ 'id' => 'smw-admin-outdated-disposal-status' ]
+ );
+ } else {
+ $this->htmlFormRenderer->addParagraph(
+ $this->msg( 'smw-admin-feature-disabled' )
+ );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [],
+ $this->htmlFormRenderer->getForm()
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ if ( !$this->isEnabledFeature( SMW_ADM_DISPOSAL ) || $this->hasPendingJob() || $this->isApiTask() ) {
+ return $this->outputFormatter->redirectToRootPage( '', [ 'tab' => 'rebuild' ] );
+ }
+
+ $job = ApplicationFactory::getInstance()->newJobFactory()->newByType(
+ 'smw.entityIdDisposer',
+ \SpecialPage::getTitleFor( 'SMWAdmin' )
+ );
+
+ $job->insert();
+
+ $this->outputFormatter->redirectToRootPage( '', [ 'tab' => 'rebuild' ] );
+ }
+
+ private function hasPendingJob() {
+ return ApplicationFactory::getInstance()->getJobQueue()->hasPendingJob( 'smw.entityIdDisposer' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DuplicateLookupTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DuplicateLookupTaskHandler.php
new file mode 100644
index 00000000..d893ce31
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/DuplicateLookupTaskHandler.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\Message;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DuplicateLookupTaskHandler extends TaskHandler {
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @since 3.0
+ *
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( OutputFormatter $outputFormatter ) {
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SUPPLEMENT;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'duplookup';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-duplookup-title' ),
+ [
+ 'action' => 'duplookup'
+ ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-duplookup-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $this->outputFormatter->setPageTitle(
+ $this->msg( 'smw-admin-supplementary-duplookup-title' )
+ );
+
+ $this->outputFormatter->addParentLink(
+ [
+ 'tab' => 'supplement'
+ ]
+ );
+
+ $this->outputFormatter->addHelpLink(
+ $this->msg( 'smw-admin-supplementary-duplookup-helplink' )
+ );
+
+ $this->outputFormatter->addHtml(
+ Html::rawElement(
+ 'p',
+ [
+ 'class' => 'plainlinks'
+ ],
+ $this->msg( 'smw-admin-supplementary-duplookup-docu', Message::PARSE )
+ )
+ );
+
+ // Ajax is doing the query and result display to avoid a timeout issue
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-admin-supplementary-duplookup',
+ 'style' => 'opacity:0.5;position: relative;',
+ 'data-config' => json_encode(
+ [
+ 'contentClass' => 'smw-admin-supplementary-duplookup-content',
+ 'errorClass' => 'smw-admin-supplementary-duplookup-error'
+ ]
+ )
+ ],
+ Html::element(
+ 'div',
+ [
+ 'class' => 'smw-admin-supplementary-duplookup-error'
+ ]
+ ) . Html::rawElement(
+ 'pre',
+ [
+ 'class' => 'smw-admin-supplementary-duplookup-content'
+ ],
+ $this->msg( 'smw-data-lookup-with-wait' ) .
+ "\n\n\n" . $this->msg( 'smw-processing' ) . "\n" .
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-overlay-spinner medium',
+ 'style' => 'transform: translate(-50%, -50%);'
+ ]
+ )
+ )
+ );
+
+ $this->outputFormatter->addHtml( $html );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/EntityLookupTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/EntityLookupTaskHandler.php
new file mode 100644
index 00000000..6b8e9de3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/EntityLookupTaskHandler.php
@@ -0,0 +1,325 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\Message;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class EntityLookupTaskHandler extends TaskHandler {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @var User|null
+ */
+ private $user;
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( Store $store, HtmlFormRenderer $htmlFormRenderer, OutputFormatter $outputFormatter ) {
+ $this->store = $store;
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SUPPLEMENT;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'lookup';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function setUser( $user = null ) {
+ $this->user = $user;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->createSpecialPageLink(
+ $this->msg( 'smw-admin-supplementary-idlookup-title' ),
+ [
+ 'action' => 'lookup'
+ ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-idlookup-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $this->outputFormatter->setPageTitle( $this->msg( 'smw-admin-supplementary-idlookup-title' ) );
+ $this->outputFormatter->addParentLink( [ 'tab' => 'supplement' ] );
+
+ // https://phabricator.wikimedia.org/T109652#1562641
+ if ( !$this->user->matchEditToken( $webRequest->getVal( 'wpEditToken' ) ) ) {
+ return $this->outputFormatter->addHtml( $this->msg( 'sessionfailure' ) );
+ }
+
+ $id = $webRequest->getText( 'id' );
+
+ if ( $this->isEnabledFeature( SMW_ADM_DISPOSAL ) && $id > 0 && $webRequest->getText( 'dispose' ) === 'yes' ) {
+ $this->doDispose( $id );
+ }
+
+ $this->outputFormatter->addHtml( $this->getForm( $webRequest, $id ) );
+ }
+
+ /**
+ * @param integer $id
+ * @param User|null $use
+ */
+ private function doDispose( $id ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $entityIdDisposerJob = $applicationFactory->newJobFactory()->newEntityIdDisposerJob(
+ \Title::newFromText( __METHOD__ )
+ );
+
+ $entityIdDisposerJob->dispose( intval( $id ) );
+
+ $manualEntryLogger = $applicationFactory->create( 'ManualEntryLogger' );
+ $manualEntryLogger->registerLoggableEventType( 'admin' );
+ $manualEntryLogger->log( 'admin', $this->user, 'Special:SMWAdmin', 'Forced removal of ID '. $id );
+ }
+
+ private function getForm( $webRequest, $id ) {
+
+ list( $result, $error ) = $this->createInfoMessageById( $webRequest, $id );
+
+ if ( $id < 1 ) {
+ $id = null;
+ }
+
+ $html = $this->htmlFormRenderer
+ ->setName( 'idlookup' )
+ ->setMethod( 'get' )
+ ->addHiddenField( 'action', 'lookup' )
+ ->addParagraph( $error )
+ ->addHeader( 'h2', $this->msg( 'smw-admin-idlookup-title' ) )
+ ->addParagraph( $this->msg( 'smw-admin-idlookup-docu' ) )
+ ->addInputField(
+ $this->msg( 'smw-admin-objectid' ),
+ 'id',
+ $id
+ )
+ ->addNonBreakingSpace()
+ ->addSubmitButton( $this->msg( 'smw-ask-search' ) )
+ ->addParagraph( $result )
+ ->getForm();
+
+ $html .= Html::element( 'p', [], '' );
+
+ if ( $id > 0 && $webRequest->getText( 'dispose' ) == 'yes' ) {
+ $result = $this->msg( ['smw-admin-iddispose-done', $id ] );
+ $id = null;
+ }
+
+ if ( !$this->isEnabledFeature( SMW_ADM_DISPOSAL ) ) {
+ return $html;
+ }
+
+ $html .= $this->htmlFormRenderer
+ ->setName( 'iddispose' )
+ ->setMethod( 'get' )
+ ->addHiddenField( 'action', 'lookup' )
+ ->addHiddenField( 'id', $id )
+ ->addHeader( 'h2', $this->msg( 'smw-admin-iddispose-title' ) )
+ ->addParagraph( $this->msg( 'smw-admin-iddispose-docu', Message::PARSE ), [ 'class' => 'plainlinks' ] )
+ ->addInputField(
+ $this->msg( 'smw-admin-objectid' ),
+ 'id',
+ $id,
+ null,
+ 20,
+ [ 'disabled' => true ]
+ )
+ ->addNonBreakingSpace()
+ ->addSubmitButton( $this->msg( 'allpagessubmit' ) )
+ ->addCheckbox(
+ $this->msg( 'smw_smwadmin_datarefreshstopconfirm', Message::ESCAPED ),
+ 'dispose',
+ 'yes'
+ )
+ ->getForm();
+
+ return $html . Html::element( 'p', [], '' );
+ }
+
+ private function createInfoMessageById( $webRequest, &$id ) {
+
+ if ( $webRequest->getText( 'action' ) !== 'lookup' || $id === '' ) {
+ return [ '', '' ];
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ if ( ctype_digit( $id ) ) {
+ $condition = 'smw_id=' . intval( $id );
+ } else {
+ $op = strpos( $id, '*' ) !== false ? ' LIKE ' : '=';
+ $condition = "smw_sortkey $op " . $connection->addQuotes( str_replace( [ '_', '*' ], [ ' ', '%' ], $id ) );
+ }
+
+ $rows = $connection->select(
+ \SMWSql3SmwIds::TABLE_NAME,
+ [
+ 'smw_id',
+ 'smw_title',
+ 'smw_namespace',
+ 'smw_iw',
+ 'smw_subobject',
+ 'smw_sortkey'
+ ],
+ $condition,
+ __METHOD__
+ );
+
+ return $this->createMessageFromRows( $id, $rows );
+ }
+
+ private function createMessageFromRows( &$id, $rows ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $references = [];
+ $formattedRows = [];
+ $output = '';
+ $error = '';
+
+ if ( $rows !== [] ) {
+ foreach ( $rows as $row ) {
+ $id = $row->smw_id;
+
+ $references[$id] = $this->store->getPropertyTableIdReferenceFinder()->searchAllTablesToFindAtLeastOneReferenceById(
+ $id
+ );
+
+ $formattedRows[$id] = (array)$row;
+
+ $row = $connection->selectRow(
+ SQLStore::FT_SEARCH_TABLE,
+ [
+ 's_id',
+ 'p_id',
+ 'o_text'
+ ],
+ [
+ 's_id' => $id
+ ],
+ __METHOD__
+ );
+
+ if ( $row !== false ) {
+ $references[$id][SQLStore::FT_SEARCH_TABLE] = (array)$row;
+ }
+ }
+ }
+
+ // ID is not unique
+ if ( count( $formattedRows ) > 1 ) {
+ $id = '';
+ }
+
+ if ( $formattedRows !== [] ) {
+ $output = '<pre>' . $this->outputFormatter->encodeAsJson( $formattedRows ) . '</pre>';
+ }
+ if ( $references !== [] ) {
+
+ $msg = $id === '' ? 'smw-admin-iddispose-references-multiple' : 'smw-admin-iddispose-references';
+ $count = isset( $references[$id] ) ? count( $references[$id] ) + 1 : 0;
+ $output .= Html::rawElement(
+ 'p',
+ [],
+ $this->msg( [ $msg, $id, $count ], Message::PARSE )
+ );
+ $output .= '<pre>' . $this->outputFormatter->encodeAsJson( $references ) . '</pre>';
+ } else {
+ $error .= Html::element(
+ 'div',
+ [
+ 'class' => 'smw-callout smw-callout-warning'
+ ],
+ $this->msg( [ 'smw-admin-iddispose-no-references', $id ] )
+ );
+
+ $id = '';
+ }
+
+ return [ $output, $error ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/FulltextSearchTableRebuildJobTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/FulltextSearchTableRebuildJobTaskHandler.php
new file mode 100644
index 00000000..a2dab6d5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/FulltextSearchTableRebuildJobTaskHandler.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\Message;
+use Title;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class FulltextSearchTableRebuildJobTaskHandler extends TaskHandler {
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @var boolean
+ */
+ public $isApiTask = true;
+
+ /**
+ * @since 2.5
+ *
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( HtmlFormRenderer $htmlFormRenderer, OutputFormatter $outputFormatter ) {
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_DATAREPAIR;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isApiTask() {
+ return $this->isApiTask;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'fulltrebuild';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $subject = DIWikiPage::newFromTitle( \SpecialPage::getTitleFor( 'SMWAdmin' ) );
+
+ if ( $this->isEnabledFeature( SMW_ADM_FULLT ) && !$this->hasPendingJob() ) {
+ $this->htmlFormRenderer
+ ->addHeader( 'h4', $this->msg( 'smw-admin-fulltext-title' ) )
+ ->addParagraph( $this->msg( 'smw-admin-fulltext-intro', Message::PARSE ), [ 'class' => 'plainlinks' ] )
+ ->setMethod( 'post' )
+ ->addHiddenField( 'action', 'fulltrebuild' )
+ ->addSubmitButton(
+ $this->msg( 'smw-admin-fulltext-button' ),
+ [
+ 'class' => $this->isApiTask() ? 'smw-admin-api-job-task' : '',
+ 'data-job' => 'SMW\FulltextSearchTableRebuildJob',
+ 'data-subject' => $subject->getHash(),
+ 'data-parameters' => json_encode( [ 'mode' => '' ] )
+ ]
+ );
+ } elseif ( $this->isEnabledFeature( SMW_ADM_FULLT ) ) {
+ $this->htmlFormRenderer
+ ->addHeader( 'h4', $this->msg( 'smw-admin-fulltext-title' ) )
+ ->addParagraph( $this->msg( 'smw-admin-fulltext-intro', Message::PARSE ), [ 'class' => 'plainlinks' ] )
+ ->addParagraph(
+ Html::element(
+ 'span',
+ [
+ 'class' => 'smw-admin-circle-orange'
+ ]
+ ) . Html::element(
+ 'span',
+ [
+ 'style' => 'font-style:italic; margin-left:25px;'
+ ],
+ $this->msg( 'smw-admin-fulltext-active' )
+ )
+ );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [],
+ $this->htmlFormRenderer->getForm()
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ if ( !$this->isEnabledFeature( SMW_ADM_FULLT ) || $this->hasPendingJob() || $this->isApiTask() ) {
+ return $this->outputFormatter->redirectToRootPage( '', [ 'tab' => 'rebuild' ] );
+ }
+
+ $job = ApplicationFactory::getInstance()->newJobFactory()->newByType(
+ 'smw.fulltextSearchTableRebuild',
+ \SpecialPage::getTitleFor( 'SMWAdmin' ),
+ [
+ 'mode' => 'full'
+ ]
+ );
+
+ $job->insert();
+
+ $this->outputFormatter->redirectToRootPage( '', [ 'tab' => 'rebuild' ] );
+ }
+
+ private function hasPendingJob() {
+ return ApplicationFactory::getInstance()->getJobQueue()->hasPendingJob( 'smw.fulltextSearchTableRebuild' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/OperationalStatisticsListTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/OperationalStatisticsListTaskHandler.php
new file mode 100644
index 00000000..f08311b4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/OperationalStatisticsListTaskHandler.php
@@ -0,0 +1,204 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\Message;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class OperationalStatisticsListTaskHandler extends TaskHandler {
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @var TaskHandler[]
+ */
+ private $taskHandlers = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param OutputFormatter $outputFormatter
+ * @param TaskHandler[] $taskHandlers
+ */
+ public function __construct( OutputFormatter $outputFormatter, array $taskHandlers = [] ) {
+ $this->outputFormatter = $outputFormatter;
+ $this->taskHandlers = $taskHandlers;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SUPPLEMENT;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+
+ $actions = [
+ 'stats',
+ 'stats/cache'
+ ];
+
+ return in_array( $task, $actions );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $link = $this->outputFormatter->getSpecialPageLinkWith(
+ $this->msg( 'smw-admin-supplementary-operational-statistics-title' ),
+ [ 'action' => 'stats' ]
+ );
+
+ return Html::rawElement(
+ 'li',
+ [],
+ $this->msg(
+ [
+ 'smw-admin-supplementary-operational-statistics-intro',
+ $link
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ $action = $webRequest->getText( 'action' );
+
+ if ( $action === 'stats' ) {
+ $this->outputHead();
+ } else {
+ foreach ( $this->taskHandlers as $taskHandler ) {
+ if ( $taskHandler->isTaskFor( $action ) ) {
+ $taskHandler->setStore( $this->getStore());
+ return $taskHandler->handleRequest( $webRequest );
+ }
+ }
+ }
+
+ $this->outputSemanticStatistics();
+ $this->outputJobStatistics();
+ $this->outputInfo();
+ }
+
+ private function outputHead() {
+
+ $this->outputFormatter->setPageTitle(
+ $this->msg( 'smw-admin-supplementary-operational-statistics-title' )
+ );
+
+ $this->outputFormatter->addParentLink(
+ [ 'tab' => 'supplement' ]
+ );
+ }
+
+ private function outputInfo() {
+
+ $list = '';
+
+ foreach ( $this->taskHandlers as $taskHandler ) {
+ $list .= $taskHandler->getHtml();
+ }
+
+ $this->outputFormatter->addHTML(
+ Html::element( 'h2', [], $this->msg( 'smw-admin-other-functions' ) )
+ );
+
+ $this->outputFormatter->addHTML(
+ Html::rawElement( 'ul', [], $list )
+ );
+ }
+
+ private function outputSemanticStatistics() {
+
+ $semanticStatistics = $this->getStore()->getStatistics();
+
+ $this->outputFormatter->addHTML(
+ Html::rawElement( 'p', [], $this->msg( [ 'smw-admin-operational-statistics' ], Message::PARSE ) )
+ );
+
+ $this->outputFormatter->addHTML(
+ Html::element( 'h2', [], $this->msg( 'smw-statistics' ) )
+ );
+
+ $this->outputFormatter->addAsPreformattedText(
+ $this->outputFormatter->encodeAsJson(
+ [
+ 'propertyValues' => $semanticStatistics['PROPUSES'],
+ 'errorCount' => $semanticStatistics['ERRORUSES'],
+ 'totalProperties' => $semanticStatistics['TOTALPROPS'],
+ 'usedProperties' => $semanticStatistics['USEDPROPS'],
+ 'ownPage' => $semanticStatistics['OWNPAGE'],
+ 'declaredType' => $semanticStatistics['DECLPROPS'],
+ 'oudatedEntities' => $semanticStatistics['DELETECOUNT'],
+ 'subobjects' => $semanticStatistics['SUBOBJECTS'],
+ 'queries' => $semanticStatistics['QUERY'],
+ 'concepts' => $semanticStatistics['CONCEPTS'],
+ ]
+ )
+ );
+ }
+
+ private function outputJobStatistics() {
+
+ $this->outputFormatter->addHTML(
+ Html::element( 'h2', [], $this->msg( 'smw-admin-statistics-job-title' ) )
+ );
+
+ $this->outputFormatter->addHTML(
+ Html::rawElement( 'p', [], $this->msg( 'smw-admin-statistics-job-docu', Message::PARSE ) )
+ );
+
+ $this->outputFormatter->addHTML(
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-admin-statistics-job',
+ 'data-config' => json_encode( [
+ 'contentClass' => 'smw-admin-statistics-job-content',
+ 'errorClass' => 'smw-admin-statistics-job-error'
+ ] ),
+ ],
+ Html::element( 'div', [ 'class' => 'smw-admin-statistics-job-error' ], '' ) .
+ Html::element( 'div', [ 'class' => 'smw-admin-statistics-job-content' ], $this->msg( 'smw-data-lookup' ) )
+ )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/OutputFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/OutputFormatter.php
new file mode 100644
index 00000000..dbcada58
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/OutputFormatter.php
@@ -0,0 +1,188 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use FormatJson;
+use Html;
+use OutputPage;
+use SMW\Message;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class OutputFormatter {
+
+ /**
+ * @var OutputPage
+ */
+ private $outputPage;
+
+ /**
+ * @since 2.5
+ *
+ * @param OutputPage $outputPage
+ */
+ public function __construct( OutputPage $outputPage ) {
+ $this->outputPage = $outputPage;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $query
+ */
+ public function addParentLink( $query = [], $title = 'smw-admin-tab-supplement' ) {
+ $this->outputPage->prependHTML( $this->createParentLink( $query, $title ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $url
+ */
+ public function addHelpLink( $url ) {
+ $this->outputPage->addHelpLink( $url, true );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $title
+ */
+ public function setPageTitle( $title ) {
+ $this->outputPage->setArticleRelated( false );
+ $this->outputPage->setPageTitle( $title );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ */
+ public function addAsPreformattedText( $html ) {
+ $this->outputPage->addHTML( '<pre>' . $html . '</pre>' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $css
+ */
+ public function addInlineStyle( $css ) {
+ $this->outputPage->addInlineStyle( $css );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $html
+ */
+ public function addHTML( $html ) {
+ $this->outputPage->addHTML( $html );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ */
+ public function addWikiText( $text ) {
+ $this->outputPage->addWikiText( $text );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $fragment
+ */
+ public function redirectToRootPage( $fragment = '', $query = [] ) {
+
+ $title = \SpecialPage::getTitleFor( 'SMWAdmin' );
+ $title->setFragment( ' ' . $fragment );
+
+ $this->outputPage->redirect( $title->getFullURL( $query ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $caption
+ * @param array $query
+ */
+ public function getSpecialPageLinkWith( $caption = '', $query = [] ) {
+ return $this->createSpecialPageLink( $caption, $query );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $caption
+ * @param array $query
+ */
+ public function createSpecialPageLink( $caption = '', $query = [] ) {
+ return '<a href="' . htmlspecialchars( \SpecialPage::getTitleFor( 'SMWAdmin' )->getFullURL( $query ) ) . '">' . $caption . '</a>';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param callable $text
+ */
+ public function formatAsRaw( callable $text ) {
+ $this->outputPage->disable(); // raw output
+ ob_start();
+
+ // @codingStandardsIgnoreStart phpcs, ignore --sniffs=Generic.Files.LineLength.MaxExceeded
+ print "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\" dir=\"ltr\">\n<head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /><title>Semantic MediaWiki</title></head><body><p><pre>";
+ // @codingStandardsIgnoreEnd
+ // header( "Content-type: text/html; charset=UTF-8" );
+ $text( $this );
+ print '</pre></p>';
+ // @codingStandardsIgnoreStart phpcs, ignore --sniffs=Generic.Files.LineLength.MaxExceeded
+ print '<b> ' . $this->getSpecialPageReturnLink() . "</b>\n";
+ // @codingStandardsIgnoreEnd
+ print '</body></html>';
+
+ ob_flush();
+ flush();
+ }
+
+ /**
+ *@note JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, and
+ * JSON_UNESCAPED_UNICOD were only added with 5.4
+ *
+ * @since 2.5
+ *
+ * @param array $input
+ *
+ * @return string
+ */
+ public function encodeAsJson( array $input ) {
+
+ if ( defined( 'JSON_PRETTY_PRINT' ) ) {
+ return json_encode( $input, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+ }
+
+ return FormatJson::encode( $input, true );
+ }
+
+ private function createParentLink( $query = [], $title = 'smwadmin' ) {
+ return Html::rawElement(
+ 'div',
+ [ 'class' => 'smw-breadcrumb-link' ],
+ Html::rawElement(
+ 'span',
+ [ 'class' => 'smw-breadcrumb-arrow-right' ],
+ ''
+ ) .
+ Html::rawElement(
+ 'a',
+ [ 'href' => \SpecialPage::getTitleFor( 'SMWAdmin')->getFullURL( $query ) ],
+ Message::get( $title, Message::TEXT, Message::USER_LANGUAGE )
+ ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/PropertyStatsRebuildJobTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/PropertyStatsRebuildJobTaskHandler.php
new file mode 100644
index 00000000..c0b94382
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/PropertyStatsRebuildJobTaskHandler.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\Message;
+use Title;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyStatsRebuildJobTaskHandler extends TaskHandler {
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @var boolean
+ */
+ public $isApiTask = true;
+
+ /**
+ * @since 2.5
+ *
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( HtmlFormRenderer $htmlFormRenderer, OutputFormatter $outputFormatter ) {
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_DATAREPAIR;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isApiTask() {
+ return $this->isApiTask;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'pstatsrebuild';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $subject = DIWikiPage::newFromTitle( \SpecialPage::getTitleFor( 'SMWAdmin' ) );
+
+ // smw-admin-propertystatistics
+ $this->htmlFormRenderer
+ ->addHeader( 'h4', $this->msg( 'smw-admin-propertystatistics-title' ) )
+ ->addParagraph( $this->msg( 'smw-admin-propertystatistics-intro', Message::PARSE ), [ 'class' => 'plainlinks' ] );
+
+ if ( $this->isEnabledFeature( SMW_ADM_PSTATS ) && !$this->hasPendingJob() ) {
+ $this->htmlFormRenderer
+ ->setMethod( 'post' )
+ ->addHiddenField( 'action', 'pstatsrebuild' )
+ ->addSubmitButton(
+ $this->msg( 'smw-admin-propertystatistics-button' ),
+ [
+ 'class' => $this->isApiTask() ? 'smw-admin-api-job-task' : '',
+ 'data-job' => 'SMW\PropertyStatisticsRebuildJob',
+ 'data-subject' => $subject->getHash()
+ ]
+ );
+ } elseif ( $this->isEnabledFeature( SMW_ADM_PSTATS ) ) {
+ $this->htmlFormRenderer->addParagraph(
+ Html::element(
+ 'span',
+ [
+ 'class' => 'smw-admin-circle-orange'
+ ]
+ ) . Html::element(
+ 'span',
+ [
+ 'style' => 'font-style:italic; margin-left:25px;'
+ ],
+ $this->msg( 'smw-admin-propertystatistics-active' )
+ )
+ );
+ } else {
+ $this->htmlFormRenderer->addParagraph(
+ $this->msg( 'smw-admin-feature-disabled' )
+ );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [],
+ $this->htmlFormRenderer->getForm()
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ if ( !$this->isEnabledFeature( SMW_ADM_PSTATS ) || $this->hasPendingJob() || $this->isApiTask() ) {
+ return $this->outputFormatter->redirectToRootPage( '', [ 'tab' => 'rebuild' ] );
+ }
+
+ $job = ApplicationFactory::getInstance()->newJobFactory()->newByType(
+ 'smw.propertyStatisticsRebuild',
+ \SpecialPage::getTitleFor( 'SMWAdmin' )
+ );
+
+ $job->insert();
+
+ $this->outputFormatter->redirectToRootPage( '', [ 'tab' => 'rebuild' ] );
+ }
+
+ private function hasPendingJob() {
+ return ApplicationFactory::getInstance()->getJobQueue()->hasPendingJob( 'smw.propertyStatisticsRebuild' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/SupportListTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/SupportListTaskHandler.php
new file mode 100644
index 00000000..91b4065f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/SupportListTaskHandler.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SupportListTaskHandler extends TaskHandler {
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @since 2.5
+ *
+ * @param HtmlFormRenderer $htmlFormRenderer
+ */
+ public function __construct( HtmlFormRenderer $htmlFormRenderer ) {
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SUPPORT;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $html = $this->createSupportForm() . $this->createRegistryForm();
+ $html .= Html::element( 'p', [], '' );
+
+ return Html::rawElement( 'div', [], $html );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function createSupportForm() {
+ $this->htmlFormRenderer
+ ->setName( 'support' )
+ ->addHeader( 'h3', $this->msg('smw-admin-support' ) )
+ ->addParagraph( $this->msg( 'smw-admin-supportdocu' ) )
+ ->addParagraph(
+ Html::rawElement( 'ul', [],
+ Html::rawElement( 'li', [], $this->msg( 'smw-admin-installfile' ) ) .
+ Html::rawElement( 'li', [], $this->msg( 'smw-admin-smwhomepage' ) ) .
+ Html::rawElement( 'li', [], $this->msg( 'smw-admin-bugsreport' ) ) .
+ Html::rawElement( 'li', [], $this->msg( 'smw-admin-questions' ) )
+ )
+ );
+
+ return $this->htmlFormRenderer->getForm();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function createRegistryForm() {
+
+ $this->htmlFormRenderer
+ ->setName( 'announce' )
+ ->setMethod( 'get' )
+ ->setActionUrl( 'https://wikiapiary.com/wiki/WikiApiary:Semantic_MediaWiki_Registry' )
+ ->addHeader( 'h3', $this->msg( 'smw-admin-announce' ) )
+ ->addParagraph( $this->msg( 'smw-admin-announce-text' ) )
+ ->addSubmitButton(
+ $this->msg( 'smw-admin-announce' ),
+ [
+ 'class' => ''
+ ]
+ );
+
+ return $this->htmlFormRenderer->getForm();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TableSchemaTaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TableSchemaTaskHandler.php
new file mode 100644
index 00000000..592528d2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TableSchemaTaskHandler.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use Html;
+use Onoi\MessageReporter\MessageReporterFactory;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\Store;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class TableSchemaTaskHandler extends TaskHandler {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( Store $store, HtmlFormRenderer $htmlFormRenderer, OutputFormatter $outputFormatter ) {
+ $this->store = $store;
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getSection() {
+ return self::SECTION_SCHEMA;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function hasAction() {
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function isTaskFor( $task ) {
+ return $task === 'updatetables';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getHtml() {
+
+ $this->htmlFormRenderer
+ ->setName( 'buildtables' )
+ ->setMethod( 'get' )
+ ->addHiddenField( 'action', 'updatetables' )
+ ->addHeader( 'h3', $this->msg( 'smw-admin-db' ) )
+ ->addParagraph( $this->msg( 'smw-admin-dbdocu' ) );
+
+ if ( $this->isEnabledFeature( SMW_ADM_SETUP ) ) {
+ $this->htmlFormRenderer
+ ->addHiddenField( 'udsure', 'yes' )
+ ->addSubmitButton(
+ $this->msg( 'smw-admin-dbbutton' ),
+ [
+ 'class' => ''
+ ]
+ );
+ } else {
+ $this->htmlFormRenderer
+ ->addParagraph( $this->msg( 'smw-admin-feature-disabled' ) );
+ }
+
+ return Html::rawElement( 'div', [], $this->htmlFormRenderer->getForm() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function handleRequest( WebRequest $webRequest ) {
+
+ if ( !$this->isEnabledFeature( SMW_ADM_SETUP ) ) {
+ return;
+ }
+
+ $this->outputFormatter->setPageTitle( $this->msg( 'smw-admin-db' ) );
+ $this->outputFormatter->addParentLink( [ 'tab' => 'rebuild' ] );
+
+ $messageReporter = MessageReporterFactory::getInstance()->newObservableMessageReporter();
+
+ $messageReporter->registerReporterCallback(
+ [
+ $this,
+ 'reportMessage'
+ ]
+ );
+
+ $this->store->setMessageReporter(
+ $messageReporter
+ );
+
+ $preparation = $webRequest->getVal( 'prep' );
+ $result = false;
+
+ $msg = Html::rawElement(
+ 'p',
+ [],
+ $this->msg( 'smw-admin-permissionswarn' )
+ );
+
+ // Reload (via JS) the page once content is displayed as separate page to inform
+ // the user about a possible delay in processing
+ if ( $preparation !== 'done' ) {
+ $this->outputFormatter->addHTML(
+ $msg . Html::rawElement(
+ 'div',
+ [
+ 'style' => 'opacity:0.5;position: relative;'
+ ],
+ Html::rawElement(
+ 'pre',
+ [
+ 'class' => 'smw-admin-db-preparation'
+ ],
+ $this->msg( 'smw-admin-db-preparation' ) .
+ "\n\n" . $this->msg( 'smw-processing' ) . "\n" .
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-overlay-spinner medium',
+ 'style' => 'transform: translate(-50%, -50%);'
+ ]
+ )
+ )
+ )
+ );
+ } else {
+ $this->outputFormatter->addHTML( $msg );
+ $this->outputFormatter->addHTML( '<pre>' );
+ $this->store->setup();
+ $this->outputFormatter->addHTML( '</pre>' );
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $message
+ */
+ public function reportMessage( $message ) {
+ $this->outputFormatter->addHTML( $message );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TaskHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TaskHandler.php
new file mode 100644
index 00000000..0a8afbc1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TaskHandler.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use SMW\Message;
+use SMW\Store;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+abstract class TaskHandler {
+
+ /**
+ * Identifies an individual section to where the task is associated with.
+ */
+ const SECTION_SUPPLEMENT = 'section.supplement';
+ const SECTION_SCHEMA = 'section.schema';
+ const SECTION_DATAREPAIR = 'section.datarepair';
+ const SECTION_DEPRECATION ='section.deprecation';
+ const SECTION_SUPPORT ='section.support';
+
+ /**
+ * @var integer
+ */
+ private $enabledFeatures = 0;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var boolean
+ */
+ protected $isApiTask = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $feature
+ *
+ * @return boolean
+ */
+ public function isEnabledFeature( $feature ) {
+ return ( ( (int)$this->enabledFeatures & $feature ) == $feature );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $enabledFeatures
+ */
+ public function setEnabledFeatures( $enabledFeatures ) {
+ $this->enabledFeatures = $enabledFeatures;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function setStore( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Store
+ */
+ public function getStore() {
+ return $this->store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getSection() {
+ return '';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function isApiTask() {
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasAction() {
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ abstract public function isTaskFor( $task );
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ abstract public function getHtml();
+
+ /**
+ * @since 2.5
+ *
+ * @param WebRequest $webRequest
+ */
+ abstract public function handleRequest( WebRequest $webRequest );
+
+ protected function msg( $key, $type = Message::TEXT ) {
+ return Message::get( $key, $type, Message::USER_LANGUAGE );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TaskHandlerFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TaskHandlerFactory.php
new file mode 100644
index 00000000..e60b6085
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Admin/TaskHandlerFactory.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Admin;
+
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class TaskHandlerFactory {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var OutputFormatter
+ */
+ private $outputFormatter;
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param OutputFormatter $outputFormatter
+ */
+ public function __construct( Store $store, HtmlFormRenderer $htmlFormRenderer, OutputFormatter $outputFormatter ) {
+ $this->store = $store;
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->outputFormatter = $outputFormatter;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return []
+ */
+ public function getTaskHandlerList( $user, $adminFeatures ) {
+
+ $taskHandlers = [
+ // TaskHandler::SECTION_SCHEMA
+ $this->newTableSchemaTaskHandler(),
+
+ // TaskHandler::SECTION_DATAREPAIR
+ $this->newDataRefreshJobTaskHandler(),
+ $this->newDisposeJobTaskHandler(),
+ $this->newPropertyStatsRebuildJobTaskHandler(),
+ $this->newFulltextSearchTableRebuildJobTaskHandler(),
+
+ // TaskHandler::SECTION_DEPRECATION
+ $this->newDeprecationNoticeTaskHandler(),
+
+ // TaskHandler::SECTION_SUPPLEMENT
+ $this->newConfigurationListTaskHandler(),
+ $this->newOperationalStatisticsListTaskHandler(),
+ $this->newDuplicateLookupTaskHandler(),
+ $this->newEntityLookupTaskHandler( $user ),
+
+ // TaskHandler::SECTION_SUPPORT
+ $this->newSupportListTaskHandler()
+ ];
+
+ \Hooks::run( 'SMW::Admin::TaskHandlerFactory', [ &$taskHandlers, $this->store, $this->outputFormatter, $user ] );
+
+ $taskHandlerList = [
+ TaskHandler::SECTION_SCHEMA => [],
+ TaskHandler::SECTION_DATAREPAIR => [],
+ TaskHandler::SECTION_DEPRECATION => [],
+ TaskHandler::SECTION_SUPPLEMENT => [],
+ TaskHandler::SECTION_SUPPORT => [],
+ 'actions' => []
+ ];
+
+ foreach ( $taskHandlers as $taskHandler ) {
+
+ if ( !is_a( $taskHandler, 'SMW\MediaWiki\Specials\Admin\TaskHandler' ) ) {
+ continue;
+ }
+
+ $taskHandler->setEnabledFeatures(
+ $adminFeatures
+ );
+
+ $taskHandler->setStore(
+ $this->store
+ );
+
+ switch ( $taskHandler->getSection() ) {
+ case TaskHandler::SECTION_SCHEMA:
+ $taskHandlerList[TaskHandler::SECTION_SCHEMA][] = $taskHandler;
+ break;
+ case TaskHandler::SECTION_DATAREPAIR:
+ $taskHandlerList[TaskHandler::SECTION_DATAREPAIR][] = $taskHandler;
+ break;
+ case TaskHandler::SECTION_DEPRECATION:
+ $taskHandlerList[TaskHandler::SECTION_DEPRECATION][] = $taskHandler;
+ break;
+ case TaskHandler::SECTION_SUPPLEMENT:
+ $taskHandlerList[TaskHandler::SECTION_SUPPLEMENT][] = $taskHandler;
+ break;
+ case TaskHandler::SECTION_SUPPORT:
+ $taskHandlerList[TaskHandler::SECTION_SUPPORT][] = $taskHandler;
+ break;
+ }
+
+ if ( $taskHandler->hasAction() ) {
+ $taskHandlerList['actions'][] = $taskHandler;
+ }
+ }
+
+ return $taskHandlerList;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return TableSchemaTaskHandler
+ */
+ public function newTableSchemaTaskHandler() {
+ return new TableSchemaTaskHandler( $this->store, $this->htmlFormRenderer, $this->outputFormatter );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return SupportListTaskHandler
+ */
+ public function newSupportListTaskHandler() {
+ return new SupportListTaskHandler( $this->htmlFormRenderer );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return ConfigurationListTaskHandler
+ */
+ public function newConfigurationListTaskHandler() {
+ return new ConfigurationListTaskHandler( $this->outputFormatter );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return OperationalStatisticsListTaskHandler
+ */
+ public function newOperationalStatisticsListTaskHandler() {
+
+ $taskHandlers = [
+ new CacheStatisticsListTaskHandler( $this->outputFormatter )
+ ];
+
+ return new OperationalStatisticsListTaskHandler( $this->outputFormatter, $taskHandlers );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return EntityLookupTaskHandler
+ */
+ public function newEntityLookupTaskHandler( $user = null ) {
+
+ $entityLookupTaskHandler = new EntityLookupTaskHandler(
+ $this->store,
+ $this->htmlFormRenderer,
+ $this->outputFormatter
+ );
+
+ $entityLookupTaskHandler->setUser(
+ $user
+ );
+
+ return $entityLookupTaskHandler;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DataRefreshJobTaskHandler
+ */
+ public function newDataRefreshJobTaskHandler() {
+ return new DataRefreshJobTaskHandler( $this->htmlFormRenderer, $this->outputFormatter );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DisposeJobTaskHandler
+ */
+ public function newDisposeJobTaskHandler() {
+ return new DisposeJobTaskHandler( $this->htmlFormRenderer, $this->outputFormatter );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return PropertyStatsRebuildJobTaskHandler
+ */
+ public function newPropertyStatsRebuildJobTaskHandler() {
+ return new PropertyStatsRebuildJobTaskHandler( $this->htmlFormRenderer, $this->outputFormatter );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return FulltextSearchTableRebuildJobTaskHandler
+ */
+ public function newFulltextSearchTableRebuildJobTaskHandler() {
+ return new FulltextSearchTableRebuildJobTaskHandler( $this->htmlFormRenderer, $this->outputFormatter );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return DeprecationNoticeTaskHandler
+ */
+ public function newDeprecationNoticeTaskHandler() {
+ return new DeprecationNoticeTaskHandler( $this->outputFormatter, $GLOBALS['smwgDeprecationNotices'] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return DuplicateLookupTaskHandler
+ */
+ public function newDuplicateLookupTaskHandler() {
+ return new DuplicateLookupTaskHandler( $this->outputFormatter );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/DownloadLinksWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/DownloadLinksWidget.php
new file mode 100644
index 00000000..e46f26aa
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/DownloadLinksWidget.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Message;
+use SMWInfolink as Infolink;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DownloadLinksWidget {
+
+ /**
+ * @since 3.0
+ *
+ * @param Infolink|null $infolink
+ *
+ * @return string
+ */
+ public static function downloadLinks( Infolink $infolink = null ) {
+
+ if ( $infolink === null ) {
+ return '';
+ }
+
+ // Avoid modifying the original object
+ $infolink = clone $infolink;
+ $downloadLinks = [];
+
+ $infolink->setParameter( 'true', 'prettyprint' );
+ $infolink->setParameter( 'true', 'unescape' );
+ $infolink->setParameter( 'json', 'format' );
+ $infolink->setParameter( 'JSON', 'searchlabel' );
+ $infolink->setCaption( 'JSON' );
+
+ $infolink->setLinkAttributes(
+ [
+ 'title' => Message::get( [ 'smw-ask-download-link-desc', 'JSON' ], Message::TEXT, Message::USER_LANGUAGE ),
+ 'class' => 'page-link'
+ ]
+ );
+
+ $downloadLinks[] = $infolink->getHtml();
+
+ $infolink->setCaption( 'CSV' );
+ $infolink->setParameter( 'csv', 'format' );
+ $infolink->setParameter( 'CSV', 'searchlabel' );
+
+ $infolink->setLinkAttributes(
+ [
+ 'title' => Message::get( [ 'smw-ask-download-link-desc', 'CSV' ], Message::TEXT, Message::USER_LANGUAGE ),
+ 'class' => 'page-link'
+ ]
+ );
+
+ $downloadLinks[] = $infolink->getHtml();
+
+ $infolink->setCaption( 'RSS' );
+ $infolink->setParameter( 'rss', 'format' );
+ $infolink->setParameter( 'RSS', 'searchlabel' );
+
+ $infolink->setLinkAttributes(
+ [
+ 'title' => Message::get( [ 'smw-ask-download-link-desc', 'RSS' ], Message::TEXT, Message::USER_LANGUAGE ),
+ 'class' => 'page-link'
+ ]
+ );
+
+ $downloadLinks[] = $infolink->getHtml();
+
+ $infolink->setCaption( 'RDF' );
+ $infolink->setParameter( 'rdf', 'format' );
+ $infolink->setParameter( 'RDF', 'searchlabel' );
+
+ $infolink->setLinkAttributes(
+ [
+ 'title' => Message::get( [ 'smw-ask-download-link-desc', 'RDF' ], Message::TEXT, Message::USER_LANGUAGE ),
+ 'class' => 'page-link'
+ ]
+ );
+
+ $downloadLinks[] = $infolink->getHtml();
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'ask-export-links',
+ 'class' => 'smw-ask-downloadlinks export-links'
+ ],
+ '<div class="smw-ui-pagination">' . implode( '', $downloadLinks ) . '</div>'
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ErrorWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ErrorWidget.php
new file mode 100644
index 00000000..2834f177
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ErrorWidget.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Message;
+use SMW\ProcessingErrorMsgHandler;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ErrorWidget {
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function disabled() {
+ return Html::element(
+ 'div',
+ [
+ 'class' => 'smw-callout smw-callout-error'
+ ],
+ Message::get( 'smw_iq_disabled', Message::TEXT, Message::USER_LANGUAGE )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public static function noResult() {
+ return Html::element(
+ 'div',
+ [
+ 'id' => 'no-result',
+ 'class' => 'smw-callout smw-callout-info'
+ ],
+ Message::get( 'smw_result_noresults', Message::TEXT, Message::USER_LANGUAGE )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function noScript() {
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'ask-status',
+ 'class' => 'smw-ask-status plainlinks'
+ ],
+ Html::rawElement(
+ 'noscript',
+ [],
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-callout smw-callout-error',
+ ],
+ Message::get( 'smw-noscript', Message::PARSE, Message::USER_LANGUAGE )
+ )
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function sessionFailure() {
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-callout smw-callout-error'
+ ],
+ Message::get( 'sessionfailure', Message::TEXT, Message::USER_LANGUAGE )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Query|null $query
+ *
+ * @return string
+ */
+ public static function queryError( Query $query = null ) {
+
+ if ( $query === null || !is_array( $query->getErrors() ) || $query->getErrors() === [] ) {
+ return '';
+ }
+
+ $errors = [];
+
+ foreach ( ProcessingErrorMsgHandler::normalizeAndDecodeMessages( $query->getErrors() ) as $value ) {
+
+ if ( $value === '' ) {
+ continue;
+ }
+
+ if ( is_array( $value ) ) {
+ $value = implode( " ", $value );
+ }
+
+ $errors[] = $value;
+ }
+
+ if ( count( $errors ) > 1 ) {
+ $error = '<ul><li>' . implode( '</li><li>', $errors ) . '</li></ul>';
+ } else {
+ $error = implode( ' ', $errors );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'result-error',
+ 'class' => 'smw-callout smw-callout-error'
+ ],
+ $error
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/FormatListWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/FormatListWidget.php
new file mode 100644
index 00000000..83b3cbcb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/FormatListWidget.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Message;
+use SMWQueryProcessor as QueryProcessor;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FormatListWidget {
+
+ /**
+ * @var array
+ */
+ private static $resultFormats = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param array $resultFormats
+ */
+ public static function setResultFormats( array $resultFormats ) {
+ self::$resultFormats = $resultFormats;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $params
+ *
+ * @return string
+ */
+ public static function selectList( Title $title, array $params ) {
+
+ $result = '';
+
+ // Default
+ $printer = QueryProcessor::getResultPrinter(
+ 'broadtable',
+ QueryProcessor::SPECIAL_PAGE
+ );
+
+ $url = $title->getLocalURL( 'showformatoptions=this.value' );
+
+ foreach ( $params as $param => $value ) {
+ if ( $param !== 'format' ) {
+ $url .= '&params[' . rawurlencode( $param ) . ']=' . rawurlencode( $value );
+ }
+ }
+
+ $defaultLocalizedName = htmlspecialchars( $printer->getName() ) . ' (' . Message::get( 'smw_ask_defaultformat', Message::TEXT, Message::USER_LANGUAGE ) . ')';
+ $defaultName = $printer->getName();
+
+ $default = '';
+ $selectedFormat = isset( $params['format'] ) ? $params['format'] : 'broadtable';
+
+ $formatList = self::formatList(
+ $url,
+ $selectedFormat,
+ $default,
+ $defaultName,
+ $defaultLocalizedName
+ );
+
+ $result = Html::rawElement(
+ 'span',
+ [
+ 'class' => "smw-ask-format-list"
+ ],
+ Html::hidden( 'eq', 'yes' ) . $formatList
+ );
+
+ return $result;
+ }
+
+ private static function formatList( $url, $selectedFormat, &$default, $defaultName, $defaultLocalizedName ) {
+
+ $formatList = Html::rawElement(
+ 'option',
+ [
+ 'value' => 'broadtable'
+ ] + ( $selectedFormat == 'broadtable' ? [ 'selected' ] : [] ),
+ $defaultLocalizedName
+ );
+
+ $formats = [];
+
+ foreach ( array_keys( self::$resultFormats ) as $format ) {
+ // Special formats "count" and "debug" currently not supported.
+ if ( $format != 'broadtable' && $format != 'count' && $format != 'debug' ) {
+ $printer = QueryProcessor::getResultPrinter(
+ $format,
+ QueryProcessor::SPECIAL_PAGE
+ );
+
+ $formats[] = [
+ 'format' => $format,
+ 'name' => htmlspecialchars( $printer->getName() ),
+ 'export' => $printer->isExportFormat()
+ ];
+ }
+ }
+
+ usort( $formats, function( $x, $y ) {
+ return strcasecmp( $x['name'] , $y['name'] );
+ } );
+
+ $default = $defaultName;
+
+ foreach ( $formats as $format ) {
+
+ $formatList .= Html::rawElement(
+ 'option',
+ [
+ 'data-isexport' => $format['export'],
+ 'value' => $format['format']
+ ] + ( $selectedFormat == $format['format'] ? [ 'selected' ] : [] ),
+ $format['name']
+ );
+
+ if ( $selectedFormat == $format['format'] ) {
+ $default = $format['name'];
+ }
+ }
+
+ $default = Html::rawElement(
+ 'a',
+ [
+ 'href' => 'https://semantic-mediawiki.org/wiki/Help:' . $selectedFormat . ' format'
+ ],
+ $default
+ );
+
+ return Html::rawElement(
+ 'select',
+ [
+ 'id' => 'formatSelector', // Used in JS as selector
+ 'class' => 'smw-ask-button smw-ask-button-lgrey smw-ask-format-selector',
+ 'name' => 'p[format]',
+ 'data-url' => $url
+ ],
+ $formatList
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/HelpWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/HelpWidget.php
new file mode 100644
index 00000000..3a583848
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/HelpWidget.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Message;
+use SMW\Utils\HtmlModal;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HelpWidget {
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function html() {
+
+ $format = 'broadtable' ;
+ $text = Message::get( 'smw-ask-help', Message::PARSE, Message::USER_LANGUAGE );
+
+ $text .= Html::rawElement(
+ 'div',
+ [
+ 'class' => 'strike',
+ 'style' => 'padding: 5px 0 5px 0;'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'style' => 'font-size: 1.2em; margin-left:0px'
+ ],
+ Message::get( 'smw-ask-format', Message::TEXT, Message::USER_LANGUAGE )
+ ) . Html::rawElement(
+ 'ul',
+ [],
+ Html::rawElement(
+ 'li',
+ [
+ 'class' => 'smw-ask-format-help-link'
+ ],
+ Message::get( [ 'smw-ask-format-help-link', $format ], Message::PARSE, Message::USER_LANGUAGE )
+ )
+ )
+ );
+
+ $text .= Html::rawElement(
+ 'div',
+ [
+ 'class' => 'strike',
+ 'style' => 'padding: 5px 0 5px 0;'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'style' => 'font-size: 1.2em; margin-left:0px'
+ ],
+ Message::get( 'smw-ask-input-assistance', Message::TEXT, Message::USER_LANGUAGE )
+ )
+ );
+
+ $text .= Message::get( 'smw-ask-condition-input-assistance', Message::PARSE, Message::USER_LANGUAGE );
+
+ $text .= Html::rawElement(
+ 'ul',
+ [],
+ Html::rawElement(
+ 'li',
+ [],
+ Message::get( 'smw-ask-condition-input-assistance-property', Message::TEXT, Message::USER_LANGUAGE )
+ ) .
+ Html::rawElement(
+ 'li',
+ [],
+ Message::get( 'smw-ask-condition-input-assistance-category', Message::TEXT, Message::USER_LANGUAGE )
+ ) .
+ Html::rawElement(
+ 'li',
+ [],
+ Message::get( 'smw-ask-condition-input-assistance-concept', Message::TEXT, Message::USER_LANGUAGE )
+ )
+ );
+
+ $html = HtmlModal::modal(
+ Message::get( 'smw-cheat-sheet', Message::TEXT, Message::USER_LANGUAGE ),
+ $text,
+ [
+ 'id' => 'ask-help',
+ 'class' => 'plainlinks',
+ 'style' => 'display:none;'
+ ]
+ );
+
+ return $html;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/HtmlForm.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/HtmlForm.php
new file mode 100644
index 00000000..8c4eaf88
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/HtmlForm.php
@@ -0,0 +1,381 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Message;
+use Title;
+use SMWQueryResult as QueryResult;
+use SMW\Utils\HtmlTabs;
+use SMW\Query\QueryLinker;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HtmlForm {
+
+ /**
+ * @var Title
+ */
+ private $title;
+
+ /**
+ * @var array
+ */
+ private $parameters = [];
+
+ /**
+ * @var string
+ */
+ private $queryString = '';
+
+ /**
+ * @var Query
+ */
+ private $query;
+
+ /**
+ * @var array
+ */
+ private $callbacks = [];
+
+ /**
+ * @var boolean
+ */
+ private $isEditMode = true;
+
+ /**
+ * @var boolean
+ */
+ private $isBorrowedMode = false;
+
+ /**
+ * @var boolean
+ */
+ private $isPostSubmit = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ */
+ public function __construct( Title $title ) {
+ $this->title = $title;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ */
+ public function setParameters( array $parameters ) {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $queryString
+ */
+ public function setQueryString( $queryString ) {
+ $this->queryString = $queryString;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ */
+ public function setQuery( Query $query = null ) {
+ $this->query = $query;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $callbacks
+ */
+ public function setCallbacks( array $callbacks ) {
+ $this->callbacks = $callbacks;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isEditMode
+ */
+ public function isEditMode( $isEditMode ) {
+ $this->isEditMode = (bool)$isEditMode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isBorrowedMode
+ */
+ public function isBorrowedMode( $isBorrowedMode ) {
+ $this->isBorrowedMode = (bool)$isBorrowedMode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isPostSubmit
+ */
+ public function isPostSubmit( $isPostSubmit ) {
+ $this->isPostSubmit = (bool)$isPostSubmit;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param UrlArgs $urlArgs
+ * @param QueryResult|string|null $queryResult
+ *
+ * @return string
+ */
+ public function getForm( UrlArgs $urlArgs, $queryResult = null, $text = '' ) {
+
+ $html = $this->buildHTML( $urlArgs, $queryResult, $text );
+
+ if ( $this->isPostSubmit ) {
+ $params = [
+ 'action' => $this->title->getLocalUrl( wfArrayToCGI( $urlArgs ) . '#search' ),
+ 'name' => 'ask',
+ 'method' => 'post'
+ ];
+ } else {
+ $params = [
+ 'action' => $GLOBALS['wgScript'],
+ 'name' => 'ask',
+ 'method' => 'get'
+ ];
+ }
+
+ return Html::rawElement( 'form', $params, $html );
+ }
+
+ private function buildHTML( $urlArgs, $queryResult, $infoText ) {
+
+ $navigation = '';
+ $queryLink = null;
+ $isFromCache = false;
+
+ if ( $queryResult instanceof QueryResult ) {
+ $navigation = NavigationLinksWidget::navigationLinks(
+ $this->title,
+ $urlArgs,
+ $queryResult->getCount(),
+ $queryResult->hasFurtherResults()
+ );
+
+ $isFromCache = $queryResult->isFromCache();
+
+ if ( $this->query !== null ) {
+ $queryLink = QueryLinker::get( $this->query, $this->parameters );
+ } elseif ( ( $query = $queryResult->getQuery() ) !== null ) {
+ $queryLink = QueryLinker::get( $query, $this->parameters );
+ }
+ }
+
+ $html = '';
+ $hideForm = false;
+ $urlArgs->set( 'eq', 'yes' );
+
+ $htmlTabs = new HtmlTabs();
+ $htmlTabs->setGroup( 'ask' );
+ $htmlTabs->setActiveTab( 'smw-askt-result' );
+
+ if ( $this->isEditMode ) {
+ $html = $this->editElements( $urlArgs );
+ $hideForm = true;
+ }
+
+ $isEmpty = $queryLink === null;
+ $editLink = $this->title->getLocalURL( $urlArgs );
+
+ // Submit
+ $html .= LinksWidget::resultSubmitLink(
+ $hideForm
+ );
+
+ if ( !$this->isEditMode && !$isEmpty ) {
+ $htmlTabs->tab(
+ 'smw-askt-edit',
+ LinksWidget::editLink( $editLink ),
+ [
+ 'hide' => $this->isBorrowedMode,
+ 'class' => 'edit-action'
+ ]
+ );
+ } elseif ( !$isEmpty ) {
+ $htmlTabs->tab(
+ 'smw-askt-compact',
+ LinksWidget::hideLink( $editLink ),
+ [
+ 'hide' => $this->isBorrowedMode,
+ 'class' => 'compact-action'
+ ]
+ );
+ }
+
+ $htmlTabs->tab(
+ 'smw-askt-result',
+ wfMessage( 'smw-ask-tab-result' )->text(),
+ [
+ 'hide' => $isEmpty,
+ 'class' => $isFromCache ? ' result-cache' : ''
+ ]
+ );
+
+ $links = [];
+
+ $htmlTabs->tab(
+ 'smw-askt-code',
+ wfMessage( 'smw-ask-tab-code' )->text(),
+ [
+ 'hide' => $this->isBorrowedMode || $isEmpty
+ ]
+ );
+
+ $code = '';
+
+ if ( isset( $this->callbacks['code_handler'] ) && is_callable( $this->callbacks['code_handler'] ) ) {
+ $code = $this->callbacks['code_handler']();
+ }
+
+ $htmlTabs->content(
+ 'smw-askt-code',
+ '<div style="margin-top:15px; margin-bottom:15px;">' .
+ LinksWidget::embeddedCodeBlock( $code, true ) . '</div>'
+ );
+
+ $clipboardLink = LinksWidget::clipboardLink( $queryLink );
+
+ $htmlTabs->tab(
+ 'smw-askt-clipboard',
+ $clipboardLink,
+ [
+ 'hide' => $clipboardLink === '',
+ 'class' => 'clipboard-bookmark smw-tab-right'
+ ]
+ );
+
+ if ( !isset( $this->parameters['source'] ) || $this->parameters['source'] === '' ) {
+ $debugLink = LinksWidget::debugLink( $this->title, $urlArgs, $isEmpty, true );
+
+ $htmlTabs->tab(
+ 'smw-askt-debug',
+ $debugLink,
+ [
+ 'hide' => $debugLink === '' || !$this->isEditMode ,
+ 'class' => 'smw-tab-right'
+ ]
+ );
+ }
+
+ if ( isset( $this->callbacks['borrowed_msg_handler'] ) && is_callable( $this->callbacks['borrowed_msg_handler'] ) ) {
+ $this->callbacks['borrowed_msg_handler']( $links, $infoText );
+ }
+
+ $basicLinks = NavigationLinksWidget::basicLinks(
+ $navigation,
+ $queryLink
+ );
+
+ $htmlTabs->content( 'smw-askt-result', $basicLinks );
+
+ if ( !$isEmpty ) {
+ $htmlTabs->tab(
+ 'smw-askt-extra',
+ wfMessage( 'smw-ask-tab-extra' )->text(),
+ [
+ 'class' => 'smw-tab-right'
+ ]
+ );
+
+ if ( is_array( $links ) ) {
+ $links[] = $infoText;
+
+ // External source cannot disable the cache
+ if ( isset( $this->parameters['source'] ) && $this->parameters['source'] !== '' ) {
+ $isFromCache = false;
+ }
+
+ if ( ( $noCacheLink = LinksWidget::noQCacheLink( $this->title, $urlArgs, $isFromCache ) ) !== '' ) {
+ $links[] = $noCacheLink;
+ }
+
+ $infoText = '<ul><li>' . implode( '</li><li>', $links ) . '</li></ul>';
+ } else {
+ $infoText = $links;
+ }
+
+ $htmlTabs->content(
+ 'smw-askt-extra',
+ '<div style="margin-top:15px;margin-bottom:20px;">' . $infoText . '</div>'
+ );
+ }
+
+ $html .= $htmlTabs->buildHTML(
+ [
+ 'id' => 'search',
+ 'class' => $this->isEditMode ? 'smw-ask-search-edit' . ( $isEmpty ? ' empty-result' : '' ) : 'smw-ask-search-compact'
+ ]
+ );
+
+ return $html;
+ }
+
+ private function editElements( $urlArgs ) {
+ $html = '';
+
+ $html .= Html::hidden( 'title', $this->title->getPrefixedDBKey() );
+ $html .= Html::hidden( '_action', 'submit' );
+
+ // Table for main query and printouts.
+ $html .= Html::rawElement(
+ 'div',
+ [
+ 'id' => 'query',
+ 'class' => 'smw-ask-query'
+ ],
+ QueryInputWidget::table(
+ $this->queryString,
+ $urlArgs->get( 'po' )
+ )
+ );
+
+ // Format selection
+ $html .= Html::rawElement(
+ 'div',
+ [
+ 'id' => 'format',
+ 'class' => "smw-ask-format"
+ ],
+ ''
+ );
+
+ // Other options fieldset
+ $html .= Html::rawElement(
+ 'div',
+ [
+ 'id' => 'options',
+ 'class' => 'smw-ask-options'
+ ],
+ ParametersWidget::fieldset(
+ $this->title,
+ $this->parameters
+ )
+ );
+
+ $urlArgs->set( 'eq', 'no' );
+
+ return $html;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/LinksWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/LinksWidget.php
new file mode 100644
index 00000000..923fb662
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/LinksWidget.php
@@ -0,0 +1,375 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Message;
+use SMWInfolink as Infolink;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class LinksWidget {
+
+ /**
+ * @return array
+ */
+ public static function getModules() {
+ return [ 'onoi.clipboard' ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ *
+ * @return string
+ */
+ public static function fieldset( $html = '' ) {
+
+ $html = '<p></p>' . $html;
+
+ return Html::rawElement(
+ 'fieldset',
+ [],
+ Html::rawElement(
+ 'legend',
+ [],
+ Message::get( 'smw-ask-search', Message::TEXT, Message::USER_LANGUAGE )
+ ) . $html
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $isEmpty
+ *
+ * @return string
+ */
+ public static function embeddedCodeLink( $isEmpty = false ) {
+
+ if ( $isEmpty ) {
+ return '';
+ }
+
+ //show|hide inline embed code
+ $embedShow = "document.getElementById('inlinequeryembed').style.display='block';" .
+ "document.getElementById('embed_hide').style.display='inline';" .
+ "document.getElementById('embed_show').style.display='none';" .
+ "document.getElementById('inlinequeryembedarea').select();";
+
+ $embedHide = "document.getElementById('inlinequeryembed').style.display='none';" .
+ "document.getElementById('embed_show').style.display='inline';" .
+ "document.getElementById('embed_hide').style.display='none';";
+
+ return Html::rawElement(
+ 'span',
+ [
+ 'id' => 'ask-embed',
+ 'class' => 'smw-ask-button smw-ask-button-lblue'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'id' => 'embed_show'
+ ], Html::rawElement(
+ 'a',
+ [
+ 'href' => '#embed_show',
+ 'rel' => 'nofollow',
+ 'onclick' => $embedShow
+ ], wfMessage( 'smw_ask_show_embed' )->escaped()
+ )
+ ) . Html::rawElement(
+ 'span',
+ [
+ 'id' => 'embed_hide',
+ 'style' => 'display: none;'
+ ], Html::rawElement(
+ 'a',
+ [
+ 'href' => '#embed_hide',
+ 'rel' => 'nofollow',
+ 'onclick' => $embedHide
+ ], wfMessage( 'smw_ask_hide_embed' )->escaped()
+ )
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $href
+ *
+ * @return string
+ */
+ public static function editLink( $href ) {
+ return Html::rawElement(
+ 'a',
+ [
+ 'href' => $href . '#search',
+ 'rel' => 'href',
+ 'style' => 'display:block; width:60px'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-icon-pen',
+ 'title' => wfMessage( 'smw_ask_editquery' )->text(),
+ ],
+ ''
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $href
+ *
+ * @return string
+ */
+ public static function hideLink( $href ) {
+ return Html::rawElement(
+ 'a',
+ [
+ 'href' => $href,
+ 'rel' => 'nofollow',
+ 'style' => 'display:block; width:60px'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-icon-compact',
+ 'title' => wfMessage( 'smw_ask_hidequery' )->text()
+ ],
+ ''
+ )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $code
+ *
+ * @return string
+ */
+ public static function embeddedCodeBlock( $code, $raw = false ) {
+
+ $code = Html::rawElement(
+ 'pre',
+ [
+ 'id' => 'inlinequeryembedarea',
+ 'readonly' => 'yes',
+ 'cols' => 20,
+ 'rows' => substr_count( $code, "\n" ) + 1,
+ 'onclick' => 'this.select()'
+ ],
+ $code
+ );
+
+ if ( $raw ) {
+ return '<p>' . wfMessage( 'smw_ask_embed_instr' )->escaped() . '</p>' . $code;
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'inlinequeryembed',
+ 'style' => 'display: none;'
+ ], Html::rawElement(
+ 'div',
+ [
+ 'id' => 'inlinequeryembedinstruct'
+ ], wfMessage( 'smw_ask_embed_instr' )->escaped()
+ ) . $code
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $isEmpty
+ *
+ * @return string
+ */
+ public static function resultSubmitLink( $isEmpty = false ) {
+
+ if ( !$isEmpty ) {
+ return '';
+ }
+
+ return Html::rawElement( 'div', [ 'class' => 'smw-ask-button-submit' ], Html::element(
+ 'input',
+ [
+ 'type' => 'submit',
+ 'class' => '',
+ 'value' => wfMessage( 'smw_ask_submit' )->escaped()
+ ], ''
+ ) . ' ' . Html::element(
+ 'input',
+ [
+ 'type' => 'hidden',
+ 'name' => 'eq',
+ 'value' => 'yes'
+ ], ''
+ ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ * @param string $urlTail
+ * @param boolean $hideForm
+ * @param boolean $isEmpty
+ *
+ * @return string
+ */
+ public static function showHideLink( Title $title, UrlArgs $urlArgs, $hideForm = false, $isEmpty = false ) {
+
+ if ( $isEmpty || $hideForm === false ) {
+ return '';
+ }
+
+ return Html::rawElement(
+ 'span',
+ [
+ 'id' => 'ask-showhide',
+ 'class' => 'smw-ask-button smw-ask-button-lblue'
+ ], Html::element(
+ 'a',
+ [
+ 'href' => $title->getLocalURL( $urlArgs ),
+ 'rel' => 'nofollow'
+ ],
+ wfMessage( ( $hideForm ? 'smw_ask_hidequery' : 'smw_ask_editquery' ) )->text()
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param string $urlTail
+ * @param boolean $isEmpty
+ *
+ * @return string
+ */
+ public static function debugLink( Title $title, UrlArgs $urlArgs, $isEmpty = false, $raw = false ) {
+
+ if ( $isEmpty ) {
+ return '';
+ }
+
+ $urlArgs->set( 'eq', 'yes' );
+ $urlArgs->set( 'debug', 'true' );
+ $urlArgs->setFragment( 'search' );
+
+ $link = Html::element(
+ 'a',
+ [
+ 'class' => '',
+ 'href' => $title->getLocalURL( $urlArgs ),
+ 'rel' => 'nofollow',
+ 'title' => Message::get( 'smw-ask-debug-desc', Message::TEXT, Message::USER_LANGUAGE )
+ ],
+ $raw ? Message::get( 'smw-ask-debug', Message::TEXT, Message::USER_LANGUAGE ) : 'ℹ'
+ );
+
+ if ( $raw ) {
+ return $link;
+ }
+
+ return Html::rawElement(
+ 'span',
+ [
+ 'id' => 'ask-debug',
+ 'class' => 'smw-ask-button smw-ask-button-right',
+ 'title' => Message::get( 'smw-ask-debug-desc', Message::TEXT, Message::USER_LANGUAGE )
+ ],
+ $link
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param string $urlTail
+ * @param boolean $isFromCache
+ *
+ * @return string
+ */
+ public static function noQCacheLink( Title $title, UrlArgs $urlArgs, $isFromCache = false ) {
+
+ if ( $isFromCache === false ) {
+ return '';
+ }
+
+ $urlArgs->set( 'cache', 'no' );
+ $urlArgs->delete( 'debug' );
+
+ $urlArgs->setFragment( 'search' );
+
+ return Html::rawElement(
+ 'span',
+ [
+ 'id' => 'ask-cache',
+ 'class' => '',
+ 'title' => Message::get( 'smw-ask-no-cache-desc', Message::TEXT, Message::USER_LANGUAGE )
+ ],
+ Html::element(
+ 'a',
+ [
+ 'class' => '',
+ 'href' => $title->getLocalURL( $urlArgs ),
+ 'rel' => 'nofollow'
+ ],
+ Message::get( 'smw-ask-no-cache', Message::TEXT, Message::USER_LANGUAGE )
+ )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Infolink|null $infolink
+ *
+ * @return string
+ */
+ public static function clipboardLink( Infolink $infolink = null ) {
+
+ if ( $infolink === null ) {
+ return '';
+ }
+
+ return Html::rawElement(
+ 'span',
+ [
+ 'id' => 'ask-clipboard ',
+ // 'class' => 'smw-ask-button smw-ask-button-right smw-ask-button-lgrey'
+ ],
+ Html::element(
+ 'a',
+ [
+ 'data-clipboard-action' => 'copy',
+ 'data-clipboard-target' => '.clipboard',
+ 'data-onoi-clipboard-field' => 'value',
+ 'class' => 'clipboard smw-icon-bookmark',
+ 'value' => $infolink->getURL(),
+ 'title' => wfMessage( 'smw-clipboard-copy-link' )->text()
+ ]
+ )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/NavigationLinksWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/NavigationLinksWidget.php
new file mode 100644
index 00000000..95420a77
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/NavigationLinksWidget.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Localizer;
+use SMW\Message;
+use SMW\Utils\HtmlModal;
+use SMW\Page\ListPager;
+use SMWInfolink as Infolink;
+use Title;
+use SMW\Utils\HtmlTabs;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class NavigationLinksWidget {
+
+ /**
+ * @var integer
+ */
+ private static $maxInlineLimit = 500;
+
+ /**
+ * @since 3.0
+ *
+ * @param string $maxInlineLimit
+ */
+ public static function setMaxInlineLimit( $maxInlineLimit ) {
+ self::$maxInlineLimit = $maxInlineLimit;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title,
+ * @param array $visibleLinks
+ *
+ * @return string
+ */
+ public static function topLinks( Title $title, $visibleLinks = [], $isEditMode = true ) {
+
+ if ( $visibleLinks === [] ) {
+ return '';
+ }
+
+ $lLinks = [];
+ $rLinks = [];
+
+ $lLinks['options'] = Html::rawElement(
+ 'a',
+ [
+ 'href' => '#options'
+ ],
+ Message::get( 'smw-ask-options', Message::TEXT, Message::USER_LANGUAGE )
+ );
+
+ $lLinks['search'] = Html::rawElement(
+ 'a',
+ [
+ 'href' => '#search'
+ ],
+ Message::get( 'smw-ask-search', Message::TEXT, Message::USER_LANGUAGE )
+ );
+
+ $lLinks['result'] = Html::rawElement(
+ 'a',
+ [
+ 'href' => '#result'
+ ],
+ Message::get( 'smw-ask-result', Message::TEXT, Message::USER_LANGUAGE )
+ );
+
+ $rLinks['empty'] = Html::rawElement(
+ 'a',
+ [
+ 'href' => $title->getLocalURL()
+ ],
+ Message::get( 'smw-ask-empty', Message::TEXT, Message::USER_LANGUAGE )
+ );
+
+ $rLinks['help'] = HtmlModal::link(
+ '<span class="smw-icon-info" style="padding: 0 0 3px 18px;background-position-x: center;"></span>',
+ [
+ 'data-id' => 'ask-help'
+ ]
+ );
+
+ $visibleLinks = array_flip( $visibleLinks );
+
+ foreach ( $lLinks as $key => $value ) {
+ if ( !isset( $visibleLinks[$key] ) ) {
+ unset( $lLinks[$key] );
+ }
+ }
+
+ foreach ( $rLinks as $key => $value ) {
+ if ( !isset( $visibleLinks[$key] ) ) {
+ unset( $rLinks[$key] );
+ }
+ }
+
+ $sep = Html::rawElement(
+ 'span',
+ [
+ 'style' => 'color:#aaa;font-size: 95%;margin-top: 2px;'
+ ],
+ '&#160;&#160;|&#160;&#160;'
+ );
+
+ $left = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'float-left'
+ ],
+ implode( "$sep", $lLinks )
+ );
+
+ $right = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'float-right'
+ ],
+ implode( "$sep", $rLinks )
+ );
+
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'id' => 'ask-toplinks',
+ 'class' => 'smw-ask-toplinks' . ( !$isEditMode ? ' hide-mode' : '' )
+ ],
+ $left . '&#160;' . $right
+ ) . Html::rawElement(
+ 'div',
+ [
+ 'class' => 'clear-both'
+ ]
+ );
+
+ return $html;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title,
+ * @param UrlArgs $urlArgs
+ * @param integer $count
+ * @param boolean $hasFurtherResults
+ *
+ * @return string
+ */
+ public static function navigationLinks( Title $title, UrlArgs $urlArgs, $count, $hasFurtherResults = false ) {
+
+ if ( $count == 0 ) {
+ return '';
+ }
+
+ $urlArgs = clone $urlArgs;
+ $limit = $urlArgs->get( 'limit' );
+ $offset = $urlArgs->get( 'offset' );
+
+ // Remove any contents that is cruft
+ if ( strpos( $urlArgs->get( 'p' ), 'cl=' ) !== false ) {
+ $urlArgs->set( 'p', mb_substr( $urlArgs->get( 'p' ), stripos( $urlArgs->get( 'p' ), '/' ) + 1 ) );
+ }
+
+ $userLanguage = Localizer::getInstance()->getUserLanguage();
+
+ $html = '<b>' .
+ Message::get( 'smw_result_results', Message::TEXT, Message::USER_LANGUAGE ) . ' ' . $userLanguage->formatNum( $offset + 1 ) .
+ ' &#150; ' .
+ $userLanguage->formatNum( $offset + $count ) .
+ '</b>&#160;';
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'ask-pagination'
+ ],
+ ListPager::pagination( $title, $limit, $offset, $count, $urlArgs->toArray() + [ '_target' => '#search' ] , $html )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $navigation
+ * @param string $infoText
+ * @param Infolink|null $infoLink
+ * @param string $editHref
+ *
+ * @return string
+ */
+ public static function basicLinks( $navigation = '', Infolink $infoLink = null ) {
+
+ if ( $navigation === '' ) {
+ return '';
+ }
+
+ $downloadLink = DownloadLinksWidget::downloadLinks(
+ $infoLink
+ );
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-ask-actions-nav'
+ ],
+ $navigation . $downloadLink
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParameterInput.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParameterInput.php
new file mode 100644
index 00000000..5a505ed3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParameterInput.php
@@ -0,0 +1,326 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use ParamProcessor\ParamDefinition;
+use Xml;
+
+/**
+ * Simple class to get a HTML input for the parameter.
+ * Usable for when creating a GUI from a parameter list.
+ *
+ * Based on 'addOptionInput' from Special:Ask in SMW 1.5.6.
+ *
+ * TODO: nicify HTML
+ *
+ * @since 1.9
+ *
+ * @ingroup SMW
+ *
+ * @licence GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class ParameterInput {
+
+ /**
+ * The parameter to print an input for.
+ *
+ * @since 1.9
+ *
+ * @var ParamDefinition
+ */
+ protected $param;
+
+ /**
+ * The current value for the parameter. When provided,
+ * it'll be used as value for the input, otherwise the
+ * parameters default value will be used.
+ *
+ * @since 1.9
+ *
+ * @var mixed: string or false
+ */
+ protected $currentValue;
+
+ /**
+ * Name for the input.
+ *
+ * @since 1.9
+ *
+ * @var string
+ */
+ protected $inputName;
+
+ /**
+ * @var array
+ */
+ private $attributes = [];
+
+ /**
+ * Constructor.
+ *
+ * @since 1.9
+ *
+ * @param ParamDefinition $param
+ * @param mixed $currentValue
+ */
+ public function __construct( ParamDefinition $param, $currentValue = false ) {
+ $this->currentValue = $currentValue;
+ $this->inputName = $param->getName();
+ $this->param = $param;
+ }
+
+ /**
+ * Sets the current value.
+ *
+ * @since 1.9
+ *
+ * @param mixed $currentValue
+ */
+ public function setCurrentValue( $currentValue ) {
+ $this->currentValue = $currentValue;
+ }
+
+ /**
+ * Sets the name for the input; defaults to the name of the parameter.
+ *
+ * @since 1.9
+ *
+ * @param string $name
+ */
+ public function setInputName( $name ) {
+ $this->inputName = $name;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $attributes
+ */
+ public function setAttributes( array $attributes ) {
+ $this->attributes = $attributes;
+ }
+
+ /**
+ * Returns the HTML for the parameter input.
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ public function getHtml() {
+ $valueList = [];
+
+ if ( is_array( $this->param->getAllowedValues() ) ) {
+ $valueList = $this->param->getAllowedValues();
+ }
+
+ if ( $valueList === [] ) {
+ switch ( $this->param->getType() ) {
+ case 'char':
+ case 'float':
+ case 'integer':
+ case 'number':
+ $html = $this->getNumberInput();
+ break;
+ case 'boolean':
+ $html = $this->getBooleanInput();
+ break;
+ case 'string':
+ default:
+ $html = $this->getStrInput();
+ break;
+ }
+ } else {
+ $html = $this->param->isList() ? $this->getCheckboxListInput( $valueList ) : $this->getSelectInput( $valueList );
+ }
+
+ return $html;
+ }
+
+ /**
+ * Returns the value to initially display with the input.
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ protected function getValueToUse() {
+ $value = $this->currentValue === false ? $this->param->getDefault() : $this->currentValue;
+
+ if ( $this->param->isList() && is_array( $value ) ) {
+ $value = implode( $this->param->getDelimiter(), $value );
+ }
+
+ // #1473
+ if ( $value === [] ) {
+ $value = '';
+ }
+
+ return $value;
+ }
+
+ /**
+ * Gets a short text input suitable for numbers.
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ protected function getNumberInput() {
+
+ $attributes = [
+ 'class' => 'parameter-number-input',
+ 'size' => 6,
+ 'style' => "width: 95%;"
+ ];
+
+ if ( $this->attributes !==[] ) {
+ $attributes = $this->attributes;
+ }
+
+ return Html::input(
+ $this->inputName,
+ $this->getValueToUse(),
+ 'text',
+ $attributes
+ );
+ }
+
+ /**
+ * Gets a text input for a string.
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ protected function getStrInput() {
+
+ $attributes = [
+ 'class' => 'parameter-string-input',
+ 'size' => 20,
+ 'style' => "width: 95%;"
+ ];
+
+ if ( $this->attributes !==[] ) {
+ $attributes = $this->attributes;
+ }
+
+ return Html::input(
+ $this->inputName,
+ $this->getValueToUse(),
+ 'text',
+ $attributes
+ );
+ }
+
+ /**
+ * Gets a checkbox.
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ protected function getBooleanInput() {
+
+ $attributes = [
+ 'class' => 'parameter-boolean-input'
+ ];
+
+ if ( $this->attributes !==[] ) {
+ $attributes = $this->attributes;
+ }
+
+ return Xml::check(
+ $this->inputName,
+ $this->getValueToUse(),
+ $attributes
+ );
+ }
+
+ /**
+ * Gets a select menu for the provided values.
+ *
+ * @since 1.9
+ *
+ * @param array $valueList
+ *
+ * @return string
+ */
+ protected function getSelectInput( array $valueList ) {
+ $options = [];
+ $options[] = '<option value=""></option>';
+
+ $currentValues = (array)$this->getValueToUse();
+ if ( is_null( $currentValues ) ) {
+ $currentValues = [];
+ }
+
+ foreach ( $valueList as $value ) {
+ $options[] =
+ '<option value="' . htmlspecialchars( $value ) . '"' .
+ ( in_array( $value, $currentValues ) ? ' selected="selected"' : '' ) . '>' . htmlspecialchars( $value ) .
+ '</option>';
+ }
+
+ return Html::rawElement(
+ 'select',
+ [
+ 'name' => $this->inputName,
+ 'class'=> 'parameter-select-input'
+ ],
+ implode( "\n", $options )
+ );
+ }
+
+ /**
+ * Gets a list of input boxes for the provided values.
+ *
+ * @since 1.9
+ *
+ * @param array $valueList
+ *
+ * @return string
+ */
+ protected function getCheckboxListInput( array $valueList ) {
+ $boxes = [];
+ $currentValues = [];
+
+ $values = $this->getValueToUse();
+
+ // List of comma separated values, see ParametersProcessor::getParameterList
+ if ( strpos( $values, ',' ) !== false ) {
+ $currentValues = array_flip(
+ array_map( 'trim', explode( ',', $values ) )
+ );
+ } elseif ( $values !== '' ) {
+ $currentValues[$values] = true;
+ }
+
+ foreach ( $valueList as $value ) {
+
+ // Use a value not a simple "true"
+ $attr = [
+ 'type' => 'checkbox',
+ 'name' => $this->inputName . '[]',
+ 'value' => $value
+ ];
+
+ $boxes[] = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'parameter-checkbox-input',
+ 'style' => 'white-space: nowrap; padding-right: 5px;'
+ ],
+ Html::rawElement(
+ 'input',
+ $attr + ( isset( $currentValues[$value] ) ? [ 'checked' ] : [] )
+ ) . Html::element( 'tt', [], $value )
+ );
+ }
+
+ return implode( "\n", $boxes );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParametersProcessor.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParametersProcessor.php
new file mode 100644
index 00000000..19d86c2e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParametersProcessor.php
@@ -0,0 +1,273 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use SMWInfolink as Infolink;
+use SMWQueryProcessor as QueryProcessor;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ParametersProcessor {
+
+ /**
+ * @var integer
+ */
+ private static $defaultLimit = 50;
+
+ /**
+ * @var integer
+ */
+ private static $maxInlineLimit = 500;
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $defaultLimit
+ */
+ public static function setDefaultLimit( $defaultLimit ) {
+ self::$defaultLimit = $defaultLimit;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $maxInlineLimit
+ */
+ public static function setMaxInlineLimit( $maxInlineLimit ) {
+ self::$maxInlineLimit = $maxInlineLimit;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param WebRequest $request
+ * @param array|null $params
+ *
+ * @return string
+ */
+ public static function process( WebRequest $request, $params ) {
+
+ // First make all inputs into a simple parameter list that can again be
+ // parsed into components later.
+ $parameterList = self::getParameterList( $request, $params );
+ $printouts = [];
+
+ // Check for q= query string, used whenever this special page calls
+ // itself (via submit or plain link):
+ if ( ( $q = $request->getText( 'q' ) ) !== '' ) {
+ $parameterList[] = $q;
+ }
+
+ // Parameters separated by newlines here (compatible with text-input for
+ // printouts)
+ if ( ( $po = $request->getText( 'po' ) ) !== '' ) {
+ $printouts = explode( "\n", $po );
+ }
+
+ // Check for param strings in po (printouts), appears in some links
+ // and in submits:
+ $parameterList = self::checkParameterList(
+ $request,
+ $parameterList,
+ $printouts
+ );
+
+ list( $queryString, $parameters, $printouts ) = QueryProcessor::getComponentsFromFunctionParams(
+ $parameterList,
+ false
+ );
+
+ unset( $parameters['cl'] );
+
+ // Try to complete undefined parameter values from dedicated URL params.
+ if ( !array_key_exists( 'format', $parameters ) ) {
+ $parameters['format'] = 'broadtable';
+ }
+
+ $sort_count = 0;
+ $empty_first_sort = false;
+
+ // First check whether the sorting options input send an
+ // request data as array
+ if ( ( $sort_values = $request->getArray( 'sort_num', [] ) ) !== [] ) {
+
+ // Find out whether something like `|?sort=,Has text` was used
+ if ( $sort_values[0] === '' ) {
+ $empty_first_sort = true;
+ }
+
+ if ( is_array( $sort_values ) ) {
+
+ // Filter all empty values
+ $sort = array_filter( $sort_values );
+ $sort_count = count( $sort );
+
+ // Add an empty element on the first position which got filter
+ // and was to prevent countless empty elements when no other sort
+ // was metioned
+ if ( $sort_count > 0 && $empty_first_sort ) {
+ array_unshift( $sort, '' );
+ $sort_count++;
+ }
+
+ $parameters['sort'] = implode( ',', $sort );
+ }
+ } elseif ( $request->getCheck( 'sort' ) ) {
+ $parameters['sort'] = $request->getVal( 'sort', '' );
+ }
+
+ // First check whether the order options input send an
+ // request data as array
+ if ( ( $order_values = $request->getArray( 'order_num', [] ) ) !== [] ) {
+
+ // Count doesn't match means we have a order from an
+ // empty (#subject) carrying around which we don't permit when
+ // sorting via columns
+ if ( is_array( $order_values ) && count( $order_values ) != $sort_count ) {
+ array_pop( $order_values );
+ }
+
+ if ( is_array( $order_values ) ) {
+ $order = array_filter( $order_values );
+ $parameters['order'] = implode( ',', $order );
+ }
+
+ } elseif ( $request->getCheck( 'order' ) ) {
+ $parameters['order'] = $request->getVal( 'order', '' );
+ } elseif ( !array_key_exists( 'order', $parameters ) ) {
+ $parameters['order'] = 'asc';
+ $parameters['sort'] = '';
+ }
+
+ if ( !array_key_exists( 'offset', $parameters ) ) {
+ $parameters['offset'] = $request->getVal( 'offset', 0 );
+ }
+
+ if ( !array_key_exists( 'limit', $parameters ) ) {
+ $parameters['limit'] = $request->getVal( 'limit', self::$defaultLimit );
+ }
+
+ $parameters['limit'] = min( $parameters['limit'], self::$maxInlineLimit );
+
+ return [ $queryString, $parameters, $printouts ];
+ }
+
+ private static function getParameterList( $request, $params ) {
+
+ // Called from wiki, get all parameters
+ if ( !$request->getCheck( 'q' ) ) {
+ return Infolink::decodeParameters( $params, true );
+ }
+
+ // Called by own Special, ignore full param string in that case
+ $query_val = $request->getVal( 'p' );
+
+ if ( !empty( $query_val ) ) {
+ // p is used for any additional parameters in certain links.
+ $parameterList = Infolink::decodeParameters( $query_val, false );
+ } else {
+ $query_values = $request->getArray( 'p' );
+
+ if ( is_array( $query_values ) ) {
+ foreach ( $query_values as $key => $val ) {
+ if ( empty( $val ) ) {
+ unset( $query_values[$key] );
+ }
+ }
+ }
+
+ // p is used for any additional parameters in certain links.
+ $parameterList = Infolink::decodeParameters( $query_values, false );
+ }
+
+ foreach ( $parameterList as $key => $value ) {
+ // Concatenate checkbox values into a simple comma separated list
+ if ( is_array( $value ) ) {
+ $parameterList[$key] = implode( ',', $value );
+ }
+ }
+
+ return $parameterList;
+ }
+
+ private static function checkParameterList( $request, $parameterList, $printouts ) {
+
+ // Add initial ? if omitted (all params considered as printouts)
+ foreach ( $printouts as $param ) {
+ $param = trim( $param );
+
+ if ( ( $param !== '' ) && ( $param { 0 } != '?' ) ) {
+ $param = '?' . $param;
+ }
+
+ $parameterList[] = $param;
+ }
+
+ $parameters = [];
+ unset( $parameterList['title'] );
+
+ // MW's internal token
+ unset( $parameterList['wpEditToken'] );
+
+ foreach ( $parameterList as $key => $value ) {
+ if ( self::hasPipe( $key, $value ) ) {
+
+ // #3523 `?TestAsk=[[Foo|Bar]]` replace `|`
+ if ( self::hasLink( $value ) ) {
+ $value = self::replace( '|', '0x7C', $value );
+ }
+
+ // #1407 Split: `?Has property=Foo|+index=1` into a [ '?Has property=Foo', '+index=1' ])
+ foreach ( explode( '|', $value ) as $k => $val ) {
+
+ // #3523 `?TestAsk=[[Foo|Bar]]|+index=1` decode
+ // the part that contains `0x7C`
+ if ( strpos( $val, '0x7C' ) !== false ) {
+ $val = self::replace( '0x7C', '|', $val );
+ }
+
+ $parameters[] = $k == 0 && $key{0} == '?' ? $key . '=' . $val : $val;
+ }
+ } elseif ( is_string( $key ) ) {
+ $parameters[$key] = $value;
+ } else {
+ $parameters[] = $value;
+ }
+ }
+
+ return $parameters;
+ }
+
+ private static function hasPipe( $key, $value ) {
+
+ if ( $key !== '' && $key{0} == '?' && strpos( $value, '|' ) !== false ) {
+ return true;
+ }
+
+ if ( is_string( $value ) && $value !== '' && $value{0} == '?' && strpos( $value, '|' ) !== false ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static function hasLink( $value ) {
+ return strpos( $value, '[[' ) !== false && strpos( $value, ']]' ) !== false ;
+ }
+
+ private static function replace( $source, $target, $value ) {
+ return preg_replace_callback(
+ '/\[\[([^\[\]]*)\]\]/xu',
+ function( array $matches ) use ( $source, $target ) {
+ return str_replace( [ $source ], [ $target ], $matches[0] );
+ },
+ $value
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParametersWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParametersWidget.php
new file mode 100644
index 00000000..b465d588
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/ParametersWidget.php
@@ -0,0 +1,359 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use ParamProcessor\ParamDefinition;
+use SMW\Message;
+use SMW\Utils\HtmlDivTable;
+use SMWQueryProcessor as QueryProcessor;
+use Title;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author mwjames
+ */
+class ParametersWidget {
+
+ /**
+ * @var boolean
+ */
+ private static $isTooltipDisplay = false;
+
+ /**
+ * @var integer
+ */
+ private static $defaultLimit = 50;
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $isTooltipDisplay
+ */
+ public static function setTooltipDisplay( $isTooltipDisplay ) {
+ self::$isTooltipDisplay = (bool)$isTooltipDisplay;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $defaultLimit
+ */
+ public static function setDefaultLimit( $defaultLimit ) {
+ self::$defaultLimit = $defaultLimit;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param array $parameters
+ *
+ * @return string
+ */
+ public static function fieldset( Title $title, array $parameters ) {
+
+ $toggle = Html::rawElement(
+ 'span',
+ [
+ 'style' => 'margin-left:5px;'
+ ],
+ '&#160;[' . Html::rawElement(
+ 'span',
+ [
+ 'class' => 'options-toggle-action',
+ ],
+ Html::rawElement(
+ 'label',
+ [
+ 'for' => 'options-toggle',
+ 'title' => Message::get( 'smw-section-expand', Message::TEXT, Message::USER_LANGUAGE )
+ ],
+ '+'
+ )
+ ) . ']&#160;'
+ );
+
+ $options = Html::rawElement(
+ 'div',
+ [
+ 'id' => 'parameter-title',
+ 'class' => 'strike'
+ ],
+ Html::rawElement(
+ 'span',
+ [],
+ Message::get( 'smw-ask-parameters', Message::TEXT, Message::USER_LANGUAGE ) . $toggle
+ )
+ ) . Html::rawElement(
+ 'div',
+ [],
+ '<input type="checkbox" id="options-toggle"/>' . Html::rawElement(
+ 'div',
+ [
+ 'id' => 'options-list',
+ 'class' => 'options-list'
+ ],
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'options-parameter-list'
+ ],
+ self::parameterList( $parameters )
+ )
+ )
+ );
+
+ return Html::rawElement(
+ 'fieldset',
+ [],
+ Html::element(
+ 'legend',
+ [],
+ Message::get( 'smw-ask-options', Message::TEXT, Message::USER_LANGUAGE )
+ ). FormatListWidget::selectList(
+ $title,
+ $parameters
+ ) . $options . SortWidget::sortSection( $parameters )
+ );
+ }
+
+ /**
+ * Display a form section showing the options for a given format,
+ * based on the getParameters() value for that format's query printer.
+ *
+ * @since 1.8
+ *
+ * @param string $format
+ * @param array $parameters The current values for the parameters (name => value)
+ *
+ * @return string
+ */
+ public static function parameterList( array $values ) {
+
+ $format = 'broadtable';
+
+ if ( isset( $values['format'] ) ) {
+ $format = $values['format'];
+ }
+
+ $optionList = self::optionList(
+ QueryProcessor::getFormatParameters( $format ),
+ $values
+ );
+
+ $i = 0;
+ $n = 0;
+
+ $rowHtml = '';
+ $resultHtml = '';
+
+ // Top info text for a collapsed option box
+ if ( self::$isTooltipDisplay === true ){
+ $resultHtml .= Html::element(
+ 'div',
+ [
+ 'style' => 'margin-bottom:10px;'
+ ],
+ Message::get( 'smw-ask-otheroptions-info', Message::TEXT, Message::USER_LANGUAGE )
+ );
+ }
+
+ // Table
+ $resultHtml = HtmlDivTable::open(
+ [
+ 'class' => 'smw-ask-options-list',
+ 'width' => '100%'
+ ]
+ );
+
+ while ( $option = array_shift( $optionList ) ) {
+ $i++;
+
+ // Collect elements for a row
+ $rowHtml .= $option;
+
+ // Create table row
+ if ( $i % 3 == 0 ) {
+ $resultHtml .= HtmlDivTable::row(
+ $rowHtml,
+ [
+ 'class' => $i % 6 == 0 ? 'smw-ask-options-row-even' : 'smw-ask-options-row-odd',
+ ]
+ );
+ $rowHtml = '';
+ $n++;
+ }
+ }
+
+ // Ensure left over elements are collected as well
+ $resultHtml .= HtmlDivTable::row(
+ $rowHtml,
+ [
+ 'class' => $n % 2 == 0 ? 'smw-ask-options-row-odd' : 'smw-ask-options-row-even',
+ ]
+ );
+
+ $resultHtml .= HtmlDivTable::close();
+
+ return $resultHtml;
+ }
+
+ private static function optionList( $definitions, $values ) {
+
+ $html = [];
+
+ /**
+ * @var \ParamProcessor\ParamDefinition $definition
+ */
+ foreach ( $definitions as $name => $definition ) {
+
+ // Ignore the format parameter, as we got a special control in the GUI for it already.
+ if ( $name == 'format' ) {
+ continue;
+ }
+
+ // Handle sort, order separate as the generated checkbox are suboptimal, and the single
+ // field interferes with the GET request on multiple sort setters
+ if ( in_array( $name, [ 'sort', 'order' ] ) ) {
+ continue;
+ }
+
+ // Maybe there is a better way but somehow I couldn't find one therefore
+ // 'source' display will be omitted where no alternative source was found or
+ // a source that was marked as default but had no other available options
+ $allowedValues = $definition->getAllowedValues();
+
+ if ( $name == 'source' && (
+ count( $allowedValues ) == 0 ||
+ in_array( 'default', $allowedValues ) && count( $allowedValues ) < 2
+ ) ) {
+
+ continue;
+ }
+
+ $currentValue = false;
+
+ if ( array_key_exists( $name, $values ) ) {
+ $currentValue = $values[$name];
+ }
+
+ // Set default values
+ if ( $name === 'limit' && ( $currentValue === null || $currentValue === false ) ) {
+ $currentValue = self::$defaultLimit;
+ }
+
+ if ( $name === 'offset' && ( $currentValue === null || $currentValue === false ) ) {
+ $currentValue = 0;
+ }
+
+ $html[] = '<td>' . self::field( $definition, $name ) . '</td>' . self::input( $definition, $currentValue );
+ }
+
+ return $html;
+ }
+
+ private static function field( ParamDefinition $definition, $name ) {
+
+ $info = '';
+ $class = '';
+
+ if ( self::$isTooltipDisplay === true ) {
+ $class = 'smw-ask-info';
+ }
+
+ if ( $definition->getMessage() !== null ) {
+ $info = Message::get( $definition->getMessage(), Message::TEXT, Message::USER_LANGUAGE );
+ }
+
+ return HtmlDivTable::cell(
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => $class,
+ 'word-wrap' => 'break-word',
+ 'data-info' => $info
+ ],
+ htmlspecialchars( $name ) . ': '
+ ),
+ [
+ 'overflow' => 'hidden',
+ 'style' => 'border:none;'
+ ]
+ );
+ }
+
+ private static function input( ParamDefinition $definition, $currentValue ) {
+
+ $description = '';
+ $info = '';
+
+ $input = new ParameterInput( $definition );
+ $input->setInputName( 'p[' . $definition->getName() . ']' );
+ //$input->setInputClass( 'smw-ask-input-' . str_replace( ' ', '-', $definition->getName() ) );
+
+ $opts = $definition->getOptions();
+ $attributes = [];
+
+ if ( isset( $opts['style'] ) ) {
+ $attributes['style'] = $opts['style'];
+ }
+
+ if ( isset( $opts['size'] ) ) {
+ $attributes['size'] = $opts['size'];
+ }
+
+ // [ 'data-props' => [
+ // 'property' => Foo, 'value' => 'Bar', 'title-prefix' => 'false'
+ // ] ]
+ if ( isset( $opts['data-props'] ) && is_array( $opts['data-props'] ) ) {
+ foreach ( $opts['data-props'] as $key => $value ) {
+ if ( is_string( $key ) ) {
+ $attributes["data-$key"] = $value;
+ }
+ }
+ }
+
+ if ( isset( $opts['class'] ) ) {
+ $attributes['class'] = $opts['class'];
+ }
+
+ if ( $attributes !== [] ) {
+ $input->setAttributes( $attributes );
+ }
+
+ if ( $currentValue !== false ) {
+ $input->setCurrentValue( $currentValue );
+ }
+
+ // Parameters description text
+ if ( !self::$isTooltipDisplay ) {
+
+ if ( $definition->getMessage() !== null ) {
+ $info = Message::get( $definition->getMessage(), Message::PARSE, Message::USER_LANGUAGE );
+ }
+
+ $description = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-ask-parameter-description'
+ ],
+ '<br />' . $info
+ );
+ }
+
+ return HtmlDivTable::cell(
+ $input->getHtml() . $description,
+ [
+ 'overflow' => 'hidden',
+ 'style' => 'width:33%;border:none;'
+ ]
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/QueryInputWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/QueryInputWidget.php
new file mode 100644
index 00000000..9f6cb532
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/QueryInputWidget.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Message;
+use SMW\Utils\HtmlDivTable;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class QueryInputWidget {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $queryString
+ * @param string $printoutString
+ *
+ * @return string
+ */
+ public static function table( $queryString , $printoutString ) {
+
+ $table = HtmlDivTable::open( [ 'style' => "width: 100%;" ] );
+
+ $table .= HtmlDivTable::row(
+ HtmlDivTable::cell(
+ "<fieldset><legend>" . Message::get( 'smw_ask_queryhead', Message::TEXT, Message::USER_LANGUAGE ) . "</legend>" .
+ '<textarea id="ask-query-condition" class="smw-ask-query-condition" name="q" rows="6" placeholder="...">' .
+ htmlspecialchars( $queryString ) . '</textarea></fieldset>',
+ [ 'class' => 'smw-ask-condition slowfade' ]
+ ) . HtmlDivTable::cell(
+ '',
+ [
+ 'style' => 'width:10px; border:0px; padding: 0px;'
+ ]
+ ) . HtmlDivTable::cell(
+ "<fieldset><legend>" . Message::get( 'smw_ask_printhead', Message::TEXT, Message::USER_LANGUAGE ) . "</legend>" .
+ '<textarea id="smw-property-input" class="smw-ask-query-printout" name="po" rows="6" placeholder="...">' .
+ htmlspecialchars( $printoutString ) . '</textarea></fieldset>',
+ [ 'class' => 'smw-ask-printhead slowfade' ]
+ )
+ );
+
+ $table .= HtmlDivTable::close();
+
+ return $table;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/SortWidget.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/SortWidget.php
new file mode 100644
index 00000000..cafecb9f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/SortWidget.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+use Html;
+use SMW\Message;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SortWidget {
+
+ /**
+ * @var boolean
+ */
+ private static $sortingSupport = false;
+
+ /**
+ * @var boolean
+ */
+ private static $randSortingSupport = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $sortingSupport
+ */
+ public static function setSortingSupport( $sortingSupport ) {
+ self::$sortingSupport = (bool)$sortingSupport;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $randSortingSupport
+ */
+ public static function setRandSortingSupport( $randSortingSupport ) {
+ self::$randSortingSupport = (bool)$randSortingSupport;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $params
+ *
+ * @return string
+ */
+ public static function sortSection( array $params ) {
+
+ if ( self::$sortingSupport === false ) {
+ return '';
+ }
+
+ if ( !array_key_exists( 'sort', $params ) || !array_key_exists( 'order', $params ) ) {
+ $orders = [];
+ $sorts = [];
+ } else {
+ $sorts = explode( ',', $params['sort'] );
+ $orders = explode( ',', $params['order'] );
+ reset( $sorts );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'options-sort',
+ 'class' => 'smw-ask-options-sort'
+ ], Html::rawElement(
+ 'div',
+ [
+ 'id' => 'sorting-title',
+ 'class' => 'strike'
+ ],
+ Html::rawElement(
+ 'span',
+ [],
+ Message::get( 'smw-ask-options-sort', Message::TEXT, Message::USER_LANGUAGE )
+ )
+ ) . Html::rawElement(
+ 'div',
+ [
+ 'id' => 'sorting-input', 'class' => ''
+ ],
+ self::sortingOptions( $sorts, $orders )
+ )
+ );
+ }
+
+ private static function sortingOptions( array $sorts, array $orders ) {
+
+ $result = '';
+
+ foreach ( $orders as $i => $order ) {
+
+ if ( in_array( $order, [ 'ASC', 'asc', 'ascending' ] )) {
+ $order = 'asc';
+ }
+
+ if ( in_array( $order, [ 'DESC', 'desc', 'descending' ] )) {
+ $order = 'desc';
+ }
+
+ if ( in_array( $order, [ 'RAND', 'rand', 'random' ] )) {
+ $order = 'rand';
+ }
+
+ if ( !isset( $sorts[$i] ) ) {
+ $sorts[$i] = '';
+ }
+
+ $html = Html::rawElement(
+ 'input',
+ [
+ 'type' => 'text',
+ 'name' => "sort_num[]",
+ 'size' => '35',
+ 'class' => 'smw-property-input autocomplete-arrow',
+ 'value' => htmlspecialchars( $sorts[$i] )
+ ]
+ );
+
+ $html .= '<select name="order_num[]"><option ';
+
+ if ( $order == 'asc' ) {
+ $html .= 'selected="selected" ';
+ }
+
+ $html .= 'value="asc">' . Message::get( 'smw_ask_ascorder', Message::TEXT, Message::USER_LANGUAGE ) . '</option><option ';
+
+ if ( $order == 'desc' ) {
+ $html .= 'selected="selected" ';
+ }
+
+ $html .= 'value="desc">' . Message::get( 'smw_ask_descorder', Message::TEXT, Message::USER_LANGUAGE ) . "</option>";
+
+ if ( self::$randSortingSupport ) {
+ $html .= '<option ';
+
+ if ( $order == 'rand' ) {
+ $html .= 'selected="selected" ';
+ }
+
+ $html .= 'value="rand">' . Message::get( 'smw-ask-order-rand', Message::TEXT, Message::USER_LANGUAGE ) . '</option>';
+ }
+
+ $html .= '</select>';
+ $html .= '<span class="smw-ask-sort-delete"><a class="smw-ask-sort-delete-action" data-target="sort_div_' . $i . '" >' . Message::get( 'delete', Message::TEXT, Message::USER_LANGUAGE ) . '</a></span>';
+
+ $result .= Html::rawElement( 'div', [ 'id' => "sort_div_$i", 'class' => "smw-ask-sort-input" ], $html );
+ }
+
+ $result .= '<div id="sorting_starter" style="display: none"><input type="text" name="sort_num[]" size="35" class="smw-property-input autocomplete-arrow" />';
+ $result .= '<select name="order_num[]">' . "\n";
+ $result .= ' <option value="asc">' . Message::get( 'smw_ask_ascorder', Message::TEXT, Message::USER_LANGUAGE ) . "</option>\n";
+ $result .= ' <option value="desc">' . Message::get( 'smw_ask_descorder', Message::TEXT, Message::USER_LANGUAGE ) . "</option>\n";
+
+ if ( self::$randSortingSupport ) {
+ $result .= ' <option value="rand">' . Message::get( 'smw-ask-order-rand', Message::TEXT, Message::USER_LANGUAGE ) . "</option>\n";
+ }
+
+ $result .= "</select>";
+ $result .= "</div>";
+ $result .= '<div id="sorting_main"></div>' . "\n";
+
+ return $result . Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-ask-sort-add'
+ ],
+ Html::rawElement(
+ 'a',
+ [
+ 'class' => 'smw-ask-sort-add-action'
+ ],
+ Message::get( 'smw-ask-sort-add-action', Message::TEXT, Message::USER_LANGUAGE )
+ )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/UrlArgs.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/UrlArgs.php
new file mode 100644
index 00000000..dee980a6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Ask/UrlArgs.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Ask;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class UrlArgs {
+
+ /**
+ * @var array
+ */
+ private $args = [];
+
+ /**
+ * @var array
+ */
+ private $fragment = '';
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function set( $key, $value ) {
+ $this->args[$key] = $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function get( $key ) {
+ return isset( $this->args[$key] ) ? $this->args[$key] : null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ */
+ public function delete( $key ) {
+ unset( $this->args[$key] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $fragment
+ */
+ public function setFragment( $fragment ) {
+ $this->fragment = $fragment;
+ }
+
+ /**
+ * @see __toString
+ */
+ public function toArray() {
+ return $this->args;
+ }
+
+ /**
+ * @see __toString
+ */
+ public function __toString() {
+ return wfArrayToCGI( $this->args ) . ( $this->fragment !== '' ? '#' . $this->fragment : '' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/FieldBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/FieldBuilder.php
new file mode 100644
index 00000000..49928301
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/FieldBuilder.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Browse;
+
+use Html;
+use SMW\Message;
+use SpecialPage;
+
+/**
+ * @private
+ *
+ * This class should eventually be injected instead of relying on static methods,
+ * for now this is the easiest way to unclutter the mammoth Browse class and
+ * splitting up responsibilities.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class FieldBuilder {
+
+ /**
+ * Creates the query form in order to quickly switch to a specific article.
+ *
+ * @since 2.5
+ *
+ * @return string
+ */
+ public static function createQueryForm( $articletext = '' ) {
+
+ $title = SpecialPage::getTitleFor( 'Browse' );
+ $dir = $title->getPageLanguage()->isRTL() ? 'rtl' : 'ltr';
+
+ $html = "<div class=\"smwb-form\">". Html::rawElement(
+ 'div',
+ [ 'style' => 'margin-top:15px;' ],
+ ''
+ );
+
+ $html .= Html::rawElement(
+ 'form',
+ [
+ 'name' => 'smwbrowse',
+ 'action' => htmlspecialchars( $title->getLocalURL() ),
+ 'method' => 'get'
+ ],
+ Html::rawElement(
+ 'input',
+ [
+ 'type' => 'hidden',
+ 'name' => 'title',
+ 'value' => $title->getPrefixedText()
+ ],
+ Message::get( 'smw_browse_article', Message::ESCAPED, Message::USER_LANGUAGE )
+ ) .
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smwb-input'
+ ],
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'input-field'
+ ],
+ Html::rawElement(
+ 'input',
+ [
+ 'type' => 'text',
+ 'dir' => $dir,
+ 'name' => 'article',
+ 'size' => 40,
+ 'id' => 'smw-page-input',
+ 'class' => 'input smw-page-input autocomplete-arrow mw-ui-input',
+ 'value' => htmlspecialchars( $articletext )
+ ]
+ )
+ ) .
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'button-field'
+ ],
+ Html::rawElement(
+ 'input',
+ [
+ 'type' => 'submit',
+ 'class' => 'input-button mw-ui-button',
+ 'value' => Message::get( 'smw_browse_go', Message::ESCAPED, Message::USER_LANGUAGE )
+ ]
+ )
+ )
+ )
+ );
+
+ return $html . "</div>";
+ }
+
+ /**
+ * Creates the HTML for a link to this page, with some parameters set.
+ *
+ * @since 2.5
+ *
+ * @param string $linkMsg
+ * @param array $parameters
+ *
+ * @return string
+ */
+ public static function createLink( $linkMsg, array $parameters ) {
+
+ $title = SpecialPage::getSafeTitleFor( 'Browse' );
+ $fragment = $linkMsg === 'smw_browse_show_incoming' ? '#smw_browse_incoming' : '';
+
+ return Html::element(
+ 'a',
+ [
+ 'href' => $title->getLocalURL( $parameters ) . $fragment,
+ 'class' => $linkMsg
+ ],
+ Message::get( $linkMsg, Message::TEXT, Message::USER_LANGUAGE )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/GroupFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/GroupFormatter.php
new file mode 100644
index 00000000..0a7885b1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/GroupFormatter.php
@@ -0,0 +1,237 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Browse;
+
+use Html;
+use SMW\DIWikiPage;
+use SMW\Message;
+use SMW\PropertySpecificationLookup;
+use SMWDataItem as DataItem;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class GroupFormatter {
+
+ /**
+ * Identifies a group label
+ */
+ const MESSAGE_GROUP_LABEL = 'smw-property-group-label-';
+
+ /**
+ * Identifies a group label
+ */
+ const MESSAGE_GROUP_DESCRIPTION = 'smw-property-group-description-';
+
+ /**
+ * @var PropertySpecificationLookup
+ */
+ private $propertySpecificationLookup;
+
+ /**
+ * @var boolean
+ */
+ private $showGroup = true;
+
+ /**
+ * @var string
+ */
+ private $lastGroup = '';
+
+ /**
+ * @var array
+ */
+ private $groupLinks = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param PropertySpecificationLookup $propertySpecificationLookup
+ */
+ public function __construct( PropertySpecificationLookup $propertySpecificationLookup ) {
+ $this->propertySpecificationLookup = $propertySpecificationLookup;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $showGroup
+ */
+ public function showGroup( $showGroup ) {
+ $this->showGroup = $showGroup;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function isLastGroup( $group ) {
+ return $this->lastGroup === $group;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasGroups() {
+ return $this->groupLinks !== [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array &$properties
+ */
+ public function findGroupMembership( array &$properties ) {
+
+ $groupedProperties = [];
+ $this->groupLinks = [];
+
+ foreach ( $properties as $key => $property ) {
+
+ $group = $this->findGroup( $property );
+
+ if ( !isset( $groupedProperties[$group] ) ) {
+ $groupedProperties[$group] = [];
+ }
+
+ $groupedProperties[$group][] = $property;
+ }
+
+ ksort( $groupedProperties, SORT_NATURAL | SORT_FLAG_CASE );
+ $properties = $groupedProperties;
+
+ $keys = array_keys( $groupedProperties );
+ $this->lastGroup = end( $keys );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $group
+ *
+ * @return string
+ */
+ public function getGroupLink( $group ) {
+
+ if ( !isset( $this->groupLinks[$group] ) || $this->groupLinks[$group] === '' ) {
+ return $group;
+ }
+
+ return Html::rawElement(
+ 'span',
+ [
+ 'class' => 'group-link'
+ ],
+ $this->groupLinks[$group]
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ * @param DIWikiPage $dataItem
+ *
+ * @return string
+ */
+ public function getMessageClassLink( $id, DIWikiPage $dataItem ) {
+
+ $gr = str_replace( '_', ' ', $dataItem->getDBKey() );
+ $key = mb_strtolower( str_replace( ' ', '-', $gr ) );
+
+ return Html::rawElement(
+ 'a',
+ [
+ 'href' => DIWikiPage::newFromText( $id . $key, NS_MEDIAWIKI )->getTitle()->getFullURL(),
+ 'class' => !Message::exists( $id . $key ) ? 'new' : ''
+ ],
+ $id . $key
+ );
+ }
+
+ private function findGroup( $property ) {
+
+ if ( $this->showGroup === false ) {
+ return '';
+ }
+
+ $group = null;
+
+ // Special handling for a `Category` property instance that itself cannot
+ // be annotated with a `Is property group` therefor use the fixed
+ // `smw-category-group` message to point to a group
+ if ( $property->getKey() === '_INST' && Message::exists( 'smw-category-group' ) ) {
+ $gr = Message::get( 'smw-category-group' );
+ } elseif( ( $group = $this->propertySpecificationLookup->getPropertyGroup( $property ) ) instanceof DataItem ) {
+ $gr = str_replace( '_', ' ', $group->getDBKey() );
+ } else {
+ return '';
+ }
+
+ $desc = '';
+ $link = '';
+
+ // Convention key to allow a category to transtable using the
+ // `smw-group-...` as key and transforms a group `Foo bar` to
+ // `smw-group-foo-bar`
+ $key = mb_strtolower( str_replace( ' ', '-', $gr ) );
+
+ if ( Message::exists( self::MESSAGE_GROUP_LABEL . $key ) ) {
+ $gr = Message::get(
+ self::MESSAGE_GROUP_LABEL . $key,
+ Message::TEXT,
+ Message::USER_LANGUAGE
+ );
+ }
+
+ if ( Message::exists( self::MESSAGE_GROUP_DESCRIPTION . $key ) ) {
+ $desc = Message::get(
+ self::MESSAGE_GROUP_DESCRIPTION . $key,
+ Message::TEXT,
+ Message::USER_LANGUAGE
+ );
+ }
+
+ if ( $group instanceof DataItem ) {
+ $link = Html::rawElement(
+ 'a',
+ [
+ 'href' => $group->getTitle()->getFullURL()
+ ],
+ $gr
+ );
+ }
+
+ if ( $desc !== '' ) {
+ $link = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-highlighter smwttinline',
+ 'data-state' => 'inline'
+ ],
+ $link . Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smwttcontent'
+ ],
+ $desc
+ )
+ );
+ }
+
+ if ( !isset( $this->groupLinks[$gr] ) || $this->groupLinks[$gr] === '' ) {
+ $this->groupLinks[$gr] = $link;
+ }
+
+ return $gr;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/HtmlBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/HtmlBuilder.php
new file mode 100644
index 00000000..9a8a71a8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/HtmlBuilder.php
@@ -0,0 +1,947 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Browse;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Message;
+use SMW\RequestOptions;
+use SMW\SemanticData;
+use SMW\Store;
+use SMW\Utils\HtmlDivTable;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author Denny Vrandecic
+ * @author mwjames
+ */
+class HtmlBuilder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var DIWikiPage
+ */
+ private $subject;
+
+ /**
+ * @var boolean
+ */
+ private $showoutgoing = true;
+
+ /**
+ * To display incoming values?
+ *
+ * @var boolean
+ */
+ private $showincoming = false;
+
+ /**
+ * At which incoming property are we currently?
+ * @var integer
+ */
+ private $offset = 0;
+
+ /**
+ * How many incoming values should be asked for
+ * @var integer
+ */
+ private $incomingValuesCount = 8;
+
+ /**
+ * How many outgoing values should be asked for
+ *
+ * @var integer
+ */
+ private $outgoingValuesCount = 200;
+
+ /**
+ * How many incoming properties should be asked for
+ * @var integer
+ */
+ private $incomingPropertiesCount = 21;
+
+ /**
+ * @var array
+ */
+ private $extraModules = [];
+
+ /**
+ * @var array
+ */
+ private $options = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param DIWikiPage $subject
+ */
+ public function __construct( Store $store, DIWikiPage $subject ) {
+ $this->store = $store;
+ $this->subject = $subject;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $options
+ */
+ public function setOptions( array $options ) {
+ $this->options = $options;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getOptions() {
+ return $this->options;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function setOption( $key, $value ) {
+ $this->options[$key] = $value;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function getOption( $key, $default = null ) {
+
+ if ( isset( $this->options[$key] ) ) {
+ return $this->options[$key];
+ }
+
+ return $default;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function legacy() {
+ return Html::rawElement(
+ 'div',
+ [
+ 'data-subject' => $this->subject->getHash(),
+ 'data-options' => json_encode( $this->options )
+ ],
+ $this->buildHTML()
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function placeholder() {
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smwb-container',
+ 'data-subject' => $this->subject->getHash(),
+ 'data-options' => json_encode( $this->options )
+ ],
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smwb-status'
+ ],
+ Html::rawElement(
+ 'noscript',
+ [],
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-callout smw-callout-error',
+ ],
+ Message::get( 'smw-noscript', Message::PARSE )
+ )
+ )
+ ) . Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smwb-emptysheet is-disabled'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-overlay-spinner large inline'
+ ]
+ ) . $this->buildEmptyHTML()
+ )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function buildHTML() {
+
+ if ( ( $offset = $this->getOption( 'offset' ) ) ) {
+ $this->offset = $offset;
+ }
+
+ $this->outgoingValuesCount = $this->getOption( 'valuelistlimit.out', 200 );
+
+ if ( $this->getOption( 'showAll' ) ) {
+ $this->incomingValuesCount = $this->getOption( 'valuelistlimit.in', 21 );
+ $this->incomingPropertiesCount = - 1;
+ $this->showoutgoing = true;
+ $this->showincoming = true;
+ }
+
+ if ( $this->getOption( 'dir' ) === 'both' || $this->getOption( 'dir' ) === 'in' ) {
+ $this->showincoming = true;
+ }
+
+ if ( $this->getOption( 'dir' ) === 'in' ) {
+ $this->showoutgoing = false;
+ }
+
+ if ( $this->getOption( 'dir' ) === 'out' ) {
+ $this->showincoming = false;
+ }
+
+ return $this->createHTML();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function buildEmptyHTML() {
+
+ $html = '';
+ $form = '';
+
+ $this->dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $this->subject
+ );
+
+ $semanticData = new SemanticData( $this->subject );
+ $this->articletext = $this->dataValue->getWikiValue();
+
+ if ( $this->getOption( 'showAll' ) ) {
+ $this->showoutgoing = true;
+ $this->showincoming = true;
+ }
+
+ $html .= $this->displayHead();
+ $html .= $this->displayActions();
+ $html .= $this->displayData( $semanticData, true, false, true );
+ $html .= $this->displayBottom( false );
+
+ if ( $this->getOption( 'printable' ) !== 'yes' && !$this->getOption( 'including' ) ) {
+ $form = FieldBuilder::createQueryForm( $this->articletext );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smwb-content'
+ ], $html
+ ) . $form;
+ }
+
+ /**
+ * Create and output HTML including the complete factbox, based on the extracted
+ * parameters in the execute comment.
+ */
+ private function createHTML() {
+
+ $html = "<div class=\"smwb-datasheet smwb-theme-light\">";
+
+ $leftside = true;
+ $modules = [];
+
+ $this->dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $this->subject
+ );
+
+ if ( !$this->dataValue->isValid() ) {
+ return $html;
+ }
+
+ $semanticData = new SemanticData(
+ $this->dataValue->getDataItem()
+ );
+
+ $html .= $this->displayHead();
+ $html .= $this->displayActions();
+
+ if ( $this->showoutgoing ) {
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->setLimit( $this->outgoingValuesCount + 1 );
+ $requestOptions->sort = true;
+
+ // Restrict the request otherwise the entire SemanticData record
+ // is fetched which can in case of a subject with a large
+ // subobject/subpage pool create excessive DB queries
+ $requestOptions->conditionConstraint = true;
+
+ $semanticData = $this->store->getSemanticData(
+ $this->dataValue->getDataItem(),
+ $requestOptions
+ );
+
+ $html .= $this->displayData( $semanticData, $leftside );
+ }
+
+ $html .= $this->displayCenter();
+
+ if ( $this->showincoming ) {
+ list( $indata, $more ) = $this->getInData();
+
+ if ( !$this->getOption( 'showInverse' ) ) {
+ $leftside = !$leftside;
+ }
+
+ $html .= $this->displayData( $indata, $leftside, true );
+ $html .= $this->displayBottom( $more );
+ }
+
+ $this->articletext = $this->dataValue->getWikiValue();
+ $html .= "</div>";
+
+ \Hooks::run(
+ 'SMW::Browse::AfterDataLookupComplete',
+ [
+ $this->store,
+ $semanticData,
+ &$html,
+ &$this->extraModules
+ ]
+ );
+
+ if ( $this->getOption( 'printable' ) !== 'yes' && !$this->getOption( 'including' ) ) {
+ $html .= FieldBuilder::createQueryForm( $this->articletext ) ;
+ }
+
+ $html .= Html::element(
+ 'div',
+ [
+ 'class' => 'smwb-modules',
+ 'data-modules' => json_encode( $this->extraModules )
+ ]
+ );
+
+ return $html;
+ }
+
+ /**
+ * Creates the HTML table displaying the data of one subject.
+ */
+ private function displayData( SemanticData $semanticData, $left = true, $incoming = false, $isLoading = false ) {
+
+ // Some of the CSS classes are different for the left or the right side.
+ // In this case, there is an "i" after the "smwb-". This is set here.
+ $dirPrefix = $left ? 'smwb-' : 'smwb-i';
+ $noresult = true;
+
+ $contextPage = $semanticData->getSubject();
+ $diProperties = $semanticData->getProperties();
+
+ $showGroup = $this->getOption( 'showGroup' ) && $this->getOption( 'group' ) !== 'hide';
+
+ $groupFormatter = new GroupFormatter(
+ ApplicationFactory::getInstance()->getPropertySpecificationLookup()
+ );
+
+ $groupFormatter->showGroup( $showGroup );
+ $groupFormatter->findGroupMembership( $diProperties );
+
+ $html = HtmlDivTable::open(
+ [
+ 'class' => "{$dirPrefix}factbox" . ( $groupFormatter->hasGroups() ? '' : ' smwb-bottom' )
+ ]
+ );
+
+ foreach ( $diProperties as $group => $properties ) {
+
+ if ( $group !== '' ) {
+
+ $c = HtmlDivTable::cell(
+ $groupFormatter->getGroupLink( $group ) . '<span></span>',
+ [
+ "class" => 'smwb-cell smwb-propval'
+ ]
+ );
+
+ $html .= HtmlDivTable::close();
+ $html .= HtmlDivTable::open(
+ [
+ 'class' => "{$dirPrefix}factbox smwb-group"
+ ]
+ );
+
+ $html .= HtmlDivTable::row(
+ $c,
+ [
+ "class" => "{$dirPrefix}propvalue"
+ ]
+ );
+
+ $html .= HtmlDivTable::close();
+ $class = ( $groupFormatter->isLastGroup( $group ) ? ' smwb-bottom' : '' );
+
+ $html .= HtmlDivTable::open(
+ [
+ 'class' => "{$dirPrefix}factbox{$class}"
+ ]
+ );
+ }
+
+ $html .= $this->buildHtmlFromData(
+ $semanticData,
+ $properties,
+ $group,
+ $incoming,
+ $left,
+ $dirPrefix,
+ $noresult
+ );
+ }
+
+ if ( !$isLoading && !$incoming && $showGroup ) {
+ $html .= $this->getGroupMessageClassLinks(
+ $groupFormatter,
+ $semanticData,
+ $dirPrefix
+ );
+ }
+
+ if ( $noresult ) {
+ $noMsgKey = $incoming ? 'smw_browse_no_incoming' : 'smw_browse_no_outgoing';
+
+ $rColumn = HtmlDivTable::cell(
+ '',
+ [
+ "class" => 'smwb-cell smwb-prophead'
+ ]
+ );
+
+ $lColumn = HtmlDivTable::cell(
+ wfMessage( $isLoading ? 'smw-browse-from-backend' : $noMsgKey )->escaped(),
+ [
+ "class" => 'smwb-cell smwb-propval'
+ ]
+ );
+
+ $html .= HtmlDivTable::row(
+ ( $left ? ( $rColumn . $lColumn ):( $lColumn . $rColumn ) ),
+ [
+ "class" => "{$dirPrefix}propvalue"
+ ]
+ );
+ }
+
+ $html .= HtmlDivTable::close();
+
+ return $html;
+ }
+
+ /**
+ * Builds HTML content that matches a group of properties and creates the
+ * display of assigned values.
+ */
+ private function buildHtmlFromData( $semanticData, $properties, $group, $incoming, $left, $dirPrefix, &$noresult ) {
+
+ $html = '';
+ $group = mb_strtolower( str_replace( ' ', '-', $group ) );
+
+ $contextPage = $semanticData->getSubject();
+ $showInverse = $this->getOption( 'showInverse' );
+ $showSort = $this->getOption( 'showSort' );
+
+ $comma = Message::get(
+ 'comma-separator',
+ Message::ESCAPED,
+ Message::USER_LANGUAGE
+ );
+
+ $and = Message::get(
+ 'and',
+ Message::ESCAPED,
+ Message::USER_LANGUAGE
+ );
+
+ $dataValueFactory = DataValueFactory::getInstance();
+
+ foreach ( $properties as $diProperty ) {
+
+ $dvProperty = $dataValueFactory->newDataValueByItem(
+ $diProperty,
+ null
+ );
+
+ $dvProperty->setContextPage(
+ $contextPage
+ );
+
+ $propertyLabel = ValueFormatter::getPropertyLabel(
+ $dvProperty,
+ $incoming,
+ $showInverse
+ );
+
+ // Make the sortkey visible which is otherwise hidden from the user
+ if ( $showSort && $diProperty->getKey() === '_SKEY' ) {
+ $propertyLabel = Message::get( 'smw-property-predefined-label-skey', Message::TEXT, Message::USER_LANGUAGE );
+ }
+
+ if ( $propertyLabel === null ) {
+ continue;
+ }
+
+ $head = HtmlDivTable::cell(
+ $propertyLabel,
+ [
+ "class" => 'smwb-cell smwb-prophead' . ( $group !== '' ? " smwb-group-$group" : '' )
+ ]
+ );
+
+ $values = $semanticData->getPropertyValues( $diProperty );
+
+ if ( $incoming && ( count( $values ) >= $this->incomingValuesCount ) ) {
+ $moreIncoming = true;
+ $moreOutgoing = false;
+ array_pop( $values );
+ } elseif ( !$incoming && ( count( $values ) >= $this->outgoingValuesCount ) ) {
+ $moreIncoming = false;
+ $moreOutgoing = true;
+ array_pop( $values );
+ } else {
+ $moreIncoming = false;
+ $moreOutgoing = false;
+ }
+
+ $list = [];
+ $value_html = '';
+
+ foreach ( $values as $dataItem ) {
+ if ( $incoming ) {
+ $dv = $dataValueFactory->newDataValueByItem( $dataItem, null );
+ } else {
+ $dv = $dataValueFactory->newDataValueByItem( $dataItem, $diProperty );
+ }
+
+ $list[] = Html::rawElement(
+ 'span',
+ [
+ 'class' => "{$dirPrefix}value"
+ ],
+ ValueFormatter::getFormattedValue( $dv, $dvProperty, $incoming )
+ );
+ }
+
+ $last = array_pop( $list );
+ $value_html = implode( $comma, $list );
+
+ if ( ( $moreOutgoing || $moreIncoming ) && $last !== '' ) {
+ $value_html .= $comma . $last;
+ } elseif( $list !== [] && $last !== '' ) {
+ $value_html .= '&nbsp;' . $and . '&nbsp;' . $last;
+ } else {
+ $value_html .= $last;
+ }
+
+ $hook = false;
+
+ if ( $moreIncoming ) {
+ // Added in 2.3
+ // link to the remaining incoming pages
+ $hook = \Hooks::run(
+ 'SMW::Browse::BeforeIncomingPropertyValuesFurtherLinkCreate',
+ [
+ $diProperty,
+ $contextPage,
+ &$value_html,
+ $this->store
+ ]
+ );
+ }
+
+ if ( $hook ) {
+ $value_html .= Html::element(
+ 'a',
+ [
+ 'href' => \SpecialPage::getSafeTitleFor( 'SearchByProperty' )->getLocalURL( [
+ 'property' => $dvProperty->getWikiValue(),
+ 'value' => $this->dataValue->getWikiValue()
+ ] )
+ ],
+ wfMessage( 'smw_browse_more' )->text()
+ );
+ }
+
+ if ( $moreOutgoing ) {
+ $value_html .= Html::element(
+ 'a',
+ [
+ 'href' => \SpecialPage::getSafeTitleFor( 'PageProperty' )->getLocalURL( [
+ 'type' => $dvProperty->getWikiValue(),
+ 'from' => $this->dataValue->getWikiValue()
+ ] )
+ ],
+ wfMessage( 'smw_browse_more' )->text()
+ );
+ }
+
+ $body = HtmlDivTable::cell(
+ $value_html,
+ [
+ "class" => 'smwb-cell smwb-propval'
+ ]
+ );
+
+ // display row
+ $html .= HtmlDivTable::row(
+ ( $left ? ( $head . $body ) : ( $body . $head ) ),
+ [
+ "class" => "{$dirPrefix}propvalue"
+ ]
+ );
+
+ $noresult = false;
+ }
+
+ return $html;
+ }
+
+ /**
+ * Displays the subject that is currently being browsed to.
+ */
+ private function displayHead() {
+ return HtmlDivTable::table(
+ HtmlDivTable::row(
+ ValueFormatter::getFormattedSubject( $this->dataValue ),
+ [
+ 'class' => 'smwb-title'
+ ]
+ ),
+ [
+ 'class' => 'smwb-factbox'
+ ]
+ );
+ }
+
+ /**
+ * Creates the HTML for the center bar including the links with further
+ * navigation options.
+ */
+ private function displayActions() {
+
+ $html = '';
+ $group = $this->getOption( 'group' );
+ $article = $this->dataValue->getLongWikiText();
+
+ if ( $this->getOption( 'showGroup' ) ) {
+
+ if ( $group === 'hide' ) {
+ $parameters = [
+ 'offset' => 0,
+ 'dir' => $this->showincoming ? 'both' : 'out',
+ 'article' => $article,
+ 'group' => 'show'
+ ];
+
+ $linkMsg = 'smw-browse-show-group';
+ } else {
+ $parameters = [
+ 'offset' => $this->offset,
+ 'dir' => $this->showincoming ? 'both' : 'out',
+ 'article' => $article,
+ 'group' => 'hide'
+ ];
+
+ $linkMsg = 'smw-browse-hide-group';
+ }
+
+ $html .= FieldBuilder::createLink( $linkMsg, $parameters );
+ $html .= '<span class="smwb-action-separator">&nbsp;</span>';
+ }
+
+ if ( $this->showoutgoing ) {
+
+ if ( $this->showincoming ) {
+ $parameters = [
+ 'offset' => 0,
+ 'dir' => 'out',
+ 'article' => $article,
+ 'group' => $group
+ ];
+
+ $linkMsg = 'smw_browse_hide_incoming';
+ } else {
+ $parameters = [
+ 'offset' => $this->offset,
+ 'dir' => 'both',
+ 'article' => $article,
+ 'group' => $group
+ ];
+
+ $linkMsg = 'smw_browse_show_incoming';
+ }
+
+ $html .= FieldBuilder::createLink( $linkMsg, $parameters );
+ }
+
+ return HtmlDivTable::table(
+ HtmlDivTable::row(
+ $html . "&#160;\n",
+ [
+ 'class' => 'smwb-actions'
+ ]
+ ),
+ [
+ 'class' => 'smwb-factbox'
+ ]
+ );
+ }
+
+ private function displayCenter() {
+ return HtmlDivTable::table(
+ HtmlDivTable::row(
+ "&#160;\n",
+ [
+ 'class' => 'smwb-center'
+ ]
+ ),
+ [
+ 'class' => 'smwb-factbox'
+ ]
+ );
+ }
+
+ /**
+ * Creates the HTML for the bottom bar including the links with further
+ * navigation options.
+ */
+ private function displayBottom( $more ) {
+
+ $article = $this->dataValue->getLongWikiText();
+
+ $open = HtmlDivTable::open(
+ [
+ 'class' => 'smwb-factbox'
+ ]
+ );
+
+ $html = HtmlDivTable::row(
+ '&#160;',
+ [
+ 'class' => 'smwb-center'
+ ]
+ );
+
+ $close = HtmlDivTable::close();
+
+ if ( $this->getOption( 'showAll' ) ) {
+ return $open . $html . $close;
+ }
+
+ if ( ( $this->offset > 0 ) || $more ) {
+ $offset = max( $this->offset - $this->incomingPropertiesCount + 1, 0 );
+
+ $parameters = [
+ 'offset' => $offset,
+ 'dir' => $this->showoutgoing ? 'both' : 'in',
+ 'article' => $article
+ ];
+
+ $linkMsg = 'smw_result_prev';
+
+ $html .= ( $this->offset == 0 ) ? wfMessage( $linkMsg )->escaped() : FieldBuilder::createLink( $linkMsg, $parameters );
+
+ $offset = $this->offset + $this->incomingPropertiesCount - 1;
+
+ $parameters = [
+ 'offset' => $offset,
+ 'dir' => $this->showoutgoing ? 'both' : 'in',
+ 'article' => $article
+ ];
+
+ $linkMsg = 'smw_result_next';
+
+ $html .= " &#160;&#160;&#160; <strong>" . wfMessage( 'smw_result_results' )->escaped() . " " . ( $this->offset + 1 ) .
+ " – " . ( $offset ) . "</strong> &#160;&#160;&#160; ";
+ $html .= $more ? FieldBuilder::createLink( $linkMsg, $parameters ) : wfMessage( $linkMsg )->escaped();
+
+ $html = HtmlDivTable::row(
+ $html,
+ [
+ 'class' => 'smwb-center'
+ ]
+ );
+ }
+
+ return $open . $html . $close;
+ }
+
+ /**
+ * Creates a Semantic Data object with the incoming properties instead of the
+ * usual outgoing properties.
+ */
+ private function getInData() {
+
+ $indata = new SemanticData(
+ $this->dataValue->getDataItem()
+ );
+
+ $propRequestOptions = new RequestOptions();
+ $propRequestOptions->sort = true;
+ $propRequestOptions->setLimit( $this->incomingPropertiesCount );
+
+ if ( $this->offset > 0 ) {
+ $propRequestOptions->offset = $this->offset;
+ }
+
+ $incomingProperties = $this->store->getInProperties(
+ $this->dataValue->getDataItem(),
+ $propRequestOptions
+ );
+
+ $more = false;
+
+ if ( count( $incomingProperties ) == $this->incomingPropertiesCount ) {
+ $more = true;
+
+ // drop the last one
+ array_pop( $incomingProperties );
+ }
+
+ $valRequestOptions = new RequestOptions();
+ $valRequestOptions->sort = true;
+ $valRequestOptions->setLimit( $this->incomingValuesCount );
+
+ foreach ( $incomingProperties as $property ) {
+
+ $values = $this->store->getPropertySubjects(
+ $property,
+ $this->dataValue->getDataItem(),
+ $valRequestOptions
+ );
+
+ foreach ( $values as $dataItem ) {
+ $indata->addPropertyObjectValue( $property, $dataItem );
+ }
+ }
+
+ // Added in 2.3
+ // Whether to show a more link or not can be set via
+ // SMW::Browse::BeforeIncomingPropertyValuesFurtherLinkCreate
+ \Hooks::run(
+ 'SMW::Browse::AfterIncomingPropertiesLookupComplete',
+ [
+ $this->store,
+ $indata,
+ $valRequestOptions
+ ]
+ );
+
+ return [ $indata, $more ];
+ }
+
+ /**
+ * Returns HTML fragments for message classes in connection with categories
+ * linked to a property group.
+ */
+ private function getGroupMessageClassLinks( $groupFormatter, $semanticData, $dirPrefix ) {
+
+ $contextPage = $semanticData->getSubject();
+
+ if ( $contextPage->getNamespace() !== NS_CATEGORY || !$semanticData->hasProperty( new DIProperty( '_PPGR' ) ) ) {
+ return '';
+ }
+
+ $group = '';
+ $html = '';
+
+ $list = [
+ 'label' => $groupFormatter->getMessageClassLink(
+ GroupFormatter::MESSAGE_GROUP_LABEL,
+ $contextPage
+ ),
+ 'description' => $groupFormatter->getMessageClassLink(
+ GroupFormatter::MESSAGE_GROUP_DESCRIPTION,
+ $contextPage
+ )
+ ];
+
+ foreach ( $list as $k => $val ) {
+
+ if ( $val === '' ) {
+ continue;
+ }
+
+ $h = HtmlDivTable::cell(
+ wfMessage( 'smw-browse-property-group-' . $k )->text(),
+ [
+ "class" => 'smwb-cell smwb-prophead'
+ ]
+ ) . HtmlDivTable::cell(
+ $val,
+ [
+ "class" => 'smwb-cell smwb-propval'
+ ]
+ );
+
+ $group .= HtmlDivTable::row(
+ $h,
+ [
+ "class" => "{$dirPrefix}propvalue"
+ ]
+ );
+ }
+
+ if ( $group !== '' ) {
+ $h = HtmlDivTable::cell(
+ wfMessage( 'smw-browse-property-group-title' )->text(),
+ [
+ "class" => 'smwb-cell smwb-propval'
+ ]
+ ) . HtmlDivTable::cell(
+ '',
+ [
+ "class" => 'smwb-cell smwb-propval'
+ ]
+ );
+
+ $html = HtmlDivTable::row(
+ $h,
+ [
+ "class" => "{$dirPrefix}propvalue smwb-group-links"
+ ]
+ ) . $group;
+ }
+
+ return $html;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/ValueFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/ValueFormatter.php
new file mode 100644
index 00000000..98a87e48
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/Browse/ValueFormatter.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\Browse;
+
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DataValues\PropertyValue;
+use SMW\DataValues\ValueFormatters\DataValueFormatter;
+use SMW\DIProperty;
+use SMW\Localizer;
+use SMWDataValue as DataValue;
+use SMWInfolink as Infolink;
+
+/**
+ * @private
+ *
+ * This class should eventually be injected instead of relying on static methods,
+ * for now this is the easiest way to unclutter the mammoth Browse class and
+ * splitting up responsibilities.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ValueFormatter {
+
+ /**
+ * @since 2.5
+ *
+ * @param DataValue $value
+ *
+ * @return string
+ */
+ public static function getFormattedSubject( DataValue $dataValue ) {
+
+ $extra = '';
+
+ if ( $dataValue->getDataItem()->getNamespace() === SMW_NS_PROPERTY ) {
+
+ $dv = DataValueFactory::getInstance()->newDataValueByItem(
+ DIProperty::newFromUserLabel( $dataValue->getDataItem()->getDBKey() )
+ );
+
+ $label = $dv->getFormattedLabel( DataValueFormatter::WIKI_LONG );
+
+ // Those with a formatted displayTitle
+ // foaf:homepage&nbsp;<span style="font-size:small;">(Foaf:homepage)</span>
+ if ( strpos( $label, '&nbsp;<span' ) !== false ) {
+ list( $label, $extra ) = explode( '&nbsp;', $label );
+ $extra = '&nbsp;' . $extra;
+ }
+
+ $dataValue->setCaption( $label );
+ }
+
+ return $dataValue->getLongHTMLText( smwfGetLinker() ) . $extra;
+ }
+
+ /**
+ * Displays a value, including all relevant links (browse and search by property)
+ *
+ * @since 2.5
+ *
+ * @param DataValue $value
+ * @param PropertyValue $property
+ * @param boolean $incoming
+ *
+ * @return string
+ */
+ public static function getFormattedValue( DataValue $dataValue, PropertyValue $propertyValue, $incoming = false, $user = null ) {
+
+ $linker = smwfGetLinker();
+ $dataItem = $dataValue->getContextPage();
+
+ // Allow the DV formatter to access a specific language code
+ $dataValue->setOption(
+ DataValue::OPT_CONTENT_LANGUAGE,
+ Localizer::getInstance()->getPreferredContentLanguage( $dataItem )->getCode()
+ );
+
+ $dataValue->setOption(
+ DataValue::OPT_USER_LANGUAGE,
+ Localizer::getInstance()->getUserLanguage()->getCode()
+ );
+
+ $outputFormat = $dataValue->getOutputFormat();
+
+ if ( $outputFormat === false ) {
+ $outputFormat = 'LOCL';
+
+ if ( Localizer::getInstance()->hasLocalTimeOffsetPreference( $user ) ) {
+ $outputFormat .= '#TO';
+ }
+ }
+
+ // Use LOCL formatting where appropriate (date)
+ $dataValue->setOutputFormat( $outputFormat );
+
+ // For a redirect, disable the DisplayTitle to show the original (aka source) page
+ if ( $propertyValue->isValid() && $propertyValue->getDataItem()->getKey() == '_REDI' ) {
+ $dataValue->setOption( 'smwgDVFeatures', ( $dataValue->getOption( 'smwgDVFeatures' ) & ~SMW_DV_WPV_DTITLE ) );
+ }
+
+ $html = $dataValue->getLongHTMLText( $linker );
+
+ if ( $dataValue->getOption( DataValue::OPT_DISABLE_INFOLINKS, false ) === true ) {
+ return $html;
+ }
+
+ $isCompactLink = $dataValue->getOption( DataValue::OPT_COMPACT_INFOLINKS, false );
+ $noInfolinks = [ '_INST', '_SKEY' ];
+
+ if ( in_array( $dataValue->getTypeID(), [ '_wpg', '_wpp', '__sob'] ) ) {
+ $infolink = Infolink::newBrowsingLink( '+', $dataValue->getLongWikiText() );
+ $infolink->setCompactLink( $isCompactLink );
+ $html .= "&#160;" . $infolink->getHTML( $linker );
+ } elseif ( $incoming && $propertyValue->isVisible() ) {
+ $infolink = Infolink::newInversePropertySearchLink( '+', $dataValue->getTitle(), $propertyValue->getDataItem()->getLabel(), 'smwsearch' );
+ $infolink->setCompactLink( $isCompactLink );
+ $html .= "&#160;" . $infolink->getHTML( $linker );
+ } elseif ( $dataValue->getProperty() instanceof DIProperty && !in_array( $dataValue->getProperty()->getKey(), $noInfolinks ) ) {
+ $html .= $dataValue->getInfolinkText( SMW_OUTPUT_HTML, $linker );
+ }
+
+ return $html;
+ }
+
+ /**
+ * Figures out the label of the property to be used. For outgoing ones it is just
+ * the text, for incoming ones we try to figure out the inverse one if needed,
+ * either by looking for an explicitly stated one or by creating a default one.
+ *
+ * @since 2.5
+ *
+ * @param PropertyValue $property
+ * @param boolean $incoming
+ * @param boolean $showInverse
+ *
+ * @return string
+ */
+ public static function getPropertyLabel( PropertyValue $propertyValue, $incoming = false, $showInverse = false ) {
+
+ $proptext = null;
+
+ $linker = smwfGetLinker();
+ $property = $propertyValue->getDataItem();
+
+ if ( $propertyValue->isVisible() ) {
+ $propertyValue->setCaption( self::findPropertyLabel( $propertyValue, $incoming, $showInverse ) );
+ $proptext = $propertyValue->getShortHTMLText( $linker ) . "\n";
+ } elseif ( $property->getKey() == '_INST' ) {
+ $proptext = $linker->specialLink( 'Categories', 'smw-category' );
+ } elseif ( $property->getKey() == '_REDI' ) {
+ $proptext = $linker->specialLink( 'Listredirects', 'isredirect' );
+ }
+
+ return $proptext;
+ }
+
+ private static function findPropertyLabel( PropertyValue $propertyValue, $incoming = false, $showInverse = false ) {
+
+ $property = $propertyValue->getDataItem();
+ $contextPage = $propertyValue->getContextPage();
+
+ // Change caption for the incoming, Has query instance
+ if ( $incoming && $property->getKey() === '_ASK' && strpos( $contextPage->getSubobjectName(), '_QUERY' ) === false ) {
+ return self::addNonBreakingSpace( wfMessage( 'smw-query-reference-link-label' )->text() );
+ }
+
+ if ( !$incoming || !$showInverse ) {
+ return self::addNonBreakingSpace( $propertyValue->getWikiValue() );
+ }
+
+ $inverseProperty = DataValueFactory::getInstance()->newPropertyValueByLabel( wfMessage( 'smw_inverse_label_property' )->text() );
+
+ $dataItems = ApplicationFactory::getInstance()->getStore()->getPropertyValues(
+ $property->getDiWikiPage(),
+ $inverseProperty->getDataItem()
+ );
+
+ if ( $dataItems !== [] ) {
+ $text = str_replace( '_', ' ', end( $dataItems )->getDBKey() );
+ } else {
+ $text = wfMessage( 'smw_inverse_label_default', $propertyValue->getWikiValue() )->text();
+ }
+
+ return self::addNonBreakingSpace( $text );
+ }
+
+ /**
+ * Replace the last two space characters with unbreakable spaces for beautification.
+ *
+ * @since 2.5
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ public static function addNonBreakingSpace( $text ) {
+
+ $nonBreakingSpace = html_entity_decode( '&#160;', ENT_NOQUOTES, 'UTF-8' );
+ $text = preg_replace( '/[\s]/u', $nonBreakingSpace, $text, - 1, $count );
+
+ if ( $count > 2) {
+ return preg_replace( '/($nonBreakingSpace)/u', ' ', $text, max( 0, $count - 2 ) );
+ }
+
+ return $text;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/PageProperty/PageBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/PageProperty/PageBuilder.php
new file mode 100644
index 00000000..adf8ec0b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/PageProperty/PageBuilder.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\PageProperty;
+
+use Html;
+use SMW\DataTypeRegistry;
+use SMW\DataValueFactory;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\Message;
+use SMW\Options;
+use SMWInfolink as Infolink;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PageBuilder {
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var Linker
+ */
+ private $linker;
+
+ /**
+ * @since 3.0
+ *
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param Options $options
+ */
+ public function __construct( HtmlFormRenderer $htmlFormRenderer, Options $options ) {
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->options = $options;
+ $this->linker = smwfGetLinker();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $count
+ *
+ * @return string
+ */
+ public function buildForm( $count = 0 ) {
+
+ $html = Html::rawElement(
+ 'p',
+ [
+ 'class' => 'plainlinks'
+ ],
+ Message::get( 'smw-special-pageproperty-description', Message::PARSE, Message::USER_LANGUAGE )
+ );
+
+ $html .= $this->createForm( $count );
+
+ $html .= Html::element(
+ 'h2',
+ [],
+ Message::get( 'smw-sp-searchbyproperty-resultlist-header', Message::PARSE, Message::USER_LANGUAGE )
+ );
+
+ return $html;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage[]|[] $results
+ *
+ * @return string
+ */
+ public function buildHtml( array $results ) {
+
+ if ( count( $results ) == 0 ) {
+ return Message::get( 'smw_result_noresults', Message::TEXT, Message::USER_LANGUAGE );
+ }
+
+ $limit = $this->options->get( 'limit' );
+ $dataValueFactory = DataValueFactory::getInstance();
+
+ $propertyValue = $dataValueFactory->newPropertyValueByLabel(
+ $this->options->get( 'property' )
+ );
+
+ $property = $propertyValue->getDataItem();
+
+ $isBrowsableType = DataTypeRegistry::getInstance()->isBrowsableType(
+ $property->findPropertyTypeID()
+ );
+
+ $list = [];
+ $count = $limit + 1;
+
+ foreach ( $results as $dataItem ) {
+ $count--;
+ $link = '';
+
+ if ( $count < 1 ) {
+ continue;
+ }
+
+ $dataValue = $dataValueFactory->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ $link = $dataValue->getLongHTMLText( $this->linker );
+
+ if ( $isBrowsableType && $this->options->safeGet( 'from', '' ) !== '' ) {
+ $val = $dataValue->getLongWikiText();
+ $infolink = Infolink::newBrowsingLink( '+', $val );
+ $infolink->setLinkAttributes( [ 'title' => $val ] );
+ } else {
+ $val = $dataValue->getWikiValue();
+ $infolink = Infolink::newPropertySearchLink( '+', $property->getLabel(), $val );
+ $infolink->setLinkAttributes( [ 'title' => $val ] );
+ }
+
+ $link .= '&#160;&#160;' . $infolink->getHTML( $this->linker );
+ $list[] = $link;
+ }
+
+ return Html::rawElement(
+ 'ul',
+ [],
+ '<li>' . implode('</li><li>', $list ) . '</li>'
+ );
+ }
+
+ private function createForm( $count ) {
+
+ // Precaution to avoid any inline breakage caused by a div element
+ // within a paragraph (e.g Highlighter content)
+ // $resultMessage = str_replace( 'div', 'span', $resultMessage );
+
+ $this->htmlFormRenderer
+ ->setName( 'pageproperty' )
+ ->withFieldset()
+ ->addParagraph( Message::get( 'smw_pp_docu', Message::TEXT, Message::USER_LANGUAGE ) )
+ ->addPaging(
+ $this->options->safeGet( 'limit', 20 ),
+ $this->options->safeGet( 'offset', 0 ),
+ $count )
+ ->addHorizontalRule()
+ ->openElement( 'div', [ 'class' => 'smw-special-pageproperty-input' ] )
+ ->addInputField(
+ Message::get( 'smw_pp_from', Message::TEXT, Message::USER_LANGUAGE ),
+ 'from',
+ $this->options->safeGet( 'from', '' ),
+ 'smw-article-input',
+ 30,
+ [ 'class' => 'is-disabled' ] )
+ ->addNonBreakingSpace()
+ ->addInputField(
+ Message::get( 'smw_sbv_property', Message::TEXT, Message::USER_LANGUAGE ),
+ 'type',
+ $this->options->safeGet( 'type', '' ),
+ 'smw-property-input',
+ 20,
+ [ 'class' => 'is-disabled' ] )
+ ->addNonBreakingSpace()
+ ->addSubmitButton( Message::get( 'smw_sbv_submit', Message::TEXT, Message::USER_LANGUAGE ) )
+ ->closeElement( 'div' );
+
+ return $this->htmlFormRenderer->renderForm();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/PropertyLabelSimilarity/ContentsBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/PropertyLabelSimilarity/ContentsBuilder.php
new file mode 100644
index 00000000..c0efb3ff
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/PropertyLabelSimilarity/ContentsBuilder.php
@@ -0,0 +1,143 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\PropertyLabelSimilarity;
+
+use Html;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\Message;
+use SMW\RequestOptions;
+use SMW\SQLStore\Lookup\PropertyLabelSimilarityLookup;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ContentsBuilder {
+
+ /**
+ * @var PropertyLabelSimilarityLookup
+ */
+ private $propertyLabelSimilarityLookup;
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @since 2.5
+ *
+ * @param PropertyLabelSimilarityLookup $propertyLabelSimilarityLookup
+ * @param HtmlFormRenderer $htmlFormRenderer
+ */
+ public function __construct( PropertyLabelSimilarityLookup $propertyLabelSimilarityLookup, HtmlFormRenderer $htmlFormRenderer ) {
+ $this->propertyLabelSimilarityLookup = $propertyLabelSimilarityLookup;
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param RequestOptions $requestOption
+ */
+ public function getHtml( RequestOptions $requestOptions ) {
+
+ $threshold = 90;
+ $type = '';
+
+ foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
+ if ( isset( $extraCondition['type'] ) ) {
+ $type = $extraCondition['type'];
+ }
+
+ if ( isset( $extraCondition['threshold'] ) ) {
+ $threshold = $extraCondition['threshold'];
+ }
+ }
+
+ $this->propertyLabelSimilarityLookup->setThreshold(
+ $threshold
+ );
+
+ $result = $this->propertyLabelSimilarityLookup->compareAndFindLabels(
+ $requestOptions
+ );
+
+ $resultCount = is_array( $result ) ? count( $result ) : 0;
+
+ $html = $this->getForm(
+ $requestOptions->getLimit(),
+ $requestOptions->getOffset(),
+ $resultCount,
+ $threshold,
+ $type
+ );
+
+ if ( $result !== [] ) {
+ $html .= '<pre>' . json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</pre>';
+ } else {
+ $html .= $this->msg( 'smw-property-label-similarity-noresult' );
+ }
+
+ return $html;
+ }
+
+ private function getForm( $limit, $offset, $resultCount, $threshold, $type ) {
+
+ $exemptionProperty = $this->propertyLabelSimilarityLookup->getExemptionProperty();
+ $lookupCount = $this->propertyLabelSimilarityLookup->getLookupCount();
+
+ // Allow for an extra range since the property pool may be larger than
+ // the reductive comparison matches, +1 is to request additional paging
+ if ( $limit + $offset < $this->propertyLabelSimilarityLookup->getPropertyMaxCount() ) {
+ $lookupCount = $limit + $offset + 1;
+ }
+
+ $html = $this->msg(
+ [ 'smw-property-label-similarity-docu', $exemptionProperty ],
+ Message::PARSE
+ );
+
+ $html .= $this->htmlFormRenderer
+ ->setName( 'smw-property-label-similarity-title' )
+ ->setMethod( 'get' )
+ ->withFieldset()
+ ->addPaging(
+ $limit,
+ $offset,
+ $lookupCount,
+ $resultCount )
+ ->addHiddenField( 'limit', $limit )
+ ->addHiddenField( 'offset', $offset )
+ ->addInputField(
+ $this->msg( 'smw-property-label-similarity-threshold' ),
+ 'threshold',
+ $threshold,
+ '',
+ 5
+ )
+ ->addNonBreakingSpace()
+ ->addCheckbox(
+ $this->msg( 'smw-property-label-similarity-type' ),
+ 'type',
+ 'yes',
+ $type === 'yes',
+ null,
+ [
+ 'style' => 'float:right'
+ ]
+ )
+ ->addQueryParameter( 'type', $type )
+ ->addSubmitButton( $this->msg( 'allpagessubmit' ) )
+ ->getForm();
+
+ return Html::rawElement( 'div', [ 'class' => 'plainlinks'], $html ) . Html::element( 'p', [], '' );
+ }
+
+ private function msg( $parameters, $type = Message::TEXT ) {
+ return Message::get( $parameters, $type, Message::USER_LANGUAGE );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/PageBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/PageBuilder.php
new file mode 100644
index 00000000..7c2a7e5a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/PageBuilder.php
@@ -0,0 +1,395 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\SearchByProperty;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DataTypeRegistry;
+use SMW\DataValueFactory;
+use SMW\DataValues\StringValue;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\MessageBuilder;
+use SMW\MediaWiki\Renderer\HtmlFormRenderer;
+use SMW\ProcessingErrorMsgHandler;
+use SMWDataValue as DataValue;
+use SMWInfolink as Infolink;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Denny Vrandecic
+ * @author Daniel Herzig
+ * @author Markus Kroetzsch
+ * @author mwjames
+ */
+class PageBuilder {
+
+ /**
+ * @var HtmlFormRenderer
+ */
+ private $htmlFormRenderer;
+
+ /**
+ * @var PageRequestOptions
+ */
+ private $pageRequestOptions;
+
+ /**
+ * @var QueryResultLookup
+ */
+ private $queryResultLookup;
+
+ /**
+ * @var MessageBuilder
+ */
+ private $messageBuilder;
+
+ /**
+ * @var Linker
+ */
+ private $linker;
+
+ /**
+ * @since 2.1
+ *
+ * @param HtmlFormRenderer $htmlFormRenderer
+ * @param PageRequestOptions $pageRequestOptions
+ * @param QueryResultLookup $queryResultLookup
+ */
+ public function __construct( HtmlFormRenderer $htmlFormRenderer, PageRequestOptions $pageRequestOptions, QueryResultLookup $queryResultLookup ) {
+ $this->htmlFormRenderer = $htmlFormRenderer;
+ $this->pageRequestOptions = $pageRequestOptions;
+ $this->queryResultLookup = $queryResultLookup;
+ $this->linker = smwfGetLinker();
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return string
+ */
+ public function getHtml() {
+
+ $this->pageRequestOptions->initialize();
+ $this->messageBuilder = $this->htmlFormRenderer->getMessageBuilder();
+
+ list( $resultMessage, $resultList, $resultCount ) = $this->getResultHtml();
+
+ if ( ( $resultList === '' || $resultList === null ) &&
+ $this->pageRequestOptions->property->getDataItem() instanceof DIProperty &&
+ $this->pageRequestOptions->valueString === '' ) {
+ list( $resultMessage, $resultList, $resultCount ) = $this->tryToFindAtLeastOnePropertyTableReferenceFor(
+ $this->pageRequestOptions->property->getDataItem()
+ );
+ }
+
+ if ( $resultList === '' || $resultList === null ) {
+ $resultList = $this->messageBuilder->getMessage( 'smw_result_noresults' )->text();
+ }
+
+ $pageDescription = Html::rawElement(
+ 'p',
+ [ 'class' => 'smw-sp-searchbyproperty-description' ],
+ $this->messageBuilder->getMessage( 'smw-sp-searchbyproperty-description' )->parse()
+ );
+
+ $resultListHeader = Html::element(
+ 'h2',
+ [],
+ $this->messageBuilder->getMessage( 'smw-sp-searchbyproperty-resultlist-header' )->text()
+ );
+
+ return $pageDescription . $this->getHtmlForm( $resultMessage, $resultCount ) . $resultListHeader . $resultList;
+ }
+
+ private function getHtmlForm( $resultMessage, $resultCount ) {
+
+ // Precaution to avoid any inline breakage caused by a div element
+ // within a paragraph (e.g Highlighter content)
+ $resultMessage = str_replace( 'div', 'span', $resultMessage );
+
+ $html = $this->htmlFormRenderer
+ ->setName( 'searchbyproperty' )
+ ->withFieldset()
+ ->addParagraph( $resultMessage )
+ ->addPaging(
+ $this->pageRequestOptions->limit,
+ $this->pageRequestOptions->offset,
+ $resultCount )
+ ->addHorizontalRule()
+ ->addInputField(
+ $this->messageBuilder->getMessage( 'smw_sbv_property' )->text(),
+ 'property',
+ $this->pageRequestOptions->propertyString,
+ 'smw-property-input' )
+ ->addNonBreakingSpace()
+ ->addInputField(
+ $this->messageBuilder->getMessage( 'smw_sbv_value' )->text(),
+ 'value',
+ $this->pageRequestOptions->valueString,
+ 'smw-value-input' )
+ ->addNonBreakingSpace()
+ ->addSubmitButton( $this->messageBuilder->getMessage( 'smw_sbv_submit' )->text() )
+ ->getForm();
+
+ return $html;
+ }
+
+ private function getResultHtml() {
+
+ $resultList = '';
+ $resultMessage = '';
+
+ if ( $this->pageRequestOptions->propertyString === '' || !$this->pageRequestOptions->propertyString ) {
+ return [ $this->messageBuilder->getMessage( 'smw_sbv_docu' )->text(), '', 0 ];
+ }
+
+ // #1728
+ if ( !$this->pageRequestOptions->property->isValid() ) {
+ return [ ProcessingErrorMsgHandler::getMessagesAsString( $this->pageRequestOptions->property->getErrors() ), '', 0 ];
+ }
+
+ if ( $this->pageRequestOptions->valueString !== '' && !$this->pageRequestOptions->value->isValid() ) {
+ return [ ProcessingErrorMsgHandler::getMessagesAsString( $this->pageRequestOptions->value->getErrors() ), '', 0 ];
+ }
+
+ // Find out where the subject is used in connection with a query
+ if ( $this->isAskQueryLinksRelatedRequest() ) {
+ $exactResults = $this->queryResultLookup->doQueryLinksReferences( $this->pageRequestOptions );
+ $exactCount = count( $exactResults );
+ $resultList = $this->makeResultList( $exactResults, $this->pageRequestOptions->limit, true );
+ return [ str_replace( '_', ' ', $resultMessage ), $resultList, $exactCount ];
+ }
+
+ $exactResults = $this->queryResultLookup->doQuery( $this->pageRequestOptions );
+ $exactCount = count( $exactResults );
+
+ if ( $this->canQueryNearbyResults( $exactCount ) ) {
+ return $this->getNearbyResults( $exactResults, $exactCount );
+ }
+
+ if ( $this->pageRequestOptions->valueString === '' ) {
+ $resultMessageKey = 'smw-sp-searchbyproperty-nonvaluequery';
+ } else {
+ $resultMessageKey = 'smw-sp-searchbyproperty-valuequery';
+ }
+
+ $resultMessage = $this->messageBuilder->getMessage(
+ $resultMessageKey,
+ $this->pageRequestOptions->property->getShortHTMLText( $this->linker ),
+ $this->pageRequestOptions->value->getShortHTMLText( $this->linker ) )->text();
+
+ if ( $exactCount > 0 ) {
+ $resultList = $this->makeResultList( $exactResults, $this->pageRequestOptions->limit, true );
+ }
+
+ return [ str_replace( '_', ' ', $resultMessage ), $resultList, $exactCount ];
+ }
+
+ private function getNearbyResults( $exactResults, $exactCount ) {
+
+ $resultList = '';
+
+ $greaterResults = $this->queryResultLookup->doQueryForNearbyResults(
+ $this->pageRequestOptions,
+ $exactCount,
+ true
+ );
+
+ $smallerResults = $this->queryResultLookup->doQueryForNearbyResults(
+ $this->pageRequestOptions,
+ $exactCount,
+ false
+ );
+
+ // Calculate how many greater and smaller results should be displayed
+ $greaterCount = count( $greaterResults );
+ $smallerCount = count( $smallerResults );
+
+ if ( ( $greaterCount + $smallerCount + $exactCount ) > $this->pageRequestOptions->limit ) {
+ $lhalf = round( ( $this->pageRequestOptions->limit - $exactCount ) / 2 );
+
+ if ( $lhalf < $greaterCount ) {
+ if ( $lhalf < $smallerCount ) {
+ $smallerCount = $lhalf;
+ $greaterCount = $lhalf;
+ } else {
+ $greaterCount = $this->pageRequestOptions->limit - ( $exactCount + $smallerCount );
+ }
+ } else {
+ $smallerCount = $this->pageRequestOptions->limit - ( $exactCount + $greaterCount );
+ }
+ }
+
+ if ( ( $greaterCount + $smallerCount + $exactCount ) == 0 ) {
+ return [ '', $resultList, 0 ];
+ }
+
+ $resultMessage = $this->messageBuilder->getMessage(
+ 'smw_sbv_displayresultfuzzy',
+ $this->pageRequestOptions->property->getShortHTMLText( $this->linker ),
+ $this->pageRequestOptions->value->getShortHTMLText( $this->linker ) )->text();
+
+ $resultList .= $this->makeResultList( $smallerResults, $smallerCount, false );
+
+ if ( $exactCount == 0 ) {
+ $resultList .= "&#160;<em><strong><small>" . $this->messageBuilder->getMessage( 'parentheses' )
+ ->rawParams( $this->pageRequestOptions->value->getLongHTMLText() )
+ ->escaped() . "</small></strong></em>";
+ } else {
+ $resultList .= $this->makeResultList( $exactResults, $exactCount, true, true );
+ }
+
+ $resultList .= $this->makeResultList( $greaterResults, $greaterCount, true );
+
+ return [ $resultMessage, $resultList, $greaterCount + $exactCount ];
+ }
+
+ /**
+ * Creates the HTML for a bullet list with all the results of the set
+ * query. Values can be highlighted to show exact matches among nearby
+ * ones.
+ *
+ * @param array $results (array of (array of one or two SMWDataValues))
+ * @param integer $number How many results should be displayed? -1 for all
+ * @param boolean $first If less results should be displayed than
+ * given, should they show the first $number results, or the last
+ * $number results?
+ * @param boolean $highlight Should the results be highlighted?
+ *
+ * @return string HTML with the bullet list, including header
+ */
+ private function makeResultList( $results, $number, $first, $highlight = false ) {
+
+ if ( $number > 0 ) {
+ $results = $first ?
+ array_slice( $results, 0, $number ) :
+ array_slice( $results, $number );
+ }
+
+ $html = '';
+
+ foreach ( $results as $result ) {
+
+ $outputFormat = $result[0]->getOutputFormat();
+ $result[0]->setOutputFormat( $outputFormat ? $outputFormat : 'LOCL' );
+
+ $listitem = $result[0]->getLongHTMLText( $this->linker );
+
+ if ( $this->canShowSearchByPropertyLink( $result[0] ) ) {
+
+ // Copy the instance for the InfoLinker
+ $res = clone $result[0];
+ $res->setOutputFormat( '' );
+
+ $value = $res instanceof StringValue && $res->getLength() < 72 ? $res->getWikiValue() : mb_substr( $res->getWikiValue(), 0, 72 );
+
+ $listitem .= '&#160;&#160;' . Infolink::newPropertySearchLink(
+ '+',
+ $this->pageRequestOptions->propertyString,
+ $value
+ )->getHTML( $this->linker );
+ } elseif ( $result[0]->getTypeID() === '_wpg' ) {
+
+ // Add browsing link for wikipage results
+ // Note: non-wikipage results are possible using inverse properties
+ $listitem .= '&#160;&#160;' . Infolink::newBrowsingLink(
+ '+',
+ $result[0]->getLongWikiText()
+ )->getHTML( $this->linker );
+ }
+
+ // Show value if not equal to the value that was searched
+ // or if the current results are to be highlighted:
+ if ( array_key_exists( 1, $result ) &&
+ ( $result[1] instanceof DataValue ) &&
+ ( !$result[1]->getDataItem() instanceof \SMWDIError ) &&
+ ( !$this->pageRequestOptions->value->getDataItem()->equals( $result[1]->getDataItem() )
+ || $highlight ) ) {
+
+ $outputFormat = $result[1]->getOutputFormat();
+ $result[1]->setOutputFormat( $outputFormat ? $outputFormat : 'LOCL' );
+
+ $listitem .= "&#160;<em><small>" . $this->messageBuilder->getMessage( 'parentheses' )
+ ->rawParams( $result[1]->getLongHTMLText( $this->linker ) )
+ ->escaped() . "</small></em>";
+ }
+
+ // Highlight values
+ if ( $highlight ) {
+ $listitem = "<strong>$listitem</strong>";
+ }
+
+ $html .= "<li>$listitem</li>";
+ }
+
+ return "<ul>$html</ul>";
+ }
+
+ private function canQueryNearbyResults( $exactCount ) {
+ return $exactCount < ( $this->pageRequestOptions->limit / 3 ) && $this->pageRequestOptions->nearbySearch && $this->pageRequestOptions->valueString !== '';
+ }
+
+ private function canShowSearchByPropertyLink ( DataValue $dataValue ) {
+
+ $dataTypeClass = DataTypeRegistry::getInstance()->getDataTypeClassById(
+ $dataValue->getTypeID()
+ );
+
+ return $this->pageRequestOptions->value instanceof $dataTypeClass && $this->pageRequestOptions->valueString === '';
+ }
+
+ private function tryToFindAtLeastOnePropertyTableReferenceFor( DIProperty $property ) {
+
+ $resultList = '';
+ $resultMessage = '';
+ $resultCount = 0;
+ $extra = '';
+
+ $dataItem = ApplicationFactory::getInstance()->getStore()->getPropertyTableIdReferenceFinder()->tryToFindAtLeastOneReferenceForProperty(
+ $property
+ );
+
+ if ( !$dataItem instanceof DIWikiPage ) {
+ $resultMessage = 'No reference found.';
+ return [ $resultMessage, $resultList, $resultCount ];
+ }
+
+ // In case the item has already been marked as deleted but is yet pending
+ // for removal
+ if ( $dataItem->getInterWiki() === ':smw-delete' ) {
+ $resultMessage = 'Item reference "' . $dataItem->getSubobjectName() . '" has already been marked for removal.';
+ $dataItem = new DIWikiPage( $dataItem->getDBKey(), $dataItem->getNamespace() );
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem
+ );
+
+ $outputFormat = $dataValue->getOutputFormat();
+ $dataValue->setOutputFormat( $outputFormat ? $outputFormat : 'LOCL' );
+
+ if ( $dataValue->isValid() ) {
+ //$resultMessage = 'Item reference for a zero-marked property.';
+ $resultList = $dataValue->getShortHtmlText( $this->linker ) . ' ' . $extra;
+ $resultCount++;
+
+ $resultList .= '&#160;&#160;' . Infolink::newBrowsingLink(
+ '+',
+ $dataValue->getLongWikiText()
+ )->getHTML( $this->linker );
+ }
+
+ return [ $resultMessage, $resultList, $resultCount ];
+ }
+
+ private function isAskQueryLinksRelatedRequest() {
+ return $this->pageRequestOptions->property !== '' &&
+ $this->pageRequestOptions->property->getDataItem()->getKey() === '_ASK' &&
+ $this->pageRequestOptions->value->isValid() &&
+ strpos( $this->pageRequestOptions->value->getWikiValue(), '_QUERY' ) === false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/PageRequestOptions.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/PageRequestOptions.php
new file mode 100644
index 00000000..0d16bf25
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/PageRequestOptions.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\SearchByProperty;
+
+use SMW\DataValueFactory;
+use SMW\DataValues\TelephoneUriValue;
+use SMW\Encoder;
+use SMWNumberValue as NumberValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class PageRequestOptions {
+
+ /**
+ * @var string
+ */
+ private $queryString;
+
+ /**
+ * @var array
+ */
+ private $requestOptions;
+
+ /**
+ * @var Encoder
+ */
+ private $urlEncoder;
+
+ /**
+ * @var PropertyValue
+ */
+ public $property;
+
+ /**
+ * @var string
+ */
+ public $propertyString;
+
+ /**
+ * @var string
+ */
+ public $valueString;
+
+ /**
+ * @var DataValue
+ */
+ public $value;
+
+ /**
+ * @var integer
+ */
+ public $limit = 20;
+
+ /**
+ * @var integer
+ */
+ public $offset = 0;
+
+ /**
+ * @var boolean
+ */
+ public $nearbySearch = false;
+
+ /**
+ * @since 2.1
+ *
+ * @param string $queryString
+ * @param array $requestOptions
+ */
+ public function __construct( $queryString, array $requestOptions ) {
+ $this->queryString = $queryString;
+ $this->requestOptions = $requestOptions;
+ $this->urlEncoder = new Encoder();
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function initialize() {
+
+ $params = explode( '/', $this->queryString );
+ reset( $params );
+ $escaped = false;
+
+ // Remove empty elements
+ $params = array_filter( $params, 'strlen' );
+
+ $property = isset( $this->requestOptions['property'] ) ? $this->requestOptions['property'] : current( $params );
+ $value = isset( $this->requestOptions['value'] ) ? $this->requestOptions['value'] : next( $params );
+
+ // Auto-generated link is marked with a leading :
+ if ( $property !== '' && $property{0} === ':' ) {
+ $escaped = true;
+ $property = $this->urlEncoder->unescape( ltrim( $property, ':' ) );
+ }
+
+ $this->property = DataValueFactory::getInstance()->newPropertyValueByLabel(
+ str_replace( [ '_' ], [ ' ' ], $property )
+ );
+
+ if ( !$this->property->isValid() ) {
+ $this->propertyString = $property;
+ $this->value = null;
+ $this->valueString = $value;
+ } else {
+ $this->propertyString = $this->property->getDataItem()->getLabel();
+ $this->valueString = $this->getValue( (string)$value, $escaped );
+ }
+
+ $this->setLimit();
+ $this->setOffset();
+ $this->setNearbySearch();
+ }
+
+ private function getValue( $value, $escaped ) {
+
+ $this->value = DataValueFactory::getInstance()->newDataValueByProperty(
+ $this->property->getDataItem()
+ );
+
+ $value = $this->unescape( $value, $escaped );
+ $this->value->setUserValue( $value );
+
+ return $this->value->isValid() ? $this->value->getWikiValue() : $value;
+ }
+
+ private function unescape( $value, $escaped ) {
+
+ if ( $this->value instanceof NumberValue ) {
+ $value = $escaped ? str_replace( [ '-20', '-2D' ], [ ' ', '-' ], $value ) : $value;
+ // Do not try to decode things like 1.2e-13
+ // Signals that we don't want any precision limitation
+ $this->value->setOption( NumberValue::NO_DISP_PRECISION_LIMIT, true );
+ } elseif ( $this->value instanceof TelephoneUriValue ) {
+ $value = $escaped ? str_replace( [ '-20', '-2D' ], [ ' ', '-' ], $value ) : $value;
+ // No encoding to avoid turning +1-201-555-0123
+ // into +1 1U523 or further obfuscate %2B1-2D201-2D555-2D0123 ...
+ } else {
+ $value = $escaped ? $this->urlEncoder->unescape( $value ) : $value;
+ }
+
+ return $value;
+ }
+
+ private function setLimit() {
+ if ( isset( $this->requestOptions['limit'] ) ) {
+ $this->limit = intval( $this->requestOptions['limit'] );
+ }
+ }
+
+ private function setOffset() {
+ if ( isset( $this->requestOptions['offset'] ) ) {
+ $this->offset = intval( $this->requestOptions['offset'] );
+ }
+ }
+
+ private function setNearbySearch() {
+
+ if ( $this->value === null ) {
+ return null;
+ }
+
+ if ( isset( $this->requestOptions['nearbySearchForType'] ) && is_array( $this->requestOptions['nearbySearchForType'] ) ) {
+ $this->nearbySearch = in_array( $this->value->getTypeID(), $this->requestOptions['nearbySearchForType'] );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/QueryResultLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/QueryResultLookup.php
new file mode 100644
index 00000000..2b689877
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SearchByProperty/QueryResultLookup.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace SMW\MediaWiki\Specials\SearchByProperty;
+
+use SMW\DataValueFactory;
+use SMW\DIWikiPage;
+use SMW\Query\DescriptionFactory;
+use SMW\Query\PrintRequest as PrintRequest;
+use SMW\SQLStore\QueryDependencyLinksStoreFactory;
+use SMW\Store;
+use SMWQuery as Query;
+use SMWRequestOptions as RequestOptions;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Denny Vrandecic
+ * @author Daniel Herzig
+ * @author Markus Kroetzsch
+ * @author mwjames
+ */
+class QueryResultLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 2.1
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param QueryOptions $pageRequestOptions
+ *
+ * @return array
+ */
+ public function doQueryLinksReferences( PageRequestOptions $pageRequestOptions ) {
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->setLimit( $pageRequestOptions->limit + 1 );
+ $requestOptions->setOffset( $pageRequestOptions->offset );
+ $requestOptions->sort = true;
+
+ $queryDependencyLinksStoreFactory = new QueryDependencyLinksStoreFactory();
+
+ $queryReferenceLinks = $queryDependencyLinksStoreFactory->newQueryReferenceBacklinks(
+ $this->store
+ );
+
+ $queryBacklinks = $queryReferenceLinks->findReferenceLinks(
+ $pageRequestOptions->value->getDataItem(),
+ $requestOptions
+ );
+
+ $results = [];
+
+ $dataValueFactory = DataValueFactory::getInstance();
+
+ foreach ( $queryBacklinks as $result ) {
+ $results[] = [
+ $dataValueFactory->newDataValueByItem( DIWikiPage::doUnserialize( $result ), null ),
+ $pageRequestOptions->value
+ ];
+ }
+
+ return $results;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param QueryOptions $pageRequestOptions
+ *
+ * @return array of array(SMWWikiPageValue, SMWDataValue) with the
+ * first being the entity, and the second the value
+ */
+ public function doQuery( PageRequestOptions $pageRequestOptions ) {
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->limit = $pageRequestOptions->limit + 1;
+ $requestOptions->offset = $pageRequestOptions->offset;
+ $requestOptions->sort = true;
+
+ if ( $pageRequestOptions->value === null || !$pageRequestOptions->value->isValid() ) {
+ $res = $this->doQueryForNonValue( $pageRequestOptions, $requestOptions );
+ } else {
+ $res = $this->doQueryForExactValue( $pageRequestOptions, $requestOptions );
+ }
+
+ $results = [];
+
+ $dataValueFactory = DataValueFactory::getInstance();
+
+ foreach ( $res as $result ) {
+ $results[] = [
+ $dataValueFactory->newDataValueByItem( $result, null ),
+ $pageRequestOptions->value
+ ];
+ }
+
+ return $results;
+ }
+
+ /**
+ * Returns all results that have a value near to the searched for value
+ * on the property, ordered, and sorted by ending with the smallest
+ * one.
+ *
+ * @param QueryOptions $pageRequestOptions
+ * @param integer $count How many entities have the exact same value on the property?
+ * @param integer $greater Should the values be bigger? Set false for smaller values.
+ *
+ * @return array of array of SMWWikiPageValue, SMWDataValue with the
+ * first being the entity, and the second the value
+ */
+ public function doQueryForNearbyResults( PageRequestOptions $pageRequestOptions, $count, $greater = true ) {
+
+ $comparator = $greater ? SMW_CMP_GRTR : SMW_CMP_LESS;
+ $sortOrder = $greater ? 'ASC' : 'DESC';
+
+ if ( $pageRequestOptions->value !== null && $pageRequestOptions->value->getTypeID() === '_txt' && strlen( $pageRequestOptions->valueString ) > 72 ) {
+ $comparator = SMW_CMP_LIKE;
+ }
+
+ $descriptionFactory = new DescriptionFactory();
+
+ if ( $pageRequestOptions->valueString === '' || $pageRequestOptions->valueString === null ) {
+ $description = $descriptionFactory->newThingDescription();
+ } else {
+ $description = $descriptionFactory->newValueDescription(
+ $pageRequestOptions->value->getDataItem(),
+ $pageRequestOptions->property->getDataItem(),
+ $comparator
+ );
+
+ $description = $descriptionFactory->newSomeProperty(
+ $pageRequestOptions->property->getDataItem(),
+ $description
+ );
+ }
+
+ $query = new Query( $description );
+
+ $query->setLimit( $pageRequestOptions->limit );
+ $query->setOffset( $pageRequestOptions->offset );
+ $query->sort = true;
+ $query->sortkeys = [
+ $pageRequestOptions->property->getDataItem()->getKey() => $sortOrder
+ ];
+
+ // Note: printrequests change the caption of properties they
+ // get (they expect properties to be given to them).
+ // Since we want to continue using the property for our
+ // purposes, we give a clone to the print request.
+ $printouts = [
+ new PrintRequest( PrintRequest::PRINT_THIS, '' ),
+ new PrintRequest( PrintRequest::PRINT_PROP, '', clone $pageRequestOptions->property )
+ ];
+
+ $query->setExtraPrintouts( $printouts );
+
+ $queryResults = $this->store->getQueryResult( $query );
+
+ $result = [];
+
+ while ( $resultArrays = $queryResults->getNext() ) {
+ $r = [];
+
+ foreach ( $resultArrays as $resultArray ) {
+ $r[] = $resultArray->getNextDataValue();
+ }
+ // Note: if results have multiple values for the property
+ // then this code just pick the first, which may not be
+ // the reason why the result is shown here, i.e., it could
+ // be out of order.
+ $result[] = $r;
+ }
+
+ if ( !$greater ) {
+ $result = array_reverse( $result );
+ }
+
+ return $result;
+ }
+
+ private function doQueryForNonValue( PageRequestOptions $pageRequestOptions, RequestOptions $requestOptions ) {
+ return $this->store->getPropertyValues(
+ null,
+ $pageRequestOptions->property->getDataItem(),
+ $requestOptions
+ );
+ }
+
+ private function doQueryForExactValue( PageRequestOptions $pageRequestOptions, RequestOptions $requestOptions ) {
+
+ $pageRequestOptions->value->setOption( 'is.search', true );
+
+ return $this->store->getPropertySubjects(
+ $pageRequestOptions->property->getDataItem(),
+ $pageRequestOptions->value->getDataItem(),
+ $requestOptions
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialAdmin.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialAdmin.php
new file mode 100644
index 00000000..d2677549
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialAdmin.php
@@ -0,0 +1,260 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Exception\ExtendedPermissionsError;
+use SMW\MediaWiki\Specials\Admin\OutputFormatter;
+use SMW\MediaWiki\Specials\Admin\TaskHandler;
+use SMW\MediaWiki\Specials\Admin\TaskHandlerFactory;
+use SMW\Message;
+use SMW\Utils\HtmlTabs;
+use SpecialPage;
+
+/**
+ * This special page for MediaWiki provides an administrative interface
+ * that allows to execute certain functions related to the maintenance
+ * of the semantic database.
+ *
+ * Access to the special page and its function is limited to users with the
+ * `smw-admin` right.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class SpecialAdmin extends SpecialPage {
+
+ /**
+ * @codeCoverageIgnore
+ */
+ public function __construct() {
+ parent::__construct( 'SMWAdmin', 'smw-admin' );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function doesWrites() {
+ return true;
+ }
+
+ /**
+ * @see SpecialPage::execute
+ */
+ public function execute( $query ) {
+
+ if ( !$this->userCanExecute( $this->getUser() ) ) {
+ // $this->mRestriction is private MW 1.23-
+ throw new ExtendedPermissionsError( 'smw-admin', [ 'smw-admin-permission-missing' ] );
+ }
+
+ // https://phabricator.wikimedia.org/T109652#1562641
+ $this->getRequest()->setVal(
+ 'wpEditToken',
+ $this->getUser()->getEditToken()
+ );
+
+ $this->setHeaders();
+ $output = $this->getOutput();
+ $output->setPageTitle( $this->msg_text( 'smw-title' ) );
+
+ $output->addModuleStyles( 'ext.smw.special.style' );
+ $output->addModules( 'ext.smw.admin' );
+
+ if ( $query !== null ) {
+ $this->getRequest()->setVal( 'action', $query );
+ }
+
+ $action = $this->getRequest()->getText( 'action' );
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $mwCollaboratorFactory = $applicationFactory->newMwCollaboratorFactory();
+
+ $htmlFormRenderer = $mwCollaboratorFactory->newHtmlFormRenderer(
+ $this->getContext()->getTitle(),
+ $this->getLanguage()
+ );
+
+ // Some functions require methods only provided by the SQLStore (or any
+ // inherit class thereof)
+ if ( !is_a( ( $store = $applicationFactory->getStore() ), '\SMW\SQLStore\SQLStore' ) ) {
+ $store = $applicationFactory->getStore( '\SMW\SQLStore\SQLStore' );
+ }
+
+ $outputFormatter = new OutputFormatter(
+ $this->getOutput()
+ );
+
+ $adminFeatures = $applicationFactory->getSettings()->get( 'smwgAdminFeatures' );
+
+ // Disable the feature in case the function is not supported
+ if ( $applicationFactory->getSettings()->get( 'smwgEnabledFulltextSearch' ) === false ) {
+ $adminFeatures = $adminFeatures & ~SMW_ADM_FULLT;
+ }
+
+ $taskHandlerFactory = new TaskHandlerFactory(
+ $store,
+ $htmlFormRenderer,
+ $outputFormatter
+ );
+
+ $taskHandlerList = $taskHandlerFactory->getTaskHandlerList(
+ $this->getUser(),
+ $adminFeatures
+ );
+
+ foreach ( $taskHandlerList['actions'] as $actionTask ) {
+ if ( $actionTask->isTaskFor( $action ) ) {
+ return $actionTask->handleRequest( $this->getRequest() );
+ }
+ }
+
+ $output->addHTML(
+ $this->buildHTML( $taskHandlerList )
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getGroupName() {
+ return 'smw_group';
+ }
+
+ private function buildHTML( $taskHandlerList ) {
+
+ $tableSchemaTaskList = $taskHandlerList[TaskHandler::SECTION_SCHEMA];
+
+ $dataRebuildSection = end( $tableSchemaTaskList )->getHtml();
+ $dataRebuildSection .= Html::rawElement(
+ 'hr',
+ [
+ 'class' => 'smw-admin-hr'
+ ],
+ ''
+ ) . Html::rawElement(
+ 'p',
+ [
+ 'class' => 'plainlinks',
+ 'style' => 'margin-top:0.8em;'
+ ],
+ $this->msg_text( 'smw-admin-job-scheduler-note', Message::PARSE )
+ );
+
+ $list = '';
+ $dataRepairTaskList = $taskHandlerList[TaskHandler::SECTION_DATAREPAIR];
+
+ foreach ( $dataRepairTaskList as $dataRepairTask ) {
+ $list .= $dataRepairTask->getHtml();
+ }
+
+ $dataRebuildSection .= Html::rawElement( 'div', [ 'class' => 'smw-admin-data-repair-section' ],
+ $list
+ );
+
+ $supplementarySection = Html::rawElement(
+ 'p',
+ [
+ 'class' => 'plainlinks'
+ ],
+ $this->msg_text( 'smw-admin-supplementary-section-intro', Message::PARSE )
+ ) . Html::rawElement(
+ 'h3',
+ [],
+ $this->msg_text( 'smw-admin-supplementary-section-subtitle' )
+ );
+
+ $list = '';
+ $supplementaryTaskList = $taskHandlerList[TaskHandler::SECTION_SUPPLEMENT];
+
+ foreach ( $supplementaryTaskList as $supplementaryTask ) {
+ $list .= $supplementaryTask->getHtml();
+ }
+
+ $supplementarySection .= Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-admin-supplementary-section'
+ ],
+ Html::rawElement( 'ul', [], $list )
+ );
+
+ $deprecationNoticeTaskList = $taskHandlerList[TaskHandler::SECTION_DEPRECATION];
+ $deprecationNoticeTaskHandler = end( $deprecationNoticeTaskList );
+
+ $deprecationNotices = $deprecationNoticeTaskHandler->getHtml();
+ $htmlTabs = new HtmlTabs();
+
+ $default = $deprecationNotices === '' ? 'general' : 'notices';
+
+ // If we want to remain on a specific tab on a GET request, use the `tab`
+ // parameter since we are unable to fetch any #href hash from a request
+ $htmlTabs->setActiveTab(
+ $this->getRequest()->getVal( 'tab', $default )
+ );
+
+ $htmlTabs->tab( 'general', $this->msg_text( 'smw-admin-tab-general' ) );
+
+ $htmlTabs->tab(
+ 'notices',
+ '⚠ ' . $this->msg_text( 'smw-admin-tab-notices' ),
+ [
+ 'hide' => $deprecationNotices === '' ? true : false,
+ 'class' => 'smw-tab-warning'
+ ]
+ );
+
+ $htmlTabs->tab( 'rebuild', $this->msg_text( 'smw-admin-tab-rebuild' ) );
+ $htmlTabs->tab( 'supplement', $this->msg_text( 'smw-admin-tab-supplement' ) );
+
+ $supportTaskList = $taskHandlerList[TaskHandler::SECTION_SUPPORT];
+ $supportListTaskHandler = end( $supportTaskList );
+
+ $html = Html::rawElement(
+ 'p',
+ [],
+ $this->msg_text( 'smw-admin-docu' )
+ ) . Html::rawElement(
+ 'h3',
+ [],
+ $this->msg_text( 'smw-admin-environment' )
+ ) . Html::rawElement(
+ 'pre',
+ [],
+ json_encode( $this->getInfo(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
+ ) . $supportListTaskHandler->createSupportForm() .
+ $supportListTaskHandler->createRegistryForm();
+
+ $htmlTabs->content( 'general', $html );
+ $htmlTabs->content( 'notices', $deprecationNotices );
+ $htmlTabs->content( 'rebuild', $dataRebuildSection );
+ $htmlTabs->content( 'supplement', $supplementarySection );
+
+ $html = $htmlTabs->buildHTML(
+ [ 'class' => 'smw-admin' ]
+ );
+
+ return $html;
+ }
+
+ private function getInfo() {
+
+ $store = ApplicationFactory::getInstance()->getStore();
+
+ return $store->getInfo() + [
+ 'smw' => SMW_VERSION,
+ 'mediawiki' => $GLOBALS['wgVersion']
+ ] + (
+ defined( 'HHVM_VERSION' ) ? [ 'hhvm' => HHVM_VERSION ] : [ 'php' => PHP_VERSION ]
+ );
+ }
+
+ private function msg_text( $key, $type = Message::TEXT) {
+ return Message::get( $key, $type , Message::USER_LANGUAGE );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialAsk.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialAsk.php
new file mode 100644
index 00000000..fe2d817d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialAsk.php
@@ -0,0 +1,714 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use Html;
+use ParamProcessor\Param;
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Specials\Ask\ErrorWidget;
+use SMW\MediaWiki\Specials\Ask\FormatListWidget;
+use SMW\MediaWiki\Specials\Ask\HelpWidget;
+use SMW\MediaWiki\Specials\Ask\LinksWidget;
+use SMW\MediaWiki\Specials\Ask\NavigationLinksWidget;
+use SMW\MediaWiki\Specials\Ask\ParametersProcessor;
+use SMW\MediaWiki\Specials\Ask\ParametersWidget;
+use SMW\MediaWiki\Specials\Ask\QueryInputWidget;
+use SMW\MediaWiki\Specials\Ask\SortWidget;
+use SMW\MediaWiki\Specials\Ask\UrlArgs;
+use SMW\MediaWiki\Specials\Ask\HtmlForm;
+use SMW\Query\PrintRequest;
+use SMW\Query\QueryLinker;
+use SMW\Query\RemoteRequest;
+use SMW\Query\Result\StringResult;
+use SMW\Utils\HtmlModal;
+use SMWInfolink as Infolink;
+use SMWOutputs;
+use SMWQuery;
+use SMWQueryProcessor as QueryProcessor;
+use SMWQueryResult as QueryResult;
+use SpecialPage;
+use SMW\Utils\HtmlTabs;
+use SMW\Message;
+
+/**
+ * This special page for MediaWiki implements a customisable form for executing
+ * queries outside of articles.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ * @author Yaron Koren
+ * @author Sanyam Goyal
+ * @author Jeroen De Dauw
+ */
+class SpecialAsk extends SpecialPage {
+
+ /**
+ * @var QuerySourceFactory
+ */
+ private $querySourceFactory;
+
+ /**
+ * @var string
+ */
+ private $queryString = '';
+
+ /**
+ * @var array
+ */
+ private $parameters = [];
+
+ /**
+ * @var array
+ */
+ private $printouts = [];
+
+ /**
+ * @var boolean
+ */
+ private $isEditMode = false;
+
+ /**
+ * @var boolean
+ */
+ private $isBorrowedMode = false;
+
+ /**
+ * @var Param[]
+ */
+ private $params = [];
+
+ public function __construct() {
+ parent::__construct( 'Ask' );
+ $this->querySourceFactory = ApplicationFactory::getInstance()->getQuerySourceFactory();
+ }
+
+ /**
+ * @see SpecialPage::doesWrites
+ *
+ * @return boolean
+ */
+ public function doesWrites() {
+ return true;
+ }
+
+ /**
+ * @see SpecialPage::execute
+ *
+ * @param string $p
+ */
+ public function execute( $p ) {
+
+ $this->setHeaders();
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $out = $this->getOutput();
+ $request = $this->getRequest();
+ $title = SpecialPage::getSafeTitleFor( 'Ask' );
+
+ // A GET form submit cannot use a fragment (aka anchor) to repositioning
+ // to a specific target after a request has completed, use a redirect
+ // with the posted query values from the submit form to add an anchor
+ // point
+ if ( $settings->is( 'smwgSpecialAskFormSubmitMethod', SMW_SASK_SUBMIT_GET_REDIRECT ) && $request->getVal( '_action' ) === 'submit' ) {
+ $vals = $request->getQueryValues();
+
+ unset( $vals['_action'] );
+ unset( $vals['title'] );
+
+ return $out->redirect(
+ $title->getLocalUrl( wfArrayToCGI( $vals ) . '#search' )
+ );
+ }
+
+ $request->setVal( 'wpEditToken',
+ $this->getUser()->getEditToken()
+ );
+
+ if ( !$GLOBALS['smwgQEnabled'] ) {
+ return $out->addHtml( ErrorWidget::disabled() );
+ }
+
+ // Administrative block when used in combination with the `RemoteRequest`.
+ // It is not to be mistaken with an auth block as you always can fetch
+ // the content from a public wiki via cURL.
+ if ( $request->getVal( 'request_type', '' ) !== '' && !$settings->isFlagSet( 'smwgRemoteReqFeatures', SMW_REMOTE_REQ_SEND_RESPONSE ) ) {
+ $out->disable();
+ return print RemoteRequest::SOURCE_DISABLED;
+ }
+
+ $this->init();
+
+ if ( $request->getCheck( 'showformatoptions' ) ) {
+ // handle Ajax action
+ $params = $request->getArray( 'params' );
+ $params['format'] = $request->getVal( 'showformatoptions' );
+ $out->disable();
+ echo ParametersWidget::parameterList( $params );
+ } else {
+ $this->extractQueryParameters( $p );
+
+ if ( $this->isBorrowedMode ) {
+ $visibleLinks = [];
+ } elseif( $request->getVal( 'eq', '' ) === 'no' || $p !== null || $request->getVal( 'x' ) || $request->getVal( 'cl' ) ) {
+ $visibleLinks = [ 'search', 'empty' ];
+ } else {
+ $visibleLinks = [ 'options', 'search', 'help', 'empty' ];
+ }
+
+ $out->addHTML(
+ NavigationLinksWidget::topLinks(
+ $title,
+ $visibleLinks,
+ $this->isEditMode
+ )
+ );
+
+ $this->makeHTMLResult();
+ }
+
+ $out->addHTML( HelpWidget::html() );
+ $this->addHelpLink( wfMessage( 'smw_ask_doculink' )->escaped(), true );
+
+ // make sure locally collected output data is pushed to the output!
+ SMWOutputs::commitToOutputPage( $out );
+ }
+
+ /**
+ * @see SpecialPage::getGroupName
+ */
+ protected function getGroupName() {
+ return 'smw_group';
+ }
+
+ private function init() {
+ $out = $this->getOutput();
+ $request = $this->getRequest();
+
+ $out->addModuleStyles( 'ext.smw.style' );
+ $out->addModuleStyles( 'ext.smw.ask.styles' );
+ $out->addModuleStyles( 'ext.smw.table.styles' );
+ $out->addModuleStyles( 'ext.smw.page.styles' );
+
+ $out->addModuleStyles(
+ HtmlModal::getModuleStyles()
+ );
+
+ $out->addModules( 'ext.smw.ask' );
+ $out->addModules( 'ext.smw.autocomplete.property' );
+
+ $out->addModules(
+ LinksWidget::getModules()
+ );
+
+ $out->addModules(
+ HtmlModal::getModules()
+ );
+
+ $out->addHTML( ErrorWidget::noScript() );
+
+ // #2590
+ if ( !$this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ) ) {
+ return $out->addHtml( ErrorWidget::sessionFailure() );
+ }
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ NavigationLinksWidget::setMaxInlineLimit(
+ $GLOBALS['smwgQMaxInlineLimit']
+ );
+
+ FormatListWidget::setResultFormats(
+ $GLOBALS['smwgResultFormats']
+ );
+
+ ParametersWidget::setTooltipDisplay(
+ $this->getUser()->getOption( 'smw-prefs-ask-options-tooltip-display' )
+ );
+
+ ParametersWidget::setDefaultLimit(
+ $GLOBALS['smwgQDefaultLimit']
+ );
+
+ SortWidget::setSortingSupport(
+ $settings->isFlagSet( 'smwgQSortFeatures', SMW_QSORT )
+ );
+
+ // @see #835
+ SortWidget::setRandSortingSupport(
+ $settings->isFlagSet( 'smwgQSortFeatures', SMW_QSORT_RANDOM )
+ );
+
+ ParametersProcessor::setDefaultLimit(
+ $GLOBALS['smwgQDefaultLimit']
+ );
+
+ ParametersProcessor::setMaxInlineLimit(
+ $GLOBALS['smwgQMaxInlineLimit']
+ );
+
+ $this->isBorrowedMode = $request->getCheck( 'bTitle' ) || $request->getCheck( 'btitle' );
+ }
+
+ /**
+ * @param string $p
+ */
+ protected function extractQueryParameters( $p ) {
+
+ $request = $this->getRequest();
+ $this->isEditMode = false;
+
+ if ( $request->getText( 'cl', '' ) !== '' ) {
+ $p = Infolink::decodeCompactLink( 'cl:' . $request->getText( 'cl' ) );
+ } else {
+ $p = Infolink::decodeCompactLink( $p );
+ }
+
+ list( $this->queryString, $this->parameters, $this->printouts ) = ParametersProcessor::process(
+ $request,
+ $p
+ );
+
+ if ( isset( $this->parameters['btitle'] ) ) {
+ $this->isBorrowedMode = true;
+ }
+
+ if ( ( $request->getVal( 'eq' ) == 'yes' ) || ( $this->queryString === '' ) ) {
+ $this->isEditMode = true;
+ }
+ }
+
+ protected function makeHTMLResult() {
+
+ $result = '';
+ $res = null;
+ $settings = ApplicationFactory::getInstance()->getSettings();
+ $queryobj = null;
+
+ $navigation = '';
+ $urlArgs = $this->newUrlArgs();
+
+ $isFromCache = false;
+ $duration = 0;
+
+ $error = '';
+ $printer = null;
+
+ if ( $this->queryString !== '' ) {
+ list( $result, $res, $duration ) = $this->fetchResults(
+ $printer,
+ $queryobj,
+ $urlArgs
+ );
+ }
+
+ if ( $printer !== null && $printer->isExportFormat() ) {
+
+ // Avoid a possible "Cannot modify header information - headers already sent by ..."
+ if ( defined( 'MW_PHPUNIT_TEST' ) && method_exists( $printer, 'disableHttpHeader' ) ) {
+ $printer->disableHttpHeader();
+ }
+
+ $this->getOutput()->disable();
+ $request_type = $this->getRequest()->getVal( 'request_type' );
+
+ if ( $request_type === 'embed' ) {
+ // Just send a furthers link output for an embedded remote request
+ echo $printer->getResult( $res, $this->params, SMW_OUTPUT_HTML ) . RemoteRequest::REQUEST_ID;
+ } elseif ( $request_type === 'special_page' ) {
+ // Generate raw content when being requested from a remote special_page
+ echo $printer->getResult( $res, $this->params, SMW_OUTPUT_FILE ) . RemoteRequest::REQUEST_ID;
+ } else {
+ return $printer->outputAsFile( $res, $this->params );
+ }
+ }
+
+ if ( $this->queryString ) {
+ $this->getOutput()->setHTMLtitle( $this->queryString );
+ } else {
+ $this->getOutput()->setHTMLtitle( wfMessage( 'ask' )->text() );
+ }
+
+ $urlArgs->set( 'offset', $this->parameters['offset'] );
+ $urlArgs->set( 'limit', $this->parameters['limit'] );
+ $urlArgs->set( 'eq', $this->isEditMode ? 'yes' : 'no' );
+
+ $result = Html::rawElement(
+ 'div',
+ [
+ 'id' => 'result',
+ 'class' => 'smw-ask-result' . ( $this->isBorrowedMode ? ' is-disabled' : '' )
+ ],
+ $result
+ );
+
+ if ( $res instanceof QueryResult ) {
+ $isFromCache = $res->isFromCache();
+ $error = ErrorWidget::queryError( $queryobj );
+ } elseif ( is_string( $res ) ) {
+ $error = $res;
+ }
+
+ $infoText = $this->getInfoText(
+ $duration,
+ $isFromCache
+ );
+
+ $htmlForm = new HtmlForm(
+ SpecialPage::getSafeTitleFor( 'Ask' )
+ );
+
+ $htmlForm->setParameters( $this->parameters );
+ $htmlForm->setQueryString( $this->queryString );
+ $htmlForm->setQuery( $queryobj );
+
+ $htmlForm->setCallbacks(
+ [
+ 'borrowed_msg_handler' => function( &$html, &$searchInfoText ) {
+ return $this->print_borrowed_msg( $html, $searchInfoText );
+ },
+ 'code_handler' => function() {
+ return $this->print_code();
+ }
+ ]
+ );
+
+ $htmlForm->isPostSubmit(
+ $settings->is( 'smwgSpecialAskFormSubmitMethod', SMW_SASK_SUBMIT_POST )
+ );
+
+ $htmlForm->isEditMode( $this->isEditMode );
+ $htmlForm->isBorrowedMode( $this->isBorrowedMode );
+
+ $form = $htmlForm->getForm(
+ $urlArgs,
+ $res,
+ $infoText
+ );
+
+ // The overall form is "soft-disabled" so that when JS is fully
+ // loaded, the ask module will remove this class and releases the form
+ // for input
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'id' => 'ask',
+ "class" => ( $this->isBorrowedMode ? '' : 'is-disabled' )
+ ],
+ $form . $error . $result
+ );
+
+ $this->getOutput()->addHTML(
+ $html
+ );
+ }
+
+ private function fetchResults( &$printer, &$queryobj, &$urlArgs ) {
+
+ list( $res, $debug, $duration, $queryobj, $native_result ) = $this->getQueryResult();
+
+ $printer = QueryProcessor::getResultPrinter(
+ $this->parameters['format'],
+ QueryProcessor::SPECIAL_PAGE
+ );
+
+ $printer->setShowErrors( false );
+
+ $hidequery = $this->getRequest()->getVal( 'eq' ) == 'no';
+ $request_type = $this->getRequest()->getVal( 'request_type', '' );
+ $result = '';
+
+ if ( isset( $this->parameters['request_type'] ) ) {
+ $request_type = $this->parameters['request_type'];
+ }
+
+ if ( !$printer->isExportFormat() ) {
+ if ( $request_type !== '' ) {
+ $this->getOutput()->disable();
+ $query_result = '';
+
+ if ( $res->getCount() > 0 ) {
+
+ if ( $request_type === 'raw' ) {
+ $query_result = $printer->getResult( $res, $this->params, SMW_OUTPUT_RAW );
+ } else {
+ $query_result = $printer->getResult( $res, $this->params, SMW_OUTPUT_HTML );
+ }
+
+ } elseif ( $res->getCountValue() > 0 ) {
+ $query_result = $res->getCountValue();
+ }
+
+ // Don't send an ID for a raw type but for all others add one
+ // so that the `RemoteRequest` can respond appropriately and
+ // filter those back-ends that don't send a clean output.
+ if ( $request_type !== 'raw' ) {
+ $query_result .= RemoteRequest::REQUEST_ID;
+ }
+
+ return print $query_result;
+ } elseif ( ( $res instanceof QueryResult && $res->getCount() > 0 ) || $res instanceof StringResult ) {
+ if ( $this->isEditMode ) {
+ $urlArgs->set( 'eq', 'yes' );
+ } elseif ( $hidequery ) {
+ $urlArgs->set( 'eq', 'no' );
+ }
+
+ $query_result = $printer->getResult( $res, $this->params, SMW_OUTPUT_HTML );
+ $result .= is_string( $debug ) ? $debug : '';
+
+ if ( is_array( $query_result ) ) {
+ $result .= $query_result[0];
+ } else {
+ $result .= $query_result;
+ }
+ } else {
+ $result = ErrorWidget::noResult();
+ $result .= is_string( $debug ) ? $debug : '';
+ }
+ }
+
+ if ( $this->getRequest()->getVal( 'score_set', false ) && ( $scoreSet = $res->getScoreSet() ) !== null ) {
+ $table = $scoreSet->asTable( 'sortable wikitable smwtable-striped broadtable' );
+
+ if ( $table !== '' ) {
+ $result .= '<h2>Score set</h2>' . $table;
+ };
+ }
+
+ if ( $native_result !== '' ) {
+ $result .= '<h2>Native result</h2>' . '<pre>' . $native_result . '</pre>';
+ }
+
+ return [ $result, $res, $duration ];
+ }
+
+ private function getInfoText( $duration, $isFromCache = false ) {
+
+ $infoText = '';
+ $source = null;
+
+ if ( isset( $this->parameters['source'] ) ) {
+ $source = $this->parameters['source'];
+ }
+
+ if ( $this->getRequest()->getVal( 'q_engine' ) === 'sql_store' ) {
+ $source = 'sql_store';
+ }
+
+ $querySource = $this->querySourceFactory->toString(
+ $source
+ );
+
+ if ( $duration > 0 ) {
+ $infoText = Message::get(
+ [ 'smw-ask-query-search-info', $this->queryString, $querySource, $isFromCache, $duration],
+ Message::PARSE,
+ $this->getLanguage()
+ );
+ }
+
+ return $infoText;
+ }
+
+ private function print_code() {
+
+ $code = $this->queryString ? htmlspecialchars( $this->queryString ) . "\n" : "\n";
+
+ foreach ( $this->printouts as $printout ) {
+ if ( ( $serialization = $printout->getSerialisation( true ) ) !== '' ) {
+ $code .= ' |' . $serialization . "\n";
+ }
+ }
+
+ foreach ( $this->params as $param ) {
+
+ if ( !isset( $this->parameters[$param->getName()] ) ) {
+ continue;
+ }
+
+ if ( !$param->wasSetToDefault() ) {
+ $code .= ' |' . htmlspecialchars( $param->getName() ) . '=';
+ $code .= htmlspecialchars( $this->parameters[$param->getName()] ) . "\n";
+ }
+ }
+
+ return '{{#ask: ' . $code . '}}';
+ }
+
+ private function print_borrowed_msg( &$html, &$searchInfoText ) {
+
+ if ( !$this->isBorrowedMode ) {
+ return;
+ }
+
+ $borrowedMessage = $this->getRequest()->getVal( 'bMsg' );
+
+ if ( isset( $this->parameters['bmsg'] ) ) {
+ $borrowedMessage = $this->parameters['bmsg'];
+ }
+
+ $searchInfoText = '';
+
+ if ( $borrowedMessage !== null && wfMessage( $borrowedMessage )->exists() ) {
+ $html = wfMessage( $borrowedMessage, $this->queryString )->parse();
+ }
+
+ $borrowedTitle = $this->getRequest()->getVal( 'bTitle' );
+
+ if ( isset( $this->parameters['btitle'] ) ) {
+ $borrowedTitle = $this->parameters['btitle'];
+ }
+
+ if ( $borrowedTitle !== null && wfMessage( $borrowedTitle )->exists() ) {
+ $this->getOutput()->setPageTitle( wfMessage( $borrowedTitle )->text() );
+ }
+ }
+
+ private function newUrlArgs() {
+
+ $urlArgs = new UrlArgs();
+
+ // build parameter strings for URLs, based on current settings
+ $urlArgs->set( 'q', $this->queryString );
+
+ $tmp_parray = [];
+
+ foreach ( $this->parameters as $key => $value ) {
+ if ( !in_array( $key, [ 'sort', 'order', 'limit', 'offset', 'title' ] ) ) {
+ $tmp_parray[$key] = $value;
+ }
+ }
+
+ $urlArgs->set( 'p', Infolink::encodeParameters( $tmp_parray ) );
+ $printoutstring = '';
+
+ /**
+ * @var PrintRequest $printout
+ */
+ foreach ( $this->printouts as $printout ) {
+ $printoutstring .= $printout->getSerialisation( true ) . "\n";
+ }
+
+ if ( $printoutstring !== '' ) {
+ $urlArgs->set( 'po', $printoutstring );
+ }
+
+ if ( array_key_exists( 'sort', $this->parameters ) ) {
+ $urlArgs->set( 'sort', $this->parameters['sort'] );
+ }
+
+ if ( array_key_exists( 'order', $this->parameters ) ) {
+ $urlArgs->set( 'order', $this->parameters['order'] );
+ }
+
+ if ( $this->getRequest()->getCheck( 'bTitle' ) ) {
+ $urlArgs->set( 'bTitle', $this->getRequest()->getVal( 'bTitle' ) );
+ $urlArgs->set( 'bMsg', $this->getRequest()->getVal( 'bMsg' ) );
+ }
+
+ if ( isset( $this->parameters['btitle'] ) ) {
+ $urlArgs->set( 'bTitle', $this->parameters['btitle'] );
+ $urlArgs->set( 'bMsg', $this->parameters['bmsg'] );
+ }
+
+ return $urlArgs;
+ }
+
+ private function getQueryResult() {
+
+ $res = null;
+ $debug = '';
+ $duration = 0;
+ $queryobj = null;
+ $native_result = '';
+
+ // Copy the printout to retain the original state while in case of no
+ // specific subject (THIS) request extend the query with a
+ // `PrintRequest::PRINT_THIS` column
+
+ QueryProcessor::addThisPrintout( $this->printouts, $this->parameters );
+
+ $params = QueryProcessor::getProcessedParams(
+ $this->parameters,
+ $this->printouts
+ );
+
+ $this->parameters['format'] = $params['format']->getValue();
+ $this->params = $params;
+
+ $queryobj = QueryProcessor::createQuery(
+ $this->queryString,
+ $params,
+ QueryProcessor::SPECIAL_PAGE,
+ $this->parameters['format'],
+ $this->printouts
+ );
+
+ if ( $this->getRequest()->getVal( 'cache' ) === 'no' ) {
+ $queryobj->setOption( SMWQuery::NO_CACHE, true );
+ }
+
+ if ( $this->getRequest()->getVal( 'native_result', false ) ) {
+ $queryobj->setOption( 'native_result', true );
+ }
+
+ $queryobj->setOption( SMWQuery::PROC_CONTEXT, 'SpecialAsk' );
+ $source = $params['source']->getValue();
+ $noSource = $source === '';
+
+ if ( $this->getRequest()->getVal( 'q_engine' ) === 'sql_store' ) {
+ $source = 'sql_store';
+ }
+
+ $qp = [];
+
+ foreach ( $params as $key => $value) {
+ $qp[$key] = $value->getValue();
+ }
+
+ $queryobj->setOption( 'query.params', $qp );
+
+ /**
+ * @var QueryEngine $queryEngine
+ */
+ $queryEngine = $this->querySourceFactory->get(
+ $source
+ );
+
+ // Measure explicit to account for a federated (sourced) query
+ $duration = microtime( true );
+
+ /**
+ * @var QueryResult $res
+ */
+ $res = $queryEngine->getQueryResult(
+ $queryobj
+ );
+
+ if ( $this->getRequest()->getVal( 'native_result', false ) && isset( $queryobj->native_result ) ) {
+ $native_result = $queryobj->native_result;
+ }
+
+ $duration = number_format( ( microtime( true ) - $duration ), 4, '.', '' );
+
+ // Allow to generate a debug output
+ if ( $this->getRequest()->getVal( 'debug' ) && $noSource ) {
+
+ $queryobj = QueryProcessor::createQuery(
+ $this->queryString,
+ $params,
+ QueryProcessor::SPECIAL_PAGE,
+ 'debug',
+ $this->printouts
+ );
+
+ $debug = $queryEngine->getQueryResult( $queryobj );
+ }
+
+ return [ $res, $debug, $duration, $queryobj, $native_result ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialBrowse.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialBrowse.php
new file mode 100644
index 00000000..dcea1342
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialBrowse.php
@@ -0,0 +1,224 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\Encoder;
+use SMW\MediaWiki\Specials\Browse\HtmlBuilder;
+use SMW\MediaWiki\Specials\Browse\FieldBuilder;
+use SMW\Message;
+use SMWInfolink as Infolink;
+use SpecialPage;
+
+/**
+ * A factbox view on one specific article, showing all the Semantic data about it
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author mwjames
+ */
+class SpecialBrowse extends SpecialPage {
+
+ /**
+ * @see SpecialPage::__construct
+ */
+ public function __construct() {
+ parent::__construct( 'Browse', '', true, false, 'default', true );
+ }
+
+ /**
+ * @see SpecialPage::execute
+ *
+ * @param string $query string
+ */
+ public function execute( $query ) {
+
+ $this->setHeaders();
+ $webRequest = $this->getRequest();
+
+ // get the GET parameters
+ $articletext = $webRequest->getVal( 'article' );
+
+ if ( $webRequest->getText( 'cl', '' ) !== '' ) {
+ $query = Infolink::decodeCompactLink( 'cl:'. $webRequest->getText( 'cl' ) );
+ } else {
+ $query = Infolink::decodeCompactLink( $query );
+ }
+
+ $isEmptyRequest = $query === null && ( $webRequest->getVal( 'article' ) === '' || $webRequest->getVal( 'article' ) === null );
+
+ // @see SMWInfolink::encodeParameters
+ if ( $query === null && $this->getRequest()->getCheck( 'x' ) ) {
+ $query = $this->getRequest()->getVal( 'x' );
+ }
+
+ // Auto-generated link is marked with a leading :
+ if ( $query !== '' && $query{0} === ':' ) {
+ $articletext = Encoder::unescape( $query );
+ } elseif ( $articletext === null ) {
+ $articletext = $query;
+ }
+
+ // no GET parameters? Then try the URL
+ if ( $articletext === null ) {
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newTypeIDValue(
+ '_wpg',
+ $articletext
+ );
+
+ $out = $this->getOutput();
+ $out->setHTMLTitle( $dataValue->getTitle() );
+
+ $out->addModuleStyles( [
+ 'mediawiki.ui',
+ 'mediawiki.ui.button',
+ 'mediawiki.ui.input',
+ 'ext.smw.browse.styles'
+ ] );
+
+ $out->addModules( [
+ 'ext.smw.browse',
+ 'ext.smw.tooltip'
+ ] );
+
+ $out->addHTML(
+ $this->buildHTML( $webRequest, $dataValue, $isEmptyRequest )
+ );
+
+ $this->addExternalHelpLinks( $dataValue );
+ }
+
+ private function buildHTML( $webRequest, $dataValue, $isEmptyRequest ) {
+
+ if ( $isEmptyRequest && !$this->including() ) {
+ return Message::get( 'smw-browse-intro', Message::TEXT, Message::USER_LANGUAGE ) . FieldBuilder::createQueryForm();
+ }
+
+ if ( !$dataValue->isValid() ) {
+
+ foreach ( $dataValue->getErrors() as $error ) {
+ $error = Message::decode( $error, Message::TEXT, Message::USER_LANGUAGE );
+ }
+
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-callout smw-callout-error'
+ ],
+ Message::get( [ 'smw-browse-invalid-subject', $error ], Message::TEXT, Message::USER_LANGUAGE )
+ );
+
+ if ( !$this->including() ) {
+ $html .= FieldBuilder::createQueryForm( $webRequest->getVal( 'article' ) );
+ }
+
+ return $html;
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $dataItem = $dataValue->getDataItem();
+
+ $htmlBuilder = $this->newHtmlBuilder(
+ $webRequest,
+ $dataItem,
+ $applicationFactory->getStore(),
+ $applicationFactory->getSettings()
+ );
+
+ $options = $htmlBuilder->getOptions();
+
+ if ( $webRequest->getVal( 'format' ) === 'json' ) {
+ $semanticDataSerializer = $applicationFactory->newSerializerFactory()->newSemanticDataSerializer();
+ $res = $semanticDataSerializer->serialize(
+ $applicationFactory->getStore()->getSemanticData( $dataItem )
+ );
+
+ $this->getOutput()->disable();
+ header( 'Content-type: ' . 'application/json' . '; charset=UTF-8' );
+ echo json_encode( $res, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+ }
+
+ if ( $webRequest->getVal( 'output' ) === 'legacy' || !$htmlBuilder->getOption( 'api' ) ) {
+ return $htmlBuilder->legacy();
+ }
+
+ // Ajax/API is doing the data fetch
+ return $htmlBuilder->placeholder();
+ }
+
+ private function newHtmlBuilder( $webRequest, $dataItem, $store, $settings ) {
+
+ $htmlBuilder = new HtmlBuilder(
+ $store,
+ $dataItem
+ );
+
+ $htmlBuilder->setOptions(
+ [
+ 'dir' => $webRequest->getVal( 'dir' ),
+ 'group' => $webRequest->getVal( 'group' ),
+ 'printable' => $webRequest->getVal( 'printable' ),
+ 'offset' => $webRequest->getVal( 'offset' ),
+ 'including' => $this->including(),
+ 'showInverse' => $settings->isFlagSet( 'smwgBrowseFeatures', SMW_BROWSE_SHOW_INVERSE ),
+ 'showAll' => $settings->isFlagSet( 'smwgBrowseFeatures', SMW_BROWSE_SHOW_INCOMING ),
+ 'showGroup' => $settings->isFlagSet( 'smwgBrowseFeatures', SMW_BROWSE_SHOW_GROUP ),
+ 'showSort' => $settings->isFlagSet( 'smwgBrowseFeatures', SMW_BROWSE_SHOW_SORTKEY ),
+ 'api' => $settings->isFlagSet( 'smwgBrowseFeatures', SMW_BROWSE_USE_API ),
+
+ // WebRequest::getGPCVal/getVal doesn't understand `.` as in
+ // `valuelistlimit.out`
+
+ 'valuelistlimit.out' => $webRequest->getVal(
+ 'valuelistlimit-out',
+ $settings->dotGet( 'smwgPagingLimit.browse.valuelist.outgoing' )
+ ),
+ 'valuelistlimit.in' => $webRequest->getVal(
+ 'valuelistlimit-in',
+ $settings->dotGet( 'smwgPagingLimit.browse.valuelist.incoming' )
+ ),
+ ]
+ );
+
+ return $htmlBuilder;
+ }
+
+ private function addExternalHelpLinks( $dataValue ) {
+
+ if ( $this->getRequest()->getVal( 'printable' ) === 'yes' ) {
+ return null;
+ }
+
+ if ( $dataValue->isValid() ) {
+ $link = SpecialPage::getTitleFor( 'ExportRDF', $dataValue->getTitle()->getPrefixedText() );
+
+ $this->getOutput()->setIndicators( [
+ 'browse' => Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-page-indicator-rdflink'
+ ],
+ Html::rawElement(
+ 'a',
+ [
+ 'href' => $link->getLocalUrl( 'syntax=rdf' )
+ ],
+ 'RDF'
+ )
+ )
+ ] );
+ }
+
+ $this->addHelpLink( wfMessage( 'smw-specials-browse-helplink' )->escaped(), true );
+ }
+
+ protected function getGroupName() {
+ return 'smw_group';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialDeferredRequestDispatcher.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialDeferredRequestDispatcher.php
new file mode 100644
index 00000000..c38343d4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialDeferredRequestDispatcher.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use SMW\ApplicationFactory;
+use SpecialPage;
+use Title;
+
+/**
+ * This class is the receiving endpoint for the `DeferredRequestDispatchManager` invoked
+ * job request.
+ *
+ * This special page is not expected to interact with a user and therefore it is
+ * unlisted.
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class SpecialDeferredRequestDispatcher extends SpecialPage {
+
+ /**
+ * @var boolean
+ */
+ private $allowedToModifyHttpHeader = true;
+
+ /**
+ * @codeCoverageIgnore
+ */
+ public function __construct() {
+ parent::__construct( 'DeferredRequestDispatcher', '', false );
+ }
+
+ /**
+ * SpecialPage::doesWrites
+ *
+ * @return boolean
+ */
+ public function doesWrites() {
+ return true;
+ }
+
+ /**
+ * @see SpecialPage::getGroupName
+ */
+ protected function getGroupName() {
+ return 'maintenance';
+ }
+
+ /**
+ * Only used during unit testing
+ *
+ * @since 2.3
+ */
+ public function disallowToModifyHttpHeader() {
+ $this->allowedToModifyHttpHeader = false;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return string
+ */
+ public static function getTargetURL() {
+ return SpecialPage::getTitleFor( 'DeferredRequestDispatcher')->getFullURL();
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $key
+ *
+ * @return string
+ */
+ public static function getRequestToken( $key ) {
+ return md5( $key . $GLOBALS['wgSecretKey'] );
+ }
+
+ /**
+ * @see SpecialPage::execute
+ */
+ public function execute( $query ) {
+
+ $this->getOutput()->disable();
+
+ if ( wfReadOnly() ) {
+ return $this->modifyHttpHeader( "HTTP/1.0 423 Locked", 'Wiki is in read-only mode.' );
+ }
+
+ if ( !$this->isHttpRequestMethod( 'HEAD' ) && !$this->isHttpRequestMethod( 'POST' ) ) {
+ return $this->modifyHttpHeader( "HTTP/1.0 400 Bad Request", 'The special page requires a POST/HEAD request.' );
+ }
+
+ $parameters = json_decode(
+ $this->getRequest()->getVal( 'parameters' ),
+ true
+ );
+
+ if ( $this->isHttpRequestMethod( 'POST' ) && self::getRequestToken( $parameters['timestamp'] ) !== $parameters['requestToken'] ) {
+ return $this->modifyHttpHeader( "HTTP/1.0 400 Bad Request", 'Invalid or staled requestToken was provided for the request' );
+ }
+
+ $this->modifyHttpHeader( "HTTP/1.0 202 Accepted" );
+
+ if ( !isset( $parameters['async-job'] ) ) {
+ return;
+ }
+
+ return $this->doRunJob( $parameters, ApplicationFactory::getInstance()->getMediaWikiLogger() );
+ }
+
+ private function modifyHttpHeader( $header, $message = '' ) {
+
+ if ( !$this->allowedToModifyHttpHeader ) {
+ return null;
+ }
+
+ ignore_user_abort( true );
+ header( $header );
+ print $message;
+ ob_flush();
+ flush();
+
+ // @see SpecialRunJobs
+ // MW 1.27 / https://phabricator.wikimedia.org/T115413
+ // Once the client receives this response, it can disconnect
+ set_error_handler( function ( $errno, $errstr ) {
+ if ( strpos( $errstr, 'Cannot modify header information' ) !== false ) {
+ return true; // bug T115413
+ }
+ // Delegate unhandled errors to the default handlers
+ return false;
+ } );
+ }
+
+ private function doRunJob( $parameters, $logger ) {
+
+ $type = $parameters['async-job']['type'];
+ $title = Title::newFromDBkey( $parameters['async-job']['title'] );
+
+ if ( $title === null ) {
+ return $logger->info( __METHOD__ . " invalid title" );
+ }
+
+ $logger->info( __METHOD__ . ' ' . $type . ' :: ' . $title->getPrefixedDBkey() . '#' . $title->getNamespace() );
+
+ $job = ApplicationFactory::getInstance()->newJobFactory()->newByType(
+ $type,
+ $title,
+ $parameters
+ );
+
+ $job->run();
+
+ return true;
+ }
+
+ // 1.19 doesn't have a getMethod
+ private function isHttpRequestMethod( $key ) {
+
+ if ( method_exists( $this->getRequest(), 'getMethod') ) {
+ return $this->getRequest()->getMethod() == $key;
+ }
+
+ return isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] == $key : false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialPageProperty.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialPageProperty.php
new file mode 100644
index 00000000..d1b287f0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialPageProperty.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\Encoder;
+use SMW\MediaWiki\Specials\PageProperty\PageBuilder;
+use SMW\Options;
+use SMW\RequestOptions;
+use SMWInfolink as Infolink;
+use SpecialPage;
+
+/**
+ * This special page implements a view on a object-relation pair, i.e. a page that
+ * shows all the values of a property for a certain page.
+ *
+ * This is typically used for overflow results from other dynamic output pages.
+ *
+ * @license GNU GPL v2+
+ * @since 1.4
+ *
+ * @author Denny Vrandecic
+ * @author mwjames
+ */
+class SpecialPageProperty extends SpecialPage {
+
+ /**
+ * @codeCoverageIgnore
+ */
+ public function __construct() {
+ parent::__construct( 'PageProperty', '', false );
+ }
+
+ /**
+ * @see SpecialPage::execute
+ */
+ public function execute( $query ) {
+
+ $request = $this->getRequest();
+
+ if ( $request->getText( 'cl', '' ) !== '' ) {
+ $query = Infolink::decodeCompactLink( 'cl:'. $request->getText( 'cl' ) );
+ } else {
+ $query = Infolink::decodeCompactLink( $query );
+ }
+
+ if ( $query !== '' ) {
+ $query = Encoder::unescape( $query );
+ }
+
+ // Get parameters
+ $pagename = $request->getVal( 'from' );
+ $propname = $request->getVal( 'type' );
+
+ // No GET parameters? Try the URL with the convention `PageName::PropertyName`
+ if ( $propname == '' ) {
+ $queryparts = explode( '::', $query );
+ $propname = $query;
+ if ( count( $queryparts ) > 1 ) {
+ $pagename = $queryparts[0];
+ $propname = implode( '::', array_slice( $queryparts, 1 ) );
+ }
+ }
+
+ $options = new Options(
+ [
+ 'from' => $pagename,
+ 'type' => $propname,
+ 'property' => $propname,
+ 'limit' => $request->getVal( 'limit', 20 ),
+ 'offset' => $request->getVal( 'offset', 0 ),
+ ]
+ );
+
+ $this->addHelpLink(
+ wfMessage( 'smw-special-pageproperty-helplink' )->escaped(),
+ true
+ );
+
+ $this->load( $options );
+ }
+
+ /**
+ * @see SpecialPage::getGroupName
+ */
+ protected function getGroupName() {
+ return 'smw_group';
+ }
+
+ private function load( $options ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $dataValueFactory = DataValueFactory::getInstance();
+
+ $subject = $dataValueFactory->newTypeIDValue(
+ '_wpg',
+ $options->get( 'from' )
+ );
+
+ $propertyValue = $dataValueFactory->newPropertyValueByLabel(
+ $options->get( 'property' )
+ );
+
+ $pagename = '';
+ $propname = '';
+
+ if ( $subject->isValid() ) {
+ $pagename = $subject->getPrefixedText();
+ }
+
+ if ( $propertyValue->isValid() ) {
+ $propname = $propertyValue->getWikiValue();
+ }
+
+ $options->set( 'from', $pagename );
+ $options->set( 'property', $propname );
+ $options->set( 'type', $propname );
+
+ $htmlFormRenderer = $applicationFactory->newMwCollaboratorFactory()->newHtmlFormRenderer(
+ $this->getContext()->getTitle(),
+ $this->getLanguage()
+ );
+
+ $pageBuilder = new PageBuilder(
+ $htmlFormRenderer,
+ $options
+ );
+
+ $html = '';
+
+ // No property given, no results
+ if ( $propname === '' ) {
+ $html .= $pageBuilder->buildForm();
+ $html .= wfMessage( 'smw_result_noresults' )->text();
+ } else {
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->setLimit( $options->get( 'limit' ) + 1 );
+ $requestOptions->setOffset( $options->get( 'offset' ) );
+ $requestOptions->sort = true;
+
+ // Restrict the request otherwise the entire SemanticData record
+ // is fetched which can in case of a subject with a large
+ // subobject/subpage pool create excessive DB queries that are not
+ // used for the display
+ $requestOptions->conditionConstraint = true;
+
+ $dataItem = $pagename !== '' ? $subject->getDataItem() : null;
+
+ $results = $applicationFactory->getStore()->getPropertyValues(
+ $dataItem,
+ $propertyValue->getDataItem(),
+ $requestOptions
+ );
+
+ $html .= $pageBuilder->buildForm( count( $results ) );
+ $html .= $pageBuilder->buildHtml( $results );
+ }
+
+ $output = $this->getOutput();
+ $output->setPagetitle( wfMessage( 'pageproperty' )->text() );
+
+ $output->addModuleStyles( 'ext.smw.special.style' );
+ $output->addModules( 'ext.smw.tooltip' );
+
+ $output->addModules( 'ext.smw.autocomplete.property' );
+ $output->addModules( 'ext.smw.autocomplete.article' );
+
+ $output->addHTML( $html );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialProcessingErrorList.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialProcessingErrorList.php
new file mode 100644
index 00000000..6236f338
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialProcessingErrorList.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use SMW\ApplicationFactory;
+use SpecialPage;
+
+/**
+ * Convenience special page that just redirects to Special:Ask with a preset
+ * of necessary parameters to query the processing error list.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SpecialProcessingErrorList extends SpecialPage {
+
+ /**
+ * @codeCoverageIgnore
+ */
+ public function __construct() {
+ parent::__construct( 'ProcessingErrorList' );
+ }
+
+ /**
+ * @see SpecialPage::execute
+ */
+ public function execute( $query ) {
+
+ $limit = ApplicationFactory::getInstance()->getSettings()->dotGet( 'smwgPagingLimit.errorlist' );
+
+ $this->getOutput()->redirect(
+ $this->getLocalAskRedirectUrl( $limit )
+ );
+
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $limit
+ *
+ * @return string
+ */
+ public function getLocalAskRedirectUrl( $limit = 20 ) {
+ return SpecialPage::getTitleFor( 'Ask' )->getLocalUrl(
+ [
+ 'q' => '[[Has processing error text::+]]',
+ 'po' => '?Has improper value for|?Has processing error text',
+ 'p' => 'class=sortable-20wikitable-20smwtable-2Dstriped',
+ 'eq' => 'no',
+ 'limit' => $limit,
+ 'bTitle' => 'processingerrorlist',
+ 'bMsg' => 'smw-processingerrorlist-intro'
+ ]
+ );
+ }
+
+ /**
+ * @see SpecialPage::getGroupName
+ */
+ protected function getGroupName() {
+ return 'smw_group';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialPropertyLabelSimilarity.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialPropertyLabelSimilarity.php
new file mode 100644
index 00000000..870eccec
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialPropertyLabelSimilarity.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Specials\PropertyLabelSimilarity\ContentsBuilder;
+use SMW\SQLStore\Lookup\PropertyLabelSimilarityLookup;
+use SpecialPage;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SpecialPropertyLabelSimilarity extends SpecialPage {
+
+ /**
+ * @codeCoverageIgnore
+ */
+ public function __construct() {
+ parent::__construct( 'PropertyLabelSimilarity' );
+ }
+
+ /**
+ * @see SpecialPage::execute
+ */
+ public function execute( $query ) {
+
+ $this->setHeaders();
+ $output = $this->getOutput();
+ $webRequest = $this->getRequest();
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $store = $applicationFactory->getStore( '\SMW\SQLStore\SQLStore' );
+
+ $propertyLabelSimilarityLookup = new PropertyLabelSimilarityLookup(
+ $store
+ );
+
+ $propertyLabelSimilarityLookup->setExemptionProperty(
+ $applicationFactory->getSettings()->get( 'smwgSimilarityLookupExemptionProperty' )
+ );
+
+ $htmlFormRenderer = $applicationFactory->newMwCollaboratorFactory()->newHtmlFormRenderer(
+ $this->getContext()->getTitle(),
+ $this->getLanguage()
+ );
+
+ $contentsBuilder = new ContentsBuilder(
+ $propertyLabelSimilarityLookup,
+ $htmlFormRenderer
+ );
+
+ $threshold = (int)$webRequest->getText( 'threshold', 90 );
+ $type = $webRequest->getText( 'type', false );
+
+ $offset = (int)$webRequest->getText( 'offset', 0 );
+ $limit = (int)$webRequest->getText( 'limit', 50 );
+
+ $requestOptions = $applicationFactory->getQueryFactory()->newRequestOptions();
+ $requestOptions->setLimit( $limit );
+ $requestOptions->setOffset( $offset );
+
+ $requestOptions->addExtraCondition(
+ [
+ 'type' => $type,
+ 'threshold' => $threshold
+ ]
+ );
+
+ $output->addHtml(
+ $contentsBuilder->getHtml( $requestOptions )
+ );
+
+ return true;
+ }
+
+ /**
+ * @see SpecialPage::getGroupName
+ */
+ protected function getGroupName() {
+ return 'smw_group';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialSearchByProperty.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialSearchByProperty.php
new file mode 100644
index 00000000..edd805a7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialSearchByProperty.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Specials\SearchByProperty\PageBuilder;
+use SMW\MediaWiki\Specials\SearchByProperty\PageRequestOptions;
+use SMW\MediaWiki\Specials\SearchByProperty\QueryResultLookup;
+use SMWInfolink as Infolink;
+use SpecialPage;
+
+/**
+ * A special page to search for entities that have a certain property with
+ * a certain value.
+ *
+ * This special page for Semantic MediaWiki implements a view on a
+ * relation-object pair,i.e. a typed backlink. For example, it shows me all
+ * persons born in Croatia, or all winners of the Academy Award for best actress.
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class SpecialSearchByProperty extends SpecialPage {
+
+ /**
+ * @codeCoverageIgnore
+ */
+ public function __construct() {
+ parent::__construct( 'SearchByProperty' );
+ }
+
+ /**
+ * @see SpecialPage::execute
+ */
+ public function execute( $query ) {
+
+ $this->setHeaders();
+ $output = $this->getOutput();
+ $request = $this->getRequest();
+
+ $output->setPageTitle( $this->msg( 'searchbyproperty' )->text() );
+ $output->addModules( 'ext.smw.tooltip' );
+ $output->addModules( 'ext.smw.autocomplete.property' );
+
+ list( $limit, $offset ) = $this->getLimitOffset();
+
+ if ( $request->getText( 'cl', '' ) !== '' ) {
+ $query = Infolink::decodeCompactLink( 'cl:'. $request->getText( 'cl' ) );
+ } else {
+ $query = Infolink::decodeCompactLink( $query );
+ }
+
+ // @see SMWInfolink::encodeParameters
+ if ( $query === null && $this->getRequest()->getCheck( 'x' ) ) {
+ $query = $this->getRequest()->getVal( 'x' );
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $requestOptions = [
+ 'limit' => $limit,
+ 'offset' => $offset,
+ 'property' => $this->getRequest()->getVal( 'property' ),
+ 'value' => $this->getRequest()->getVal( 'value' ),
+ 'nearbySearchForType' => $applicationFactory->getSettings()->get( 'smwgSearchByPropertyFuzzy' )
+ ];
+
+ $htmlFormRenderer = $applicationFactory->newMwCollaboratorFactory()->newHtmlFormRenderer(
+ $this->getContext()->getTitle(),
+ $this->getLanguage()
+ );
+
+ $pageBuilder = new PageBuilder(
+ $htmlFormRenderer,
+ new PageRequestOptions( $query, $requestOptions ),
+ new QueryResultLookup( $applicationFactory->getStore() )
+ );
+
+ $output->addHTML( $pageBuilder->getHtml() );
+ }
+
+ private function getLimitOffset() {
+ return $this->getRequest()->getLimitOffset();
+ }
+
+ protected function getGroupName() {
+ return 'smw_group';
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialURIResolver.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialURIResolver.php
new file mode 100644
index 00000000..ed1d30dd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/Specials/SpecialURIResolver.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace SMW\MediaWiki\Specials;
+
+use SMW\Exporter\Escaper;
+use SpecialPage;
+use Title;
+
+/**
+ * Resolve (redirect) pretty URIs (or "short URIs") to the equivalent full MediaWiki
+ * representation.
+ *
+ * @license GNU GPL v2+
+ * @since 1.0
+ *
+ * @author Denny Vrandecic
+ */
+class SpecialURIResolver extends SpecialPage {
+
+ /**
+ * @see SpecialPage::__construct
+ */
+ public function __construct() {
+ parent::__construct( 'URIResolver', '', false );
+ }
+
+ /**
+ * @see SpecialPage::execute
+ *
+ * @param string $query string
+ */
+ public function execute( $query ) {
+ $out = $this->getOutput();
+
+ // #2344, It is believed that when no HTTP_ACCEPT is available then a
+ // request came from a "defect" mobile device without a correct accept
+ // header
+ if ( !isset( $_SERVER['HTTP_ACCEPT'] ) ) {
+ $_SERVER['HTTP_ACCEPT'] = '';
+ }
+
+ if ( $query === null || trim( $query ) === '' ) {
+ if ( stristr( $_SERVER['HTTP_ACCEPT'], 'RDF' ) ) {
+ $out->redirect( SpecialPage::getTitleFor( 'ExportRDF' )->getFullURL( [ 'stats' => '1' ] ), '303' );
+ } else {
+ $this->setHeaders();
+ $out->addHTML(
+ '<p>' .
+ wfMessage( 'smw_uri_doc', 'https://www.w3.org/2001/tag/issues.html#httpRange-14' )->parse() .
+ '</p>'
+ );
+ }
+ } else {
+ $query = Escaper::decodeUri( $query );
+ $query = str_replace( '_', '%20', $query );
+ $query = urldecode( $query );
+ $title = Title::newFromText( $query );
+
+ // In case the title doesn't exist throw an error page
+ if ( $title === null ) {
+ $out->showErrorPage( 'badtitle', 'badtitletext' );
+ } elseif ( stristr( $_SERVER['HTTP_ACCEPT'], 'RDF' ) ) {
+ $out->redirect(
+ SpecialPage::getTitleFor( 'ExportRDF', $title->getPrefixedText() )->getFullURL( [ 'xmlmime' => 'rdf' ] )
+ );
+ } else {
+ $out->redirect( $title->getFullURL(), '303' );
+ }
+ }
+ }
+
+ /**
+ * @see SpecialPage::getGroupName
+ */
+ protected function getGroupName() {
+ return 'smw_group';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/StripMarkerDecoder.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/StripMarkerDecoder.php
new file mode 100644
index 00000000..ec52d8b6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/StripMarkerDecoder.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Parser;
+use StripState;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class StripMarkerDecoder {
+
+ /**
+ * @var StripState
+ */
+ private $stripState;
+
+ /**
+ * @var boolean
+ */
+ private $isSupported = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param StripState $stripState
+ */
+ public function __construct( StripState $stripState ) {
+ $this->stripState = $stripState;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $decoderState
+ */
+ public function isSupported( $isSupported ) {
+ $this->isSupported = $isSupported;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function canUse() {
+ return $this->isSupported;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return boolean
+ */
+ public function hasStripMarker( $text ) {
+ return strpos( $text, Parser::MARKER_SUFFIX );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $value
+ *
+ * @return boolean
+ */
+ public function decode( $value ) {
+
+ $hasStripMarker = false;
+
+ if ( $this->canUse() ) {
+ $hasStripMarker = $this->hasStripMarker( $value );
+ }
+
+ if ( $hasStripMarker ) {
+ $value = $this->unstrip( $value );
+ }
+
+ return $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return text
+ */
+ public function unstrip( $text ) {
+
+ // Escape the text case to avoid any HTML elements
+ // cause an issue during parsing
+ return str_replace(
+ [ '<', '>', ' ', '[', '{', '=', "'", ':', "\n" ],
+ [ '&lt;', '&gt;', ' ', '&#x005B;', '&#x007B;', '&#x003D;', '&#x0027;', '&#58;', "<br />" ],
+ $this->doUnstrip( $text )
+ );
+ }
+
+ public function doUnstrip( $text ) {
+
+ if ( ( $value = $this->stripState->unstripNoWiki( $text ) ) !== '' && !$this->hasStripMarker( $value ) ) {
+ return $this->addNoWikiToUnstripValue( $value );
+ }
+
+ if ( ( $value = $this->stripState->unstripGeneral( $text ) ) !== '' && !$this->hasStripMarker( $value ) ) {
+ return $value;
+ }
+
+ return $this->doUnstrip( $value );
+ }
+
+ private function addNoWikiToUnstripValue( $text ) {
+ return '<nowiki>' . $text . '</nowiki>';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/TitleFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/TitleFactory.php
new file mode 100644
index 00000000..73035cb0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/TitleFactory.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class TitleFactory {
+
+ /**
+ * @since 2.0
+ *
+ * @param string $text
+ *
+ * @return Title|null
+ */
+ public function newFromText( $text, $namespace = null ) {
+
+ if ( $namespace === null ) {
+ $namespace = NS_MAIN;
+ }
+
+ return Title::newFromText( $text, $namespace );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ *
+ * @return Title|null
+ */
+ public function newFromID( $id ) {
+ return Title::newFromID( $id );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $ids
+ *
+ * @return Title[]
+ */
+ public function newFromIDs( $ids ) {
+ return Title::newFromIDs( $ids );
+ }
+ /**
+ * @since 3.0
+ *
+ * @param int $ns
+ * @param string $title
+ * @param string $fragment
+ * @param string $interwiki
+ *
+ * @return Title|null
+ */
+ public function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
+ return Title::makeTitleSafe( $ns, $title, $fragment, $interwiki );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/TitleLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/TitleLookup.php
new file mode 100644
index 00000000..15f0377f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/MediaWiki/TitleLookup.php
@@ -0,0 +1,202 @@
+<?php
+
+namespace SMW\MediaWiki;
+
+use RuntimeException;
+use Title;
+
+/**
+ * A convenience class to encapsulate MW related database interaction
+ *
+ * @note This is an internal class and should not be used outside of smw-core
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9.2
+ *
+ * @author mwjames
+ */
+class TitleLookup {
+
+ /**
+ * @var Database
+ */
+ private $connection = null;
+
+ /**
+ * @var integer
+ */
+ private $namespace = null;
+
+ /**
+ * @since 1.9.2
+ *
+ * @param Database $connection
+ */
+ public function __construct( Database $connection ) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @param int $namespace
+ *
+ * @return TitleLookup
+ */
+ public function setNamespace( $namespace ) {
+ $this->namespace = $namespace;
+ return $this;
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @return Title[]
+ * @throws RuntimeException
+ */
+ public function selectAll() {
+
+ if ( $this->namespace === null ) {
+ throw new RuntimeException( 'Unrestricted selection without a namespace is not supported' );
+ }
+
+ if ( $this->namespace === NS_CATEGORY ) {
+ $tableName = 'category';
+ $fields = [ 'cat_title' ];
+ $conditions = '';
+ $options = [ 'USE INDEX' => 'cat_title' ];
+ } else {
+ $tableName = 'page';
+ $fields = [ 'page_namespace', 'page_title' ];
+ $conditions = [ 'page_namespace' => $this->namespace ];
+ $options = [ 'USE INDEX' => 'PRIMARY' ];
+ }
+
+ $res = $this->connection->select(
+ $tableName,
+ $fields,
+ $conditions,
+ __METHOD__,
+ $options
+ );
+
+ return $this->makeTitlesFromSelection( $res );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return Title[]
+ */
+ public function getRedirectPages() {
+
+ $conditions = [];
+ $options = [];
+
+ $res = $this->connection->select(
+ [ 'page', 'redirect' ],
+ [ 'page_namespace', 'page_title' ],
+ $conditions,
+ __METHOD__,
+ $options,
+ [ 'page' => [ 'INNER JOIN', [ 'page_id=rd_from' ] ] ]
+ );
+
+ return $this->makeTitlesFromSelection( $res );
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @param int $startId
+ * @param int $endId
+ *
+ * @return Title[]
+ * @throws RuntimeException
+ */
+ public function selectByIdRange( $startId = 0, $endId = 0 ) {
+
+ if ( $this->namespace === null ) {
+ throw new RuntimeException( 'Unrestricted selection without a namespace is not supported' );
+ }
+
+ if ( $this->namespace === NS_CATEGORY ) {
+ $tableName = 'category';
+ $fields = [ 'cat_title', 'cat_id' ];
+ $conditions = [ "cat_id BETWEEN $startId AND $endId" ];
+ $options = [ 'ORDER BY' => 'cat_id ASC', 'USE INDEX' => 'cat_title' ];
+ } else {
+ $tableName = 'page';
+ $fields = [ 'page_namespace', 'page_title', 'page_id' ];
+ $conditions = [ "page_id BETWEEN $startId AND $endId" ] + [ 'page_namespace' => $this->namespace ];
+ $options = [ 'ORDER BY' => 'page_id ASC', 'USE INDEX' => 'PRIMARY' ];
+ }
+
+ $res = $this->connection->select(
+ $tableName,
+ $fields,
+ $conditions,
+ __METHOD__,
+ $options
+ );
+
+ return $this->makeTitlesFromSelection( $res );
+ }
+
+ /**
+ * @since 1.9.2
+ *
+ * @return int
+ */
+ public function getMaxId() {
+
+ if ( $this->namespace === NS_CATEGORY ) {
+ $tableName = 'category';
+ $var = 'MAX(cat_id)';
+ } else {
+ $tableName = 'page';
+ $var = 'MAX(page_id)';
+ }
+
+ return (int)$this->connection->selectField(
+ $tableName,
+ $var,
+ false,
+ __METHOD__
+ );
+ }
+
+ protected function makeTitlesFromSelection( $res ) {
+
+ $pages = [];
+
+ if ( $res === false ) {
+ return $pages;
+ }
+
+ foreach ( $res as $row ) {
+ $pages[] = $this->newTitleFromRow( $row );
+ }
+
+ return $pages;
+ }
+
+ private function newTitleFromRow( $row ) {
+
+ if ( $this->namespace === NS_CATEGORY ) {
+ $ns = NS_CATEGORY;
+ $title = $row->cat_title;
+ } elseif ( isset( $row->rd_namespace ) ) {
+ $ns = $row->rd_namespace;
+ $title = $row->rd_title;
+ } else {
+ $ns = $row->page_namespace;
+ $title = $row->page_title;
+ }
+
+ return Title::makeTitle( $ns, $title );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Message.php b/www/wiki/extensions/SemanticMediaWiki/src/Message.php
new file mode 100644
index 00000000..7548a673
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Message.php
@@ -0,0 +1,254 @@
+<?php
+
+namespace SMW;
+
+use Closure;
+use Language;
+
+/**
+ * @private
+ *
+ * Object agnostic handler class that encapsulates a foreign Message object
+ * (e.g MW's Message class). It is expected that a registered handler returns a
+ * simple string representation for the parameters, type, and language given.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class Message {
+
+ /**
+ * @var array
+ */
+ private static $messageCache = null;
+
+ /**
+ * PoolCache ID
+ */
+ const POOLCACHE_ID = 'message.cache';
+
+ /**
+ * MW processing mode
+ */
+ const TEXT = 0x2;
+ const ESCAPED = 0x4;
+ const PARSE = 0x8;
+
+ /**
+ * Predefined language mode
+ */
+ const CONTENT_LANGUAGE = 0x32;
+ const USER_LANGUAGE = 0x64;
+
+ /**
+ * @var array
+ */
+ private static $messageHandler = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param $type
+ * @param Closure $handler
+ */
+ public static function registerCallbackHandler( $type, Closure $handler ) {
+ self::$messageHandler[$type] = $handler;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param $type
+ */
+ public static function deregisterHandlerFor( $type ) {
+ unset( self::$messageHandler[$type] );
+ }
+
+ /**
+ * @since 2.4
+ */
+ public static function clear() {
+ self::$messageCache = null;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return FixedInMemoryLruCache
+ */
+ public static function getCache() {
+
+ if ( self::$messageCache === null ) {
+ self::$messageCache = InMemoryPoolCache::getInstance()->getPoolCacheById( self::POOLCACHE_ID, 1000 );
+ }
+
+ return self::$messageCache;
+ }
+
+ /**
+ * Encodes a message into a JSON representation that can transferred,
+ * transformed, and stored while allowing to add an infinite amount of
+ * arguments.
+ *
+ * '[2,"Foo", "Bar"]' => Preferred output type, Message ID, Argument $1 ... $
+ *
+ * @since 2.5
+ *
+ * @param string|array $parameters
+ * @param integer|null $type
+ *
+ * @return string
+ */
+ public static function encode( $message, $type = null ) {
+
+ if ( is_string( $message ) && json_decode( $message ) && json_last_error() === JSON_ERROR_NONE ) {
+ return $message;
+ }
+
+ if ( $type === null ) {
+ $type = self::TEXT;
+ }
+
+ if ( $message === [] ) {
+ return '';
+ }
+
+ $message = (array)$message;
+ $encode = [];
+ $encode[] = $type;
+
+ foreach ( $message as $value ) {
+ // Check if the value is already encoded, and if decode to keep the
+ // structure intact
+ if ( substr( $value, 0, 1 ) === '[' && ( $dc = json_decode( $value, true ) ) && json_last_error() === JSON_ERROR_NONE ) {
+ $encode += $dc;
+ } else {
+ // Normalize arguments like "<strong>Expression error:
+ // Unrecognized word "yyyy".</strong>"
+ $value = strip_tags( htmlspecialchars_decode( $value, ENT_QUOTES ) );
+
+ // - Internally encoded to circumvent the strip_tags which would
+ // remove <, > from values that represent a range
+ // - Encode `::` to prevent the annotation parser to pick the
+ // message value
+ $value = str_replace( [ '%3C', '%3E', "::" ], [ '>', '<', "&#58;&#58;" ], $value );
+
+ $encode[] = $value;
+ }
+ }
+
+ return json_encode( $encode );
+ }
+
+ /**
+ * @FIXME Needs to be MW agnostic !
+ *
+ * @since 2.5
+ *
+ * @param string $messageId
+ *
+ * @return boolean
+ */
+ public static function exists( $message ) {
+ return wfMessage( $message )->exists();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $json
+ * @param integer|null $type
+ * @param integer|null $language
+ *
+ * @return string|boolean
+ */
+ public static function decode( $message, $type = null, $language = null ) {
+
+ $message = json_decode( $message );
+ $asType = null;
+
+ if ( json_last_error() !== JSON_ERROR_NONE || $message === '' || $message === null ) {
+ return false;
+ }
+
+ // If the first element is numeric then its signals the expected message
+ // formatter type
+ if ( isset( $message[0] ) && is_numeric( $message[0] ) ) {
+ $asType = array_shift( $message );
+ }
+
+ // Is it a msgKey or a simple text?
+ if ( isset( $message[0] ) && !self::exists( $message[0] ) ) {
+ return $message[0];
+ }
+
+ return self::get( $message, ( $type !== null ? $type: $asType ), $language );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string|array $parameters
+ * @param integer|null $type
+ * @param integer|null $language
+ *
+ * @return string
+ */
+ public static function get( $parameters, $type = null, $language = null ) {
+
+ $handler = null;
+ $parameters = (array)$parameters;
+
+ if ( $type === null ) {
+ $type = self::TEXT;
+ }
+
+ if ( $language === null || !$language ) {
+ $language = self::CONTENT_LANGUAGE;
+ }
+
+ $hash = self::getHash( $parameters, $type, $language );
+
+ if ( $content = self::getCache()->fetch( $hash ) ) {
+ return $content;
+ }
+
+ if ( isset( self::$messageHandler[$type] ) && is_callable( self::$messageHandler[$type] ) ) {
+ $handler = self::$messageHandler[$type];
+ }
+
+ if ( $handler === null ) {
+ return '';
+ }
+
+ $message = call_user_func_array(
+ $handler,
+ [ $parameters, $language ]
+ );
+
+ self::getCache()->save( $hash, $message );
+
+ return $message;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array $parameters
+ * @param integer $type
+ * @param integer|string|Language $language
+ *
+ * @return string
+ */
+ public static function getHash( $parameters, $type = null, $language = null ) {
+
+ if ( $language instanceof Language ) {
+ $language = $language->getCode();
+ }
+
+ return md5( json_encode( $parameters ) . '#' . $type . '#' . $language );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/NamespaceExaminer.php b/www/wiki/extensions/SemanticMediaWiki/src/NamespaceExaminer.php
new file mode 100644
index 00000000..113110a5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/NamespaceExaminer.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace SMW;
+
+use InvalidArgumentException;
+use MWNamespace;
+
+/**
+ * Examines if a specific namespace is enabled for the usage of the
+ * Semantic MediaWiki extension
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class NamespaceExaminer {
+
+ /** @var array */
+ private static $instance = null;
+
+ /** @var array */
+ private $registeredNamespaces = [];
+
+ /**
+ * @since 1.9
+ *
+ * @param array $registeredNamespaces
+ */
+ public function __construct( array $registeredNamespaces ) {
+ $this->registeredNamespaces = $registeredNamespaces;
+ }
+
+ /**
+ * Returns a static instance with an invoked global settings array
+ *
+ * @par Example:
+ * @code
+ * \SMW\NamespaceExaminer::getInstance()->isSemanticEnabled( NS_MAIN )
+ * @endcode
+ *
+ * @note Used in smwfIsSemanticsProcessed
+ *
+ * @since 1.9
+ *
+ * @return NamespaceExaminer
+ */
+ public static function getInstance() {
+
+ if ( self::$instance === null ) {
+ self::$instance = self::newFromArray( Settings::newFromGlobals()->get( 'smwgNamespacesWithSemanticLinks' ) );
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Registers an array of available namespaces
+ *
+ * @par Example:
+ * @code
+ * \SMW\NamespaceExaminer::newFromArray( array( ... ) )->isSemanticEnabled( NS_MAIN )
+ * @endcode
+ *
+ * @since 1.9
+ *
+ * @return NamespaceExaminer
+ */
+ public static function newFromArray( $registeredNamespaces ) {
+ return new self( $registeredNamespaces );
+ }
+
+ /**
+ * Resets static instance
+ *
+ * @since 1.9
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * Returns if a namespace is enabled for semantic processing
+ *
+ * @since 1.9
+ *
+ * @param integer $namespace
+ *
+ * @return boolean
+ * @throws InvalidArgumentException
+ */
+ public function isSemanticEnabled( $namespace ) {
+
+ if ( !is_int( $namespace ) ) {
+ throw new InvalidArgumentException( "{$namespace} is not a number" );
+ }
+
+ if ( !in_array( $namespace, MWNamespace::getValidNamespaces() ) ) {
+ // Bug 51435
+ return false;
+ }
+
+ return $this->isEnabled( $namespace );
+ }
+
+ /**
+ * Asserts if a namespace is enabled
+ *
+ * @since 1.9
+ *
+ * @param integer $namespace
+ *
+ * @return boolean
+ */
+ protected function isEnabled( $namespace ) {
+ return !empty( $this->registeredNamespaces[$namespace] );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/NamespaceManager.php b/www/wiki/extensions/SemanticMediaWiki/src/NamespaceManager.php
new file mode 100644
index 00000000..d29c81da
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/NamespaceManager.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace SMW;
+
+use SMW\Lang\Lang;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ * @author others
+ */
+class NamespaceManager {
+
+ /**
+ * @var Lang
+ */
+ private $lang;
+
+ /**
+ * @since 1.9
+ *
+ * @param Lang|null $lang
+ */
+ public function __construct( Lang $lang = null ) {
+ $this->lang = $lang;
+
+ if ( $this->lang === null ) {
+ $this->lang = Lang::getInstance();
+ }
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param &$vars
+ */
+ public function init( &$vars ) {
+
+ if ( !$this->isDefinedConstant( 'SMW_NS_PROPERTY' ) ) {
+ $this->initCustomNamespace( $vars );
+ }
+
+ // Legacy seeting in case some extension request a `smwgContLang` reference
+ if ( empty( $vars['smwgContLang'] ) ) {
+ $vars['smwgContLang'] = $this->lang->fetch( $vars['wgLanguageCode'] );
+ }
+
+ $this->addNamespaceSettings( $vars );
+ $this->addExtraNamespaceSettings( $vars );
+ }
+
+ /**
+ * @see Hooks:CanonicalNamespaces
+ * CanonicalNamespaces initialization
+ *
+ * @note According to T104954 registration via wgExtensionFunctions is to late
+ * and should happen before that
+ *
+ * @see https://phabricator.wikimedia.org/T104954#2391291
+ * @see https://www.mediawiki.org/wiki/Manual:Hooks/CanonicalNamespaces
+ * @Bug 34383
+ *
+ * @since 2.5
+ *
+ * @param array &$namespaces
+ */
+ public static function initCanonicalNamespaces( array &$namespaces ) {
+
+ $canonicalNames = self::initCustomNamespace( $GLOBALS )->getCanonicalNames();
+ $namespacesByName = array_flip( $namespaces );
+
+ // https://phabricator.wikimedia.org/T160665
+ // Find any namespace that uses the same canonical name and remove it
+ foreach ( $canonicalNames as $id => $name ) {
+ if ( isset( $namespacesByName[$name] ) ) {
+ unset( $namespaces[$namespacesByName[$name]] );
+ }
+ }
+
+ $namespaces += $canonicalNames;
+
+ return true;
+ }
+
+ /**
+ * @see Hooks:CanonicalNamespaces
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ public static function getCanonicalNames() {
+
+ $canonicalNames = [
+ SMW_NS_PROPERTY => 'Property',
+ SMW_NS_PROPERTY_TALK => 'Property_talk',
+ SMW_NS_CONCEPT => 'Concept',
+ SMW_NS_CONCEPT_TALK => 'Concept_talk',
+ SMW_NS_SCHEMA => 'smw/schema',
+ SMW_NS_SCHEMA_TALK => 'smw/schema_talk',
+ SMW_NS_RULE => 'Rule',
+ SMW_NS_RULE_TALK => 'Rule_talk'
+ ];
+
+ return $canonicalNames;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param integer offset
+ *
+ * @return array
+ */
+ public static function buildNamespaceIndex( $offset ) {
+
+ // 100 and 101 used to be occupied by SMW's now obsolete namespaces
+ // "Relation" and "Relation_Talk"
+
+ // 106 and 107 are occupied by the Semantic Forms, we define them here
+ // to offer some (easy but useful) support to SF
+
+ $namespaceIndex = [
+ 'SMW_NS_PROPERTY' => $offset + 2,
+ 'SMW_NS_PROPERTY_TALK' => $offset + 3,
+ //'SF_NS_FORM' => $offset + 6,
+ //'SF_NS_FORM_TALK' => $offset + 7,
+ 'SMW_NS_CONCEPT' => $offset + 8,
+ 'SMW_NS_CONCEPT_TALK' => $offset + 9,
+
+ // #3019 notes "Conflicts with the DPLforum extension ..."
+ //'SMW_NS_SCHEMA' => $offset + 10,
+ //'SMW_NS_SCHEMA_TALK' => $offset + 11,
+
+ 'SMW_NS_SCHEMA' => $offset + 12,
+ 'SMW_NS_SCHEMA_TALK' => $offset + 13,
+
+ 'SMW_NS_RULE' => $offset + 14,
+ 'SMW_NS_RULE_TALK' => $offset + 15,
+ ];
+
+ return $namespaceIndex;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param array &$vars
+ * @param Lang|null $lang
+ */
+ public static function initCustomNamespace( &$vars, Lang $lang = null ) {
+
+ $instance = new self( $lang );
+
+ if ( !isset( $vars['smwgNamespaceIndex'] ) ) {
+ $vars['smwgNamespaceIndex'] = 100;
+ }
+
+ $defaultSettings = [
+ 'wgNamespaceAliases',
+ 'wgExtraNamespaces',
+ 'wgNamespacesWithSubpages',
+ 'smwgNamespacesWithSemanticLinks',
+ 'smwgNamespaceIndex',
+ 'wgCanonicalNamespaceNames'
+ ];
+
+ foreach ( $defaultSettings as $key ) {
+ $vars[$key] = !isset( $vars[$key] ) ? [] : $vars[$key];
+ }
+
+ foreach ( $instance->buildNamespaceIndex( $vars['smwgNamespaceIndex'] ) as $ns => $index ) {
+ if ( !$instance->isDefinedConstant( $ns ) ) {
+ define( $ns, $index );
+ };
+ }
+
+ $extraNamespaces = $instance->getNamespacesByLanguageCode(
+ $vars['wgLanguageCode']
+ );
+
+ $namespaceAliases = $instance->getNamespaceAliasesByLanguageCode(
+ $vars['wgLanguageCode']
+ );
+
+ $vars['wgCanonicalNamespaceNames'] += $instance->getCanonicalNames();
+ $vars['wgExtraNamespaces'] += $extraNamespaces + $instance->getCanonicalNames();
+ $vars['wgNamespaceAliases'] = $namespaceAliases + array_flip( $extraNamespaces ) + array_flip( $instance->getCanonicalNames() ) + $vars['wgNamespaceAliases'];
+
+ $instance->addNamespaceSettings( $vars );
+
+ return $instance;
+ }
+
+ private function addNamespaceSettings( &$vars ) {
+
+ /**
+ * Default settings for the SMW specific NS which can only
+ * be defined after SMW_NS_PROPERTY is declared
+ */
+ $smwNamespacesSettings = [
+ SMW_NS_PROPERTY => true,
+ SMW_NS_PROPERTY_TALK => false,
+ SMW_NS_CONCEPT => true,
+ SMW_NS_CONCEPT_TALK => false,
+ SMW_NS_SCHEMA => true,
+ SMW_NS_SCHEMA_TALK => false,
+ ];
+
+ // Combine default values with values specified in other places
+ // (LocalSettings etc.)
+ $vars['smwgNamespacesWithSemanticLinks'] = array_replace(
+ $smwNamespacesSettings,
+ $vars['smwgNamespacesWithSemanticLinks']
+ );
+
+ $vars['wgNamespaceContentModels'][SMW_NS_SCHEMA] = CONTENT_MODEL_SMW_SCHEMA;
+ }
+
+ private function addExtraNamespaceSettings( &$vars ) {
+
+ /**
+ * Indicating which namespaces allow sub-pages
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:$wgNamespacesWithSubpages
+ */
+ $vars['wgNamespacesWithSubpages'] = $vars['wgNamespacesWithSubpages'] + [
+ SMW_NS_PROPERTY_TALK => true,
+ SMW_NS_CONCEPT_TALK => true,
+ ];
+
+ /**
+ * Allow custom namespaces to be acknowledged as containing useful content
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:$wgContentNamespaces
+ */
+ $vars['wgContentNamespaces'] = $vars['wgContentNamespaces'] + [
+ SMW_NS_PROPERTY,
+ SMW_NS_CONCEPT
+ ];
+
+ /**
+ * To indicate which namespaces are enabled for searching by default
+ *
+ * @see https://www.mediawiki.org/wiki/Manual:$wgNamespacesToBeSearchedDefault
+ */
+ $vars['wgNamespacesToBeSearchedDefault'] = $vars['wgNamespacesToBeSearchedDefault'] + [
+ SMW_NS_PROPERTY => true,
+ SMW_NS_CONCEPT => true
+ ];
+ }
+
+ protected function isDefinedConstant( $constant ) {
+ return defined( $constant );
+ }
+
+ protected function getNamespacesByLanguageCode( $languageCode ) {
+ $GLOBALS['smwgContLang'] = $this->lang->fetch( $languageCode );
+ return $GLOBALS['smwgContLang']->getNamespaces();
+ }
+
+ private function getNamespaceAliasesByLanguageCode( $languageCode ) {
+ return $this->lang->fetch( $languageCode )->getNamespaceAliases();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/NamespaceUriFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/NamespaceUriFinder.php
new file mode 100644
index 00000000..b1635548
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/NamespaceUriFinder.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace SMW;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class NamespaceUriFinder {
+
+ /**
+ * @var array
+ */
+ private static $namespaceUriList = [
+ 'owl' => 'http://www.w3.org/2002/07/owl#',
+ 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
+ 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#',
+ 'swivt' => 'http://semantic-mediawiki.org/swivt/1.0#',
+ 'xsd' => 'http://www.w3.org/2001/XMLSchema#',
+ 'skos' => 'http://www.w3.org/2004/02/skos/core#',
+ 'foaf' => 'http://xmlns.com/foaf/0.1/',
+ 'dc' => 'http://purl.org/dc/elements/1.1/'
+ ];
+
+ /**
+ * @since 2.4
+ *
+ * @param string $key
+ *
+ * @return false|string
+ */
+ public static function getUri( $key ) {
+
+ $key = strtolower( $key );
+
+ if ( isset( self::$namespaceUriList[$key] ) ) {
+ return self::$namespaceUriList[$key];
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Options.php b/www/wiki/extensions/SemanticMediaWiki/src/Options.php
new file mode 100644
index 00000000..763a8070
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Options.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace SMW;
+
+use InvalidArgumentException;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class Options {
+
+ /**
+ * @var array
+ */
+ protected $options = [];
+
+ /**
+ * @since 2.3
+ */
+ public function __construct( array $options = [] ) {
+ $this->options = $options;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set( $key, $value ) {
+ $this->options[$key] = $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ */
+ public function delete( $key ) {
+ unset( $this->options[ $key ] );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function has( $key ) {
+ return isset( $this->options[$key] ) || array_key_exists( $key, $this->options );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param string $value
+ *
+ * @return boolean
+ */
+ public function is( $key, $value ) {
+ return $this->get( $key ) === $value;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $key
+ *
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public function get( $key ) {
+
+ if ( $this->has( $key ) ) {
+ return $this->options[$key];
+ }
+
+ throw new InvalidArgumentException( "{$key} is an unregistered option" );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function safeGet( $key, $default = false ) {
+ return $this->has( $key ) ? $this->options[$key] : $default;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function dotGet( $key, $default = false ) {
+ return $this->digDeep( $this->options, $key, $default );
+ }
+
+ private function digDeep( $array, $key, $default ) {
+
+ if ( strpos( $key, '.' ) !== false ) {
+ $list = explode( '.', $key, 2 );
+
+ foreach ( $list as $k => $v ) {
+ if ( isset( $array[$v] ) ) {
+ return $this->digDeep( $array[$v], $list[$k+1], $default );
+ }
+ }
+ }
+
+ if ( isset( $array[$key] ) ) {
+ return $array[$key];
+ }
+
+ return $default;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param integer $flag
+ *
+ * @return boolean
+ */
+ public function isFlagSet( $key, $flag ) {
+ return ( ( (int)$this->safeGet( $key, 0 ) & $flag ) == $flag );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function toArray() {
+ return $this->options;
+ }
+
+ /**
+ * @deprecated since 3.0, use Options::toArray
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getOptions() {
+ return $this->toArray();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $keys
+ *
+ * @return array
+ */
+ public function filter( array $keys ) {
+
+ $options = [];
+
+ foreach ( $keys as $key ) {
+ if ( isset( $this->options[$key] ) ) {
+ $options[$key] = $this->options[$key];
+ }
+ }
+
+ return $options;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Page/ConceptPage.php b/www/wiki/extensions/SemanticMediaWiki/src/Page/ConceptPage.php
new file mode 100644
index 00000000..56ebf905
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Page/ConceptPage.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace SMW\Page;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DIConcept;
+use SMW\DIProperty;
+use SMW\MediaWiki\Collator;
+use SMW\Message;
+use SMWDataItem as DataItem;
+use SMW\Utils\HtmlTabs;
+use SMW\Page\ListBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ConceptPage extends Page {
+
+ /**
+ * @var DIProperty
+ */
+ private $property;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var DataValue
+ */
+ private $propertyValue;
+
+ /**
+ * @see Page::initParameters()
+ *
+ * @note We use a smaller limit here; property pages might become large.
+ */
+ protected function initParameters() {
+ $this->limit = $this->getOption( 'pagingLimit' );
+ }
+
+ /**
+ * Returns the HTML which is added to $wgOut after the article text.
+ *
+ * @return string
+ */
+ protected function getHtml() {
+
+ $context = $this->getContext();
+ $context->getOutput()->addModuleStyles( 'ext.smw.page.styles' );
+
+ $request = $context->getRequest();
+ $store = ApplicationFactory::getInstance()->getStore();
+
+ // limit==0: configuration setting to disable this completely
+ if ( $this->limit > 0 ) {
+ $descriptionFactory = ApplicationFactory::getInstance()->getQueryFactory()->newDescriptionFactory();
+
+ $description = $descriptionFactory->newConceptDescription( $this->getDataItem() );
+ $query = \SMWPageLister::getQuery( $description, $this->limit, $this->from, $this->until );
+
+ $query->setLimit( $request->getVal( 'limit', $this->getOption( 'pagingLimit' ) ) );
+ $query->setOffset( $request->getVal( 'offset', '0' ) );
+ $query->setContextPage( $this->getDataItem() );
+ $query->setOption( $query::NO_DEPENDENCY_TRACE, true );
+ $query->setOption( $query::NO_CACHE, true );
+
+ $queryResult = $store->getQueryResult( $query );
+
+ $diWikiPages = $queryResult->getResults();
+
+ if ( $this->until !== '' ) {
+ $diWikiPages = array_reverse( $diWikiPages );
+ }
+
+ $errors = $queryResult->getErrors();
+ } else {
+ $diWikiPages = [];
+ $errors = [];
+ }
+
+ // Make navigation point to the result list.
+ $this->mTitle->setFragment( '#smw-result' );
+
+ $titleText = htmlspecialchars( $this->mTitle->getText() );
+ $resultCount = count( $diWikiPages );
+
+ $limit = $request->getVal( 'limit', $this->getOption( 'pagingLimit' ) );
+ $offset = $request->getVal( 'offset', '0' );
+
+ $query = [
+ 'from' => $request->getVal( 'from', '' ),
+ 'until' => $request->getVal( 'until', '' ),
+ 'value' => $request->getVal( 'value', '' )
+ ];
+
+ $navigationLinks = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-page-navigation'
+ ],
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'clearfix'
+ ],
+ ListPager::pagination( $this->mTitle, $limit, $offset, $resultCount, $query + [ '_target' => '#smw-result' ] )
+ ) . Html::rawElement(
+ 'div',
+ [
+ 'style' => 'margin-top:10px;margin-bottom:10px;'
+ ],
+ wfMessage( 'smw_conceptarticlecount', ( $resultCount < $limit ? $resultCount : $limit ) )->parse()
+ )
+ );
+
+ $htmlTabs = new HtmlTabs();
+ $htmlTabs->setGroup( 'concept' );
+
+ if ( $this->mTitle->exists() ) {
+
+ $listBuilder = new ListBuilder(
+ $store
+ );
+
+ $html = $navigationLinks . $listBuilder->getColumnList( $diWikiPages );
+ } else {
+ $html = '';
+ }
+
+ $htmlTabs->tab(
+ 'smw-concept-list',
+ $this->msg( 'smw-concept-tab-list' ) . $this->getCachedCount( $store ),
+ [
+ 'hide' => $html === ''
+ ]
+ );
+
+ $htmlTabs->content( 'smw-concept-list', $html );
+
+ // Improperty values
+ $html = smwfEncodeMessages( $errors );
+
+ $htmlTabs->tab( 'smw-concept-errors', $this->msg( 'smw-concept-tab-errors' ), [ 'hide' => $html === '' ] );
+ $htmlTabs->content( 'smw-concept-errors', $html );
+
+ $html = $htmlTabs->buildHTML(
+ [ 'class' => 'smw-concept clearfix' ]
+ );
+
+ return Html::element(
+ 'div',
+ [
+ 'id' => 'smwfootbr'
+ ]
+ ) . Html::element(
+ 'a',
+ [
+ 'name' => 'smw-result'
+ ],
+ null
+ ) . Html::rawElement(
+ 'div',
+ [
+ 'id' => 'mw-pages'
+ ],
+ $html
+ );
+ }
+
+ private function getCachedCount( $store ) {
+
+ $concept = $store->getConceptCacheStatus(
+ $this->getDataItem()
+ );
+
+ if ( !$concept instanceof DIConcept || $concept->getCacheStatus() !== 'full' ) {
+ return '';
+ }
+
+ $cacheCount = $concept->getCacheCount();
+ $date = $this->getContext()->getLanguage()->timeanddate( $concept->getCacheDate() );
+
+ $countMsg = Message::get( [ 'smw-concept-indicator-cache-update', $date ] );
+ $indicatorClass = ( $cacheCount < 25000 ? ( $cacheCount > 5000 ? ' moderate' : '' ) : ' high' );
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'title' => $countMsg,
+ 'class' => 'usage-count' . $indicatorClass
+ ],
+ $cacheCount
+ );
+ }
+
+ private function msg( $params, $type = Message::TEXT, $lang = Message::USER_LANGUAGE ) {
+ return Message::get( $params, $type, $lang );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder.php
new file mode 100644
index 00000000..0aacd65a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace SMW\Page;
+
+use SMW\Store;
+use SMW\Message;
+use SMW\MediaWiki\Collator;
+use SMW\DataValueFactory;
+use SMWInfolink as Infolink;
+use SMWDataItem as DataItem;
+use SMW\Utils\HtmlColumns;
+use Linker;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ListBuilder {
+
+ /**
+ * @var Store
+ */
+ public $store;
+
+ /**
+ * @var Collator
+ */
+ public $collator;
+
+ /**
+ * @var callable
+ */
+ public $itemFormatter;
+
+ /**
+ * @var Linker
+ */
+ public $linker = false;
+
+ /**
+ * @var integer
+ */
+ public $sort = SORT_NATURAL;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param Collator|null $collator
+ */
+ public function __construct( Store $store, Collator $collator = null ) {
+ $this->store = $store;
+ $this->collator = $collator;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param callable $itemFormatter
+ */
+ public function setItemFormatter( callable $itemFormatter ) {
+ $this->itemFormatter = $itemFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Linker|false $linker
+ */
+ public function setLinker( $linker ) {
+ $this->linker = $linker;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $sort
+ */
+ public function sort( $sort ) {
+ $this->sort = $sort;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage[] $dataItems
+ *
+ * @return array
+ */
+ public function getList( array $dataItems ) {
+ return $this->buildList( $dataItems );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage[] $dataItems
+ *
+ * @return string
+ */
+ public function getColumnList( array $dataItems ) {
+
+ $htmlColumns = new HtmlColumns();
+
+ if ( count( $dataItems ) > 10 ) {
+ $htmlColumns->setColumnClass( 'smw-column-responsive' );
+ }
+
+ $htmlColumns->setContinueAbbrev(
+ Message::get( 'listingcontinuesabbrev', Message::PARSE, Message::USER_LANGUAGE )
+ );
+
+ $htmlColumns->setColumns( 1 );
+
+ $htmlColumns->setContents(
+ $this->buildList( $dataItems ),
+ HtmlColumns::INDEXED_LIST
+ );
+
+ return $htmlColumns->getHtml();
+ }
+
+ private function buildList( $dataItems ) {
+
+ $dataValueFactory = DataValueFactory::getInstance();
+
+ if ( $this->linker === false ) {
+ $this->linker = smwfGetLinker();
+ }
+
+ if ( $this->collator === null ) {
+ $this->collator = Collator::singleton();
+ }
+
+ $contents = [];
+
+ foreach ( $dataItems as $dataItem ) {
+
+ $dataValue = $dataValueFactory->newDataValueByItem( $dataItem, null );
+ $startChar = $this->getFirstLetter( $dataItem );
+
+ if ( $startChar === '' ) {
+ $startChar = '...';
+ }
+
+ if ( !isset( $contents[$startChar] ) ) {
+ $contents[$startChar] = [];
+ }
+
+ if ( is_callable( $this->itemFormatter ) ) {
+ // Use of ( ... )( ) only possible with PHP7
+ // $contents[$startChar][] = ( $this->itemFormatter )( $dataValue, $this->linker );
+ $contents[$startChar][] = call_user_func_array( $this->itemFormatter, [ $dataValue, $this->linker ] );
+ } else {
+ $searchlink = Infolink::newBrowsingLink( '+', $dataValue->getWikiValue() );
+ $contents[$startChar][] = $dataValue->getLongHTMLText( $this->linker ) . '&#160;' . $searchlink->getHTML( $this->linker );
+ }
+ }
+
+ ksort( $contents, $this->sort );
+
+ return $contents;
+ }
+
+ private function getFirstLetter( DataItem $dataItem ) {
+
+ $sortKey = $dataItem->getSortKey();
+
+ if ( $dataItem->getDIType() === DataItem::TYPE_WIKIPAGE ) {
+ $sortKey = $this->store->getWikiPageSortKey( $dataItem );
+ }
+
+ return $this->collator->getFirstLetter( $sortKey );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder/ListBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder/ListBuilder.php
new file mode 100644
index 00000000..98185608
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder/ListBuilder.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace SMW\Page\ListBuilder;
+
+use Html;
+use SMW\DIProperty;
+use SMW\RequestOptions;
+use SMW\Store;
+use SMWDataItem as DataItem;
+use SMWPageLister as PageLister;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ListBuilder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var string
+ */
+ private $languageCode = 'en';
+
+ /**
+ * @var integer
+ */
+ private $listLimit = 0;
+
+ /**
+ * @var string
+ */
+ private $listHeader = '';
+
+ /**
+ * @var boolean
+ */
+ private $isUserDefined = false;
+
+ /**
+ * @var boolean
+ */
+ private $checkProperty = true;
+
+ /**
+ * @var integer
+ */
+ private $itemCount = 0;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $languageCode
+ */
+ public function setLanguageCode( $languageCode ) {
+ $this->languageCode = $languageCode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isUserDefined
+ */
+ public function isUserDefined( $isUserDefined ) {
+ $this->isUserDefined = $isUserDefined;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $listLimit
+ */
+ public function setListLimit( $listLimit ) {
+ $this->listLimit = $listLimit;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $listHeader
+ */
+ public function setListHeader( $listHeader ) {
+ $this->listHeader = $listHeader;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $checkProperty
+ */
+ public function checkProperty( $checkProperty ) {
+ $this->checkProperty = $checkProperty;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return integer
+ */
+ public function getItemCount() {
+ return $this->itemCount;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ * @param DataItem $dataItem
+ * @param RequestOptions $requestOptions
+ *
+ * @return string
+ */
+ public function createHtml( DIProperty $property, DataItem $dataItem, RequestOptions $requestOptions ) {
+
+ $subjectList = $this->store->getPropertySubjects(
+ $property,
+ $dataItem,
+ $requestOptions
+ );
+
+ // May return an iterator
+ if ( $subjectList instanceof \Iterator ) {
+ $subjectList = iterator_to_array( $subjectList );
+ }
+
+ $more = false;
+
+ // Pop the +1 look ahead from the list
+ if ( is_array( $subjectList ) && count( $subjectList ) > $this->listLimit ) {
+ array_pop( $subjectList );
+ $more = true;
+ }
+
+ $result = '';
+ $this->itemCount = is_array( $subjectList ) ? count( $subjectList ) : 0;
+
+ $callback = null;
+ $message = wfMessage( 'smw-propertylist-count', $this->itemCount )->text();
+
+ if ( $more ) {
+ $callback = function() use ( $property, $dataItem ) {
+ return \Html::element(
+ 'a',
+ [
+ 'href' => \SpecialPage::getSafeTitleFor( 'SearchByProperty' )->getLocalURL( [
+ 'property' => $property->getLabel(),
+ 'value' => $dataItem->getDBKey()
+ ] )
+ ],
+ wfMessage( 'smw_browse_more' )->text()
+ );
+ };
+
+ $message = wfMessage( 'smw-propertylist-count-more-available', $this->itemCount )->text();
+ }
+
+ if ( $this->itemCount > 0 ) {
+ $titleText = htmlspecialchars( str_replace( '_', ' ', $dataItem->getDBKey() ) );
+ $result .= "<div id=\"{$this->listHeader}\">" . "\n<p>";
+
+ $result .= $message . "</p>";
+ $property = $this->checkProperty ? $property : null;
+
+ if ( $this->itemCount < 6 ) {
+ $result .= PageLister::getShortList( 0, $this->itemCount, $subjectList, $property, $callback );
+ } else {
+ $result .= PageLister::getColumnList( 0, $this->itemCount, $subjectList, $property, $callback );
+ }
+
+ $result .= "\n</div>";
+ }
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder/ValueListBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder/ValueListBuilder.php
new file mode 100644
index 00000000..1abe84a4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Page/ListBuilder/ValueListBuilder.php
@@ -0,0 +1,392 @@
+<?php
+
+namespace SMW\Page\ListBuilder;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMW\Localizer;
+use SMW\MediaWiki\Collator;
+use SMW\Message;
+use SMW\Page\ListPager;
+use SMW\Query\Language\SomeProperty;
+use SMW\RequestOptions;
+use SMW\Store;
+use SMW\Utils\HtmlDivTable;
+use SMW\Utils\NextPager;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+use SMWInfolink as Infolink;
+use SMWPageLister as PageLister;
+use WebRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ValueListBuilder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var integer
+ */
+ private $pagingLimit = 0;
+
+ /**
+ * @var integer
+ */
+ private $maxPropertyValues = 3;
+
+ /**
+ * @var string
+ */
+ private $languageCode = 'en';
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $pagingLimit
+ */
+ public function setPagingLimit( $pagingLimit ) {
+ $this->pagingLimit = $pagingLimit;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $languageCode
+ */
+ public function setLanguageCode( $languageCode ) {
+ $this->languageCode = $languageCode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $maxPropertyValues
+ */
+ public function setMaxPropertyValues( $maxPropertyValues ) {
+ $this->maxPropertyValues = $maxPropertyValues;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ * @param DataItem $dataItem
+ *
+ * @return string
+ */
+ public function createHtml( DIProperty $property, DataItem $dataItem, array $query = [] ) {
+
+ $limit = isset( $query['limit'] ) ? (int)$query['limit'] : 0;
+ $offset = isset( $query['offset'] ) ? (int)$query['offset'] : 0;
+ $from = isset( $query['from'] ) ? $query['from'] : 0;
+ $until = isset( $query['until'] ) ? $query['until'] : 0;
+ $filter = isset( $query['filter'] ) ? $query['filter'] : '';
+
+ // limit==0: configuration setting to disable this completely
+ if ( $limit < 1 ) {
+ return '';
+ }
+
+ $dataItems = [];
+ $isValueSearch = false;
+
+ $options = PageLister::getRequestOptions( $limit, $from, $until );
+ $options->setOffset( $offset );
+
+ if ( $filter !== '' ) {
+ $dataItems = $this->filterByValue( $property, $filter, $options );
+ $isValueSearch = true;
+ } else {
+ $dataItems = $this->store->getAllPropertySubjects( $property, $options );
+ }
+
+ if ( $dataItems instanceof \Traversable ) {
+ $dataItems = iterator_to_array( $dataItems );
+ }
+
+ if ( !$options->ascending ) {
+ $dataItems = array_reverse( $dataItems );
+ }
+
+ $result = '';
+
+ if ( count( $dataItems ) < 1 && !$isValueSearch ) {
+ return $result;
+ }
+
+ $title = $dataItem->getTitle();
+ $title->setFragment( '#SMWResults' );
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $property
+ );
+
+ // Allow the DV formatter to access a specific language code
+ $dataValue->setOption(
+ DataValue::OPT_USER_LANGUAGE,
+ $this->languageCode
+ );
+
+ $titleText = htmlspecialchars( $dataValue->getWikiValue() );
+ $resultCount = count( $dataItems );
+
+ $topic = $isValueSearch ? 'smw-property-page-list-search-count' : 'smw-property-page-list-count';
+
+ $navNote = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-page-nav-note'
+ ],
+ Message::get(
+ [ $topic, ( $resultCount < $limit ? $resultCount : $limit ), $filter ],
+ Message::PARSE,
+ $this->languageCode
+ ) . Html::rawElement(
+ 'div',
+ [],
+ ''
+ )
+ );
+
+ $objectList = $this->createValueList(
+ $property,
+ $dataItem,
+ $dataItems,
+ $limit,
+ $until
+ );
+
+ $navContainer = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-page-nav-container'
+ ],
+ Html::rawElement(
+ 'div' ,
+ [
+ 'class' => 'smw-page-nav-left'
+ ],
+ ListPager::pagination( $title, $limit, $offset, $resultCount, $query )
+ ) . Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-page-nav-right'
+ ],
+ ListPager::filter( $title, $limit, $offset, $filter )
+ )
+ );
+
+ $result .= Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-page-navigation'
+ ],
+ $navContainer . $navNote
+ ) . $objectList;
+
+ return Html::rawElement(
+ 'a',
+ [ 'name' => 'SMWResults' ],
+ ''
+ ) . Html::rawElement(
+ 'div',
+ [ 'id' => 'mw-pages' ],
+ $result
+ );
+ }
+
+ private function createValueList( DIProperty $property, DataItem $dataItem, $diWikiPages, $limit, $until ) {
+
+ if ( $diWikiPages instanceof \Iterator ) {
+ $diWikiPages = iterator_to_array( $diWikiPages );
+ }
+
+ $ac = count( $diWikiPages );
+ //$contentLanguage = Localizer::getInstance()->getContentLanguage();
+ $title = $dataItem->getTitle();
+
+ if ( $ac > $limit ) {
+ if ( $until !== '' ) {
+ $start = 1;
+ } else {
+ $start = 0;
+ $ac = $ac - 1;
+ }
+ } else {
+ $start = 0;
+ }
+
+ $html = '';
+ $prev_start_char = 'None';
+
+ for ( $index = $start; $index < $ac; $index++ ) {
+ $diWikiPage = $diWikiPages[$index];
+ $dvWikiPage = DataValueFactory::getInstance()->newDataValueByItem( $diWikiPage, null );
+
+ $sortKey = $this->store->getWikiPageSortKey( $diWikiPage );
+ $start_char = Collator::singleton()->getFirstLetter( $sortKey );
+
+ // Header for index letters
+ if ( $start_char != $prev_start_char ) {
+ $html .= HtmlDivTable::row(
+ HtmlDivTable::cell(
+ '<div id="' . htmlspecialchars( $start_char ) . '">' . htmlspecialchars( $start_char ) . "</div>",
+ [
+ 'class' => "header-title"
+ ]
+ ) . HtmlDivTable::cell(
+ '<div></div>',
+ [
+ 'class' => "header-title"
+ ]
+ ),
+ [
+ 'class' => "header-row"
+ ]
+ );
+ $prev_start_char = $start_char;
+ }
+
+ // Property values
+ $ropts = new RequestOptions();
+ $ropts->limit = $this->maxPropertyValues + 1;
+
+ // Restrict the request otherwise the entire SemanticData record
+ // is fetched which can in case of a subject with a large
+ // subobject/subpage pool create excessive DB queries that are not
+ // used for the display
+ $ropts->conditionConstraint = true;
+
+ $values = $this->store->getPropertyValues( $diWikiPage, $property, $ropts );
+
+ // May return an iterator
+ if ( $values instanceof \Iterator ) {
+ $values = iterator_to_array( $values );
+ }
+
+ $hasLocalTimeOffsetPreference = Localizer::getInstance()->hasLocalTimeOffsetPreference();
+
+ $i = 0;
+ $pvCells = '';
+
+ foreach ( $values as $di ) {
+ if ( $i != 0 ) {
+ $pvCells .= ', ';
+ }
+ $i++;
+
+ if ( $i < $this->maxPropertyValues + 1 ) {
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem( $di, $property );
+ $outputFormat = $dataValue->getOutputFormat();
+
+ if ( $outputFormat === false ) {
+ $outputFormat = 'LOCL' . ( $hasLocalTimeOffsetPreference ? '#TO' : '' );
+ }
+
+ $dataValue->setOutputFormat( $outputFormat );
+
+ $pvCells .= $dataValue->getShortHTMLText( smwfGetLinker() ) . $dataValue->getInfolinkText( SMW_OUTPUT_HTML, smwfGetLinker() );
+ } else {
+ $searchlink = Infolink::newInversePropertySearchLink( '…', $dvWikiPage->getWikiValue(), $title->getText() );
+ $pvCells .= $searchlink->getHTML( smwfGetLinker() );
+ }
+ }
+
+ // Property name
+ $searchlink = Infolink::newBrowsingLink( '+', $dvWikiPage->getWikiValue() );
+ $html .= HtmlDivTable::row(
+ HtmlDivTable::cell(
+ $dvWikiPage->getShortHTMLText( smwfGetLinker() ) . '&#160;' . $searchlink->getHTML( smwfGetLinker() ),
+ [
+ 'class' => "smwpropname",
+ 'data-list-index' => $index
+ ]
+ ) . HtmlDivTable::cell(
+ $pvCells,
+ [
+ 'class' => "smwprops"
+ ]
+ ),
+ [
+ 'class' => "value-row"
+ ]
+ );
+ }
+
+ return HtmlDivTable::table(
+ $html,
+ [
+ 'class' => "smw-property-page-results",
+ 'style' => "width: 100%;"
+ ]
+ );
+ }
+
+ private function filterByValue( $property, $value, $options ) {
+
+ $queryFactory = ApplicationFactory::getInstance()->getQueryFactory();
+ $queryParser = $queryFactory->newQueryParser();
+
+ $description = $queryParser->getQueryDescription(
+ $queryParser->createCondition( $property, $value )
+ );
+
+ if ( $queryParser->getErrors() !== [] ) {
+ return [];
+ }
+
+ // Make sure that no subproperty is included while executing the
+ // query
+ if ( $description instanceof SomeProperty ) {
+ $description->setHierarchyDepth( 0 );
+ }
+
+ $query = $queryFactory->newQuery( $description );
+ $query->setLimit( $options->limit );
+ $query->setOffset( $options->offset );
+
+ // We are not sorting via the backend as an ORDER BY will cause a
+ // SQL filesort and means for a large pool of value assignments a
+ // slow query
+ $res = $this->store->getQueryResult( $query );
+ $results = $res->getResults();
+
+ $sort = [];
+ $collator = Collator::singleton();
+
+ foreach ( $results as $result ) {
+
+ $firstLetter = $collator->getFirstLetter(
+ $this->store->getWikiPageSortKey( $result )
+ );
+
+ $sort[$firstLetter . '#' . $result->getHash()] = $result;
+ }
+
+ // Sort on the spot via PHP, which should be enough for the search
+ // and match functionality
+ ksort( $sort );
+
+ return array_values( $sort );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Page/ListPager.php b/www/wiki/extensions/SemanticMediaWiki/src/Page/ListPager.php
new file mode 100644
index 00000000..b900a8db
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Page/ListPager.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace SMW\Page;
+
+use Html;
+use SMW\Localizer;
+use SMW\Message;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ListPager {
+
+ /**
+ * @var string
+ */
+ public static $language = '';
+
+ /**
+ * @since 2.4
+ */
+ public static function pagination( Title $title, $limit, $offset = 0, $count = 0, array $query = [], $prefix = '' ) {
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-ui-pagination'
+ ],
+ self::getPagingLinks( $title, $limit, $offset, $count, $query, $prefix )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param integer $limit
+ * @param integer $offset
+ *
+ * @return string
+ */
+ public static function filter( Title $title, $limit = 0, $offset = 0, $filter = '' ) {
+
+ $form = \Xml::tags(
+ 'form',
+ [
+ 'id' => 'search',
+ 'name' => 'foo',
+ 'action' => $GLOBALS['wgScript']
+ ],
+ Html::hidden(
+ 'title',
+ strtok( $title->getPrefixedText(), '/' )
+ ) . Html::hidden(
+ 'limit',
+ $limit
+ ) . Html::hidden(
+ 'offset',
+ $offset
+ )
+ );
+
+ $label = Message::get( 'smw-filter', Message::TEXT, Message::USER_LANGUAGE );
+
+ $form .= Html::rawElement(
+ 'label',
+ [],
+ $label .
+ Html::rawElement(
+ 'input',
+ [
+ 'type' => 'search',
+ 'name' => 'filter',
+ 'value' => $filter,
+ 'form' => 'search',
+ 'autocomplete' => 'off',
+ 'placeholder' => '...'
+ ]
+ )
+ );
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-ui-input-filter'
+ ],
+ $form
+ );
+ }
+
+ /**
+ * Generate (prev x| next x) (20|50|100...) type links for paging
+ *
+ * @param Title $title Title object to link
+ * @param int $offset
+ * @param int $limit
+ * @param integer $count
+ * @param array $query Optional URL query parameter string
+ * @return string
+ */
+ public static function getPagingLinks( Title $title, $limit, $offset, $count = 0, array $query = [], $prefix = '' ) {
+
+ $list = [];
+ $limit = (int)$limit;
+ $offset = (int)$offset;
+ $count = (int)$count;
+
+ $atend = $count < $limit;
+ $disabled = $count > 0 ? '' : ' disabled';
+
+ if ( self::$language === '' ) {
+ $language = Localizer::getInstance()->getUserLanguage();
+ } else {
+ $language = Localizer::getInstance()->getLanguage( self::$language );
+ }
+
+ if ( $prefix !== '' ) {
+ $prefix = Html::rawElement( 'a', [ 'class' => 'page-link link-disabled' ], $prefix );
+ }
+
+ # Make 'previous' link
+ $prev = wfMessage( 'prevn' )->inLanguage( $language )->title( $title )->numParams( $limit )->text();
+
+ if ( $offset > 0 ) {
+ $plink = self::numLink( $title, max( $offset - $limit, 0 ), $limit, $query, $prev, 'prevn-title', 'mw-prevlink', $disabled, $language );
+ } else {
+ $plink = Html::element( 'a', [ 'class' => 'page-link link-disabled' ], htmlspecialchars( $prev ) );
+ }
+
+ # Make 'next' link
+ $next = wfMessage( 'nextn' )->inLanguage( $language )->title( $title )->numParams( $limit )->text();
+
+ if ( $atend ) {
+ $nlink = Html::element( 'a', [ 'class' => 'page-link link-disabled' ], htmlspecialchars( $next ) );
+ } else {
+ $nlink = self::numLink( $title, $offset + $limit, $limit, $query, $next, 'nextn-title', 'mw-nextlink', $disabled, $language );
+ }
+
+ # Make links to set number of items per page
+
+ foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
+ $list[] = self::numLink(
+ $title,
+ $offset,
+ $num,
+ $query,
+ $language->formatNum( $num ),
+ 'shown-title',
+ 'mw-numlink',
+ $disabled,
+ $language,
+ $num === $limit
+ );
+ }
+
+ return $prefix . $plink . implode( '', $list ) . $nlink;
+ }
+
+ /**
+ * Helper function for viewPrevNext() that generates links
+ *
+ * @param Title $title Title object to link
+ * @param int $offset
+ * @param int $limit
+ * @param array $query Extra query parameters
+ * @param string $link Text to use for the link; will be escaped
+ * @param string $tooltipMsg Name of the message to use as tooltip
+ * @param string $class Value of the "class" attribute of the link
+ * @return string HTML fragment
+ */
+ private static function numLink( Title $title, $offset, $limit, array $query, $link, $tooltipMsg, $class, $disabled, $language, $active = false ) {
+ $query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
+
+ $tooltip = wfMessage( $tooltipMsg )->inLanguage( $language )->title( $title )->numParams( $limit )->text();
+ $target = '';
+
+ if ( isset( $query['_target' ] ) ) {
+ $target = $query['_target' ];
+ unset( $query['_target' ] );
+ }
+
+ return Html::element( 'a',
+ [
+ 'href' => $title->getLocalURL( $query ) . $target,
+ 'title' => $tooltip,
+ 'class' => 'page-link' . ( $active ? ' link-active' : '' )
+ ],
+ $link
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Page/Page.php b/www/wiki/extensions/SemanticMediaWiki/src/Page/Page.php
new file mode 100644
index 00000000..5e682193
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Page/Page.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace SMW\Page;
+
+use Article;
+use SMW\DIWikiPage;
+use SMW\Options;
+use SMWOutputs as Outputs;
+
+/**
+ * Abstract subclass of MediaWiki's Article that handles the common tasks of
+ * article pages for Concept and Property pages. This is mainly parameter
+ * handling and some very basic output control.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Nikolas Iwan
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ */
+abstract class Page extends Article {
+
+ /**
+ * Limit for results per page.
+ *
+ * @var integer
+ */
+ protected $limit;
+
+ /**
+ * Start string: print $limit results from here.
+ *
+ * @var string
+ */
+ protected $from;
+
+ /**
+ * End string: print $limit results strictly before this article.
+ *
+ * @var string
+ */
+ protected $until;
+
+ /**
+ * Cache for the current skin, obtained from $wgUser.
+ *
+ * @var Skin
+ */
+ protected $skin;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * Overwrite Article::view to add additional HTML to the output.
+ *
+ * @see Article::view
+ */
+ public function view() {
+
+ $outputPage = $this->getContext()->getOutput();
+ $outputPage->addModuleStyles( 'ext.smw.page.styles' );
+
+ if ( !$this->getOption( 'smwgSemanticsEnabled' ) ) {
+ $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() );
+ $outputPage->addHTML( wfMessage( 'smw-semantics-not-enabled' )->text() );
+ return;
+ }
+
+ if ( ( $redirectTargetURL = $this->getRedirectTargetURL() ) !== false ) {
+ $outputPage->redirect( $redirectTargetURL );
+ }
+
+ $this->initParameters();
+
+ // Copied from CategoryPage
+ $user = $this->getContext()->getUser();
+ $request = $this->getContext()->getRequest();
+
+ $diff = $request->getVal( 'diff' );
+ $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) );
+
+ if ( !isset( $diff ) || !$diffOnly ) {
+ // MW 1.25+
+ if ( method_exists( $outputPage, 'setIndicators' ) && ( $indicators = $this->getTopIndicators() ) !== '' ) {
+ $outputPage->setIndicators( $indicators );
+ }
+
+ $outputPage->addHTML( $this->initHtml() );
+ $outputPage->addHTML( $this->beforeView() );
+ }
+
+ if ( $this->isLockedView() === false ) {
+ parent::view();
+ }
+
+ if ( !isset( $diff ) || !$diffOnly ) {
+ $this->showList();
+ }
+
+ $outputPage->addHTML( $this->afterHtml() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function getOption( $key ) {
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ return $this->options->safeGet( $key, false );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function setOption( $key, $value ) {
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ return $this->options->set( $key, $value );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string|boolean
+ */
+ protected function getRedirectTargetURL() {
+ return false;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ protected function getTopIndicators() {
+ return '';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ protected function initHtml() {
+ return $this->getIntroductoryText();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ protected function getIntroductoryText() {
+ return '';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ protected function isLockedView() {
+ return false;
+ }
+
+ /**
+ * Main method for adding all additional HTML to the output stream.
+ */
+ protected function showList() {
+
+ $outputPage = $this->getContext()->getOutput();
+ $request = $this->getContext()->getRequest();
+
+ $this->from = $request->getVal( 'from', '' );
+ $this->until = $request->getVal( 'until', '' );
+
+ $outputPage->addHTML( $this->getHtml() );
+
+ Outputs::commitToOutputPage( $outputPage );
+ }
+
+ /**
+ * Initialise some parameters that might be changed by subclasses
+ * (e.g. $limit). Method can be overwritten in this case.
+ * If the method returns false, nothing will be printed besides
+ * the original article.
+ *
+ * @return true
+ */
+ protected function initParameters() {
+ $this->limit = 20;
+ }
+
+ /**
+ * Returns HTML to be displayed before the article text.
+ *
+ * @return string
+ */
+ protected function beforeView() {
+ return '';
+ }
+
+ /**
+ * Returns HTML to be displayed after the list display.
+ *
+ * @return string
+ */
+ protected function afterHtml() {
+ return '';
+ }
+
+ /**
+ * Returns the HTML which is added to $wgOut after the article text.
+ *
+ * @return string
+ */
+ protected abstract function getHtml();
+
+ /**
+ * Like Article's getTitle(), but returning a suitable SMWDIWikiPage.
+ *
+ * @since 1.6
+ *
+ * @return SMWDIWikiPage
+ */
+ protected function getDataItem() {
+ return DIWikiPage::newFromTitle( $this->getTitle() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Page/PageFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Page/PageFactory.php
new file mode 100644
index 00000000..00d6caa4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Page/PageFactory.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace SMW\Page;
+
+use RuntimeException;
+use SMW\ApplicationFactory;
+use SMW\PropertySpecificationReqExaminer;
+use SMW\PropertySpecificationReqMsgBuilder;
+use SMW\Store;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PageFactory {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ *
+ * @return PageView
+ * @throws RuntimeException
+ */
+ public function newPageFromTitle( Title $title ) {
+
+ if ( $title->getNamespace() === SMW_NS_PROPERTY ) {
+ return $this->newPropertyPage( $title );
+ } elseif ( $title->getNamespace() === SMW_NS_CONCEPT ) {
+ return $this->newConceptPage( $title );
+ }
+
+ throw new RuntimeException( 'No supported ContentPage instance for namespace ' . $title->getNamespace() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ *
+ * @return PropertyPage
+ */
+ public function newPropertyPage( Title $title ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ $propertySpecificationReqExaminer = new PropertySpecificationReqExaminer(
+ $this->store,
+ $applicationFactory->singleton( 'ProtectionValidator' )
+ );
+
+ $propertySpecificationReqExaminer->setChangePropagationProtection(
+ $settings->get( 'smwgChangePropagationProtection' )
+ );
+
+ $propertySpecificationReqMsgBuilder = new PropertySpecificationReqMsgBuilder(
+ $this->store,
+ $propertySpecificationReqExaminer
+ );
+
+ $propertySpecificationReqMsgBuilder->setPropertyReservedNameList(
+ $settings->get( 'smwgPropertyReservedNameList' )
+ );
+
+ $propertyPage = new PropertyPage(
+ $title,
+ $this->store,
+ $propertySpecificationReqMsgBuilder
+ );
+
+ $propertyPage->setOption(
+ 'smwgSemanticsEnabled',
+ $settings->get( 'smwgSemanticsEnabled' )
+ );
+
+ $propertyPage->setOption(
+ 'pagingLimit',
+ $settings->dotGet( 'smwgPagingLimit.property' )
+ );
+
+ $propertyPage->setOption(
+ 'smwgPropertyListLimit',
+ $settings->get( 'smwgPropertyListLimit' )
+ );
+
+ $propertyPage->setOption(
+ 'smwgMaxPropertyValues',
+ $settings->get( 'smwgMaxPropertyValues' )
+ );
+
+ return $propertyPage;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ *
+ * @return ConceptPage
+ */
+ public function newConceptPage( Title $title ) {
+
+ $conceptPage = new ConceptPage( $title );
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $conceptPage->setOption(
+ 'smwgSemanticsEnabled',
+ $settings->get( 'smwgSemanticsEnabled' )
+ );
+
+ $conceptPage->setOption(
+ 'pagingLimit',
+ $settings->dotGet( 'smwgPagingLimit.concept' )
+ );
+
+ return $conceptPage;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Page/PropertyPage.php b/www/wiki/extensions/SemanticMediaWiki/src/Page/PropertyPage.php
new file mode 100644
index 00000000..c8580337
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Page/PropertyPage.php
@@ -0,0 +1,380 @@
+<?php
+
+namespace SMW\Page;
+
+use Html;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DataValues\ValueFormatters\DataValueFormatter;
+use SMW\DIProperty;
+use SMW\Message;
+use SMW\Page\ListBuilder\ListBuilder as SimpleListBuilder;
+use SMW\Page\ListBuilder\ValueListBuilder;
+use SMW\PropertyRegistry;
+use SMW\PropertySpecificationReqMsgBuilder;
+use SMW\RequestOptions;
+use SMW\Store;
+use SMW\StringCondition;
+use Title;
+use SMW\Utils\HtmlTabs;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PropertyPage extends Page {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertySpecificationReqMsgBuilder
+ */
+ private $propertySpecificationReqMsgBuilder;
+
+ /**
+ * @var DIProperty
+ */
+ private $property;
+
+ /**
+ * @var DataValue
+ */
+ private $propertyValue;
+
+ /**
+ * @var ListBuilder
+ */
+ private $listBuilder;
+
+ /**
+ * @see 3.0
+ *
+ * @param Title $title
+ * @param Store $store
+ * @param PropertySpecificationReqMsgBuilder $propertySpecificationReqMsgBuilder
+ */
+ public function __construct( Title $title, Store $store, PropertySpecificationReqMsgBuilder $propertySpecificationReqMsgBuilder ) {
+ parent::__construct( $title );
+ $this->store = $store;
+ $this->propertySpecificationReqMsgBuilder = $propertySpecificationReqMsgBuilder;
+ }
+
+ /**
+ * @see Page::initParameters()
+ */
+ protected function initParameters() {
+ // We use a smaller limit here; property pages might become large
+ $this->limit = $this->getOption( 'pagingLimit' );
+ $this->property = DIProperty::newFromUserLabel( $this->getTitle()->getText() );
+ $this->propertyValue = DataValueFactory::getInstance()->newDataValueByItem( $this->property );
+ }
+
+ /**
+ * @see Page::getIntroductoryText
+ *
+ * @since 3.0
+ *
+ * @return string
+ */
+ protected function getIntroductoryText() {
+
+ $redirectTarget = $this->store->getRedirectTarget( $this->property );
+
+ if ( !$redirectTarget->equals( $this->property ) ) {
+ return '';
+ }
+
+ $this->propertySpecificationReqMsgBuilder->setSemanticData(
+ $this->fetchSemanticDataFromEditInfo()
+ );
+
+ $this->propertySpecificationReqMsgBuilder->check(
+ $this->property
+ );
+
+ return $this->propertySpecificationReqMsgBuilder->getMessage();
+ }
+
+ /**
+ * @see Page::isLockedView
+ *
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ protected function isLockedView() {
+ return $this->propertySpecificationReqMsgBuilder->reqLock();
+ }
+
+ /**
+ * @see Page::getRedirectTargetURL
+ *
+ * @since 3.0
+ *
+ * @return string|boolean
+ */
+ protected function getRedirectTargetURL() {
+
+ $label = $this->getTitle()->getText();
+
+ $property = new DIProperty(
+ PropertyRegistry::getInstance()->findPropertyIdByLabel( $label )
+ );
+
+ // Ensure to redirect to `Property:Modification date` and not using
+ // a possible user contextualized version such as `Property:Date de modification`
+ $canonicalLabel = $property->getCanonicalLabel();
+
+ if ( $canonicalLabel !== '' && $label !== $canonicalLabel ) {
+ return $property->getCanonicalDiWikiPage()->getTitle()->getFullURL();
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the HTML which is added to $wgOut after the article text.
+ *
+ * @return string
+ */
+ protected function getHtml() {
+
+ if ( !$this->store->getRedirectTarget( $this->property )->equals( $this->property ) ) {
+ return '';
+ }
+
+ $context = $this->getContext();
+ $languageCode = $context->getLanguage()->getCode();
+
+ $html = '';
+ $matches = [];
+
+ $context->getOutput()->addModuleStyles( 'ext.smw.page.styles' );
+ $context->getOutput()->addModules( 'smw.property.page' );
+
+ $context->getOutput()->setPageTitle(
+ $this->propertyValue->getFormattedLabel( DataValueFormatter::WIKI_LONG )
+ );
+
+ $this->listBuilder = new SimpleListBuilder(
+ $this->store
+ );
+
+ $this->listBuilder->setLanguageCode(
+ $languageCode
+ );
+
+ $this->listBuilder->isUserDefined(
+ $this->property->isUserDefined()
+ );
+
+ if ( $this->mParserOutput instanceof \ParserOutput ) {
+ preg_match_all(
+ "/" . "<section class=\"smw-property-specification\"(.*)?>([\s\S]*?)<\/section>" . "/m",
+ $this->mParserOutput->getText(),
+ $matches
+ );
+ }
+
+ $isFirst = true;
+
+ $htmlTabs = new HtmlTabs();
+ $htmlTabs->setGroup( 'property' );
+
+ $html = $this->makeValueList( $languageCode );
+ $isFirst = $html === '';
+
+ $htmlTabs->tab( 'smw-property-value', $this->msg( 'smw-property-tab-usage' ) . $this->getUsageCount(), [ 'hide' => $html === '' ] );
+ $htmlTabs->content( 'smw-property-value', $html );
+
+ // Redirects
+ list( $html, $itemCount ) = $this->makeList( 'redirect', '_REDI', true );
+ $isFirst = $isFirst && $html === '';
+
+ $htmlTabs->tab( 'smw-property-redi', $this->msg( 'smw-property-tab-redirects' ) . $itemCount, [ 'hide' => $html === '' ] );
+ $htmlTabs->content( 'smw-property-redi', $html );
+
+ // Subproperties
+ list( $html, $itemCount ) = $this->makeList( 'subproperty', '_SUBP', true );
+ $isFirst = $isFirst && $html === '';
+
+ $htmlTabs->tab( 'smw-property-subp', $this->msg( 'smw-property-tab-subproperties' ) . $itemCount, [ 'hide' => $html === '' ] );
+ $htmlTabs->content( 'smw-property-subp', $html );
+
+ // Improperty values
+ list( $html, $itemCount ) = $this->makeList( 'error', '_ERRP', false );
+ $isFirst = $isFirst && $html === '';
+
+ $htmlTabs->tab( 'smw-property-errp', $this->msg( 'smw-property-tab-errors' ) . $itemCount, [ 'hide' => $html === '', 'class' => 'smw-tab-warning' ] );
+ $htmlTabs->content( 'smw-property-errp', $html );
+
+ if ( isset( $matches[2] ) && $matches[2] !== [] ) {
+ $html = "<div>" . implode('</div><div>', $matches[2] ) . "</div>";
+ } else {
+ $html = '';
+ }
+
+ $htmlTabs->tab(
+ 'smw-property-spec',
+ $this->msg( 'smw-property-tab-specification' ),
+ [
+ 'hide' => $html === '',
+ 'class' => $isFirst ? 'smw-tab-spec' : 'smw-tab-spec smw-tab-right'
+ ]
+ );
+
+ $htmlTabs->content( 'smw-property-spec', $html );
+
+ $html = $htmlTabs->buildHTML(
+ [ 'class' => 'smw-property clearfix' ]
+ );
+
+ return $html;
+ }
+
+ private function fetchSemanticDataFromEditInfo() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ if ( $this->getPage()->getRevision() === null ) {
+ return null;
+ }
+
+ $editInfoProvider = $applicationFactory->newMwCollaboratorFactory()->newEditInfoProvider(
+ $this->getPage(),
+ $this->getPage()->getRevision()
+ );
+
+ return $editInfoProvider->fetchSemanticData();
+ }
+
+ private function makeList( $key, $propertyKey, $checkProperty = true ) {
+
+ // Ignore the list when a filter is present
+ if ( $this->getContext()->getRequest()->getVal( 'filter', '' ) !== '' ) {
+ return [ '', '' ];
+ }
+
+ $propertyListLimit = $this->getOption( 'smwgPropertyListLimit' );
+ $listLimit = $propertyListLimit[$key];
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->sort = true;
+ $requestOptions->ascending = true;
+
+ // +1 look ahead
+ $requestOptions->setLimit(
+ $listLimit + 1
+ );
+
+ $this->listBuilder->setListLimit(
+ $listLimit
+ );
+
+ $this->listBuilder->setListHeader(
+ 'smw-propertylist-' . $key
+ );
+
+ $this->listBuilder->checkProperty(
+ $checkProperty
+ );
+
+ $html = $this->listBuilder->createHtml(
+ new DIProperty( $propertyKey ),
+ $this->getDataItem(),
+ $requestOptions
+ );
+
+ $itemCount = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'item-count'
+ ],
+ $this->listBuilder->getItemCount()
+ );
+
+ return [ $html, $itemCount ];
+ }
+
+ private function makeValueList( $languageCode ) {
+
+ $request = $this->getContext()->getRequest();
+
+ $valueListBuilder = new ValueListBuilder(
+ $this->store
+ );
+
+ $valueListBuilder->setLanguageCode(
+ $languageCode
+ );
+
+ $valueListBuilder->setPagingLimit(
+ $this->getOption( 'pagingLimit' )
+ );
+
+ $valueListBuilder->setMaxPropertyValues(
+ $this->getOption( 'smwgMaxPropertyValues' )
+ );
+
+ return $valueListBuilder->createHtml(
+ $this->property,
+ $this->getDataItem(),
+ [
+ 'limit' => $request->getVal( 'limit', $this->getOption( 'pagingLimit' ) ),
+ 'offset' => $request->getVal( 'offset', '0' ),
+ 'from' => $request->getVal( 'from', '' ),
+ 'until' => $request->getVal( 'until', '' ),
+ 'filter' => $request->getVal( 'filter', '' )
+ ]
+ );
+ }
+
+ private function msg( $params, $type = Message::TEXT, $lang = Message::USER_LANGUAGE ) {
+ return Message::get( $params, $type, $lang );
+ }
+
+ private function getUsageCount() {
+
+ $requestOptions = new RequestOptions();
+ $requestOptions->setLimit( 1 );
+
+ // Label that corresponds to the display and sort characteristics
+ if ( $this->property->isUserDefined() ) {
+ $searchLabel = $this->propertyValue->getSearchLabel();
+ } else {
+ $searchLabel = $this->property->getKey();
+ $requestOptions->setOption( RequestOptions::SEARCH_FIELD, 'smw_title' );
+ }
+
+ $requestOptions->addStringCondition( $searchLabel, StringCondition::COND_EQ );
+
+ $cachedLookupList = $this->store->getPropertiesSpecial( $requestOptions );
+ $usageList = $cachedLookupList->fetchList();
+
+ if ( !$usageList || $usageList === [] ) {
+ return '';
+ }
+
+ $usage = end( $usageList );
+ $usageCount = $usage[1];
+ $date = $this->getContext()->getLanguage()->timeanddate( $cachedLookupList->getTimestamp() );
+
+ $countMsg = Message::get( [ 'smw-property-indicator-last-count-update', $date ] );
+ $indicatorClass = ( $usageCount < 25000 ? ( $usageCount > 5000 ? ' moderate' : '' ) : ' high' );
+
+ return Html::rawElement(
+ 'span',
+ [
+ 'title' => $countMsg,
+ 'class' => 'usage-count' . $indicatorClass
+ ],
+ $usageCount
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PageInfo.php b/www/wiki/extensions/SemanticMediaWiki/src/PageInfo.php
new file mode 100644
index 00000000..3cb75e34
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PageInfo.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace SMW;
+
+/**
+ * Facade interface to specify access to page information
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+interface PageInfo {
+
+ /**
+ * Returns a modification date
+ *
+ * @since 1.9
+ *
+ * @return integer
+ */
+ public function getModificationDate();
+
+ /**
+ * Returns a creation date
+ *
+ * @since 1.9
+ *
+ * @return integer
+ */
+ public function getCreationDate();
+
+ /**
+ * Whether the page object is new or not
+ *
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function isNewPage();
+
+ /**
+ * Returns a user object for the last editor
+ *
+ * @since 1.9
+ *
+ * @return Title
+ */
+ public function getLastEditor();
+
+ /**
+ * @since 1.9.1
+ *
+ * @return boolean
+ */
+ public function isFilePage();
+
+ /**
+ * @see File::getMediaType
+ *
+ * @since 1.9.1
+ *
+ * @return string|null
+ */
+ public function getMediaType();
+
+ /**
+ * @see File::getMimeType
+ *
+ * @since 1.9.1
+ *
+ * @return string|null
+ */
+ public function getMimeType();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParameterListDocBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/ParameterListDocBuilder.php
new file mode 100644
index 00000000..369d66c8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParameterListDocBuilder.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace SMW;
+
+use ParamProcessor\ParamDefinition;
+
+/**
+ * @since 2.4
+ *
+ * @license GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class ParameterListDocBuilder {
+
+ /**
+ * @var callable
+ */
+ private $msg;
+
+ /**
+ * @param callable $messageFunction
+ */
+ public function __construct( callable $messageFunction ) {
+ $this->msg = $messageFunction;
+ }
+
+ /**
+ * Returns the wikitext for a table listing the provided parameters.
+ *
+ * @param ParamDefinition[] $paramDefinitions
+ *
+ * @return string
+ */
+ public function getParameterTable( array $paramDefinitions ) {
+ $tableRows = [];
+ $hasAliases = $this->containsAliases( $paramDefinitions );
+
+ foreach ( $paramDefinitions as $parameter ) {
+ if ( $parameter->getName() !== 'format' ) {
+ $tableRows[] = $this->getDescriptionRow( $parameter, $hasAliases );
+ }
+ }
+
+ if ( empty( $tableRows ) ) {
+ return '';
+ }
+
+ $tableRows = array_merge( [
+ '!' . $this->msg( 'validator-describe-header-parameter' ) ."\n" .
+ ( $hasAliases ? '!' . $this->msg( 'validator-describe-header-aliases' ) ."\n" : '' ) .
+ '!' . $this->msg( 'validator-describe-header-type' ) ."\n" .
+ '!' . $this->msg( 'validator-describe-header-default' ) ."\n" .
+ '!' . $this->msg( 'validator-describe-header-description' )
+ ], $tableRows );
+
+ return '{| class="wikitable sortable"' . "\n" .
+ implode( "\n|-\n", $tableRows ) .
+ "\n|}";
+ }
+
+ /**
+ * @param ParamDefinition[] $paramDefinitions
+ *
+ * @return boolean
+ */
+ private function containsAliases( array $paramDefinitions ) {
+ foreach ( $paramDefinitions as $parameter ) {
+ if ( !empty( $parameter->getAliases() ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the wikitext for a table row describing a single parameter.
+ *
+ * @param ParamDefinition $parameter
+ * @param boolean $hasAliases
+ *
+ * @return string
+ */
+ private function getDescriptionRow( ParamDefinition $parameter, $hasAliases ) {
+ if ( $hasAliases ) {
+ $aliases = $parameter->getAliases();
+ $aliases = count( $aliases ) > 0 ? implode( ', ', $aliases ) : ' -';
+ }
+
+ $description = $this->msg( $parameter->getMessage() );
+
+ $type = $this->msg( $parameter->getTypeMessage() );
+
+ $default = $parameter->isRequired() ? "''" . $this->msg( 'validator-describe-required' ) . "''" : $parameter->getDefault();
+ if ( is_array( $default ) ) {
+ $default = implode( ', ', $default );
+ }
+ elseif ( is_bool( $default ) ) {
+ $default = $default ? 'yes' : 'no';
+ }
+
+ if ( $default === '' ) {
+ $default = "''" . $this->msg( 'validator-describe-empty' ) . "''";
+ }
+
+ return "|{$parameter->getName()}\n"
+ . ( $hasAliases ? '|' . $aliases . "\n" : '' ) .
+ <<<EOT
+|{$type}
+|{$default}
+|{$description}
+EOT;
+ }
+
+ private function msg() {
+ return call_user_func_array( $this->msg, func_get_args() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParameterProcessorFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/ParameterProcessorFactory.php
new file mode 100644
index 00000000..c9db2b54
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParameterProcessorFactory.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace SMW;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class ParameterProcessorFactory {
+
+ /**
+ * @since 1.9
+ *
+ * @param array $parameters
+ *
+ * @return ParserParameterProcessor
+ */
+ public static function newFromArray( array $parameters ) {
+ $instance = new self();
+ return $instance->newParserParameterProcessor( $parameters );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param array $parameters
+ *
+ * @return ParserParameterProcessor
+ */
+ public function newParserParameterProcessor( array $parameters ) {
+
+ if ( isset( $parameters[0] ) && is_object( $parameters[0] ) ) {
+ array_shift( $parameters );
+ }
+
+ return new ParserParameterProcessor( $parameters );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Parameters.php b/www/wiki/extensions/SemanticMediaWiki/src/Parameters.php
new file mode 100644
index 00000000..5088d29d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Parameters.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace SMW;
+
+use InvalidArgumentException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Parameters {
+
+ /**
+ * @var array
+ */
+ private $parameters = [];
+
+ /**
+ * @since 3.0
+ */
+ public function __construct( array $parameters = [] ) {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set( $key, $value ) {
+ $this->parameters[$key] = $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param array $value
+ */
+ public function merge( $key, array $value ) {
+
+ if ( !isset( $this->parameters[$key] ) ) {
+ $this->parameters[$key] = [];
+ }
+
+ $this->parameters[$key] += $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function has( $key ) {
+ return isset( $this->parameters[$key] ) || array_key_exists( $key, $this->parameters );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public function get( $key ) {
+
+ if ( $this->has( $key ) ) {
+ return $this->parameters[$key];
+ }
+
+ throw new InvalidArgumentException( "$key is an unregistered key." );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Parser/InTextAnnotationParser.php b/www/wiki/extensions/SemanticMediaWiki/src/Parser/InTextAnnotationParser.php
new file mode 100644
index 00000000..f38ff33e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Parser/InTextAnnotationParser.php
@@ -0,0 +1,462 @@
+<?php
+
+namespace SMW\Parser;
+
+use Hooks;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\Localizer;
+use SMW\MediaWiki\MagicWordsFinder;
+use SMW\MediaWiki\RedirectTargetFinder;
+use SMW\MediaWiki\StripMarkerDecoder;
+use SMW\ParserData;
+use SMW\Utils\Timer;
+use SMWOutputs;
+use Title;
+
+/**
+ * Class collects all functions for wiki text parsing / processing that are
+ * relevant for SMW
+ *
+ * This class is contains all functions necessary for parsing wiki text before
+ * it is displayed or previewed while identifying SMW related annotations.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Markus Krötzsch
+ * @author Denny Vrandecic
+ * @author mwjames
+ */
+class InTextAnnotationParser {
+
+ /**
+ * Internal state for switching SMW link annotations off/on during parsing
+ * ([[SMW::on]] and [[SMW:off]])
+ */
+ const OFF = '[[SMW::off]]';
+ const ON = '[[SMW::on]]';
+
+ /**
+ * @var ParserData
+ */
+ private $parserData;
+
+ /**
+ * @var LinksProcessor
+ */
+ private $linksProcessor;
+
+ /**
+ * @var MagicWordsFinder
+ */
+ private $magicWordsFinder;
+
+ /**
+ * @var RedirectTargetFinder
+ */
+ private $redirectTargetFinder;
+
+ /**
+ * @var DataValueFactory
+ */
+ private $dataValueFactory = null;
+
+ /**
+ * @var ApplicationFactory
+ */
+ private $applicationFactory = null;
+
+ /**
+ * @var StripMarkerDecoder
+ */
+ private $stripMarkerDecoder;
+
+ /**
+ * @var boolean
+ */
+ protected $isEnabledNamespace;
+
+ /**
+ * Internal state for switching SMW link annotations off/on during parsing
+ * ([[SMW::on]] and [[SMW:off]])
+ * @var boolean
+ */
+ protected $isAnnotation = true;
+
+ /**
+ * @var boolean|integer
+ */
+ private $isLinksInValues = false;
+
+ /**
+ * @var boolean
+ */
+ private $showErrors = true;
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserData $parserData
+ * @param LinksProcessor $linksProcessor
+ * @param MagicWordsFinder $magicWordsFinder
+ * @param RedirectTargetFinder $redirectTargetFinder
+ */
+ public function __construct( ParserData $parserData, LinksProcessor $linksProcessor, MagicWordsFinder $magicWordsFinder, RedirectTargetFinder $redirectTargetFinder ) {
+ $this->parserData = $parserData;
+ $this->linksProcessor = $linksProcessor;
+ $this->magicWordsFinder = $magicWordsFinder;
+ $this->redirectTargetFinder = $redirectTargetFinder;
+ $this->dataValueFactory = DataValueFactory::getInstance();
+ $this->applicationFactory = ApplicationFactory::getInstance();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $isLinksInValues
+ */
+ public function isLinksInValues( $isLinksInValues ) {
+ $this->isLinksInValues = $isLinksInValues;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $showErrors
+ */
+ public function showErrors( $showErrors ) {
+ $this->showErrors = (bool)$showErrors;
+ }
+
+ /**
+ * Parsing text before an article is displayed or previewed, strip out
+ * semantic properties and add them to the ParserOutput object
+ *
+ * @since 1.9
+ *
+ * @param string &$text
+ */
+ public function parse( &$text ) {
+
+ $title = $this->parserData->getTitle();
+ Timer::start( __CLASS__ );
+
+ // Identifies the current parser run (especially when called recursively)
+ $this->parserData->getSubject()->setContextReference( 'intp:' . uniqid() );
+
+ $this->doStripMagicWordsFromText( $text );
+
+ $this->isEnabledNamespace = $this->isSemanticEnabledForNamespace( $title );
+
+ $this->addRedirectTargetAnnotationFromText(
+ $text
+ );
+
+ // Obscure [/] to find a set of [[ :: ... ]] while those in-between are left for
+ // decoding in a post-processing so that the regex can split the text
+ // appropriately
+ if ( $this->isLinksInValues ) {
+ $text = LinksEncoder::findAndEncodeLinks( $text, $this );
+ }
+
+ // No longer used with 3.0 given that the LinksEncoder is safer and faster
+ $linksInValuesPcre = false;
+
+ $text = preg_replace_callback(
+ $this->getRegexpPattern( $linksInValuesPcre ),
+ $linksInValuesPcre ? 'self::process' : 'self::preprocess',
+ $text
+ );
+
+ // Ensure remaining encoded entities are decoded again
+ $text = LinksEncoder::removeLinkObfuscation( $text );
+
+ if ( $this->isEnabledNamespace ) {
+ $this->parserData->getOutput()->addModules( $this->getModules() );
+ $this->parserData->addExtraParserKey( 'userlang' );
+ }
+
+ $this->parserData->pushSemanticDataToParserOutput();
+
+ $this->parserData->addLimitReport(
+ 'intext-parsertime',
+ Timer::getElapsedTime( __CLASS__, 3 )
+ );
+
+ SMWOutputs::commitToParserOutput( $this->parserData->getOutput() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return boolean
+ */
+ public static function hasMarker( $text ) {
+ return strpos( $text, self::OFF ) !== false || strpos( $text, self::ON ) !== false;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function decodeSquareBracket( $text ) {
+ return LinksEncoder::decodeSquareBracket( $text );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function obfuscateAnnotation( $text ) {
+ return LinksEncoder::obfuscateAnnotation( $text );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function removeAnnotation( $text ) {
+ return LinksEncoder::removeAnnotation( $text );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param StripMarkerDecoder $stripMarkerDecoder
+ */
+ public function setStripMarkerDecoder( StripMarkerDecoder $stripMarkerDecoder ) {
+ $this->stripMarkerDecoder = $stripMarkerDecoder;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Title|null $redirectTarget
+ */
+ public function setRedirectTarget( Title $redirectTarget = null ) {
+ $this->redirectTargetFinder->setRedirectTarget( $redirectTarget );
+ }
+
+ protected function addRedirectTargetAnnotationFromText( $text ) {
+
+ if ( !$this->isEnabledNamespace ) {
+ return;
+ }
+
+ $this->redirectTargetFinder->findRedirectTargetFromText( $text );
+
+ $propertyAnnotatorFactory = $this->applicationFactory->singleton( 'PropertyAnnotatorFactory' );
+
+ $propertyAnnotator = $propertyAnnotatorFactory->newNullPropertyAnnotator(
+ $this->parserData->getSemanticData()
+ );
+
+ $redirectPropertyAnnotator = $propertyAnnotatorFactory->newRedirectPropertyAnnotator(
+ $propertyAnnotator,
+ $this->redirectTargetFinder
+ );
+
+ $redirectPropertyAnnotator->addAnnotation();
+ }
+
+ /**
+ * Returns required resource modules
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ protected function getModules() {
+ return [
+ 'ext.smw.style',
+ 'ext.smw.tooltips'
+ ];
+ }
+
+ /**
+ * @see LinksProcessor::getRegexpPattern
+ * @since 1.9
+ *
+ * @param boolean $linksInValues
+ *
+ * @return string
+ */
+ public function getRegexpPattern( $linksInValues = false ) {
+ return LinksProcessor::getRegexpPattern( $linksInValues );
+ }
+
+ /**
+ * @see linksProcessor::preprocess
+ * @since 1.9
+ *
+ * @param array $semanticLink expects (linktext, properties, value|caption)
+ *
+ * @return string
+ */
+ public function preprocess( array $semanticLink ) {
+
+ $semanticLinks = $this->linksProcessor->preprocess( $semanticLink );
+
+ if ( is_string( $semanticLinks ) ) {
+ return $semanticLinks;
+ }
+
+ return $this->process( $semanticLinks );
+ }
+
+ /**
+ * @see linksProcessor::process
+ * @since 1.9
+ *
+ * @param array $semanticLink expects (linktext, properties, value|caption)
+ *
+ * @return string
+ */
+ protected function process( array $semanticLink ) {
+
+ $valueCaption = false;
+ $property = '';
+ $value = '';
+
+ $semanticLinks = $this->linksProcessor->process(
+ $semanticLink
+ );
+
+ $this->isAnnotation = $this->linksProcessor->isAnnotation();
+
+ if ( is_string( $semanticLinks ) ) {
+ return $semanticLinks;
+ }
+
+ list( $properties, $value, $valueCaption ) = $semanticLinks;
+
+ $subject = $this->parserData->getSubject();
+
+ if ( ( $propertyLink = $this->getPropertyLink( $subject, $properties, $value, $valueCaption ) ) !== '' ) {
+ return $propertyLink;
+ }
+
+ return $this->addPropertyValue( $subject, $properties, $value, $valueCaption );
+ }
+
+ /**
+ * Adds property values to the ParserOutput instance
+ *
+ * @since 1.9
+ *
+ * @param array $properties
+ *
+ * @return string
+ */
+ protected function addPropertyValue( $subject, array $properties, $value, $valueCaption ) {
+
+ $origValue = $value;
+
+ if ( $this->stripMarkerDecoder !== null ) {
+ $value = $this->stripMarkerDecoder->decode( $value );
+ }
+
+ // Add properties to the semantic container
+ foreach ( $properties as $property ) {
+ $dataValue = $this->dataValueFactory->newDataValueByText(
+ $property,
+ $value,
+ $valueCaption,
+ $subject
+ );
+
+ if (
+ $this->isEnabledNamespace &&
+ $this->isAnnotation &&
+ $this->parserData->canUse() ) {
+ $this->parserData->addDataValue( $dataValue );
+ }
+ }
+
+ // Return the wikitext or the unmodified text representation in case of
+ // a strip marker in order for the standard Parser to work its magic since
+ // we were only interested in the value for the annotation
+ if ( $origValue !== $value ) {
+ $result = $origValue;
+ } else {
+ $result = $dataValue->getShortWikitext( true );
+ }
+
+ // If necessary add an error text
+ if ( ( $this->showErrors &&
+ $this->isEnabledNamespace && $this->isAnnotation ) &&
+ ( !$dataValue->isValid() ) ) {
+ // Encode `:` to avoid a comment block and instead of the nowiki tag
+ // use &#58; as placeholder
+ $result = str_replace( ':', '&#58;', $result ) . $dataValue->getErrorText();
+ }
+
+ return $result;
+ }
+
+ protected function doStripMagicWordsFromText( &$text ) {
+
+ $words = [];
+
+ $this->magicWordsFinder->setOutput( $this->parserData->getOutput() );
+
+ $magicWords = [
+ 'SMW_NOFACTBOX',
+ 'SMW_SHOWFACTBOX'
+ ];
+
+ Hooks::run( 'SMW::Parser::BeforeMagicWordsFinder', [ &$magicWords ] );
+
+ foreach ( $magicWords as $magicWord ) {
+ $words[] = $this->magicWordsFinder->findMagicWordInText( $magicWord, $text );
+ }
+
+ $this->magicWordsFinder->pushMagicWordsToParserOutput( $words );
+
+ return $words;
+ }
+
+ private function isSemanticEnabledForNamespace( Title $title ) {
+ return $this->applicationFactory->getNamespaceExaminer()->isSemanticEnabled( $title->getNamespace() );
+ }
+
+ private function getPropertyLink( $subject, $properties, $value, $valueCaption ) {
+
+ // #1855
+ if ( substr( $value, 0, 3 ) !== '@@@' ) {
+ return '';
+ }
+
+ $property = end( $properties );
+
+ $dataValue = $this->dataValueFactory->newPropertyValueByLabel(
+ $property,
+ $valueCaption,
+ $subject
+ );
+
+ if ( ( $lang = Localizer::getAnnotatedLanguageCodeFrom( $value ) ) !== false ) {
+ $dataValue->setOption( $dataValue::OPT_USER_LANGUAGE, $lang );
+ $dataValue->setCaption(
+ $valueCaption === false ? $dataValue->getWikiValue() : $valueCaption
+ );
+ }
+
+ $dataValue->setOption( $dataValue::OPT_HIGHLIGHT_LINKER, true );
+
+ return $dataValue->getShortWikitext( smwfGetLinker() );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Parser/LinksEncoder.php b/www/wiki/extensions/SemanticMediaWiki/src/Parser/LinksEncoder.php
new file mode 100644
index 00000000..eb31a0c1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Parser/LinksEncoder.php
@@ -0,0 +1,235 @@
+<?php
+
+namespace SMW\Parser;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class LinksEncoder {
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ * @param InTextAnnotationParser $parser
+ *
+ * @return text
+ */
+ public static function findAndEncodeLinks( $text, InTextAnnotationParser $parser ) {
+
+ // #2193
+ // Use &#x005B; instead of &#91; to distinguish it from the MW's Sanitizer
+ // who uses the same decode sequence and avoid issues when removing links
+ // after obfuscation
+
+ // #2671
+ // Use &#x005D; instead of &#93; (]) since CiteExtension use the later as
+ // encoding for the ref brackets
+
+ // Filter simple [ ... ] from [[ ... ]] links and ensure to find the correct
+ // start and end in case of [[Foo::[[Bar]]]] or [[Foo::[http://example.org/foo]]]
+ $text = str_replace(
+ [ '[', ']', '&#x005B;&#x005B;', '&#x005D;&#x005D;&#x005D;&#x005D;', '&#x005D;&#x005D;&#x005D;', '&#x005D;&#x005D;' ],
+ [ '&#x005B;', '&#x005D;', '[[', ']]]]', '&#x005D;]]', ']]' ],
+ $text
+ );
+
+ // Deep nesting is NOT supported as in [[Foo::[[abc]] [[Bar::123[[abc]] ]] ]]
+ return self::matchAndReplace( $text, $parser );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function removeLinkObfuscation( $text ) {
+
+ $from = [ '&#x005B;', '&#x005D;', '&#124;' ];
+ $to = [ '[', ']', '|' ];
+
+ return str_replace( $from, $to, $text );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function encodeLinks( $text ) {
+ return str_replace(
+ [ '[', ']', '|' ],
+ [ '&#x005B;', '&#x005D;', '&#124;' ],
+ $text
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function decodeSquareBracket( $text ) {
+ return str_replace( [ '%5B', '%5D' ], [ '[', ']' ], $text );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function obfuscateAnnotation( $text ) {
+ return preg_replace_callback(
+ LinksProcessor::getRegexpPattern( false ),
+ function( array $matches ) {
+ return str_replace( '[', '&#91;', $matches[0] );
+ },
+ self::decodeSquareBracket( $text )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function removeAnnotation( $text ) {
+
+ if ( strpos( $text, '::' ) === false && strpos( $text, ':=' ) === false ) {
+ return $text;
+ }
+
+ return preg_replace_callback(
+ LinksProcessor::getRegexpPattern( false ),
+ 'self::doRemoveAnnotation',
+ self::decodeSquareBracket( $text )
+ );
+ }
+
+ private static function doRemoveAnnotation( array $matches ) {
+
+ $caption = false;
+ $value = '';
+
+ // #1453
+ if ( $matches[0] === InTextAnnotationParser::OFF || $matches[0] === InTextAnnotationParser::ON ) {
+ return false;
+ }
+
+ // Strict mode matching
+ if ( array_key_exists( 1, $matches ) ) {
+ if ( strpos( $matches[1], ':' ) !== false && isset( $matches[2] ) ) {
+ list( $matches[1], $matches[2] ) = explode( '::', $matches[1] . '::' . $matches[2], 2 );
+ }
+ }
+
+ if ( array_key_exists( 2, $matches ) ) {
+
+ // #1747
+ if ( strpos( $matches[1], '|' ) !== false ) {
+ return $matches[0];
+ }
+
+ $parts = explode( '|', $matches[2] );
+ $value = array_key_exists( 0, $parts ) ? $parts[0] : '';
+ $caption = array_key_exists( 1, $parts ) ? $parts[1] : false;
+ }
+
+ // #1855
+ if ( $value === '@@@' ) {
+ $value = '';
+ }
+
+ return $caption !== false ? $caption : $value;
+ }
+
+ private static function matchAndReplace( $text, $parser ) {
+
+ /**
+ * @see http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/
+ *
+ * \[{2} # find the first opening '[['.
+ * (?: # start a new group, this is so '|' below does not apply/affect the opening '['.
+ * [^\[\]]+ # skip ahead happily if no '[' or ']'.
+ * | # ...otherwise...
+ * (?R) # we may be at the start of a new group, repeat whole pattern.
+ * )
+ * * # nesting can be many levels deep.
+ * \]{2} # finally, expect a balanced closing ']]'
+ */
+ preg_match_all("/\[{2}(?:[^\[\]]+|(?R))*\]{2}/is", $text, $matches );
+ $isOffAnnotation = false;
+
+ // At this point we distinguish between a normal [[Foo::bar]] annotation
+ // and a compound construct such as [[Foo::[[Foobar::Bar]] ]] and
+ // [[Foo::[http://example.org/foo foo] [[Foo::123|Bar]] ]].
+ //
+ // Only the compound is being processed and matched as we require to
+ // identify the boundaries of the enclosing annotation
+ foreach ( $matches[0] as $match ) {
+
+ // Ignore simple links like `[[:Property:Has URL|Has URL]]` but
+ // do parse `[[Has description::[[foo]][[:bar]]]]` (:= legacy notation)
+ if ( strpos( $match, '[[' ) !== false && strpos( $match, '::' ) === false && strpos( $match, ':=' ) === false ) {
+ continue;
+ }
+
+ // Remember whether the text contains OFF/ON marker (added by
+ // recursive parser, template, embedded result printer)
+ if ( $isOffAnnotation === false ) {
+ $isOffAnnotation = $match === InTextAnnotationParser::OFF;
+ }
+
+ $annotationOpenNum = substr_count( $match, '[[' );
+
+ // Only engage if the match contains more than one [[ :: ]] pair
+ if ( $annotationOpenNum > 1 ) {
+ $replace = self::replace( $match, $parser, $isOffAnnotation );
+ $text = str_replace( $match, $replace, $text );
+ }
+ }
+
+ return $text;
+ }
+
+ private static function replace( $match, $parser, $isOffAnnotation = false ) {
+
+ // Remove the Leading and last square bracket to avoid distortion
+ // during the annotation parsing
+ $match = substr( substr( $match, 2 ), 0, -2 );
+
+ // Restore OFF/ON for the recursive processing
+ if ( $isOffAnnotation === true ) {
+ $match = InTextAnnotationParser::OFF . $match . InTextAnnotationParser::ON;
+ }
+
+ // Only match annotations of style [[...::...]] during a recursive
+ // obfuscation process, any other processing is being done by the
+ // InTextAnnotation parser hereafter
+ //
+ // [[Foo::Bar]] annotation therefore run a pattern match and
+ // obfuscate the returning [[, |, ]] result
+ $replace = self::encodeLinks( preg_replace_callback(
+ LinksProcessor::getRegexpPattern( false ),
+ [ $parser, 'preprocess' ],
+ $match
+ ) );
+
+ // Restore the square brackets
+ return '[[' . $replace . ']]';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Parser/LinksProcessor.php b/www/wiki/extensions/SemanticMediaWiki/src/Parser/LinksProcessor.php
new file mode 100644
index 00000000..f6318e70
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Parser/LinksProcessor.php
@@ -0,0 +1,206 @@
+<?php
+
+namespace SMW\Parser;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class LinksProcessor {
+
+ /**
+ * Internal state for switching SMW link annotations off/on during parsing
+ * ([[SMW::on]] and [[SMW:off]])
+ *
+ * @var boolean
+ */
+ private $isAnnotation = true;
+
+ /**
+ * @var boolean
+ */
+ private $isStrictMode = true;
+
+ /**
+ * Whether a strict interpretation (e.g [[property::value:partOfTheValue::alsoPartOfTheValue]])
+ * or a more loose interpretation (e.g. [[property1::property2::value]]) for
+ * annotations is expected.
+ *
+ * @since 2.3
+ *
+ * @param boolean $isStrictMode
+ */
+ public function isStrictMode( $isStrictMode ) {
+ $this->isStrictMode = (bool)$isStrictMode;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function isAnnotation() {
+ return $this->isAnnotation;
+ }
+
+ /**
+ * $smwgLinksInValues (default = false) determines which regexp pattern
+ * is returned, either a more complex (lib PCRE may cause segfaults if text
+ * is long) or a simpler (no segfaults found for those, but no links
+ * in values) pattern.
+ *
+ * If enabled (SMW accepts inputs like [[property::Some [[link]] in value]]),
+ * this may lead to PHP crashes (!) when very long texts are
+ * used as values. This is due to limitations in the library PCRE that
+ * PHP uses for pattern matching.
+ *
+ * @since 1.9
+ *
+ * @param boolean $linksInValues
+ *
+ * @return string
+ */
+ public static function getRegexpPattern( $linksInValues = false ) {
+
+ if ( $linksInValues ) {
+ return '/\[\[ # Beginning of the link
+ (?:([^:][^]]*):[=:])+ # Property name (or a list of those)
+ ( # After that:
+ (?:[^|\[\]] # either normal text (without |, [ or ])
+ |\[\[[^]]*\]\] # or a [[link]]
+ |\[[^]]*\] # or an [external link]
+ )*) # all this zero or more times
+ (?:\|([^]]*))? # Display text (like "text" in [[link|text]]), optional
+ \]\] # End of link
+ /xu';
+ }
+
+ return '/\[\[ # Beginning of the link
+ (?:([^:][^]]*):[=:])+ # Property name (or a list of those)
+ ([^\[\]]*) # content: anything but [, |, ]
+ \]\] # End of link
+ /xu';
+ }
+
+ /**
+ * A method that precedes the process method, it takes care of separating
+ * value and caption (instead of leaving this to a more complex regexp).
+ *
+ * @since 1.9
+ *
+ * @param array $semanticLink expects (linktext, properties, value|caption)
+ *
+ * @return string
+ */
+ public function preprocess( array $semanticLink ) {
+
+ $value = '';
+ $caption = false;
+
+ if ( array_key_exists( 2, $semanticLink ) ) {
+
+ // #1747 avoid a mismatch on an annotation like [[Foo|Bar::Foobar]]
+ // where the left part of :: is split and would contain "Foo|Bar"
+ // hence this type is categorized as no value annotation
+ if ( strpos( $semanticLink[1], '|' ) !== false ) {
+ return $semanticLink[0];
+ }
+
+ $parts = explode( '|', $semanticLink[2] );
+
+ if ( array_key_exists( 0, $parts ) ) {
+ $value = $parts[0];
+ }
+ if ( array_key_exists( 1, $parts ) ) {
+ $caption = $parts[1];
+ }
+ }
+
+ if ( $caption !== false ) {
+ return [ $semanticLink[0], $semanticLink[1], $value, $caption ];
+ }
+
+ return [ $semanticLink[0], $semanticLink[1], $value ];
+ }
+
+ /**
+ * Function strips out the semantic attributes from a wiki link.
+ *
+ * @since 1.9
+ *
+ * @param array $semanticLink expects (linktext, properties, value|caption)
+ *
+ * @return string
+ */
+ public function process( array $semanticLink ) {
+
+ $valueCaption = false;
+ $property = '';
+ $value = '';
+
+ if ( array_key_exists( 1, $semanticLink ) ) {
+
+ // Use case [[Foo::=Bar]] (:= being the legacy notation < 1.4) where
+ // the regex splits it into `Foo:` and `Bar` loosing `=` from the value.
+ // Restore the link to its previous form of `Foo::=Bar` and reapply
+ // a simple split.
+ if( strpos( $semanticLink[0], '::=' ) && substr( $semanticLink[1], -1 ) == ':' ) {
+ list( $semanticLink[1], $semanticLink[2] ) = explode( '::', $semanticLink[1] . ':=' . $semanticLink[2], 2 );
+ }
+
+ // #1252 Strict mode being disabled for support of multi property
+ // assignments (e.g. [[property1::property2::value]])
+
+ // #1066 Strict mode is to check for colon(s) produced by something
+ // like [[Foo::Bar::Foobar]], [[Foo:::0049 30 12345678]]
+ // In case a colon appears (in what is expected to be a string without a colon)
+ // then concatenate the string again and split for the first :: occurrence
+ // only
+ if ( $this->isStrictMode && strpos( $semanticLink[1], ':' ) !== false && isset( $semanticLink[2] ) ) {
+ list( $semanticLink[1], $semanticLink[2] ) = explode( '::', $semanticLink[1] . '::' . $semanticLink[2], 2 );
+ }
+
+ $property = $semanticLink[1];
+ }
+
+ if ( array_key_exists( 2, $semanticLink ) ) {
+ $value = $semanticLink[2];
+ }
+
+ $value = LinksEncoder::removeLinkObfuscation( $value );
+
+ if ( $value === '' ) { // silently ignore empty values
+ return '';
+ }
+
+ if ( $property == 'SMW' ) {
+ return $this->setAnnotation( $value );
+ }
+
+ if ( array_key_exists( 3, $semanticLink ) ) {
+ $valueCaption = $semanticLink[3];
+ }
+
+ // Extract annotations and create tooltip.
+ $properties = preg_split( '/:[=:]/u', $property );
+
+ return [ $properties, $value, $valueCaption ];
+ }
+
+ private function setAnnotation( $value ) {
+
+ switch ( $value ) {
+ case 'on':
+ $this->isAnnotation = true;
+ break;
+ case 'off':
+ $this->isAnnotation = false;
+ break;
+ }
+
+ return '';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Parser/RecursiveTextProcessor.php b/www/wiki/extensions/SemanticMediaWiki/src/Parser/RecursiveTextProcessor.php
new file mode 100644
index 00000000..52973d40
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Parser/RecursiveTextProcessor.php
@@ -0,0 +1,360 @@
+<?php
+
+namespace SMW\Parser;
+
+use Parser;
+use ParserOptions;
+use ParserOutput;
+use RuntimeException;
+use SMW\Localizer;
+use SMW\ParserData;
+use Title;
+
+/**
+ * @private
+ *
+ * Helper class in processing content that requires to be parsed internally and
+ * recursively mostly in connection with templates.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class RecursiveTextProcessor {
+
+ /**
+ * @see Special:ExpandTemplates
+ * @var int Maximum size in bytes to include. 50MB allows fixing those huge pages
+ */
+ const MAX_INCLUDE_SIZE = 50000000;
+
+ /**
+ * @var Parser
+ */
+ private $parser;
+
+ /**
+ * Incremented while expanding templates inserted during printout; stop
+ * expansion at some point
+ *
+ * @var integer
+ */
+ private $recursionDepth = 0;
+
+ /**
+ * @var integer
+ */
+ private $maxRecursionDepth = 2;
+
+ /**
+ * @var boolean
+ */
+ private $recursiveAnnotation = false;
+
+ /**
+ * @var integer
+ */
+ private $uniqid;
+
+ /**
+ * @var string
+ */
+ private $error = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param Parser $parser|null
+ */
+ public function __construct( Parser $parser = null ) {
+ $this->parser = $parser;
+
+ if ( $this->parser === null ) {
+ $this->parser = $GLOBALS['wgParser'];
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Parser
+ */
+ public function getParser() {
+ return $this->parser;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getError() {
+ return $this->error;
+ }
+
+ /**
+ * Track recursive processing
+ *
+ * @since 3.0
+ *
+ * @param string|integer|null $uniqid
+ */
+ public function uniqid( $uniqid = null ) {
+
+ if ( $uniqid === null ) {
+ $uniqid = uniqid();
+ }
+
+ $this->uniqid = $uniqid;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $maxRecursionDepth
+ */
+ public function setMaxRecursionDepth( $maxRecursionDepth ) {
+ $this->maxRecursionDepth = $maxRecursionDepth;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $transcludeAnnotation
+ */
+ public function transcludeAnnotation( $transcludeAnnotation ) {
+
+ if ( $this->parser->getOutput() === null || $transcludeAnnotation === true ) {
+ return;
+ }
+
+ if ( $this->uniqid === null ) {
+ throw new RuntimeException( "Expected a uniqid and not null." );
+ }
+
+ $parserOutput = $this->parser->getOutput();
+ $track = $parserOutput->getExtensionData( ParserData::ANNOTATION_BLOCK );
+
+ if ( $track === null ) {
+ $track = [];
+ }
+
+ // Track each embedded #ask process to ensure to remove
+ // blocks on the correct recursive iteration (e.g Page A containing
+ // #ask is transcluded in Page B using a #ask -> is embedded ...
+ // etc.)
+ $track[$this->uniqid] = true;
+
+ $parserOutput->setExtensionData( ParserData::ANNOTATION_BLOCK, $track );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function releaseAnnotationBlock() {
+
+ if ( $this->parser->getOutput() === null ) {
+ return;
+ }
+
+ $parserOutput = $this->parser->getOutput();
+ $track = $parserOutput->getExtensionData( ParserData::ANNOTATION_BLOCK );
+
+ if ( $track !== [] ) {
+ unset( $track[$this->uniqid] );
+ }
+
+ // No recursive tracks left, set it to false
+ if ( $track === [] ) {
+ $track = false;
+ }
+
+ $parserOutput->setExtensionData( ParserData::ANNOTATION_BLOCK, $track );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function releaseAnyAnnotationBlock() {
+ if ( $this->parser->getOutput() !== null ) {
+ $this->parser->getOutput()->setExtensionData( ParserData::ANNOTATION_BLOCK, false );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $recursiveAnnotation
+ */
+ public function setRecursiveAnnotation( $recursiveAnnotation ) {
+ $this->recursiveAnnotation = (bool)$recursiveAnnotation;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ParserData $parserData
+ */
+ public function copyData( ParserData $parserData ) {
+ if ( $this->recursiveAnnotation ) {
+ $parserData->importFromParserOutput( $this->parser->getOutput() );
+ }
+ }
+
+ /**
+ * @see Special:ExpandTemplates
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public function expandTemplates( $text ) {
+
+ if ( $this->parser === null ) {
+ throw new RuntimeException( 'Missing a parser instance!' );
+ }
+
+ $options = $this->parser->getOptions();
+
+ if ( !$options instanceof ParserOptions ) {
+ $options = new ParserOptions();
+ $options->setRemoveComments( true );
+ $options->setTidy( true );
+ $options->setMaxIncludeSize( self::MAX_INCLUDE_SIZE );
+ }
+
+ $title = $this->parser->getTitle();
+
+ if ( !$title instanceof Title ) {
+ $title = $GLOBALS['wgTitle'];
+
+ if ( !$title instanceof Title ) {
+ $title = Title::newFromText( 'UNKNOWN_TITLE' );
+ }
+ }
+
+ $text = $this->parser->preprocess( $text, $title, $options );
+
+ $text = str_replace(
+ [ '_&lt;nowiki&gt;_', '_&lt;/nowiki&gt;_', '_&lt;nowiki */&gt;_', '<nowiki>', '</nowiki>' ],
+ '',
+ $text
+ );
+
+ return $text;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public function recursivePreprocess( $text ) {
+
+ // not during parsing, no preprocessing needed, still protect the result
+ if ( $this->parser === null || !$this->parser->getTitle() instanceof Title || !$this->parser->getOptions() instanceof ParserOptions ) {
+ return $this->recursiveAnnotation ? $text : '[[SMW::off]]' . $text . '[[SMW::on]]';
+ }
+
+ $this->recursionDepth++;
+
+ // restrict recursion
+ if ( $this->recursionDepth <= $this->maxRecursionDepth && $this->recursiveAnnotation ) {
+ $text = $this->parser->recursivePreprocess( $text );
+ } elseif ( $this->recursionDepth <= $this->maxRecursionDepth ) {
+ $text = '[[SMW::off]]' . $this->parser->replaceVariables( $text ) . '[[SMW::on]]';
+ } else {
+ $this->error = [ 'smw-parser-recursion-level-exceeded', $this->maxRecursionDepth ];
+ $text = '';
+ }
+
+ // During a block request remove any categories from the text since we
+ // cannot block the annotation during a parse, this ensures that
+ // categories don't appear in the source text and hereby in any successive
+ // parse
+ $this->pruneCategory( $text );
+
+ $this->recursionDepth--;
+
+ return $text;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public function recursiveTagParse( $text ) {
+
+ if ( $this->parser === null ) {
+ throw new RuntimeException( 'Missing a parser instance!' );
+ }
+
+ $this->recursionDepth++;
+ $isValid = $this->parser->getTitle() instanceof Title && $this->parser->getOptions() instanceof ParserOptions;
+
+ if ( $this->recursionDepth <= $this->maxRecursionDepth && $isValid ) {
+ $text = $this->parser->recursiveTagParse( $text );
+ } elseif ( $this->recursionDepth <= $this->maxRecursionDepth ) {
+ $title = $GLOBALS['wgTitle'];
+
+ if ( $title === null ) {
+ $title = Title::newFromText( 'UNKNOWN_TITLE' );
+ }
+
+ $popt = new ParserOptions();
+
+ // FIXME: Remove the if block once compatibility with MW <1.31 is dropped
+ if ( ! defined( '\ParserOutput::SUPPORTS_STATELESS_TRANSFORMS' ) || \ParserOutput::SUPPORTS_STATELESS_TRANSFORMS !== 1 ) {
+ $popt->setEditSection( false );
+ }
+ $parserOutput = $this->parser->parse( $text . '__NOTOC__', $title, $popt );
+
+ // Maybe better to use Parser::recursiveTagParseFully ??
+
+ /// NOTE: as of MW 1.14SVN, there is apparently no better way to hide the TOC
+ \SMWOutputs::requireFromParserOutput( $parserOutput );
+ $text = $parserOutput->getText( [ 'enableSectionEditLinks' => false ] );
+ } else {
+ $this->error = [ 'smw-parser-recursion-level-exceeded', $this->maxRecursionDepth ];
+ $text = '';
+ }
+
+ $this->recursionDepth--;
+
+ return $text;
+ }
+
+ private function pruneCategory( &$text ) {
+
+ if ( $this->parser->getOutput() === null ) {
+ return;
+ }
+
+ $parserOutput = $this->parser->getOutput();
+
+ if ( ( $track = $parserOutput->getExtensionData( ParserData::ANNOTATION_BLOCK ) ) === false ) {
+ return;
+ }
+
+ // Content language dep. category name
+ $category = Localizer::getInstance()->getNamespaceTextById(
+ NS_CATEGORY
+ );
+
+ if ( isset( $track[$this->uniqid] ) ) {
+ $text = preg_replace(
+ "/\[\[(Category|{$category}):(.*)\]\]/U",
+ '',
+ $text
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Parser/SemanticLinksParser.php b/www/wiki/extensions/SemanticMediaWiki/src/Parser/SemanticLinksParser.php
new file mode 100644
index 00000000..277f86ea
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Parser/SemanticLinksParser.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace SMW\Parser;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SemanticLinksParser {
+
+ /**
+ * @var LinksProcessor
+ */
+ private $linksProcessor;
+
+ /**
+ * @since 2.5
+ *
+ * @param LinksProcessor $linksProcessor
+ */
+ public function __construct( LinksProcessor $linksProcessor ) {
+ $this->linksProcessor = $linksProcessor;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param $text
+ *
+ * @return array
+ */
+ public function parse( $text ) {
+
+ $matches = [];
+
+ preg_match(
+ $this->linksProcessor->getRegexpPattern(),
+ $text,
+ $matches
+ );
+
+ if ( $matches === [] ) {
+ return [];
+ }
+
+ $semanticLinks = $this->linksProcessor->preprocess( $matches );
+
+ if ( is_string( $semanticLinks ) ) {
+ return [];
+ }
+
+ $semanticLinks = $this->linksProcessor->process( $semanticLinks );
+
+ if ( is_string( $semanticLinks ) ) {
+ return [];
+ }
+
+ return $semanticLinks;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserData.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserData.php
new file mode 100644
index 00000000..03d8d45f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserData.php
@@ -0,0 +1,512 @@
+<?php
+
+namespace SMW;
+
+use Onoi\Cache\Cache;
+use ParserOptions;
+use ParserOutput;
+use Psr\Log\LoggerAwareTrait;
+use SMWDataValue as DataValue;
+use Title;
+
+/**
+ * Handling semantic data exchange with a ParserOutput object
+ *
+ * Provides access to a semantic data container that is generated
+ * either from the ParserOutput or is a newly created container
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class ParserData {
+
+ use LoggerAwareTrait;
+
+ /**
+ * Identifies the extension data
+ */
+ const DATA_ID = 'smwdata';
+
+ /**
+ * Identifies the cache namespace for update markers
+ */
+ const CACHE_NAMESPACE = 'smw:update';
+
+ /**
+ * Option that allows to force an update even in cases where an update
+ * marker exists
+ */
+ const OPT_FORCED_UPDATE = 'smw:opt.forced.update';
+
+ /**
+ * Option whether creation of iteratibe update jobs are allowed
+ */
+ const OPT_CREATE_UPDATE_JOB = 'smw:opt.create.update.job';
+
+ /**
+ * Indicates that an update was caused by a change propagation request
+ */
+ const OPT_CHANGE_PROP_UPDATE = 'smw:opt.change.prop.update';
+
+ /**
+ * Indicates that no #ask dependency tracking should occur
+ */
+ const NO_QUERY_DEPENDENCY_TRACE = 'no.query.dependency.trace';
+
+ /**
+ * Indicates that no #ask dependency tracking should occur
+ */
+ const ANNOTATION_BLOCK = 'smw-blockannotation';
+
+ /**
+ * @var Title
+ */
+ private $title;
+
+ /**
+ * @var ParserOutput
+ */
+ private $parserOutput;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var ParserOptions
+ */
+ private $parserOptions;
+
+ /**
+ * @var SemanticData
+ */
+ private $semanticData;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @var $canCreateUpdateJob
+ */
+ private $canCreateUpdateJob = true;
+
+ /**
+ * Identifies the origin of a request.
+ *
+ * @var string
+ */
+ private $origin = '';
+
+ /**
+ * @var Options
+ */
+ private $options = null;
+
+ /**
+ * @since 1.9
+ *
+ * @param Title $title
+ * @param ParserOutput $parserOutput
+ * @param Cache|null $cache
+ */
+ public function __construct( Title $title, ParserOutput $parserOutput, Cache $cache = null ) {
+ $this->title = $title;
+ $this->parserOutput = $parserOutput;
+ $this->cache = $cache;
+
+ if ( $this->cache === null ) {
+ $this->cache = ApplicationFactory::getInstance()->getCache();
+ }
+
+ $this->initSemanticData();
+ }
+
+ /**
+ * @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();
+ }
+
+ return $this->options->safeGet( $key, $default );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function setOption( $key, $value ) {
+
+ if ( !$this->options instanceof Options ) {
+ $this->options = new Options();
+ }
+
+ return $this->options->set( $key, $value );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $origin
+ */
+ public function setOrigin( $origin ) {
+ $this->origin = $origin;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return DIWikiPage
+ */
+ public function getSubject() {
+ return $this->getSemanticData()->getSubject();
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return ParserOutput
+ */
+ public function getOutput() {
+ return $this->parserOutput;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ParserOptions $parserOptions
+ */
+ public function setParserOptions( ParserOptions $parserOptions ) {
+ $this->parserOptions = $parserOptions;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return ParserOptions|null
+ */
+ public function addExtraParserKey( $key ) {
+ // Looks odd in 1.30 "Saved in parser cache ... idhash:19989-0!canonical!userlang!dateformat!userlang!dateformat!userlang!dateformat!userlang!dateformat and ..."
+ // threfore use the ParserOutput::recordOption instead
+ if ( $key === 'userlang' || $key === 'dateformat' ) {
+ $this->parserOutput->recordOption( $key );
+ } elseif ( $this->parserOptions !== null ) {
+ $this->parserOptions->addExtraKey( $key );
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return boolean
+ */
+ public function isBlocked() {
+
+ // ParserOutput::getExtensionData returns null if no value was set for this key
+ if ( $this->parserOutput->getExtensionData( self::ANNOTATION_BLOCK ) !== null &&
+ $this->parserOutput->getExtensionData( self::ANNOTATION_BLOCK ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function canUse() {
+ return !$this->isBlocked();
+ }
+
+ /**
+ * Returns collected errors occurred during processing
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 1.9
+ */
+ public function addError( $error ) {
+ $this->errors = array_merge( $this->errors, (array)$error );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param SemanticData $semanticData
+ */
+ public function setSemanticData( SemanticData $semanticData ) {
+ $this->semanticData = $semanticData;
+ }
+
+ /**
+ * @deprecated since 2.0, use setSemanticData
+ */
+ public function setData( SemanticData $semanticData ) {
+ $this->setSemanticData( $semanticData );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return SemanticData
+ */
+ public function getSemanticData() {
+ return $this->semanticData;
+ }
+
+ /**
+ * @deprecated since 2.0, use getSemanticData
+ */
+ public function getData() {
+ return $this->getSemanticData();
+ }
+
+ /**
+ * @since 2.1
+ */
+ public function setEmptySemanticData() {
+ $this->setSemanticData( new SemanticData( DIWikiPage::newFromTitle( $this->title ) ) );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param ParserOutput|null
+ */
+ public function importFromParserOutput( ParserOutput $parserOutput = null ) {
+
+ if ( $parserOutput === null ) {
+ return;
+ }
+
+ $semanticData = $parserOutput->getExtensionData( self::DATA_ID );
+
+ // Only import data that is known to be different
+ if ( $semanticData !== null &&
+ $this->getSubject()->equals( $semanticData->getSubject() ) &&
+ $semanticData->getHash() !== $this->getSemanticData()->getHash() ) {
+
+ $this->getSemanticData()->importDataFrom( $semanticData );
+ }
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function copyToParserOutput() {
+
+ // Ensure that errors are reported and recorded
+ $processingErrorMsgHandler = new ProcessingErrorMsgHandler(
+ $this->getSubject()
+ );
+
+ foreach ( $this->errors as $error ) {
+ $processingErrorMsgHandler->addToSemanticData(
+ $this->semanticData,
+ $processingErrorMsgHandler->newErrorContainerFromMsg( $error )
+ );
+ }
+
+ $this->markParserOutput();
+ $this->parserOutput->setExtensionData( self::DATA_ID, $this->semanticData );
+ }
+
+ /**
+ * @deprecated since 3.0, use copyToParserOutput
+ */
+ public function pushSemanticDataToParserOutput() {
+ $this->copyToParserOutput();
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function markParserOutput() {
+
+ $this->parserOutput->setTimestamp( wfTimestampNow() );
+
+ $this->parserOutput->setProperty(
+ 'smw-semanticdata-status',
+ $this->semanticData->getProperties() !== []
+ );
+ }
+
+ /**
+ * @deprecated since 3.0, use pushSemanticDataToParserOutput
+ */
+ public function setSemanticDataStateToParserOutputProperty() {
+ $this->markParserOutput();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ParserOutput $parserOutput
+ *
+ * @return boolean
+ */
+ public static function hasSemanticData( ParserOutput $parserOutput ) {
+ return (bool)$parserOutput->getProperty( 'smw-semanticdata-status' );
+ }
+
+ /**
+ * @see SemanticData::addDataValue
+ *
+ * @since 1.9
+ *
+ * @param SMWDataValue $dataValue
+ */
+ public function addDataValue( DataValue $dataValue ) {
+ $this->semanticData->addDataValue( $dataValue );
+ }
+
+ /**
+ * Persistent marker to identify an update with a revision ID and allow
+ * to filter successive updates with that very same ID.
+ *
+ * @see LinksUpdateConstructed::process
+ *
+ * @since 3.0
+ *
+ * @param integer $rev
+ */
+ public function markUpdate( $rev ) {
+ $this->cache->save( smwfCacheKey( self::CACHE_NAMESPACE, $this->semanticData->getSubject()->getHash() ), $rev, 3600 );
+ }
+
+ /**
+ * @private This method is not for public use
+ *
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function updateStore( $opts = [] ) {
+
+ $isDeferrableUpdate = false;
+
+ // @legacy
+ if ( $opts === true ) {
+ $isDeferrableUpdate = true;
+ }
+
+ if ( isset( $opts['defer'] ) && $opts['defer'] ) {
+ $isDeferrableUpdate = true;
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $latestRevID = $this->title->getLatestRevID( Title::GAID_FOR_UPDATE );
+
+ if ( $this->skipUpdate( $latestRevID ) ) {
+
+ $this->logger->info(
+ [ 'Update', 'Skipping update', 'Found revision', '{revID}' ],
+ [ 'role' => 'user', 'revID' => $latestRevID ]
+ );
+
+ return false;
+ }
+
+ $this->semanticData->setOption(
+ Enum::OPT_SUSPEND_PURGE,
+ $this->getOption( Enum::OPT_SUSPEND_PURGE )
+ );
+
+ $dataUpdater = $applicationFactory->newDataUpdater(
+ $this->semanticData
+ );
+
+ $dataUpdater->canCreateUpdateJob(
+ $this->getOption( self::OPT_CREATE_UPDATE_JOB, true )
+ );
+
+ $dataUpdater->isChangeProp(
+ $this->getOption( self::OPT_CHANGE_PROP_UPDATE )
+ );
+
+ $dataUpdater->isDeferrableUpdate(
+ $isDeferrableUpdate
+ );
+
+ $dataUpdater->setOrigin(
+ $this->origin
+ );
+
+ $dataUpdater->doUpdate();
+
+ return true;
+ }
+
+ /**
+ * @note ParserOutput::setLimitReportData
+ *
+ * @since 2.4
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function addLimitReport( $key, $value ) {
+ $this->parserOutput->setLimitReportData( 'smw-limitreport-' . $key, $value );
+ }
+
+ /**
+ * Setup the semantic data container either from the ParserOutput or
+ * if not available create an empty container
+ */
+ private function initSemanticData() {
+
+ $this->semanticData = $this->parserOutput->getExtensionData( self::DATA_ID );
+
+ if ( !( $this->semanticData instanceof SemanticData ) ) {
+ $this->setEmptySemanticData();
+ }
+ }
+
+ private function skipUpdate( $rev ) {
+
+ if ( $this->getOption( self::OPT_FORCED_UPDATE, false ) ) {
+ return false;
+ }
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ $this->semanticData->getSubject()->getHash()
+ );
+
+ return $this->cache->fetch( $key ) === $rev;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctionFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctionFactory.php
new file mode 100644
index 00000000..a38f404c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctionFactory.php
@@ -0,0 +1,521 @@
+<?php
+
+namespace SMW;
+
+// Fatal error: Cannot use SMW\ParserFunctions\SubobjectParserFunction as SubobjectParserFunction because the name is already in use
+use Parser;
+use SMW\Parser\RecursiveTextProcessor;
+use SMW\ParserFunctions\AskParserFunction;
+use SMW\ParserFunctions\ConceptParserFunction;
+use SMW\ParserFunctions\DeclareParserFunction;
+use SMW\ParserFunctions\ExpensiveFuncExecutionWatcher;
+use SMW\ParserFunctions\RecurringEventsParserFunction as RecurringEventsParserFunc;
+use SMW\ParserFunctions\SetParserFunction;
+use SMW\ParserFunctions\ShowParserFunction;
+use SMW\ParserFunctions\SubobjectParserFunction as SubobjectParserFunc;
+use SMW\Utils\CircularReferenceGuard;
+
+/**
+ * @see http://www.semantic-mediawiki.org/wiki/Help:ParserFunction
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class ParserFunctionFactory {
+
+ /**
+ * @var Parser
+ */
+ private $parser;
+
+ /**
+ * @since 1.9
+ *
+ * @param Parser|null $parser
+ */
+ public function __construct( Parser $parser = null ) {
+ $this->parser = $parser;
+ }
+
+ /**
+ * Convenience instantiation of a ParserFunctionFactory object
+ *
+ * @since 1.9
+ *
+ * @param Parser $parser
+ *
+ * @return ParserFunctionFactory
+ */
+ public static function newFromParser( Parser $parser ) {
+ return new self( $parser );
+ }
+
+ /**
+ * @deprecated since 2.1, use newSubobjectParserFunction
+ */
+ public function getSubobjectParser() {
+ return $this->newSubobjectParserFunction( $this->parser );
+ }
+
+ /**
+ * @deprecated since 2.1, use newRecurringEventsParserFunction
+ */
+ public function getRecurringEventsParser() {
+ return $this->newRecurringEventsParserFunction( $this->parser );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Parser $parser
+ */
+ public function registerFunctionHandlers( Parser $parser ) {
+
+ list( $name, $definition, $flag ) = $this->getAskParserFunctionDefinition();
+ $parser->setFunctionHook( $name, $definition, $flag );
+
+ list( $name, $definition, $flag ) = $this->getShowParserFunctionDefinition();
+ $parser->setFunctionHook( $name, $definition, $flag );
+
+ list( $name, $definition, $flag ) = $this->getSubobjectParserFunctionDefinition();
+ $parser->setFunctionHook( $name, $definition, $flag );
+
+ list( $name, $definition, $flag ) = $this->getSetRecurringEventParserFunctionDefinition();
+ $parser->setFunctionHook( $name, $definition, $flag );
+
+ list( $name, $definition, $flag ) = $this->getSetParserFunctionDefinition();
+ $parser->setFunctionHook( $name, $definition, $flag );
+
+ list( $name, $definition, $flag ) = $this->getConceptParserFunctionDefinition();
+ $parser->setFunctionHook( $name, $definition, $flag );
+
+ list( $name, $definition, $flag ) = $this->getDeclareParserFunctionDefinition();
+ $parser->setFunctionHook( $name, $definition, $flag );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Parser $parser
+ *
+ * @return AskParserFunction
+ */
+ public function newAskParserFunction( Parser $parser ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $circularReferenceGuard = new CircularReferenceGuard( 'ask-parser' );
+ $circularReferenceGuard->setMaxRecursionDepth( 2 );
+
+ $parserData = $applicationFactory->newParserData(
+ $parser->getTitle(),
+ $parser->getOutput()
+ );
+
+ if ( isset( $parser->getOptions()->smwAskNoDependencyTracking ) ) {
+ $parserData->setOption( $parserData::NO_QUERY_DEPENDENCY_TRACE, $parser->getOptions()->smwAskNoDependencyTracking );
+ }
+
+ // Avoid possible actions during for example stashedit etc.
+ $parserData->setOption( 'request.action', $GLOBALS['wgRequest']->getVal( 'action' ) );
+
+ $parserData->setParserOptions(
+ $parser->getOptions()
+ );
+
+ $messageFormatter = new MessageFormatter(
+ $parser->getTargetLanguage()
+ );
+
+ $expensiveFuncExecutionWatcher = new ExpensiveFuncExecutionWatcher(
+ $parserData
+ );
+
+ $expensiveFuncExecutionWatcher->setExpensiveThreshold(
+ $applicationFactory->getSettings()->get( 'smwgQExpensiveThreshold' )
+ );
+
+ $expensiveFuncExecutionWatcher->setExpensiveExecutionLimit(
+ $applicationFactory->getSettings()->get( 'smwgQExpensiveExecutionLimit' )
+ );
+
+ $askParserFunction = new AskParserFunction(
+ $parserData,
+ $messageFormatter,
+ $circularReferenceGuard,
+ $expensiveFuncExecutionWatcher
+ );
+
+ $askParserFunction->setPostProcHandler(
+ $applicationFactory->create( 'PostProcHandler', $parser->getOutput() )
+ );
+
+ $askParserFunction->setRecursiveTextProcessor(
+ new RecursiveTextProcessor( $parser )
+ );
+
+ return $askParserFunction;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Parser $parser
+ *
+ * @return ShowParserFunction
+ */
+ public function newShowParserFunction( Parser $parser ) {
+
+ $showParserFunction = new ShowParserFunction(
+ $this->newAskParserFunction( $parser )
+ );
+
+ return $showParserFunction;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Parser $parser
+ *
+ * @return SetParserFunction
+ */
+ public function newSetParserFunction( Parser $parser ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $parserData = $applicationFactory->newParserData(
+ $parser->getTitle(),
+ $parser->getOutput()
+ );
+
+ $messageFormatter = new MessageFormatter(
+ $parser->getTargetLanguage()
+ );
+
+ $mediaWikiCollaboratorFactory = $applicationFactory->newMwCollaboratorFactory();
+
+ $stripMarkerDecoder = $mediaWikiCollaboratorFactory->newStripMarkerDecoder(
+ $parser->mStripState
+ );
+
+ $setParserFunction = new SetParserFunction(
+ $parserData,
+ $messageFormatter,
+ $mediaWikiCollaboratorFactory->newWikitextTemplateRenderer()
+ );
+
+ $setParserFunction->setStripMarkerDecoder(
+ $stripMarkerDecoder
+ );
+
+ return $setParserFunction;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Parser $parser
+ *
+ * @return ConceptParserFunction
+ */
+ public function newConceptParserFunction( Parser $parser ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $parserData = $applicationFactory->newParserData(
+ $parser->getTitle(),
+ $parser->getOutput()
+ );
+
+ $messageFormatter = new MessageFormatter(
+ $parser->getTargetLanguage()
+ );
+
+ $conceptParserFunction = new ConceptParserFunction(
+ $parserData,
+ $messageFormatter
+ );
+
+ $conceptParserFunction->setPostProcHandler(
+ $applicationFactory->create( 'PostProcHandler', $parser->getOutput() )
+ );
+
+ return $conceptParserFunction;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Parser $parser
+ *
+ * @return SubobjectParserFunction
+ */
+ public function newSubobjectParserFunction( Parser $parser ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $parserData = $applicationFactory->newParserData(
+ $parser->getTitle(),
+ $parser->getOutput()
+ );
+
+ $subobject = new Subobject( $parser->getTitle() );
+
+ $messageFormatter = new MessageFormatter(
+ $parser->getTargetLanguage()
+ );
+
+ $subobjectParserFunction = new SubobjectParserFunc(
+ $parserData,
+ $subobject,
+ $messageFormatter
+ );
+
+ $subobjectParserFunction->isCapitalLinks(
+ Site::isCapitalLinks()
+ );
+
+ $subobjectParserFunction->isComparableContent(
+ $applicationFactory->getSettings()->get( 'smwgUseComparableContentHash' )
+ );
+
+ $stripMarkerDecoder = $applicationFactory->newMwCollaboratorFactory()->newStripMarkerDecoder(
+ $parser->mStripState
+ );
+
+ $subobjectParserFunction->setStripMarkerDecoder(
+ $stripMarkerDecoder
+ );
+
+ return $subobjectParserFunction;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Parser $parser
+ *
+ * @return RecurringEventsParserFunction
+ */
+ public function newRecurringEventsParserFunction( Parser $parser ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ $parserData = $applicationFactory->newParserData(
+ $parser->getTitle(),
+ $parser->getOutput()
+ );
+
+ $subobject = new Subobject( $parser->getTitle() );
+
+ $messageFormatter = new MessageFormatter(
+ $parser->getTargetLanguage()
+ );
+
+ $recurringEvents = new RecurringEvents();
+
+ $recurringEvents->setDefaultNumRecurringEvents(
+ $settings->get( 'smwgDefaultNumRecurringEvents' )
+ );
+
+ $recurringEvents->setMaxNumRecurringEvents(
+ $settings->get( 'smwgMaxNumRecurringEvents' )
+ );
+
+ $recurringEventsParserFunction = new RecurringEventsParserFunc(
+ $parserData,
+ $subobject,
+ $messageFormatter,
+ $recurringEvents
+ );
+
+ $recurringEventsParserFunction->isCapitalLinks(
+ Site::isCapitalLinks()
+ );
+
+ $recurringEventsParserFunction->isComparableContent(
+ $settings->get( 'smwgUseComparableContentHash' )
+ );
+
+ return $recurringEventsParserFunction;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param Parser $parser
+ *
+ * @return DeclareParserFunction
+ */
+ public function newDeclareParserFunction( Parser $parser ) {
+
+ $parserData = ApplicationFactory::getInstance()->newParserData(
+ $parser->getTitle(),
+ $parser->getOutput()
+ );
+
+ $declareParserFunction = new DeclareParserFunction(
+ $parserData
+ );
+
+ return $declareParserFunction;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getAskParserFunctionDefinition() {
+
+ $askParserFunctionDefinition = function( $parser ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ $askParserFunction = $this->newAskParserFunction(
+ $parser
+ );
+
+ if ( !$settings->get( 'smwgQEnabled' ) ) {
+ return $settings->isFlagSet( 'smwgParserFeatures', SMW_PARSER_INL_ERROR ) ? $askParserFunction->isQueryDisabled(): '';
+ }
+
+ return $askParserFunction->parse( func_get_args() );
+ };
+
+ return [ 'ask', $askParserFunctionDefinition, 0 ];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getShowParserFunctionDefinition() {
+
+ $showParserFunctionDefinition = function( $parser ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ $showParserFunction = $this->newShowParserFunction(
+ $parser
+ );
+
+ if ( !$settings->get( 'smwgQEnabled' ) ) {
+ return $settings->isFlagSet( 'smwgParserFeatures', SMW_PARSER_INL_ERROR ) ? $showParserFunction->isQueryDisabled(): '';
+ }
+
+ return $showParserFunction->parse( func_get_args() );
+ };
+
+ return [ 'show', $showParserFunctionDefinition, 0 ];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getSubobjectParserFunctionDefinition() {
+
+ $subobjectParserFunctionDefinition = function( $parser ) {
+
+ $subobjectParserFunction = $this->newSubobjectParserFunction(
+ $parser
+ );
+
+ return $subobjectParserFunction->parse(
+ ParameterProcessorFactory::newFromArray( func_get_args() )
+ );
+ };
+
+ return [ 'subobject', $subobjectParserFunctionDefinition, 0 ];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getSetRecurringEventParserFunctionDefinition() {
+
+ $recurringEventsParserFunctionDefinition = function( $parser ) {
+
+ $recurringEventsParserFunction = $this->newRecurringEventsParserFunction(
+ $parser
+ );
+
+ return $recurringEventsParserFunction->parse(
+ ParameterProcessorFactory::newFromArray( func_get_args() )
+ );
+ };
+
+ return [ 'set_recurring_event', $recurringEventsParserFunctionDefinition, 0 ];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getSetParserFunctionDefinition() {
+
+ $setParserFunctionDefinition = function( $parser ) {
+
+ $setParserFunction = $this->newSetParserFunction(
+ $parser
+ );
+
+ return $setParserFunction->parse(
+ ParameterProcessorFactory::newFromArray( func_get_args() )
+ );
+ };
+
+ return [ 'set', $setParserFunctionDefinition, 0 ];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getConceptParserFunctionDefinition() {
+
+ $conceptParserFunctionDefinition = function( $parser ) {
+
+ $conceptParserFunction = $this->newConceptParserFunction(
+ $parser
+ );
+
+ return $conceptParserFunction->parse( func_get_args() );
+ };
+
+ return [ 'concept', $conceptParserFunctionDefinition, 0 ];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getDeclareParserFunctionDefinition() {
+
+ $declareParserFunctionDefinition = function( $parser, $frame, $args ) {
+
+ $declareParserFunction = $this->newDeclareParserFunction(
+ $parser
+ );
+
+ return $declareParserFunction->parse( $frame, $args );
+ };
+
+ return [ 'declare', $declareParserFunctionDefinition, Parser::SFH_OBJECT_ARGS ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/AskParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/AskParserFunction.php
new file mode 100644
index 00000000..6f0b79d3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/AskParserFunction.php
@@ -0,0 +1,450 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use Parser;
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\MessageFormatter;
+use SMW\Parser\RecursiveTextProcessor;
+use SMW\ParserData;
+use SMW\PostProcHandler;
+use SMW\ProcessingErrorMsgHandler;
+use SMW\Query\Deferred;
+use SMW\Utils\CircularReferenceGuard;
+use SMWQuery as Query;
+use SMWQueryProcessor as QueryProcessor;
+
+/**
+ * Provides the {{#ask}} parser function
+ *
+ * @see http://www.semantic-mediawiki.org/wiki/Help:Ask
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class AskParserFunction {
+
+ /**
+ * Fixed identifier for a deferred query request
+ */
+ const DEFERRED_REQUEST = '@deferred';
+
+ /**
+ * Fixed identifier
+ */
+ const NO_TRACE = '@notrace';
+
+ /**
+ * Fixed identifier to signal to the PostProcHandler that a post update is
+ * required with the output being used as input value for an annotation.
+ */
+ const IS_ANNOTATION = '@annotation';
+
+ /**
+ * @var ParserData
+ */
+ private $parserData;
+
+ /**
+ * @var MessageFormatter
+ */
+ private $messageFormatter;
+
+ /**
+ * @var CircularReferenceGuard
+ */
+ private $circularReferenceGuard;
+
+ /**
+ * @var ExpensiveFuncExecutionWatcher
+ */
+ private $expensiveFuncExecutionWatcher;
+
+ /**
+ * @var boolean
+ */
+ private $showMode = false;
+
+ /**
+ * @var integer
+ */
+ private $context = QueryProcessor::INLINE_QUERY;
+
+ /**
+ * @var PostProcHandler
+ */
+ private $postProcHandler;
+
+ /**
+ * @var RecursiveTextProcessor
+ */
+ private $recursiveTextProcessor;
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserData $parserData
+ * @param MessageFormatter $messageFormatter
+ * @param CircularReferenceGuard $circularReferenceGuard
+ * @param ExpensiveFuncExecutionWatcher $expensiveFuncExecutionWatcher
+ */
+ public function __construct( ParserData $parserData, MessageFormatter $messageFormatter, CircularReferenceGuard $circularReferenceGuard, ExpensiveFuncExecutionWatcher $expensiveFuncExecutionWatcher ) {
+ $this->parserData = $parserData;
+ $this->messageFormatter = $messageFormatter;
+ $this->circularReferenceGuard = $circularReferenceGuard;
+ $this->expensiveFuncExecutionWatcher = $expensiveFuncExecutionWatcher;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param PostProcHandler $postProcHandler
+ */
+ public function setPostProcHandler( PostProcHandler $postProcHandler ) {
+ $this->postProcHandler = $postProcHandler;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param RecursiveTextProcessor $recursiveTextProcessor
+ */
+ public function setRecursiveTextProcessor( RecursiveTextProcessor $recursiveTextProcessor ) {
+ $this->recursiveTextProcessor = $recursiveTextProcessor;
+ }
+
+ /**
+ * Enable showMode (normally only invoked by {{#show}})
+ *
+ * @since 1.9
+ *
+ * @return AskParserFunction
+ */
+ public function setShowMode( $mode ) {
+ $this->showMode = $mode;
+ return $this;
+ }
+
+ /**
+ * {{#ask}} is disabled (see $smwgQEnabled)
+ *
+ * @since 1.9
+ *
+ * @return string|null
+ */
+ public function isQueryDisabled() {
+ return $this->messageFormatter->addFromKey( 'smw_iq_disabled' )->getHtml();
+ }
+
+ /**
+ * Parse parameters, return results from the query printer and update the
+ * ParserOutput with meta data from the query
+ *
+ * FIXME $rawParams use IParameterFormatter -> QueryParameterFormatter class
+ * Parse parameters and return query results to the ParserOutput
+ * object and output result data from the QueryProcessor
+ *
+ * @todo $rawParams should be of IParameterFormatter
+ * QueryParameterFormatter class
+ *
+ * @since 1.9
+ *
+ * @param array $functionParams
+ *
+ * @return string|null
+ */
+ public function parse( array $functionParams ) {
+
+ // Do we still need this?
+ // Reference found in SRF_Exhibit.php, SRF_Ploticus.php, SRF_Timeline.php, SRF_JitGraph.php
+ $GLOBALS['smwgIQRunningNumber']++;
+ $result = '';
+
+ list( $functionParams, $extraKeys ) = $this->prepareFunctionParameters(
+ $functionParams
+ );
+
+ if ( !isset( $extraKeys[self::NO_TRACE] ) ) {
+ $extraKeys[self::NO_TRACE] = $this->parserData->getOption( ParserData::NO_QUERY_DEPENDENCY_TRACE );
+ }
+
+ // No trace on queries invoked by special pages
+ if ( $this->parserData->getTitle()->getNamespace() === NS_SPECIAL ) {
+ $extraKeys[self::NO_TRACE] = true;
+ }
+
+ $result = $this->doFetchResultsFromFunctionParameters(
+ $functionParams,
+ $extraKeys
+ );
+
+ if ( $this->context === QueryProcessor::DEFERRED_QUERY ) {
+ Deferred::registerResources( $this->parserData->getOutput() );
+ }
+
+ $this->parserData->copyToParserOutput();
+
+ // 'userlang' will trigger a cache fragmentation by user language
+ $this->parserData->addExtraParserKey( 'userlang' );
+
+ // 'dateformat' will trigger a cache fragmentation by date preference
+ $this->parserData->addExtraParserKey( 'dateformat' );
+
+ return $result;
+ }
+
+ private function prepareFunctionParameters( array $functionParams ) {
+
+ // Remove parser object from parameters array
+ if( isset( $functionParams[0] ) && $functionParams[0] instanceof Parser ) {
+ array_shift( $functionParams );
+ }
+
+ $extraKeys = [];
+ $previous = false;
+
+ // Filter invalid parameters
+ foreach ( $functionParams as $key => $value ) {
+
+ if ( $value === self::DEFERRED_REQUEST ) {
+ $this->context = QueryProcessor::DEFERRED_QUERY;
+ unset( $functionParams[$key] );
+ continue;
+ }
+
+ if ( $value === self::NO_TRACE ) {
+ $extraKeys[self::NO_TRACE] = true;
+ unset( $functionParams[$key] );
+ continue;
+ }
+
+ if ( $value === self::IS_ANNOTATION ) {
+ $extraKeys[self::IS_ANNOTATION] = true;
+ unset( $functionParams[$key] );
+ continue;
+ }
+
+ // @see ParserOptionsRegister hook, use registered `localTime` key
+ if ( strpos( $value, '#LOCL#TO' ) !== false ) {
+ $this->parserData->addExtraParserKey( 'localTime' );
+ }
+
+ // Skip the first (being the condition) and other marked
+ // printrequests
+ if ( $key == 0 || ( $value !== '' && $value{0} === '?' ) ) {
+ continue;
+ }
+
+ // The MW parser swallows any `|` char (as it is used as field
+ // separator) hence make an educated guess about the condition when
+ // it contains `[[`...`]]` and the previous value was empty (expected
+ // due to ||) then the string should be concatenated and interpret
+ // as [[Foo]] || [[Bar]]
+ if (
+ ( $key > 0 && $previous === '' ) &&
+ ( strpos( $value, '[[' ) !== false && strpos( $value, ']]' ) !== false ) ) {
+ $functionParams[0] .= " || $value";
+ unset( $functionParams[$key] );
+ }
+
+ // Filter parameters that can not be split into
+ // argument=value
+ if ( strpos( $value, '=' ) === false ) {
+ unset( $functionParams[$key] );
+ }
+
+ $previous = $value;
+ }
+
+ return [ $functionParams, $extraKeys ];
+ }
+
+ private function doFetchResultsFromFunctionParameters( array $functionParams, array $extraKeys ) {
+
+ $contextPage = $this->parserData->getSubject();
+ $action = $this->parserData->getOption( 'request.action' );
+ $status = [];
+
+ if ( $extraKeys[self::NO_TRACE] === true ) {
+ $contextPage = null;
+ }
+
+ list( $query, $this->params ) = QueryProcessor::getQueryAndParamsFromFunctionParams(
+ $functionParams,
+ SMW_OUTPUT_WIKI,
+ $this->context,
+ $this->showMode,
+ $contextPage
+ );
+
+ if ( ( $result = $this->hasReachedExpensiveExecutionLimit( $query ) ) !== false ) {
+ return $result;
+ }
+
+ $query->setOption( Query::PROC_CONTEXT, 'AskParserFunction' );
+ $query->setOption( Query::NO_DEPENDENCY_TRACE, $extraKeys[self::NO_TRACE] );
+ $query->setOption( 'request.action', $action );
+
+ $queryHash = $query->getHash();
+
+ if ( $this->postProcHandler !== null && isset( $extraKeys[self::IS_ANNOTATION] ) ) {
+ $status[] = 100;
+ $this->postProcHandler->addUpdate( $query );
+ }
+
+ if ( $this->context === QueryProcessor::DEFERRED_QUERY ) {
+ $status[] = 200;
+ }
+
+ $this->circularReferenceGuard->mark( $queryHash );
+
+ // If we caught in a circular loop (due to a template referencing to itself)
+ // then we stop here before the next query execution to avoid an infinite
+ // self-reference
+ if ( $this->circularReferenceGuard->isCircular( $queryHash ) ) {
+ return '';
+ }
+
+ // #3230
+ // If the query contains a self reference (embedding page is part of the
+ // query condition) for a `edit` action then set an extra key so that the
+ // parser uses a different parser cache hereby allows for an additional
+ // parse on the next GET request to retrieve newly stored values that may
+ // have been appended during the `edit`.
+ if ( ( $action === 'submit' || $action === 'stashedit' ) && $query->getOption( 'self.reference' ) ) {
+ $this->parserData->addExtraParserKey( 'smwq' );
+ }
+
+ QueryProcessor::setRecursiveTextProcessor(
+ $this->recursiveTextProcessor
+ );
+
+ $params = [];
+
+ foreach ( $this->params as $key => $value) {
+ $params[$key] = $value->getValue();
+ }
+
+ $query->setOption( 'query.params', $params );
+
+ // Only request a result_hash in case the `check-query` is enabled
+ if ( $this->postProcHandler !== null ) {
+ $query->setOption( 'calc.result_hash', $this->postProcHandler->getOption( 'check-query' ) );
+ }
+
+ $result = QueryProcessor::getResultFromQuery(
+ $query,
+ $this->params,
+ SMW_OUTPUT_WIKI,
+ $this->context
+ );
+
+ if ( $this->postProcHandler !== null && $this->context !== QueryProcessor::DEFERRED_QUERY ) {
+ $this->postProcHandler->addCheck( $query );
+ }
+
+ $format = $this->params['format']->getValue();
+
+ if ( $this->recursiveTextProcessor !== null ) {
+ $this->recursiveTextProcessor->copyData( $this->parserData );
+ }
+
+ $this->circularReferenceGuard->unmark( $queryHash );
+ $this->expensiveFuncExecutionWatcher->incrementExpensiveCount( $query );
+
+ // In case of an query error add a marker to the subject for discoverability
+ // of a failed query, don't bail-out as we can have results and errors
+ // at the same time
+ $this->addProcessingError( $query->getErrors() );
+
+ $query->setOption( Query::PROC_STATUS_CODE, $status );
+
+ $this->addQueryProfile(
+ $query,
+ $format,
+ $extraKeys
+ );
+
+ return $result;
+ }
+
+ private function hasReachedExpensiveExecutionLimit( $query ) {
+
+ if ( $this->expensiveFuncExecutionWatcher->hasReachedExpensiveLimit( $query ) === false ) {
+ return false;
+ }
+
+ // Adding to error in order to be discoverable
+ $this->addProcessingError( [ 'smw-parser-function-expensive-execution-limit' ] );
+
+ return $this->messageFormatter->addFromKey( 'smw-parser-function-expensive-execution-limit' )->getHtml();
+ }
+
+ private function addQueryProfile( $query, $format, $extraKeys ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ // If the smwgQueryProfiler is marked with FALSE then just don't create a profile.
+ if ( $settings->get( 'smwgQueryProfiler' ) === false || $extraKeys[self::NO_TRACE] === true ) {
+ return;
+ }
+
+ if ( !$settings->isFlagSet( 'smwgQueryProfiler', SMW_QPRFL_DUR ) ) {
+ $query->setOption( Query::PROC_QUERY_TIME, 0 );
+ }
+
+ if ( $settings->isFlagSet( 'smwgQueryProfiler', SMW_QPRFL_PARAMS ) ) {
+ $query->setOption( Query::OPT_PARAMETERS, true );
+ }
+
+ $query->setContextPage(
+ $this->parserData->getSubject()
+ );
+
+ $profileAnnotatorFactory = $applicationFactory->getQueryFactory()->newProfileAnnotatorFactory();
+
+ $profileAnnotator = $profileAnnotatorFactory->newProfileAnnotator(
+ $query,
+ $format
+ );
+
+ $profileAnnotator->pushAnnotationsTo(
+ $this->parserData->getSemanticData()
+ );
+ }
+
+ private function addProcessingError( $errors ) {
+
+ if ( $errors === [] ) {
+ return;
+ }
+
+ $processingErrorMsgHandler = new ProcessingErrorMsgHandler(
+ $this->parserData->getSubject()
+ );
+
+ foreach ( $errors as $error ) {
+
+ if ( ( $property = $processingErrorMsgHandler->grepPropertyFromRestrictionErrorMsg( $error ) ) === null ) {
+ $property = new DIProperty( '_ASK' );
+ }
+
+ $container = $processingErrorMsgHandler->newErrorContainerFromMsg(
+ $error,
+ $property
+ );
+
+ $processingErrorMsgHandler->addToSemanticData(
+ $this->parserData->getSemanticData(),
+ $container
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ConceptParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ConceptParserFunction.php
new file mode 100644
index 00000000..39acd316
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ConceptParserFunction.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use Html;
+use Parser;
+use SMW\ApplicationFactory;
+use SMW\DIConcept;
+use SMW\DIProperty;
+use SMW\MessageFormatter;
+use SMW\ParserData;
+use SMW\PostProcHandler;
+use SMWInfolink;
+use SMWQueryProcessor as QueryProcessor;
+use Title;
+
+/**
+ * Class that provides the {{#concept}} parser function
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class ConceptParserFunction {
+
+ /**
+ * @var ParserData
+ */
+ private $parserData;
+
+ /**
+ * @var MessageFormatter
+ */
+ private $messageFormatter;
+
+ /**
+ * @var PostProcHandler
+ */
+ private $postProcHandler;
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserData $parserData
+ * @param MessageFormatter $messageFormatter
+ */
+ public function __construct( ParserData $parserData, MessageFormatter $messageFormatter ) {
+ $this->parserData = $parserData;
+ $this->messageFormatter = $messageFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param PostProcHandler $postProcHandler
+ */
+ public function setPostProcHandler( PostProcHandler $postProcHandler ) {
+ $this->postProcHandler = $postProcHandler;
+ }
+
+ /**
+ * Parse parameters, return concept information box and update the
+ * ParserOutput with the concept object
+ *
+ * @since 1.9
+ *
+ * @param array $params
+ *
+ * @return string|null
+ */
+ public function parse( array $rawParams ) {
+ $this->parserData->getOutput()->addModules( 'ext.smw.style' );
+
+ $title = $this->parserData->getTitle();
+ $property = new DIProperty( '_CONC' );
+
+ if ( !( $title->getNamespace() === SMW_NS_CONCEPT ) ) {
+ return $this->messageFormatter->addFromKey( 'smw_no_concept_namespace' )->getHtml();
+ } elseif ( count( $this->parserData->getSemanticData()->getPropertyValues( $property ) ) > 0 ) {
+ return $this->messageFormatter->addFromKey( 'smw_multiple_concepts' )->getHtml();
+ }
+
+ // Remove parser object from parameters array
+ if( isset( $rawParams[0] ) && $rawParams[0] instanceof Parser ) {
+ array_shift( $rawParams );
+ }
+
+ // Use first parameter as concept (query) string
+ $conceptQuery = array_shift( $rawParams );
+
+ // Use second parameter, if any as a description
+ $conceptDocu = array_shift( $rawParams );
+
+ $query = $this->buildQuery( $conceptQuery );
+
+ $conceptQueryString = $query->getDescription()->getQueryString();
+
+ $this->parserData->getSemanticData()->addPropertyObjectValue(
+ $property,
+ new DIConcept(
+ $conceptQueryString,
+ $conceptDocu,
+ $query->getDescription()->getQueryFeatures(),
+ $query->getDescription()->getSize(),
+ $query->getDescription()->getDepth()
+ )
+ );
+
+ $this->messageFormatter
+ ->addFromArray( $query->getErrors() )
+ ->addFromArray( $this->parserData->getErrors() );
+
+ if ( $this->postProcHandler !== null ) {
+ $this->postProcHandler->addCheck( $query );
+ }
+
+ $this->addQueryProfile( $query );
+
+ $this->parserData->pushSemanticDataToParserOutput();
+
+ if ( $this->messageFormatter->exists() ) {
+ return $this->messageFormatter->getHtml();
+ }
+
+ return $this->createHtml( $title, $conceptQueryString, $conceptDocu );
+ }
+
+ private function createHtml( Title $title, $queryString, $documentation ) {
+
+ $message = '';
+
+ if ( wfMessage( 'smw-concept-introductory-message' )->exists() ) {
+ $message = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'plainlinks smw-callout smw-callout-info'
+ ],
+ wfMessage( 'smw-concept-introductory-message', $title->getText() )->text()
+ );
+ }
+
+ return $message . Html::rawElement( 'div', [ 'class' => 'smwfact' ],
+ Html::rawElement( 'span', [ 'class' => 'smwfactboxhead' ],
+ wfMessage( 'smw_concept_description', $title->getText() )->text() ) .
+ Html::rawElement( 'span', [ 'class' => 'smwrdflink' ], $this->getRdfLink( $title )->getWikiText() ) .
+ Html::element( 'br', [] ) .
+ Html::element( 'p', [ 'class' => 'concept-documenation' ], $documentation ? $documentation : '' ) .
+ Html::rawElement( 'pre', [], str_replace( '[', '&#91;', $queryString ) ) .
+ Html::element( 'br', [] )
+ );
+ }
+
+ private function getRdfLink( Title $title ) {
+ return SMWInfolink::newInternalLink(
+ wfMessage( 'smw_viewasrdf' )->text(),
+ $title->getPageLanguage()->getNsText( NS_SPECIAL ) . ':ExportRDF/' . $title->getPrefixedText(), 'rdflink'
+ );
+ }
+
+ private function buildQuery( $conceptQueryString ) {
+ $rawParams = [ $conceptQueryString ];
+
+ list( $query, ) = QueryProcessor::getQueryAndParamsFromFunctionParams(
+ $rawParams,
+ SMW_OUTPUT_WIKI,
+ QueryProcessor::CONCEPT_DESC,
+ false
+ );
+
+ return $query;
+ }
+
+ private function addQueryProfile( $query ) {
+
+ // If the smwgQueryProfiler is marked with FALSE then just don't create a profile.
+ if ( ApplicationFactory::getInstance()->getSettings()->get( 'smwgQueryProfiler' ) === false ) {
+ return;
+ }
+
+ $query->setContextPage(
+ $this->parserData->getSemanticData()->getSubject()
+ );
+
+ $profileAnnotatorFactory = ApplicationFactory::getInstance()->getQueryFactory()->newProfileAnnotatorFactory();
+
+ $descriptionProfileAnnotator = $profileAnnotatorFactory->newDescriptionProfileAnnotator(
+ $query
+ );
+
+ $descriptionProfileAnnotator->pushAnnotationsTo(
+ $this->parserData->getSemanticData()
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/DeclareParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/DeclareParserFunction.php
new file mode 100644
index 00000000..e0dc8cb2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/DeclareParserFunction.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use Parser;
+use PPFrame;
+use SMW\DataValueFactory;
+use SMW\ParserData;
+use SMWPropertyValue as PropertyValue;
+
+/**
+ * Class that provides the {{#declare}} parser function
+ *
+ * @see http://semantic-mediawiki.org/wiki/Help:Argument_declaration_in_templates
+ *
+ * @license GNU GPL v2+
+ * @since 1.5.3
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ */
+class DeclareParserFunction {
+
+ /**
+ * @var ParserData
+ */
+ private $parserData;
+
+ /**
+ * @var DIWikiPage
+ */
+ private $subject;
+
+ /**
+ * @since 2.1
+ *
+ * @param ParserData $parserData
+ */
+ public function __construct( ParserData $parserData ) {
+ $this->parserData = $parserData;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param PPFrame $frame
+ * @param array $args
+ */
+ public function parse( PPFrame $frame, array $args ) {
+
+ // @todo Save as metadata
+ if ( !$frame->isTemplate() ) {
+ return '';
+ }
+
+ $this->subject = $this->parserData->getSemanticData()->getSubject();
+
+ foreach ( $args as $arg ) {
+ if ( trim( $arg ) !== '' ) {
+ $expanded = trim( $frame->expand( $arg ) );
+ $parts = explode( '=', $expanded, 2 );
+
+ if ( count( $parts ) == 1 ) {
+ $propertystring = $expanded;
+ $argumentname = $expanded;
+ } else {
+ $propertystring = $parts[0];
+ $argumentname = $parts[1];
+ }
+
+ $propertyValue = DataValueFactory::getInstance()->newPropertyValueByLabel( $propertystring );
+ $argument = $frame->getArgument( $argumentname );
+ $valuestring = $frame->expand( $argument );
+
+ if ( $propertyValue->isValid() ) {
+ $this->matchValueArgument( $propertyValue, $propertystring, $valuestring );
+ }
+ }
+ }
+
+ $this->parserData->pushSemanticDataToParserOutput();
+
+ return '';
+ }
+
+ private function matchValueArgument( PropertyValue $propertyValue, $propertystring, $valuestring ) {
+
+ if ( $propertyValue->getPropertyTypeID() === '_wpg' ) {
+ $matches = [];
+ preg_match_all( '/\[\[([^\[\]]*)\]\]/u', $valuestring, $matches );
+ $objects = $matches[1];
+
+ if ( count( $objects ) == 0 ) {
+ if ( trim( $valuestring ) !== '' ) {
+ $this->addDataValue( $propertystring, $valuestring );
+ }
+ } else {
+ foreach ( $objects as $object ) {
+ $this->addDataValue( $propertystring, $object );
+ }
+ }
+ } elseif ( trim( $valuestring ) !== '' ) {
+ $this->addDataValue( $propertystring, $valuestring );
+ }
+
+ // $value = \SMW\DataValueFactory::getInstance()->newDataValueByProperty( $property->getDataItem(), $valuestring );
+ // if (!$value->isValid()) continue;
+ }
+
+ private function addDataValue( $property, $value ) {
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByText(
+ $property,
+ $value,
+ false,
+ $this->subject
+ );
+
+ $this->parserData->addDataValue( $dataValue );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/DocumentationParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/DocumentationParserFunction.php
new file mode 100644
index 00000000..fa739b8a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/DocumentationParserFunction.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use ParamProcessor\ParamDefinition;
+use ParamProcessor\ProcessedParam;
+use ParamProcessor\ProcessingError;
+use ParamProcessor\ProcessingResult;
+use Parser;
+use ParserHooks\HookDefinition;
+use ParserHooks\HookHandler;
+use SMW\ParameterListDocBuilder;
+use SMWQueryProcessor as QueryProcessor;
+
+/**
+ * Class that provides the {{#smwdoc}} parser function, which displays parameter
+ * documentation for a specified result format.
+ *
+ * @ingroup ParserFunction
+ *
+ * @license GNU GPL v2+
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class DocumentationParserFunction implements HookHandler {
+
+ /**
+ * @var string
+ */
+ private $language = 'en';
+
+ /**
+ * @param Parser $parser
+ * @param ProcessingResult $result
+ *
+ * @return mixed
+ */
+ public function handle( Parser $parser, ProcessingResult $result ) {
+
+ if ( $result->hasFatal() ) {
+ return $this->getOutputForErrors( $result->getErrors() );
+ }
+
+ $parameters = $result->getParameters();
+ $format = $parameters['format']->getValue();
+
+ $formatParameters = QueryProcessor::getFormatParameters(
+ $format
+ );
+
+ $this->language = $parameters['language']->getValue();
+
+ if ( $formatParameters === [] ) {
+ return $this->msg( 'smw-smwdoc-default-no-parameter-list', $format );
+ }
+
+ return $this->buildParameterListDocumentation( $parameters, $formatParameters );
+ }
+
+ /**
+ * @return HookDefinition
+ */
+ public static function getHookDefinition() {
+ return new HookDefinition(
+ 'smwdoc',
+ [
+ [
+ 'name' => 'format',
+ 'message' => 'smw-smwdoc-par-format',
+ 'values' => array_keys( $GLOBALS['smwgResultFormats'] ),
+ ],
+ [
+ 'name' => 'language',
+ 'message' => 'smw-smwdoc-par-language',
+ 'default' => $GLOBALS['wgLanguageCode'],
+ ],
+ [
+ 'name' => 'parameters',
+ 'message' => 'smw-smwdoc-par-parameters',
+ 'values' => [ 'all', 'specific', 'base' ],
+ 'default' => 'specific',
+ ],
+ ],
+ [ 'format', 'language', 'parameters' ]
+ );
+ }
+
+ /**
+ * @param ProcessedParam[] $parameters
+ *
+ * @return string
+ */
+ private function buildParameterListDocumentation( array $parameters, $formatParameters ) {
+
+ if ( $parameters['parameters']->getValue() === 'specific' ) {
+ foreach ( array_keys( QueryProcessor::getParameters() ) as $name ) {
+ unset( $formatParameters[$name] );
+ }
+ } elseif ( $parameters['parameters']->getValue() === 'base' ) {
+ foreach ( array_diff_key( $formatParameters, QueryProcessor::getParameters() ) as $param ) {
+ unset( $formatParameters[$param->getName()] );
+ }
+ }
+
+ $docBuilder = new ParameterListDocBuilder(
+ [ $this, 'msg' ]
+ );
+
+ if ( ( $parameterTable = $docBuilder->getParameterTable( $formatParameters ) ) !== '' ) {
+ return $parameterTable;
+ }
+
+ return $this->msg( 'smw-smwdoc-default-no-parameter-list', $parameters['format']->getValue() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ...$args
+ *
+ * @return string
+ */
+ public function msg( ...$args ) {
+ return wfMessage( array_shift( $args ) )->params( $args )->useDatabase( true )->inLanguage( $this->language )->text();
+ }
+
+ /**
+ * @param ProcessingError[] $errors
+ * @return string
+ */
+ private function getOutputForErrors( $errors ) {
+ // TODO: see https://github.com/SemanticMediaWiki/SemanticMediaWiki/issues/1485
+ return 'A fatal error occurred in the #smwdoc parser function';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ExpensiveFuncExecutionWatcher.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ExpensiveFuncExecutionWatcher.php
new file mode 100644
index 00000000..4fe50cec
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ExpensiveFuncExecutionWatcher.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use SMW\ParserData;
+use SMWQuery as Query;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ExpensiveFuncExecutionWatcher {
+
+ /**
+ * Idenitifer
+ */
+ const EXPENSIVE_COUNTER = 'smw-expensiveparsercount';
+
+ /**
+ * @var ParserData
+ */
+ private $parserData;
+
+ /**
+ * @var integer
+ */
+ private $expensiveThreshold = 10;
+
+ /**
+ * @var integer|boolean
+ */
+ private $expensiveExecutionLimit = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param ParserData $parserData
+ */
+ public function __construct( ParserData $parserData ) {
+ $this->parserData = $parserData;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $expensiveThreshold
+ */
+ public function setExpensiveThreshold( $expensiveThreshold ) {
+ $this->expensiveThreshold = $expensiveThreshold;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer|boolean $expensiveExecutionLimit
+ */
+ public function setExpensiveExecutionLimit( $expensiveExecutionLimit ) {
+ $this->expensiveExecutionLimit = $expensiveExecutionLimit;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ *
+ * @return boolean
+ */
+ public function hasReachedExpensiveLimit( Query $query ) {
+
+ if ( $this->expensiveExecutionLimit === false ) {
+ return false;
+ }
+
+ if ( $query->getLimit() == 0 ) {
+ return false;
+ }
+
+ if ( $this->parserData->getOutput()->getExtensionData( self::EXPENSIVE_COUNTER ) < $this->expensiveExecutionLimit ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ *
+ * @return boolean
+ */
+ public function incrementExpensiveCount( Query $query ) {
+
+ if ( $this->expensiveExecutionLimit === false || $query->getLimit() == 0 || $query->getOption( Query::PROC_QUERY_TIME ) < $this->expensiveThreshold ) {
+ return;
+ }
+
+ $output = $this->parserData->getOutput();
+ $expensiveCount = $output->getExtensionData( self::EXPENSIVE_COUNTER );
+
+ if ( !is_int( $expensiveCount ) ) {
+ $expensiveCount = 0;
+ }
+
+ $expensiveCount++;
+ $output->setExtensionData( self::EXPENSIVE_COUNTER, $expensiveCount );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/InfoParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/InfoParserFunction.php
new file mode 100644
index 00000000..482007d9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/InfoParserFunction.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use ParamProcessor\ProcessingError;
+use ParamProcessor\ProcessingResult;
+use Parser;
+use ParserHooks\HookDefinition;
+use ParserHooks\HookHandler;
+use SMWOutputs;
+
+/**
+ * Class that provides the {{#info}} parser function
+ *
+ * @ingroup ParserFunction
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ */
+class InfoParserFunction implements HookHandler {
+
+ /**
+ * @param Parser $parser
+ * @param ProcessingResult $result
+ *
+ * @return mixed
+ */
+ public function handle( Parser $parser, ProcessingResult $result ) {
+ if ( $result->hasFatal() ) {
+ return $this->getOutputForErrors( $result->getErrors() );
+ }
+
+ $parameters = $result->getParameters();
+
+ if ( !isset( $parameters['message'] ) ) {
+ return '';
+ }
+
+ $message = $parser->mStripState ? $parser->mStripState->unstripBoth( $parameters[ 'message' ]->getValue() ) : $parameters[ 'message' ]->getValue();
+
+ if ( $message === '' ) {
+ return '';
+ }
+
+ /**
+ * Non-escaping is safe bacause a user's message is passed through parser, which will
+ * handle unsafe HTM elements.
+ */
+ $result = smwfEncodeMessages(
+ [ $message ],
+ $parameters['icon']->getValue(),
+ ' <!--br-->',
+ false // No escaping.
+ );
+
+ if ( !is_null( $parser->getTitle() ) && $parser->getTitle()->isSpecialPage() ) {
+ global $wgOut;
+ SMWOutputs::commitToOutputPage( $wgOut );
+ }
+ else {
+ SMWOutputs::commitToParser( $parser );
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param ProcessingError[] $errors
+ * @return string
+ */
+ private function getOutputForErrors( $errors ) {
+ // TODO: see https://github.com/SemanticMediaWiki/SemanticMediaWiki/issues/1485
+ return 'A fatal error occurred in the #info parser function';
+ }
+
+ public static function getHookDefinition() {
+ return new HookDefinition(
+ 'info',
+ [
+ [
+ 'name' => 'message',
+ 'message' => 'smw-info-par-message',
+ ],
+ [
+ 'name' => 'icon',
+ 'message' => 'smw-info-par-icon',
+ 'default' => 'info',
+ 'values' => [ 'info', 'warning', 'error', 'note' ],
+ ],
+ ],
+ [
+ 'message',
+ 'icon'
+ ]
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/RecurringEventsParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/RecurringEventsParserFunction.php
new file mode 100644
index 00000000..9a161e0c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/RecurringEventsParserFunction.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use SMW\MessageFormatter;
+use SMW\ParserData;
+use SMW\ParserParameterProcessor;
+use SMW\RecurringEvents;
+use SMW\Subobject;
+
+/**
+ * @private This class should not be instantiated directly, please use
+ * ParserFunctionFactory::newRecurringEventsParserFunction
+ *
+ * Class that provides the {{#set_recurring_event}} parser function
+ *
+ * @see http://semantic-mediawiki.org/wiki/Help:Recurring_events
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class RecurringEventsParserFunction extends SubobjectParserFunction {
+
+ /**
+ * @var RecurringEvents
+ */
+ private $recurringEvents;
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserData $parserData
+ * @param Subobject $subobject
+ * @param MessageFormatter $messageFormatter
+ * @param RecurringEvents $recurringEvents
+ */
+ public function __construct( ParserData $parserData, Subobject $subobject, MessageFormatter $messageFormatter, RecurringEvents $recurringEvents ) {
+ parent::__construct ( $parserData, $subobject, $messageFormatter );
+ $this->recurringEvents = $recurringEvents;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserParameterProcessor $parameters
+ *
+ * @return string|null
+ */
+ public function parse( ParserParameterProcessor $parameters ) {
+
+ $this->useFirstElementAsPropertyLabel( true );
+
+ $this->recurringEvents->parse(
+ $parameters->toArray()
+ );
+
+ $this->messageFormatter->addFromArray(
+ $this->recurringEvents->getErrors()
+ );
+
+ foreach ( $this->recurringEvents->getDates() as $date_str ) {
+
+ // Override existing parameters array with the returned
+ // pre-processed parameters array from recurring events
+ $parameters->setParameters( $this->recurringEvents->getParameters() );
+
+ // Add the date string as individual property / value parameter
+ $parameters->addParameter(
+ $this->recurringEvents->getProperty(),
+ $date_str
+ );
+
+ // @see SubobjectParserFunction::addDataValuesToSubobject
+ // Each new $parameters set will add an additional subobject
+ // to the instance
+ if ( $this->addDataValuesToSubobject( $parameters ) ) {
+ $this->parserData->getSemanticData()->addSubobject( $this->subobject );
+ }
+
+ // Collect errors that occurred during processing
+ $this->messageFormatter->addFromArray( $this->subobject->getErrors() );
+ }
+
+ // Update ParserOutput
+ $this->parserData->pushSemanticDataToParserOutput();
+
+ $this->messageFormatter->addFromArray(
+ $this->parserData->getErrors()
+ );
+
+ return $this->messageFormatter->getHtml();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SectionTag.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SectionTag.php
new file mode 100644
index 00000000..f37aea24
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SectionTag.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use Parser;
+use PPFrame;
+use Html;
+
+/**
+ * To support the generation of <section> ... </section>
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SectionTag {
+
+ /**
+ * @var Parser
+ */
+ private $parser;
+
+ /**
+ * @var PPFrame
+ */
+ private $frame;
+
+ /**
+ * @since 3.0
+ *
+ * @param Parser $parser
+ * @param PPFrame $frame
+ */
+ public function __construct( Parser $parser, PPFrame $frame ) {
+ $this->parser = $parser;
+ $this->frame = $frame;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Parser $parser
+ * @param boolean $supportSectionTag
+ *
+ * @return boolean
+ */
+ public static function register( Parser $parser, $supportSectionTag = true ) {
+
+ if ( $supportSectionTag === false ) {
+ return false;
+ }
+
+ $parser->setHook( 'section', function( $input, array $args, Parser $parser, PPFrame $frame ) {
+ return ( new self( $parser, $frame ) )->parse( $input, $args );
+ } );
+
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $input
+ * @param array $args
+ *
+ * @return string
+ */
+ public function parse( $input, array $args ) {
+
+ $attributes = [];
+ $title = $this->parser->getTitle();
+
+ foreach( $args as $name => $value ) {
+ $value = htmlspecialchars( $value );
+
+ if ( $name === 'class' ) {
+ $attributes['class'] = $value;
+ }
+
+ if ( $name === 'id' ) {
+ $attributes['id'] = $value;
+ }
+ }
+
+ if ( $title !== null && $title->getNamespace() === SMW_NS_PROPERTY ) {
+ $attributes['class'] = ( isset( $attributes['class'] ) ? ' ' : '' ) . "smw-property-specification";
+ }
+
+ return Html::rawElement(
+ 'section',
+ $attributes,
+ $this->parser->recursiveTagParse( $input, $this->frame )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SetParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SetParserFunction.php
new file mode 100644
index 00000000..32e37896
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SetParserFunction.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use Parser;
+use SMW\DataValueFactory;
+use SMW\MediaWiki\Renderer\WikitextTemplateRenderer;
+use SMW\MediaWiki\StripMarkerDecoder;
+use SMW\MessageFormatter;
+use SMW\ParserData;
+use SMW\ParserParameterProcessor;
+
+/**
+ * Class that provides the {{#set}} parser function
+ *
+ * @see http://semantic-mediawiki.org/wiki/Help:Properties_and_types#Silent_annotations_using_.23set
+ * @see http://www.semantic-mediawiki.org/wiki/Help:Setting_values
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class SetParserFunction {
+
+ /**
+ * @var ParserData
+ */
+ private $parserData;
+
+ /**
+ * @var MessageFormatter
+ */
+ private $messageFormatter;
+
+ /**
+ * @var WikitextTemplateRenderer
+ */
+ private $templateRenderer;
+
+ /**
+ * @var StripMarkerDecoder
+ */
+ private $stripMarkerDecoder;
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserData $parserData
+ * @param MessageFormatter $messageFormatter
+ * @param WikitextTemplateRenderer $templateRenderer
+ */
+ public function __construct( ParserData $parserData, MessageFormatter $messageFormatter, WikitextTemplateRenderer $templateRenderer ) {
+ $this->parserData = $parserData;
+ $this->messageFormatter = $messageFormatter;
+ $this->templateRenderer = $templateRenderer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param StripMarkerDecoder $stripMarkerDecoder
+ */
+ public function setStripMarkerDecoder( StripMarkerDecoder $stripMarkerDecoder ) {
+ $this->stripMarkerDecoder = $stripMarkerDecoder;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserParameterProcessor $parameters
+ *
+ * @return string|null
+ */
+ public function parse( ParserParameterProcessor $parameters ) {
+
+ $count = 0;
+ $template = '';
+ $subject = $this->parserData->getSemanticData()->getSubject();
+
+ $parametersToArray = $parameters->toArray();
+
+ if ( isset( $parametersToArray['template'] ) ) {
+ $template = $parametersToArray['template'][0];
+ unset( $parametersToArray['template'] );
+ }
+
+ foreach ( $parametersToArray as $property => $values ) {
+
+ $last = count( $values ) - 1; // -1 because the key starts with 0
+
+ foreach ( $values as $key => $value ) {
+
+ if ( $this->stripMarkerDecoder !== null ) {
+ $value = $this->stripMarkerDecoder->decode( $value );
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByText(
+ $property,
+ $value,
+ false,
+ $subject
+ );
+
+ if ( $this->parserData->canUse() ) {
+ $this->parserData->addDataValue( $dataValue );
+ }
+
+ $this->messageFormatter->addFromArray( $dataValue->getErrors() );
+
+ $this->addFieldsToTemplate(
+ $template,
+ $dataValue,
+ $property,
+ $value,
+ $last == $key,
+ $count
+ );
+ }
+ }
+
+ $this->parserData->pushSemanticDataToParserOutput();
+
+ $html = $this->templateRenderer->render() . $this->messageFormatter
+ ->addFromArray( $parameters->getErrors() )
+ ->getHtml();
+
+ return [ $html, 'noparse' => $template === '', 'isHTML' => false ];
+ }
+
+ private function addFieldsToTemplate( $template, $dataValue, $property, $value, $isLastElement, &$count ) {
+
+ if ( $template === '' || !$dataValue->isValid() ) {
+ return '';
+ }
+
+ $this->templateRenderer->addField( 'property', $property );
+ $this->templateRenderer->addField( 'value', $value );
+ $this->templateRenderer->addField( 'last-element', $isLastElement );
+ $this->templateRenderer->addField( '#', $count++ );
+ $this->templateRenderer->packFieldsForTemplate( $template );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ShowParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ShowParserFunction.php
new file mode 100644
index 00000000..81d149f0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/ShowParserFunction.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+/**
+ * Class that provides the {{#show}} parser function
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class ShowParserFunction {
+
+ /**
+ * @var AskParserFunction
+ */
+ private $askParserFunction;
+
+ /**
+ * @since 1.9
+ *
+ * @param AskParserFunction $askParserFunction
+ */
+ public function __construct( AskParserFunction $askParserFunction ) {
+ $this->askParserFunction = $askParserFunction;
+ }
+
+ /**
+ * Parse parameters, return results from the query printer and update the
+ * ParserOutput with meta data from the query
+ *
+ * @note The {{#show}} parser function internally uses the AskParserFunction
+ * and while an extra ShowParserFunction constructor is not really necessary
+ * it allows for separate unit testing
+ *
+ * @since 1.9
+ *
+ * @param array $params
+ *
+ * @return string|null
+ */
+ public function parse( array $rawParams ) {
+ $this->askParserFunction->setShowMode( true );
+ return $this->askParserFunction->parse( $rawParams );
+ }
+
+ /**
+ * Returns a message about inline queries being disabled
+ * @see $smwgQEnabled
+ *
+ * @since 1.9
+ *
+ * @return string|null
+ */
+ public function isQueryDisabled() {
+ return $this->askParserFunction->isQueryDisabled();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SubobjectParserFunction.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SubobjectParserFunction.php
new file mode 100644
index 00000000..c2bf1cd9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserFunctions/SubobjectParserFunction.php
@@ -0,0 +1,336 @@
+<?php
+
+namespace SMW\ParserFunctions;
+
+use Parser;
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMW\HashBuilder;
+use SMW\MediaWiki\StripMarkerDecoder;
+use SMW\Message;
+use SMW\MessageFormatter;
+use SMW\ParserData;
+use SMW\ParserParameterProcessor;
+use SMW\SemanticData;
+use SMW\Subobject;
+
+/**
+ * @private This class should not be instantiated directly, please use
+ * ParserFunctionFactory::newSubobjectParserFunction
+ *
+ * Provides the {{#subobject}} parser function
+ *
+ * @see http://www.semantic-mediawiki.org/wiki/Help:ParserFunction
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class SubobjectParserFunction {
+
+ /**
+ * Fixed identifier that describes the sortkey annotation parameter
+ */
+ const PARAM_SORTKEY = '@sortkey';
+
+ /**
+ * Fixed identifier that describes a category parameter
+ *
+ * Those will not be visible by the "standard" category list as the handling
+ * of assigned categories is SMW specific for subobjects.
+ */
+ const PARAM_CATEGORY = '@category';
+
+ /**
+ * Fixed identifier that describes a property that can auto-linked the
+ * embeddedding subject
+ */
+ const PARAM_LINKWITH = '@linkWith';
+
+ /**
+ * @var ParserData
+ */
+ protected $parserData;
+
+ /**
+ * @var Subobject
+ */
+ protected $subobject;
+
+ /**
+ * @var MessageFormatter
+ */
+ protected $messageFormatter;
+
+ /**
+ * @var StripMarkerDecoder
+ */
+ private $stripMarkerDecoder;
+
+ /**
+ * @var boolean
+ */
+ private $useFirstElementAsPropertyLabel = false;
+
+ /**
+ * @var boolean
+ */
+ private $isCapitalLinks = true;
+
+ /**
+ * @var boolean
+ */
+ private $isComparableContent = false;
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserData $parserData
+ * @param Subobject $subobject
+ * @param MessageFormatter $messageFormatter
+ */
+ public function __construct( ParserData $parserData, Subobject $subobject, MessageFormatter $messageFormatter ) {
+ $this->parserData = $parserData;
+ $this->subobject = $subobject;
+ $this->messageFormatter = $messageFormatter;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param StripMarkerDecoder $stripMarkerDecoder
+ */
+ public function setStripMarkerDecoder( StripMarkerDecoder $stripMarkerDecoder ) {
+ $this->stripMarkerDecoder = $stripMarkerDecoder;
+ }
+
+ /**
+ * @see $wgCapitalLinks
+ *
+ * @since 2.5
+ *
+ * @param boolean $isCapitalLinks
+ */
+ public function isCapitalLinks( $isCapitalLinks ) {
+ $this->isCapitalLinks = $isCapitalLinks;
+ }
+
+ /**
+ * Ensures that unordered parameters and property names are normalized and
+ * sorted to produce the same hash even if elements of the same literal
+ * representation are placed differently.
+ *
+ * @since 3.0
+ *
+ * @param boolean $isComparableContent
+ */
+ public function isComparableContent( $isComparableContent = true ) {
+ $this->isComparableContent = (bool)$isComparableContent;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param boolean $useFirstElementAsPropertyLabel
+ *
+ * @return SubobjectParserFunction
+ */
+ public function useFirstElementAsPropertyLabel( $useFirstElementAsPropertyLabel = true ) {
+ $this->useFirstElementAsPropertyLabel = (bool)$useFirstElementAsPropertyLabel;
+ return $this;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param ParserParameterProcessor $params
+ *
+ * @return string|null
+ */
+ public function parse( ParserParameterProcessor $parameters ) {
+
+ if (
+ $this->parserData->canUse() &&
+ $this->addDataValuesToSubobject( $parameters ) &&
+ $this->subobject->getSemanticData()->isEmpty() === false ) {
+ $this->parserData->getSemanticData()->addSubobject( $this->subobject );
+ }
+
+ $this->parserData->pushSemanticDataToParserOutput();
+
+ $html = $this->messageFormatter->addFromArray( $this->subobject->getErrors() )
+ ->addFromArray( $this->parserData->getErrors() )
+ ->addFromArray( $parameters->getErrors() )
+ ->getHtml();
+
+ // An empty output in MW forces an extra <br> element.
+ //if ( $html == '' ) {
+ // $html = '<p></p>';
+ //}
+
+ return $html;
+ }
+
+ protected function addDataValuesToSubobject( ParserParameterProcessor $parserParameterProcessor ) {
+
+ // Named subobjects containing a "." in the first five characters are
+ // reserved to be used by extensions only in order to separate them from
+ // user land and avoid having them accidentally to refer to the same
+ // named ID (i.e. different access restrictions etc.)
+ if ( strpos( mb_substr( $parserParameterProcessor->getFirst(), 0, 5 ), '.' ) !== false ) {
+ return $this->parserData->addError(
+ Message::encode( [ 'smw-subobject-parser-invalid-naming-scheme', $parserParameterProcessor->getFirst() ] )
+ );
+ }
+
+ list( $parameters, $id ) = $this->getParameters(
+ $parserParameterProcessor
+ );
+
+ $this->subobject->setEmptyContainerForId(
+ $id
+ );
+
+ $subject = $this->subobject->getSubject();
+
+ foreach ( $parameters as $property => $values ) {
+
+ if ( $property === self::PARAM_SORTKEY ) {
+ $property = DIProperty::TYPE_SORTKEY;
+ }
+
+ if ( $property === self::PARAM_CATEGORY ) {
+ $property = DIProperty::TYPE_CATEGORY;
+ }
+
+ foreach ( $values as $value ) {
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByText(
+ $property,
+ $value,
+ false,
+ $subject
+ );
+
+ $this->subobject->addDataValue( $dataValue );
+ }
+ }
+
+ $this->augment( $this->subobject->getSemanticData() );
+
+ return true;
+ }
+
+ private function getParameters( ParserParameterProcessor $parserParameterProcessor ) {
+
+ $id = $parserParameterProcessor->getFirst();
+ $isAnonymous = in_array( $id, [ null, '' ,'-' ] );
+
+ $useFirst = $this->useFirstElementAsPropertyLabel && !$isAnonymous;
+
+ $parameters = $this->preprocess(
+ $parserParameterProcessor,
+ $useFirst
+ );
+
+ // FIXME remove the check with 3.1, should be standard by then!
+ if ( !$this->isComparableContent ) {
+ $p = $parameters;
+ } else {
+ $p = $parameters;
+ // Sort the copy not the parameters itself
+ $parserParameterProcessor->sort( $p );
+ }
+
+ // Reclaim the ID to be content hash based
+ if ( $useFirst || $isAnonymous ) {
+ $id = HashBuilder::createFromContent( $p, '_' );
+ }
+
+ return [ $parameters, $id ];
+ }
+
+ private function preprocess( ParserParameterProcessor $parserParameterProcessor, $useFirst ) {
+
+ if ( $parserParameterProcessor->hasParameter( self::PARAM_LINKWITH ) ) {
+ $val = $parserParameterProcessor->getParameterValuesByKey( self::PARAM_LINKWITH );
+ $parserParameterProcessor->addParameter(
+ end( $val ),
+ $this->parserData->getTitle()->getPrefixedText()
+ );
+
+ $parserParameterProcessor->removeParameterByKey( self::PARAM_LINKWITH );
+ }
+
+ if ( $useFirst ) {
+ $parserParameterProcessor->addParameter(
+ $parserParameterProcessor->getFirst(),
+ $this->parserData->getTitle()->getPrefixedText()
+ );
+ }
+
+ $parameters = $this->decode(
+ $parserParameterProcessor->toArray()
+ );
+
+ foreach ( $parameters as $property => $values ) {
+
+ $prop = $property;
+
+ // Normalize property names to generate the same hash for when
+ // CapitalLinks is enabled (has foo === Has foo)
+ if ( $property !== '' && $property{0} !== '@' && $this->isCapitalLinks ) {
+ $property = mb_strtoupper( mb_substr( $property, 0, 1 ) ) . mb_substr( $property, 1 );
+ }
+
+ unset( $parameters[$prop] );
+ $parameters[$property] = $values;
+ }
+
+ return $parameters;
+ }
+
+ private function decode( $parameters ) {
+
+ if ( $this->stripMarkerDecoder === null || !$this->stripMarkerDecoder->canUse() ) {
+ return $parameters;
+ }
+
+ // Any decoding has to happen before the subject ID is generated otherwise
+ // the value would contain something like `UNIQ--nowiki-00000011-QINU`
+ // and be part of the hash. `UNIQ--nowiki-00000011-QINU` isn't stable
+ // and changes to text will create new marker positions therefore it
+ // cannot be part of the hash computation
+ foreach ( $parameters as $property => &$values ) {
+ foreach ( $values as &$value ) {
+ $value = $this->stripMarkerDecoder->decode( $value );
+ }
+ }
+
+ return $parameters;
+ }
+
+ private function augment( $semanticData ) {
+
+ // Data block created by a user
+ $semanticData->setOption( SemanticData::PROC_USER, true );
+
+ $sortkey = new DIProperty( DIProperty::TYPE_SORTKEY );
+ $displayTitle = new DIProperty( DIProperty::TYPE_DISPLAYTITLE );
+
+ if ( $semanticData->hasProperty( $sortkey ) || !$semanticData->hasProperty( $displayTitle ) ) {
+ return null;
+ }
+
+ $pv = $semanticData->getPropertyValues(
+ $displayTitle
+ );
+
+ $semanticData->addPropertyObjectValue(
+ $sortkey,
+ end( $pv )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ParserParameterProcessor.php b/www/wiki/extensions/SemanticMediaWiki/src/ParserParameterProcessor.php
new file mode 100644
index 00000000..a6866225
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ParserParameterProcessor.php
@@ -0,0 +1,336 @@
+<?php
+
+namespace SMW;
+
+use SMW\Utils\ErrorCodeFormatter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class ParserParameterProcessor {
+
+ /**
+ * @var string
+ */
+ private $defaultSeparator = ',';
+
+ /**
+ * @var array
+ */
+ private $rawParameters;
+
+ /**
+ * @var array
+ */
+ private $parameters;
+
+ /**
+ * @var null
+ */
+ private $first = null;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @since 1.9
+ *
+ * @param array $rawParameters
+ */
+ public function __construct( array $rawParameters = [] ) {
+ $this->rawParameters = $rawParameters;
+ $this->parameters = $this->doMap( $rawParameters );
+ }
+
+ /**
+ * Returns collected errors
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * Adds an error
+ *
+ * @since 1.9
+ *
+ * @param mixed $error
+ */
+ public function addError( $error ) {
+ $this->errors = array_merge( (array)$error === $error ? $error : [ $error ], $this->errors );
+ }
+
+ /**
+ * @deprecated since 2.3, use ParserParameterProcessor::getFirstParameter
+ */
+ public function getFirst() {
+ return $this->getFirstParameter();
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return string
+ */
+ public function getFirstParameter() {
+ return $this->first;
+ }
+
+ /**
+ * Returns raw parameters
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ public function getRaw() {
+ return $this->rawParameters;
+ }
+
+ /**
+ * Returns remapped parameters
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ public function toArray() {
+ return $this->parameters;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function hasParameter( $key ) {
+ return isset( $this->parameters[$key] ) || array_key_exists( $key, $this->parameters );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $key
+ */
+ public function removeParameterByKey( $key ) {
+ unset( $this->parameters[$key] );
+ }
+
+ /**
+ * @deprecated since 2.5, use ParserParameterProcessor::getParameterValuesByKey
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getParameterValuesFor( $key ) {
+ return $this->getParameterValuesByKey( $key );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $key
+ *
+ * @return array
+ */
+ public function getParameterValuesByKey( $key ) {
+
+ if ( $this->hasParameter( $key ) ) {
+ return $this->parameters[$key];
+ }
+
+ return [];
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param array $parameters
+ */
+ public function setParameters( array $parameters ) {
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function addParameter( $key, $value ) {
+ if( $key !== '' && $value !== '' ) {
+ $this->parameters[$key][] = $value;
+ }
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $key
+ * @param array $values
+ */
+ public function setParameter( $key, array $values ) {
+ if ( $key !== '' && $values !== [] ) {
+ $this->parameters[$key] = $values;
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ * @param boolean $associative
+ */
+ public static function sort( array &$parameters, $associative = true ) {
+
+ // Associative vs. simple index array sort
+ if ( $associative ) {
+ ksort( $parameters );
+ } else {
+ sort( $parameters );
+ }
+
+ foreach ( $parameters as $key => &$value ) {
+ if ( is_array( $value ) ) {
+ self::sort( $value, is_int( $key ) );
+ }
+ }
+ }
+
+ /**
+ * Map raw parameters array into an 2n-array for simplified
+ * via [key] => [value1, value2]
+ */
+ private function doMap( array $params ) {
+ $results = [];
+ $previousProperty = null;
+
+ while ( key( $params ) !== null ) {
+
+ $pipe = false;
+ $values = [];
+
+ // Only strings are allowed for processing
+ if( !is_string( current ( $params ) ) ) {
+ next( $params );
+ }
+
+ // Get the current element and divide it into parts
+ $currentElement = explode( '=', trim( current ( $params ) ), 2 );
+
+ // Looking to the next element for comparison
+ $separator = $this->lookAheadOnNextElement( $params, $pipe );
+
+ // First named parameter
+ if ( count( $currentElement ) == 1 && $previousProperty === null ) {
+ $this->first = str_replace( ' ', '_', $currentElement[0] );
+ }
+
+ // Here we allow to support assignments of type |Has property=Test1|Test2|Test3
+ // for multiple values with the same preceding property
+ if ( count( $currentElement ) == 1 && $previousProperty !== null ) {
+ $currentElement[1] = $currentElement[0];
+ $currentElement[0] = $previousProperty;
+ } else {
+ $previousProperty = $currentElement[0];
+ }
+
+ // Reassign values
+ if ( $separator !== '' && isset( $currentElement[1] ) ) {
+ $values = explode( $separator, $currentElement[1] );
+ } elseif ( isset( $currentElement[1] ) ) {
+ $values[] = $currentElement[1];
+ }
+
+ // Remap properties and values to output a simple array
+ foreach ( $values as $value ) {
+ if ( $value !== '' ) {
+ $results[$currentElement[0]][] = trim( $value );
+ }
+ }
+
+ // +pipe indicates that elements are expected to be concatenated
+ // with a | that was removed during a #parserFunction invocation
+ if ( $pipe ) {
+ $results[$currentElement[0]] = [ implode( '|', $results[$currentElement[0]] ) ];
+ }
+ }
+
+ return $this->parseFromJson( $results );
+ }
+
+ private function lookAheadOnNextElement( &$params, &$pipe ) {
+
+ $separator = '';
+
+ if( !next( $params ) ) {
+ return $separator;
+ }
+
+ $nextElement = explode( '=', trim( current( $params ) ), 2 );
+
+ if ( $nextElement !== [] ) {
+ // This allows assignments of type |Has property=Test1,Test2|+sep=,
+ // as a means to support multiple value declaration
+ if ( substr( $nextElement[0], - 5 ) === '+sep' ) {
+ $separator = isset( $nextElement[1] ) ? $nextElement[1] !== '' ? $nextElement[1] : $this->defaultSeparator : $this->defaultSeparator;
+ next( $params );
+ }
+ }
+
+ if ( current( $params ) === '+pipe' ) {
+ $pipe = true;
+ next( $params );
+ }
+
+ return $separator;
+ }
+
+ private function parseFromJson( $results ) {
+
+ if ( !isset( $results['@json'] ) || !isset( $results['@json'][0] ) ) {
+ return $results;
+ }
+
+ // Restrict the depth to avoid resolving recursive assignment
+ // that can not be handled beyond the 2:n
+ $depth = 3;
+ $params = json_decode( $results['@json'][0], true, $depth );
+
+ if ( $params === null || json_last_error() !== JSON_ERROR_NONE ) {
+ $this->addError( Message::encode(
+ [
+ 'smw-parser-invalid-json-format',
+ ErrorCodeFormatter::getStringFromJsonErrorCode( json_last_error() )
+ ]
+ ) );
+ return $results;
+ }
+
+ array_walk( $params, function( &$value, $key ) {
+
+ if ( $value === '' ) {
+ $value = [];
+ }
+
+ if ( !is_array( $value ) ) {
+ $value = [ $value ];
+ }
+ } );
+
+ unset( $results['@json'] );
+ return array_merge( $results, $params );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PermissionPthValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/PermissionPthValidator.php
new file mode 100644
index 00000000..f8a5e005
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PermissionPthValidator.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace SMW;
+
+use SMW\DataValues\AllowsPatternValue;
+use SMW\Protection\ProtectionValidator;
+use Title;
+use User;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class PermissionPthValidator {
+
+ /**
+ * @var ProtectionValidator
+ */
+ private $protectionValidator;
+
+ /**
+ * @since 2.5
+ *
+ * @param ProtectionValidator $protectionValidator
+ */
+ public function __construct( ProtectionValidator $protectionValidator ) {
+ $this->protectionValidator = $protectionValidator;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title &$title
+ * @param User $user
+ * @param string $action
+ * @param array &$errors
+ *
+ * @return boolean
+ */
+ public function checkQuickPermission( Title &$title, User $user, $action, &$errors ) {
+ return $this->hasUserPermission( $title, $user, $action, $errors );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Title &$title
+ * @param User $user
+ * @param string $action
+ * @param array &$errors
+ *
+ * @return boolean
+ */
+ public function hasUserPermission( Title &$title, User $user, $action, &$errors ) {
+
+ if ( $title->getNamespace() === SMW_NS_SCHEMA ) {
+ return $this->checkSchemaNamespacePermission( $title, $user, $action, $errors );
+ }
+
+ if ( $action !== 'edit' && $action !== 'delete' && $action !== 'move' && $action !== 'upload' ) {
+ return true;
+ }
+
+ if ( $title->getNamespace() === NS_MEDIAWIKI ) {
+ return $this->checkMwNamespacePatternEditPermission( $title, $user, $action, $errors );
+ }
+
+ if ( $this->protectionValidator->getCreateProtectionRight() && $title->getNamespace() === SMW_NS_PROPERTY ) {
+ return $this->checkPropertyNamespaceCreatePermission( $title, $user, $action, $errors );
+ }
+
+ if ( $title->getNamespace() === NS_CATEGORY ) {
+ return $this->checkChangePropagationProtection( $title, $user, $action, $errors );
+ }
+
+ if ( !$title->exists() ) {
+ return true;
+ }
+
+ if ( $title->getNamespace() === SMW_NS_PROPERTY ) {
+ return $this->checkPropertyNamespaceEditPermission( $title, $user, $action, $errors );
+ }
+
+ if ( $this->protectionValidator->hasEditProtectionOnNamespace( $title ) ) {
+ return $this->checkEditPermissionOn( $title, $user, $action, $errors );
+ }
+
+ return true;
+ }
+
+ private function checkMwNamespacePatternEditPermission( Title &$title, User $user, $action, &$errors ) {
+
+ // @see https://www.semantic-mediawiki.org/wiki/Help:Special_property_Allows_pattern
+ if ( $title->getDBKey() !== AllowsPatternValue::REFERENCE_PAGE_ID || $user->isAllowed( 'smw-patternedit' ) ) {
+ return true;
+ }
+
+ $errors[] = [ 'smw-patternedit-protection', 'smw-patternedit' ];
+
+ return false;
+ }
+
+ private function checkSchemaNamespacePermission( Title &$title, User $user, $action, &$errors ) {
+
+ if ( !$user->isAllowed( 'smw-schemaedit' ) ) {
+ $errors[] = [ 'smw-schema-namespace-edit-protection', 'smw-schemaedit' ];
+ return false;
+ }
+
+ // Disallow to change the content model
+ if ( $action === 'editcontentmodel' ) {
+ $errors[] = [ 'smw-schema-namespace-editcontentmodel-disallowed' ];
+ return false;
+ }
+
+ return true;
+ }
+
+ private function checkPropertyNamespaceCreatePermission( Title &$title, User $user, $action, &$errors ) {
+
+ $createProtectionRight = $this->protectionValidator->getCreateProtectionRight();
+
+ if ( $user->isAllowed( $createProtectionRight ) ) {
+ return $this->checkPropertyNamespaceEditPermission( $title, $user, $action, $errors );;
+ }
+
+ $msg = 'smw-create-protection';
+
+ if ( $title->exists() ) {
+ $msg = 'smw-create-protection-exists';
+ }
+
+ $errors[] = [ $msg, $title->getText(), $createProtectionRight ];
+
+ return false;
+ }
+
+ private function checkPropertyNamespaceEditPermission( Title &$title, User $user, $action, &$errors ) {
+
+ // This renders full protection until the ChangePropagationDispatchJob was run
+ if ( !$this->protectionValidator->hasChangePropagationProtection( $title ) ) {
+ return $this->checkEditPermissionOn( $title, $user, $action, $errors );
+ }
+
+ $errors[] = [ 'smw-change-propagation-protection' ];
+
+ return false;
+ }
+
+ private function checkChangePropagationProtection( Title &$title, User $user, $action, &$errors ) {
+
+ // This renders full protection until the ChangePropagationDispatchJob was run
+ if ( !$this->protectionValidator->hasChangePropagationProtection( $title ) ) {
+ return true;
+ }
+
+ $errors[] = [ 'smw-change-propagation-protection' ];
+
+ return false;
+ }
+
+ private function checkEditPermissionOn( Title &$title, User $user, $action, &$errors ) {
+
+ $editProtectionRight = $this->protectionValidator->getEditProtectionRight();
+
+ // @see https://www.semantic-mediawiki.org/wiki/Help:Special_property_Is_edit_protected
+ if ( !$this->protectionValidator->hasProtection( $title ) || $user->isAllowed( $editProtectionRight ) ) {
+ return true;
+ }
+
+ $errors[] = [ 'smw-edit-protection', $editProtectionRight ];
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PostProcHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/PostProcHandler.php
new file mode 100644
index 00000000..beb4a0e8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PostProcHandler.php
@@ -0,0 +1,362 @@
+<?php
+
+namespace SMW;
+
+use Html;
+use Onoi\Cache\Cache;
+use ParserOutput;
+use SMW\SQLStore\ChangeOp\ChangeDiff;
+use SMW\SQLStore\QueryDependency\DependencyLinksUpdateJournal;
+use SMWQuery as Query;
+use Title;
+use WebRequest;
+
+/**
+ * Some updates require to be handled in a "post" process meaning after an update
+ * has already taken place to iterate over those results as input for a value
+ * dependency.
+ *
+ * The post process can only happen after the Store and hereby related processes
+ * have been updated. A simple null edit is in most cases inappropriate and
+ * therefore it is necessary to a complete a re-parse (triggered by the UpdateJob)
+ * to ensure consistency among the stored and displayed data.
+ *
+ * The PostProc relies on an API request to initiate related updates and once
+ * finished will handle the reload of the page.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PostProcHandler {
+
+ /**
+ * Identifier on whether an update to subject is to be carried out or not
+ * given the query reference used as part of an @annotation request.
+ */
+ const POST_EDIT_UPDATE = 'smw-postedit-update';
+
+ /**
+ * Check registered queries and its results on wether the result_hash before
+ * and after is different or not.
+ */
+ const POST_EDIT_CHECK = 'smw-postedit-check';
+
+ /**
+ * Specifies the TTL for the temporary tracking of a post edit
+ * update.
+ */
+ const POST_UPDATE_TTL = 86400;
+
+ /**
+ * @var ParserOutput
+ */
+ private $parserOutput;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var boolean
+ */
+ private $isEnabled = true;
+
+ /**
+ * @var []
+ */
+ private $options = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param ParserOutput $parserOutput
+ * @param Cache $cache
+ */
+ public function __construct( ParserOutput $parserOutput, Cache $cache ) {
+ $this->parserOutput = $parserOutput;
+ $this->cache = $cache;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isEnabled
+ */
+ public function isEnabled( $isEnabled ) {
+ $this->isEnabled = (bool)$isEnabled;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $options
+ */
+ public function setOptions( array $options ) {
+ $this->options = $options;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getOption( $key, $default = false ) {
+
+ if ( isset( $this->options[$key] ) ) {
+ return $this->options[$key];
+ }
+
+ return $default;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array|string
+ */
+ public function getModules() {
+ return 'ext.smw.postproc';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param WebRequest $webRequest
+ *
+ * @return string
+ */
+ public function getHtml( Title $title, WebRequest $webRequest ) {
+
+ if ( $this->isEnabled === false ) {
+ return '';
+ }
+
+ $subject = DIWikiPage::newFromTitle(
+ $title
+ );
+
+ $attributes = [
+ 'class' => 'smw-postproc',
+ 'data-subject' => $subject->getHash()
+ ];
+
+ // Ensure to detect the post edit process to distinguish between an edit
+ // event and any other post, get request in order to only sent a html
+ // fragment once on the edit request and avoid an infinite loop when the
+ // page is reloaded using an API request
+ // @see Article::view
+ $postEdit = $webRequest->getCookie(
+ \EditPage::POST_EDIT_COOKIE_KEY_PREFIX . $title->getLatestRevID()
+ );
+
+ $jobs = [];
+
+ if ( $postEdit !== null && isset( $this->options['run-jobs'] ) ) {
+ $jobs = $this->find_jobs( $this->options['run-jobs'] );
+ }
+
+ if ( $jobs !== [] ) {
+ $attributes['data-jobs'] = json_encode( $jobs );
+ }
+
+ // Was the edit SMW specific or contains it an unrelated (e.g altered
+ // some text unrelated to any property/value annotation) change?
+ if ( $postEdit !== null && ( $changeDiff = ChangeDiff::fetch( $this->cache, $subject ) ) !== false ) {
+ $postEdit = $this->checkDiff( $changeDiff );
+ }
+
+ // Is `@annotation` available as part of a #ask query?
+ $refs = $this->parserOutput->getExtensionData( self::POST_EDIT_UPDATE );
+
+ if ( $refs !== null && $refs !== [] ) {
+ $postEdit = $this->checkRef( $title, $postEdit );
+ }
+
+ if ( $postEdit !== null && $refs !== null && $refs !== [] ) {
+ $attributes['data-ref'] = json_encode( array_keys( $refs ) );
+ }
+
+ if (
+ $postEdit !== null &&
+ isset( $this->options['check-query'] ) &&
+ ( $queries = $this->parserOutput->getExtensionData( self::POST_EDIT_CHECK ) ) !== null ) {
+ $attributes['data-query'] = json_encode( $queries );
+ }
+
+ // The element is only added temporarily in the event of a postEdit, a
+ // reload of the page will not have the cookie being set and is therefore
+ // neglected
+ if ( $postEdit !== null || $jobs !== [] ) {
+ return Html::rawElement( 'div', $attributes );
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ */
+ public function addUpdate( Query $query ) {
+
+ // Query:getHash returns a hash based on a fingerprint
+ // (when $smwgQueryResultCacheType is set) that eliminates duplicate
+ // queries, yet for the post processing it is necessary to know each
+ // single query (same-condition, different printout) to allow running
+ // alternating updates as in case of cascading value dependencies
+ $queryRef = HashBuilder::createFromArray( $query->toArray() );
+
+ $data = $this->parserOutput->getExtensionData( self::POST_EDIT_UPDATE );
+
+ if ( $data === null ) {
+ $data = [];
+ }
+
+ $data[$queryRef] = true;
+
+ $this->parserOutput->setExtensionData(
+ self::POST_EDIT_UPDATE,
+ $data
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ */
+ public function addCheck( Query $query ) {
+
+ if ( !isset( $this->options['check-query'] ) || $this->options['check-query'] === false ) {
+ return;
+ }
+
+ $q_array = $query->toArray();
+
+ // Build a concatenated hash from the query and the result_hash
+ $hash = md5( json_encode( $q_array ) ) . '#';
+ $data = $this->parserOutput->getExtensionData( self::POST_EDIT_CHECK );
+
+ if ( $data === null ) {
+ $data = [];
+ }
+
+ // Use the result hash to determine whether results differ during the
+ // post-edit examination when running the same query
+ if ( $query->getOption( 'result_hash' ) ) {
+ $hash .= $query->getOption( 'result_hash' );
+ }
+
+ $data[$hash] = $q_array;
+
+ $this->parserOutput->setExtensionData(
+ self::POST_EDIT_CHECK,
+ $data
+ );
+ }
+
+ private function checkRef( $title, $postEdit ) {
+
+ $key = DependencyLinksUpdateJournal::makeKey( $title );
+
+ // Is a postEdit, mark the update to avoid running in circles
+ // when the pageCache is purged, use the latestRevID to distinguish
+ // content changes
+ if ( $postEdit !== null ) {
+
+ $record = [
+ $title->getLatestRevID() => true
+ ];
+
+ $this->cache->save( $key . ':post', $record, self::POST_UPDATE_TTL );
+
+ return $postEdit;
+ }
+
+ // Run outside of a postEdit, check if the dependency journal contains an
+ // active reference to the article and run once (== hash that set by the
+ // dependency journal which is == revID that initiated the change)
+ $hash = $this->cache->fetch( $key );
+ $record = $this->cache->fetch( $key . ':post' );
+
+ if ( $hash !== false && ( $record === false || !isset( $record[$hash] ) ) ) {
+ $postEdit = true;
+
+ if ( !is_array( $record ) ) {
+ $record = [];
+ }
+
+ $record[$hash] = true;
+
+ // Add an update marker (1h) to avoid running twice in case the
+ // journal reference hasn't been deleted yet as result of an existing
+ // PostProcHandler update request.
+ $this->cache->save( $key . ':post', $record, self::POST_UPDATE_TTL );
+ }
+
+ return $postEdit;
+ }
+
+ private function checkDiff( $changeDiff ) {
+
+ $propertyList = $changeDiff->getPropertyList(
+ 'flip'
+ );
+
+ // Investigate whether the changeDiff contains a user invoked modification
+ // and if so, allow the postEdit process to continue in order to act
+ // on SMW data and not on text that doesn't involve changes to a property
+ // value pair.
+ foreach ( $changeDiff->getTableChangeOps() as $tableChangeOp ) {
+ foreach ( $tableChangeOp->getFieldChangeOps() as $fieldChangeOp ) {
+ $pid = $fieldChangeOp->get( 'p_id' );
+
+ if ( !isset( $propertyList[$pid] ) ) {
+ continue;
+ }
+
+ // Does the change involve an operation with a user defined
+ // property?
+ //
+ // Some data were altered but since we cannot (within the request
+ // framework and without further computation) anticipate whether
+ // this influences a query or not, it is a good enough heuristic
+ // to allow to continue the postProc.
+ if ( $propertyList[$pid]{0} !== '_' ) {
+ return true;
+ }
+
+ if ( $propertyList[$pid] === '_INST' || $propertyList[$pid] === '_ASK' ) {
+ return true;
+ }
+ }
+ }
+
+ // Avoid any update since the condition of the diff containing any altered
+ // SMW data was not meet.
+ return null;
+ }
+
+ private function find_jobs( $jobs ) {
+
+ // Not enabled, no need to invoke a job!
+ if ( isset( $this->options['smwgEnabledQueryDependencyLinksStore'] ) && $this->options['smwgEnabledQueryDependencyLinksStore'] === false ) {
+ unset( $jobs['smw.parserCachePurge'] );
+ }
+
+ if ( isset( $this->options['smwgEnabledFulltextSearch'] ) && $this->options['smwgEnabledFulltextSearch'] === false ) {
+ unset( $jobs['smw.fulltextSearchTableUpdate'] );
+ }
+
+ return $jobs;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/ProcessingErrorMsgHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/ProcessingErrorMsgHandler.php
new file mode 100644
index 00000000..eb1edfa2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/ProcessingErrorMsgHandler.php
@@ -0,0 +1,246 @@
+<?php
+
+namespace SMW;
+
+use SMWContainerSemanticData as ContainerSemanticData;
+use SMWDataValue as DataValue;
+use SMWDIBlob as DIBlob;
+use SMWDIContainer as DIContainer;
+
+/**
+ * The handler encodes errors into a representation that can be retrieved from
+ * the back-end and turn it into a string representation at a convenient time.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ProcessingErrorMsgHandler {
+
+ /**
+ * @var DIWikiPage
+ */
+ private $subject;
+
+ /**
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ */
+ public function __construct( DIWikiPage $subject ) {
+ $this->subject = $subject;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $message
+ *
+ * @return DIProperty|null
+ */
+ public static function grepPropertyFromRestrictionErrorMsg( $message ) {
+ return PropertyRestrictionExaminer::grepPropertyFromRestrictionErrorMsg( $message );
+ }
+
+ /**
+ * Turns an encoded array of messages or text elements into a compacted array
+ * with msg keys and arguments.
+ *
+ * @since 2.5
+ *
+ * @param array $messages
+ * @param integer|null $type
+ * @param integer|null $language
+ *
+ * @return array
+ */
+ public static function normalizeAndDecodeMessages( array $messages, $type = null, $language = null ) {
+
+ $normalizedMessages = [];
+
+ if ( $type === null ) {
+ $type = Message::TEXT;
+ }
+
+ if ( $language === null ) {
+ $language = Message::USER_LANGUAGE;
+ }
+
+ foreach ( $messages as $message ) {
+
+ if ( is_array( $message ) ) {
+ foreach ( self::normalizeAndDecodeMessages( $message ) as $msg ) {
+ if ( is_string( $msg ) ) {
+ $normalizedMessages[md5($msg)] = $msg;
+ } else {
+ $normalizedMessages[] = $msg;
+ }
+ }
+ continue;
+ }
+
+ $exists = false;
+
+ if ( is_string( $message ) && ( $decodedMessage = Message::decode( $message, $type, $language ) ) !== false ) {
+ $message = $decodedMessage;
+ $exists = true;
+ }
+
+ if ( !$exists && is_string( $message ) && wfMessage( $message )->exists() ) {
+ $message = Message::get( $message, $type, $language );
+ }
+
+ if ( is_string( $message ) ) {
+ $normalizedMessages[md5($message)] = $message;
+ } else {
+ $normalizedMessages[] = $message;
+ }
+ }
+
+ return array_values( $normalizedMessages );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $messages
+ * @param integer|null $type
+ * @param integer|null $language
+ *
+ * @return string
+ */
+ public static function getMessagesAsString( array $messages, $type = null, $language = null ) {
+
+ $normalizedMessages = self::normalizeAndDecodeMessages( $messages, $type, $language );
+ $msg = [];
+
+ foreach ( $normalizedMessages as $message ) {
+
+ if ( !is_string( $message ) ) {
+ continue;
+ }
+
+ $msg[] = $message;
+ }
+
+ return implode( ',', $msg );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param SemanticData $semanticData
+ * @param DIContainer|null $container
+ */
+ public function addToSemanticData( SemanticData $semanticData, DIContainer $container = null ) {
+
+ if ( $container === null ) {
+ return;
+ }
+
+ $semanticData->addPropertyObjectValue(
+ new DIProperty( '_ERRC' ),
+ $container
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array|string $errorMsg
+ * @param DIProperty|null $property
+ *
+ * @return DIContainer
+ */
+ public function newErrorContainerFromMsg( $error, DIProperty $property = null ) {
+
+ if ( $property !== null && $property->isInverse() ) {
+ $property = new DIProperty( $property->getKey() );
+ }
+
+ $error = Message::encode( $error );
+ $hash = $error;
+
+ if ( $property !== null ) {
+ $hash .= $property->getKey();
+ }
+
+ $containerSemanticData = $this->newContainerSemanticData( $hash );
+
+ $this->addToContainerSemanticData(
+ $containerSemanticData,
+ $property,
+ $error
+ );
+
+ return new DIContainer( $containerSemanticData );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DataValue $dataValue
+ *
+ * @return DIContainer|null
+ */
+ public function newErrorContainerFromDataValue( DataValue $dataValue ) {
+
+ if ( $dataValue->getErrors() === [] ) {
+ return null;
+ }
+
+ $property = $dataValue->getProperty();
+
+ if ( $property !== null ) {
+ $hash = $property->getKey();
+ } else {
+ $hash = $dataValue->getDataItem()->getHash();
+ }
+
+ $containerSemanticData = $this->newContainerSemanticData( $hash );
+
+ foreach ( $dataValue->getErrors() as $error ) {
+ $this->addToContainerSemanticData( $containerSemanticData, $property, Message::encode( $error ) );
+ }
+
+ return new DIContainer( $containerSemanticData );
+ }
+
+ private function addToContainerSemanticData( $containerSemanticData, $property, $error ) {
+
+ if ( $property !== null ) {
+ $containerSemanticData->addPropertyObjectValue(
+ new DIProperty( '_ERRP' ),
+ new DIWikiPage( $property->getKey(), SMW_NS_PROPERTY )
+ );
+ }
+
+ $containerSemanticData->addPropertyObjectValue(
+ new DIProperty( '_ERRT' ),
+ new DIBlob( $error )
+ );
+ }
+
+ private function newContainerSemanticData( $hash ) {
+
+ if ( $this->subject === null ) {
+ $containerSemanticData = ContainerSemanticData::makeAnonymousContainer();
+ $containerSemanticData->skipAnonymousCheck();
+ } else {
+ $subobjectName = '_ERR' . md5( $hash );
+
+ $subject = new DIWikiPage(
+ $this->subject->getDBkey(),
+ $this->subject->getNamespace(),
+ $this->subject->getInterwiki(),
+ $subobjectName
+ );
+
+ $containerSemanticData = new ContainerSemanticData( $subject );
+ }
+
+ return $containerSemanticData;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAliasFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAliasFinder.php
new file mode 100644
index 00000000..d43c057d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAliasFinder.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace SMW;
+
+use Onoi\Cache\Cache;
+
+/**
+ * @license GNU GPL v2
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class PropertyAliasFinder {
+
+ /**
+ * Identifies the cache namespace
+ */
+ const CACHE_NAMESPACE = 'smw:property:alias';
+
+ /**
+ * Identifies the cache TTL (one week)
+ */
+ const CACHE_TTL = 604800;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * Array with entries "property alias" => "property id"
+ *
+ * @var string[]
+ */
+ private $propertyAliases = [];
+
+ /**
+ * @var string[]
+ */
+ private $propertyAliasesByMsgKey = [];
+
+ /**
+ * @var string[]
+ */
+ private $canonicalPropertyAliases = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param Cache $cache
+ * @param array $propertyAliases
+ * @param array $canonicalPropertyAliases
+ */
+ public function __construct( Cache $cache, array $propertyAliases = [], array $canonicalPropertyAliases = [] ) {
+ $this->cache = $cache;
+ $this->canonicalPropertyAliases = $canonicalPropertyAliases;
+
+ foreach ( $propertyAliases as $alias => $id ) {
+ $this->registerAliasByFixedLabel( $id, $alias );
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getKnownPropertyAliases() {
+ return $this->propertyAliases;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getKnownPropertyAliasesWithMsgKey() {
+ return $this->propertyAliasesByMsgKey;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $languageCode
+ *
+ * @return array
+ */
+ public function getKnownPropertyAliasesByLanguageCode( $languageCode = 'en' ) {
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [
+ $languageCode,
+ $this->propertyAliasesByMsgKey
+ ]
+ );
+
+ if ( ( $propertyAliases = $this->cache->fetch( $key ) ) !== false ) {
+ return $propertyAliases;
+ }
+
+ $propertyAliases = [];
+
+ foreach ( $this->propertyAliasesByMsgKey as $msgKey => $id ) {
+ $propertyAliases[Message::get( $msgKey, Message::TEXT, $languageCode )] = $id;
+ }
+
+ $this->cache->save( $key, $propertyAliases, self::CACHE_TTL );
+
+ return $propertyAliases;
+ }
+
+ /**
+ * Add a new alias label to an existing property ID. Note that every ID
+ * should have a primary label.
+ *
+ * @param string $id string
+ * @param string $label
+ */
+ public function registerAliasByFixedLabel( $id, $label ) {
+
+ // Prevent an extension to register an already known
+ // label
+ if ( isset( $this->canonicalPropertyAliases[$label] ) && $this->canonicalPropertyAliases[$label] !== $id ) {
+ return;
+ }
+
+ // Indicates an untranslated MW message key
+ if ( $label !== '' && $label{0} === '<' ) {
+ return null;
+ }
+
+ $this->propertyAliases[$label] = $id;
+ }
+
+ /**
+ * Register an alias using a message key to allow fetching localized
+ * labels dynamically.
+ *
+ * @since 2.4
+ *
+ * @param string $id
+ * @param string $msgKey
+ */
+ public function registerAliasByMsgKey( $id, $msgKey ) {
+ $this->propertyAliasesByMsgKey[$msgKey] = $id;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $id
+ *
+ * @return string|boolean
+ */
+ public function findCanonicalPropertyAliasById( $id ) {
+ return array_search( $id, $this->canonicalPropertyAliases );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $id
+ *
+ * @return string|boolean
+ */
+ public function findPropertyAliasById( $id ) {
+ return array_search( $id, $this->propertyAliases );
+ }
+
+ /**
+ * Find and return the ID for the pre-defined property of the given
+ * local label. If the label does not belong to a pre-defined property,
+ * return false.
+ *
+ * @param string $alias
+ *
+ * @return string|boolean
+ */
+ public function findPropertyIdByAlias( $alias ) {
+
+ if ( isset( $this->propertyAliases[$alias] ) ) {
+ return $this->propertyAliases[$alias];
+ } elseif ( isset( $this->canonicalPropertyAliases[$alias] ) ) {
+ return $this->canonicalPropertyAliases[$alias];
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotator.php
new file mode 100644
index 00000000..045f8959
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotator.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace SMW;
+
+/**
+ * Interface specifying available methods to interact with the Decorator
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+interface PropertyAnnotator {
+
+ /**
+ * Returns a SemanticData container
+ *
+ * @since 1.9
+ *
+ * @return SemanticData
+ */
+ public function getSemanticData();
+
+ /**
+ * Add annotations to the SemanticData container
+ *
+ * @since 1.9
+ *
+ * @return PropertyAnnotator
+ */
+ public function addAnnotation();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotatorFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotatorFactory.php
new file mode 100644
index 00000000..ae19b43c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotatorFactory.php
@@ -0,0 +1,226 @@
+<?php
+
+namespace SMW;
+
+use SMw\MediaWiki\RedirectTargetFinder;
+use SMW\PropertyAnnotators\CategoryPropertyAnnotator;
+use SMW\PropertyAnnotators\DisplayTitlePropertyAnnotator;
+use SMW\PropertyAnnotators\EditProtectedPropertyAnnotator;
+use SMW\PropertyAnnotators\MandatoryTypePropertyAnnotator;
+use SMW\PropertyAnnotators\NullPropertyAnnotator;
+use SMW\PropertyAnnotators\PredefinedPropertyAnnotator;
+use SMW\PropertyAnnotators\RedirectPropertyAnnotator;
+use SMW\PropertyAnnotators\SchemaPropertyAnnotator;
+use SMW\PropertyAnnotators\SortKeyPropertyAnnotator;
+use SMW\PropertyAnnotators\TranslationPropertyAnnotator;
+use SMW\Store;
+use SMW\Schema\Schema;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class PropertyAnnotatorFactory {
+
+ /**
+ * @since 2.0
+ *
+ * @param SemanticData $semanticData
+ *
+ * @return NullPropertyAnnotator
+ */
+ public function newNullPropertyAnnotator( SemanticData $semanticData ) {
+ return new NullPropertyAnnotator( $semanticData );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param SemanticData $semanticData
+ * @param RedirectTargetFinder $redirectTargetFinder
+ *
+ * @return RedirectPropertyAnnotator
+ */
+ public function newRedirectPropertyAnnotator( PropertyAnnotator $propertyAnnotator, RedirectTargetFinder $redirectTargetFinder ) {
+ return new RedirectPropertyAnnotator(
+ $propertyAnnotator,
+ $redirectTargetFinder
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param Schema $schema
+ *
+ * @return SchemaPropertyAnnotator
+ */
+ public function newSchemaPropertyAnnotator( PropertyAnnotator $propertyAnnotator, Schema $schema = null ) {
+
+ $schemaPropertyAnnotator = new SchemaPropertyAnnotator(
+ $propertyAnnotator,
+ $schema
+ );
+
+ return $schemaPropertyAnnotator;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param SemanticData $semanticData
+ * @param PageInfo $pageInfo
+ *
+ * @return PredefinedPropertyAnnotator
+ */
+ public function newPredefinedPropertyAnnotator( PropertyAnnotator $propertyAnnotator, PageInfo $pageInfo ) {
+
+ $predefinedPropertyAnnotator = new PredefinedPropertyAnnotator(
+ $propertyAnnotator,
+ $pageInfo
+ );
+
+ $predefinedPropertyAnnotator->setPredefinedPropertyList(
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgPageSpecialProperties' )
+ );
+
+ return $predefinedPropertyAnnotator;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param SemanticData $semanticData
+ * @param Title $title
+ *
+ * @return EditProtectedPropertyAnnotator
+ */
+ public function newEditProtectedPropertyAnnotator( PropertyAnnotator $propertyAnnotator, Title $title ) {
+
+ $editProtectedPropertyAnnotator = new EditProtectedPropertyAnnotator(
+ $propertyAnnotator,
+ $title
+ );
+
+ $editProtectedPropertyAnnotator->setEditProtectionRight(
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgEditProtectionRight' )
+ );
+
+ return $editProtectedPropertyAnnotator;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param SemanticData $semanticData
+ * @param string $sortkey
+ *
+ * @return SortKeyPropertyAnnotator
+ */
+ public function newSortKeyPropertyAnnotator( PropertyAnnotator $propertyAnnotator, $sortkey ) {
+ return new SortKeyPropertyAnnotator(
+ $propertyAnnotator,
+ $sortkey
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param SemanticData $semanticData
+ * @param arrat|null $translation
+ *
+ * @return TranslationPropertyAnnotator
+ */
+ public function newTranslationPropertyAnnotator( PropertyAnnotator $propertyAnnotator, $translation ) {
+
+ $translationPropertyAnnotator = new TranslationPropertyAnnotator(
+ $propertyAnnotator,
+ $translation
+ );
+
+ $translationPropertyAnnotator->setPredefinedPropertyList(
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgPageSpecialProperties' )
+ );
+
+ return $translationPropertyAnnotator;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param SemanticData $semanticData
+ * @param string|false $displayTitle
+ * @param string $defaultSort
+ *
+ * @return DisplayTitlePropertyAnnotator
+ */
+ public function newDisplayTitlePropertyAnnotator( PropertyAnnotator $propertyAnnotator, $displayTitle, $defaultSort ) {
+
+ $displayTitlePropertyAnnotator = new DisplayTitlePropertyAnnotator(
+ $propertyAnnotator,
+ $displayTitle,
+ $defaultSort
+ );
+
+ $displayTitlePropertyAnnotator->canCreateAnnotation(
+ ( ApplicationFactory::getInstance()->getSettings()->get( 'smwgDVFeatures' ) & SMW_DV_WPV_DTITLE ) != 0
+ );
+
+ return $displayTitlePropertyAnnotator;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param SemanticData $semanticData
+ * @param array $categories
+ *
+ * @return CategoryPropertyAnnotator
+ */
+ public function newCategoryPropertyAnnotator( PropertyAnnotator $propertyAnnotator, array $categories ) {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $categoryPropertyAnnotator = new CategoryPropertyAnnotator(
+ $propertyAnnotator,
+ $categories
+ );
+
+ $categoryPropertyAnnotator->showHiddenCategories(
+ $settings->isFlagSet( 'smwgParserFeatures', SMW_PARSER_HID_CATS )
+ );
+
+ $categoryPropertyAnnotator->useCategoryInstance(
+ $settings->isFlagSet( 'smwgCategoryFeatures', SMW_CAT_INSTANCE )
+ );
+
+ $categoryPropertyAnnotator->useCategoryHierarchy(
+ $settings->isFlagSet( 'smwgCategoryFeatures', SMW_CAT_HIERARCHY )
+ );
+
+ $categoryPropertyAnnotator->useCategoryRedirect(
+ $settings->isFlagSet( 'smwgCategoryFeatures', SMW_CAT_REDIRECT )
+ );
+
+ return $categoryPropertyAnnotator;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param SemanticData $semanticData
+ *
+ * @return MandatoryTypePropertyAnnotator
+ */
+ public function newMandatoryTypePropertyAnnotator( PropertyAnnotator $propertyAnnotator ) {
+ return new MandatoryTypePropertyAnnotator(
+ $propertyAnnotator
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/CategoryPropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/CategoryPropertyAnnotator.php
new file mode 100644
index 00000000..2b2201f3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/CategoryPropertyAnnotator.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\ProcessingErrorMsgHandler;
+use SMW\PropertyAnnotator;
+
+/**
+ * Handling category annotation
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class CategoryPropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * @var array
+ */
+ private $categories;
+
+ /**
+ * @var array|null
+ */
+ private $hiddenCategories = null;
+
+ /**
+ * @var boolean
+ */
+ private $showHiddenCategories = true;
+
+ /**
+ * @var boolean
+ */
+ private $useCategoryInstance = true;
+
+ /**
+ * @var boolean
+ */
+ private $useCategoryHierarchy = true;
+
+ /**
+ * @var boolean
+ */
+ private $useCategoryRedirect = true;
+
+ /**
+ * @since 1.9
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param array $categories
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator, array $categories ) {
+ parent::__construct( $propertyAnnotator );
+ $this->categories = $categories;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param boolean $showHiddenCategories
+ */
+ public function showHiddenCategories( $showHiddenCategories ) {
+ $this->showHiddenCategories = (bool)$showHiddenCategories;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param boolean $useCategoryInstance
+ */
+ public function useCategoryInstance( $useCategoryInstance ) {
+ $this->useCategoryInstance = (bool)$useCategoryInstance;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param boolean $useCategoryHierarchy
+ */
+ public function useCategoryHierarchy( $useCategoryHierarchy ) {
+ $this->useCategoryHierarchy = (bool)$useCategoryHierarchy;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $useCategoryRedirect
+ */
+ public function useCategoryRedirect( $useCategoryRedirect ) {
+ $this->useCategoryRedirect = (bool)$useCategoryRedirect;
+ }
+
+ /**
+ * @see PropertyAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+
+ $namespace = $this->getSemanticData()->getSubject()->getNamespace();
+ $property = null;
+
+ $this->processingErrorMsgHandler = new ProcessingErrorMsgHandler(
+ $this->getSemanticData()->getSubject()
+ );
+
+ if ( $this->useCategoryInstance && ( $namespace !== NS_CATEGORY ) ) {
+ $property = new DIProperty( DIProperty::TYPE_CATEGORY );
+ }
+
+ if ( $this->useCategoryHierarchy && ( $namespace === NS_CATEGORY ) ) {
+ $property = new DIProperty( DIProperty::TYPE_SUBCATEGORY );
+ }
+
+ foreach ( $this->categories as $catname ) {
+
+ if ( ( !$this->showHiddenCategories && $this->isHiddenCategory( $catname ) ) || $property === null ) {
+ continue;
+ }
+
+ $this->modifySemanticData( $property, $catname );
+ }
+ }
+
+ private function modifySemanticData( $property, $catname ) {
+
+ $cat = new DIWikiPage( $catname, NS_CATEGORY );
+
+ if ( ( $cat = $this->getRedirectTarget( $cat ) ) && $cat->getNamespace() === NS_CATEGORY ) {
+ return $this->getSemanticData()->addPropertyObjectValue(
+ $property,
+ $cat
+ );
+ }
+
+ $container = $this->processingErrorMsgHandler->newErrorContainerFromMsg(
+ [
+ 'smw-category-invalid-redirect-target',
+ str_replace( '_', ' ', $catname )
+ ]
+ );
+
+ $this->processingErrorMsgHandler->addToSemanticData(
+ $this->getSemanticData(),
+ $container
+ );
+ }
+
+ private function isHiddenCategory( $catName ) {
+
+ if ( $this->hiddenCategories === null ) {
+
+ $wikipage = ApplicationFactory::getInstance()->newPageCreator()->createPage(
+ $this->getSemanticData()->getSubject()->getTitle()
+ );
+
+ $this->hiddenCategories = $wikipage->getHiddenCategories();
+ }
+
+ foreach ( $this->hiddenCategories as $hiddenCategory ) {
+
+ if ( $hiddenCategory->getText() === $catName ) {
+ return true;
+ };
+
+ }
+
+ return false;
+ }
+
+ private function getRedirectTarget( $subject ) {
+
+ if ( $this->useCategoryRedirect ) {
+ return ApplicationFactory::getInstance()->getStore()->getRedirectTarget( $subject );
+ }
+
+ return $subject;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/DisplayTitlePropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/DisplayTitlePropertyAnnotator.php
new file mode 100644
index 00000000..bc7bbd8e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/DisplayTitlePropertyAnnotator.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\PropertyAnnotator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class DisplayTitlePropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * @var string|false
+ */
+ private $displayTitle;
+
+ /**
+ * @var string
+ */
+ private $defaultSort;
+
+ /**
+ * @var boolean
+ */
+ private $canCreateAnnotation = true;
+
+ /**
+ * @since 2.4
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param string|false $displayTitle
+ * @param string $defaultSort
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator, $displayTitle = false, $defaultSort = '' ) {
+ parent::__construct( $propertyAnnotator );
+ $this->displayTitle = $displayTitle;
+ $this->defaultSort = $defaultSort;
+ }
+
+ /**
+ * @see SMW_DV_WPV_DTITLE in $GLOBALS['smwgDVFeatures']
+ *
+ * @since 2.5
+ *
+ * @param boolean $canCreateAnnotation
+ */
+ public function canCreateAnnotation( $canCreateAnnotation ) {
+ $this->canCreateAnnotation = (bool)$canCreateAnnotation;
+ }
+
+ protected function addPropertyValues() {
+
+ if ( !$this->canCreateAnnotation || !$this->displayTitle || $this->displayTitle === '' ) {
+ return;
+ }
+
+ // #1439, #1611
+ $dataItem = $this->dataItemFactory->newDIBlob(
+ strip_tags( htmlspecialchars_decode( $this->displayTitle, ENT_QUOTES ) )
+ );
+
+ $this->getSemanticData()->addPropertyObjectValue(
+ $this->dataItemFactory->newDIProperty( '_DTITLE' ),
+ $dataItem
+ );
+
+ // If the defaultSort is empty then no explicit sortKey was expected
+ // therefore use the title content before the SortKeyPropertyAnnotator
+ if ( $this->defaultSort === '' ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ $this->dataItemFactory->newDIProperty( '_SKEY' ),
+ $dataItem
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/EditProtectedPropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/EditProtectedPropertyAnnotator.php
new file mode 100644
index 00000000..8bb392f0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/EditProtectedPropertyAnnotator.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use Html;
+use ParserOutput;
+use SMW\Message;
+use SMW\PropertyAnnotator;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class EditProtectedPropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * Indicates whether the annotation was maintained by
+ * the system or not.
+ */
+ const SYSTEM_ANNOTATION = 'editprotectedpropertyannotator.system.annotation';
+
+ /**
+ * @var Title
+ */
+ private $title;
+
+ /**
+ * @var boolean
+ */
+ private $editProtectionRight = false;
+
+ /**
+ * @since 1.9
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param Title $title
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator, Title $title ) {
+ parent::__construct( $propertyAnnotator );
+ $this->title = $title;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|boolean $editProtectionRight
+ */
+ public function setEditProtectionRight( $editProtectionRight ) {
+ $this->editProtectionRight = $editProtectionRight;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ParserOutput
+ */
+ public function addTopIndicatorTo( ParserOutput $parserOutput ) {
+
+ if ( $this->editProtectionRight === false ) {
+ return false;
+ }
+
+ // FIXME 3.0; Only MW 1.25+ (ParserOutput::setIndicator)
+ if ( !method_exists( $parserOutput, 'setIndicator' ) ) {
+ return false;
+ }
+
+ $property = $this->dataItemFactory->newDIProperty( '_EDIP' );
+
+ if ( !$this->isEnabledProtection( $property ) && !$this->hasEditProtection() ) {
+ return;
+ }
+
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-edit-protection',
+ 'title' => Message::get( 'smw-edit-protection-enabled', Message::TEXT, Message::USER_LANGUAGE )
+ ], ''
+ );
+
+ $parserOutput->setIndicator(
+ 'smw-protection-indicator',
+ Html::rawElement( 'div', [ 'class' => 'smw-protection-indicator' ], $html )
+ );
+ }
+
+ /**
+ * @see PropertyAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+
+ if ( $this->editProtectionRight === false ) {
+ return false;
+ }
+
+ $property = $this->dataItemFactory->newDIProperty( '_EDIP' );
+
+ if ( $this->getSemanticData()->hasProperty( $property ) || !$this->hasEditProtection() ) {
+ return;
+ }
+
+ // Notify preceding processes that this property is set as part of the
+ // protection restriction detection in order to decide whether this
+ // property was added manually or by the system
+ $dataItem = $this->dataItemFactory->newDIBoolean( true );
+ $dataItem->setOption( self::SYSTEM_ANNOTATION, true );
+
+ // Since edit protection is active, add the property as indicator this is
+ // especially to retain the status when purging a page
+ $this->getSemanticData()->addPropertyObjectValue(
+ $property,
+ $dataItem
+ );
+ }
+
+ private function hasEditProtection() {
+
+ //$this->title->flushRestrictions();
+
+ if ( !$this->title->isProtected( 'edit' ) ) {
+ return false;
+ }
+
+ $restrictions = array_flip( $this->title->getRestrictions( 'edit' ) );
+
+ // There could by any edit protections but the `Is edit protected` is
+ // bound to the `smwgEditProtectionRight` setting
+ return isset( $restrictions[$this->editProtectionRight] );
+ }
+
+ private function isEnabledProtection( $property ) {
+
+ if ( !$this->getSemanticData()->hasProperty( $property ) ) {
+ return false;
+ }
+
+ $semanticData = $this->getSemanticData();
+
+ $dataItems = $semanticData->getPropertyValues( $property );
+ $isEnabledProtection = false;
+
+ // In case of two competing values, true always wins
+ foreach ( $dataItems as $dataItem ) {
+
+ $isEnabledProtection = $dataItem->getBoolean();
+
+ if ( $isEnabledProtection ) {
+ break;
+ }
+ }
+
+ return $isEnabledProtection;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/MandatoryTypePropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/MandatoryTypePropertyAnnotator.php
new file mode 100644
index 00000000..f18ff017
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/MandatoryTypePropertyAnnotator.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\DataTypeRegistry;
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class MandatoryTypePropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * Indicates a forced removal
+ */
+ const IMPO_REMOVED_TYPE = 'mandatorytype.propertyannotator.impo.removed.type';
+
+ protected function addPropertyValues() {
+
+ $subject = $this->getSemanticData()->getSubject();
+
+ if ( $subject->getNamespace() !== SMW_NS_PROPERTY ) {
+ return;
+ }
+
+ $property = DIProperty::newFromUserLabel(
+ str_replace( '_', ' ', $subject->getDBKey() )
+ );
+
+ if ( !$property->isUserDefined() ) {
+ return;
+ }
+
+ $this->findMandatoryTypeForImportVocabulary();
+ }
+
+ private function findMandatoryTypeForImportVocabulary() {
+
+ $property = new DIProperty( '_IMPO' );
+
+ $dataItems = $this->getSemanticData()->getPropertyValues(
+ $property
+ );
+
+ if ( $dataItems === null || $dataItems === [] ) {
+ return;
+ }
+
+ $this->addTypeFromImportVocabulary( $property, current( $dataItems ) );
+ }
+
+ private function addTypeFromImportVocabulary( $property, $dataItem ) {
+
+ $importValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ if ( strpos( $importValue->getTermType(), ':' ) === false ) {
+ return;
+ }
+
+ $property = new DIProperty( '_TYPE' );
+
+ list( $ns, $type ) = explode( ':', $importValue->getTermType(), 2 );
+
+ $typeId = DataTypeRegistry::getInstance()->findTypeId( $type );
+
+ if ( $typeId === '' ) {
+ return;
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByProperty(
+ $property,
+ $typeId
+ );
+
+ $this->replaceAnyTypeByImportType( $property, $dataValue );
+ }
+
+ private function replaceAnyTypeByImportType( DIProperty $property, $dataValue ) {
+
+ foreach ( $this->getSemanticData()->getPropertyValues( $property ) as $dataItem ) {
+ $this->getSemanticData()->setOption( self::IMPO_REMOVED_TYPE, $dataItem );
+
+ $this->getSemanticData()->removePropertyObjectValue(
+ $property,
+ $dataItem
+ );
+ }
+
+ $this->getSemanticData()->addDataValue( $dataValue );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/NullPropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/NullPropertyAnnotator.php
new file mode 100644
index 00000000..ea92a40f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/NullPropertyAnnotator.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\PropertyAnnotator;
+use SMW\SemanticData;
+
+/**
+ * Root object representing the initial data transfer object to interact with
+ * a Decorator
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class NullPropertyAnnotator implements PropertyAnnotator {
+
+ /**
+ * @var SemanticData
+ */
+ private $semanticData;
+
+ /**
+ * @since 1.9
+ *
+ * @param SemanticData $semanticData
+ */
+ public function __construct( SemanticData $semanticData ) {
+ $this->semanticData = $semanticData;
+ }
+
+ /**
+ * @see PropertyAnnotator::getSemanticData
+ *
+ * @since 1.9
+ */
+ public function getSemanticData() {
+ return $this->semanticData;
+ }
+
+ /**
+ * @see PropertyAnnotator::addAnnotation
+ *
+ * @since 1.9
+ */
+ public function addAnnotation() {
+ return $this;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/PredefinedPropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/PredefinedPropertyAnnotator.php
new file mode 100644
index 00000000..9b8576e3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/PredefinedPropertyAnnotator.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\PageInfo;
+use SMW\PropertyAnnotator;
+use SMW\PropertyRegistry;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWDIBoolean as DIBoolean;
+use SMWDITime as DITime;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class PredefinedPropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * @var PageInfo
+ */
+ private $pageInfo;
+
+ /**
+ * @var array
+ */
+ private $predefinedPropertyList = [];
+
+ /**
+ * @since 1.9
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param PageInfo $pageInfo
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator, PageInfo $pageInfo ) {
+ parent::__construct( $propertyAnnotator );
+ $this->pageInfo = $pageInfo;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param array $predefinedPropertyList
+ */
+ public function setPredefinedPropertyList( array $predefinedPropertyList ) {
+ $this->predefinedPropertyList = $predefinedPropertyList;
+ }
+
+ protected function addPropertyValues() {
+
+ $cachedProperties = [];
+
+ foreach ( $this->predefinedPropertyList as $propertyId ) {
+
+ if ( $this->isRegisteredPropertyId( $propertyId, $cachedProperties ) ) {
+ continue;
+ }
+
+ $propertyDI = new DIProperty( $propertyId );
+
+ if ( $this->getSemanticData()->getPropertyValues( $propertyDI ) !== [] ) {
+ $cachedProperties[$propertyId] = true;
+ continue;
+ }
+
+ $dataItem = $this->createDataItemByPropertyId( $propertyId );
+
+ if ( $dataItem instanceof DataItem ) {
+ $cachedProperties[$propertyId] = true;
+ $this->getSemanticData()->addPropertyObjectValue( $propertyDI, $dataItem );
+ }
+ }
+ }
+
+ protected function isRegisteredPropertyId( $propertyId, $cachedProperties ) {
+ return ( PropertyRegistry::getInstance()->getPropertyValueTypeById( $propertyId ) === '' ) ||
+ array_key_exists( $propertyId, $cachedProperties );
+ }
+
+ protected function createDataItemByPropertyId( $propertyId ) {
+
+ $dataItem = null;
+
+ switch ( $propertyId ) {
+ case DIProperty::TYPE_MODIFICATION_DATE :
+ $dataItem = DITime::newFromTimestamp( $this->pageInfo->getModificationDate() );
+ break;
+ case DIProperty::TYPE_CREATION_DATE :
+ $dataItem = DITime::newFromTimestamp( $this->pageInfo->getCreationDate() );
+ break;
+ case DIProperty::TYPE_NEW_PAGE :
+ $dataItem = new DIBoolean( $this->pageInfo->isNewPage() );
+ break;
+ case DIProperty::TYPE_LAST_EDITOR :
+ $dataItem = $this->pageInfo->getLastEditor() ? DIWikiPage::newFromTitle( $this->pageInfo->getLastEditor() ) : null;
+ break;
+ case DIProperty::TYPE_MEDIA : // @codingStandardsIgnoreStart phpcs, ignore --sniffs=Generic.Files.LineLength
+ $dataItem = $this->pageInfo->isFilePage() && $this->pageInfo->getMediaType() !== '' && $this->pageInfo->getMediaType() !== null ? new DIBlob( $this->pageInfo->getMediaType() ) : null;
+ // @codingStandardsIgnoreEnd
+ break;
+ case DIProperty::TYPE_MIME : // @codingStandardsIgnoreStart phpcs, ignore --sniffs=Generic.Files.LineLength
+ $dataItem = $this->pageInfo->isFilePage() && $this->pageInfo->getMimeType() !== '' && $this->pageInfo->getMimeType() !== null ? new DIBlob( $this->pageInfo->getMimeType() ) : null;
+ // @codingStandardsIgnoreEnd
+ break;
+ }
+
+ return $dataItem;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/PropertyAnnotatorDecorator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/PropertyAnnotatorDecorator.php
new file mode 100644
index 00000000..6f5d73ab
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/PropertyAnnotatorDecorator.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\DataItemFactory;
+use SMW\PropertyAnnotator;
+
+/**
+ * Decorator that contains the reference to the invoked PropertyAnnotator
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+abstract class PropertyAnnotatorDecorator implements PropertyAnnotator {
+
+ /**
+ * @var PropertyAnnotator
+ */
+ protected $propertyAnnotator;
+
+ /**
+ * @var DataItemFactory
+ */
+ protected $dataItemFactory;
+
+ /**
+ * @since 1.9
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator ) {
+ $this->propertyAnnotator = $propertyAnnotator;
+ $this->dataItemFactory = new DataItemFactory();
+ }
+
+ /**
+ * @see PropertyAnnotator::getSemanticData
+ *
+ * @since 1.9
+ *
+ * @return SemanticData
+ */
+ public function getSemanticData() {
+ return $this->propertyAnnotator->getSemanticData();
+ }
+
+ /**
+ * @see PropertyAnnotator::addAnnotation
+ *
+ * @since 1.9
+ *
+ * @return PropertyAnnotator
+ */
+ public function addAnnotation() {
+
+ $this->propertyAnnotator->addAnnotation();
+ $this->addPropertyValues();
+
+ return $this;
+ }
+
+ /**
+ * @since 1.9
+ */
+ protected abstract function addPropertyValues();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/RedirectPropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/RedirectPropertyAnnotator.php
new file mode 100644
index 00000000..e06c3618
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/RedirectPropertyAnnotator.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\RedirectTargetFinder;
+use SMW\PropertyAnnotator;
+
+/**
+ * Handling redirect annotation
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class RedirectPropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * @var RedirectTargetFinder
+ */
+ private $redirectTargetFinder;
+
+ /**
+ * @since 1.9
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param RedirectTargetFinder $redirectTargetFinder
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator, RedirectTargetFinder $redirectTargetFinder ) {
+ parent::__construct( $propertyAnnotator );
+ $this->redirectTargetFinder = $redirectTargetFinder;
+ }
+
+ /**
+ * @see PropertyAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+
+ if ( !$this->redirectTargetFinder->hasRedirectTarget() ) {
+ return;
+ }
+
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_REDI' ),
+ DIWikiPage::newFromTitle( $this->redirectTargetFinder->getRedirectTarget() )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/SchemaPropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/SchemaPropertyAnnotator.php
new file mode 100644
index 00000000..1a84ccf5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/SchemaPropertyAnnotator.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\DIProperty;
+use SMW\PropertyAnnotator;
+use SMW\Schema\Schema;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SchemaPropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * @var Schema
+ */
+ private $schema;
+
+ /**
+ * @since 3.0
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param Schema $schema
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator, Schema $schema = null ) {
+ parent::__construct( $propertyAnnotator );
+ $this->schema = $schema;
+ }
+
+ protected function addPropertyValues() {
+
+ if ( $this->schema === null ) {
+ return;
+ }
+
+ $semanticData = $this->getSemanticData();
+
+ $semanticData->addPropertyObjectValue(
+ new DIProperty( '_SCHEMA_TYPE' ),
+ $this->dataItemFactory->newDIBlob( $this->schema->get( Schema::SCHEMA_TYPE ) )
+ );
+
+ $semanticData->addPropertyObjectValue(
+ new DIProperty( '_SCHEMA_DEF' ),
+ $this->dataItemFactory->newDIBlob( $this->schema )
+ );
+
+ if ( ( $desc = $this->schema->get( Schema::SCHEMA_DESCRIPTION, '' ) ) !== '' ) {
+ $semanticData->addPropertyObjectValue(
+ new DIProperty( '_SCHEMA_DESC' ),
+ $this->dataItemFactory->newDIBlob( $desc )
+ );
+ }
+
+ foreach ( $this->schema->get( Schema::SCHEMA_TAG, [] ) as $tag ) {
+ $semanticData->addPropertyObjectValue(
+ new DIProperty( '_SCHEMA_TAG' ),
+ $this->dataItemFactory->newDIBlob( mb_strtolower( $tag ) )
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/SortKeyPropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/SortKeyPropertyAnnotator.php
new file mode 100644
index 00000000..387608f1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/SortKeyPropertyAnnotator.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\DIProperty;
+use SMW\PropertyAnnotator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class SortKeyPropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * @var string
+ */
+ private $defaultSort;
+
+ /**
+ * @since 1.9
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param string $defaultSort
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator, $defaultSort ) {
+ parent::__construct( $propertyAnnotator );
+ $this->defaultSort = $defaultSort;
+ }
+
+ protected function addPropertyValues() {
+
+ $sortkey = $this->defaultSort ? $this->defaultSort : $this->getSemanticData()->getSubject()->getSortKey();
+
+ $property = $this->dataItemFactory->newDIProperty(
+ DIProperty::TYPE_SORTKEY
+ );
+
+ if ( !$this->getSemanticData()->hasProperty( $property ) ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ $property,
+ $this->dataItemFactory->newDIBlob( $sortkey )
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/TranslationPropertyAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/TranslationPropertyAnnotator.php
new file mode 100644
index 00000000..2eb6fbab
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyAnnotators/TranslationPropertyAnnotator.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace SMW\PropertyAnnotators;
+
+use SMW\PropertyAnnotator;
+use SMW\DataModel\ContainerSemanticData;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TranslationPropertyAnnotator extends PropertyAnnotatorDecorator {
+
+ /**
+ * @var array|null
+ */
+ private $translation;
+
+ /**
+ * @var array
+ */
+ private $predefinedPropertyList = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param PropertyAnnotator $propertyAnnotator
+ * @param array|null $translation
+ */
+ public function __construct( PropertyAnnotator $propertyAnnotator, $translation ) {
+ parent::__construct( $propertyAnnotator );
+ $this->translation = $translation;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $predefinedPropertyList
+ */
+ public function setPredefinedPropertyList( array $predefinedPropertyList ) {
+ $this->predefinedPropertyList = array_flip( $predefinedPropertyList );
+ }
+
+ protected function addPropertyValues() {
+
+ // Expected identifiers, @see https://gerrit.wikimedia.org/r/387548
+ if ( !is_array( $this->translation ) || !isset( $this->predefinedPropertyList['_TRANS'] ) ) {
+ return;
+ }
+
+ $containerSemanticData = null;
+
+ if ( isset( $this->translation['languagecode'] ) ) {
+ $languageCode = $this->translation['languagecode'];
+ $containerSemanticData = $this->newContainerSemanticData( $languageCode );
+
+ // Translation.Language code
+ $containerSemanticData->addPropertyObjectValue(
+ $this->dataItemFactory->newDIProperty( '_LCODE' ),
+ $this->dataItemFactory->newDIBlob( $languageCode )
+ );
+ }
+
+ if ( isset( $this->translation['sourcepagetitle'] ) && $this->translation['sourcepagetitle'] instanceof Title ) {
+ // Translation.Translation source
+ $containerSemanticData->addPropertyObjectValue(
+ $this->dataItemFactory->newDIProperty( '_TRANS_SOURCE' ),
+ $this->dataItemFactory->newDIWikiPage( $this->translation['sourcepagetitle'] )
+ );
+ }
+
+ if ( isset( $this->translation['messagegroupid'] ) ) {
+ // Translation.Translation group
+ $containerSemanticData->addPropertyObjectValue(
+ $this->dataItemFactory->newDIProperty( '_TRANS_GROUP' ),
+ $this->dataItemFactory->newDIBlob( $this->translation['messagegroupid'] )
+ );
+ }
+
+ if ( $containerSemanticData !== null ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ $this->dataItemFactory->newDIProperty( '_TRANS' ),
+ $this->dataItemFactory->newDIContainer( $containerSemanticData )
+ );
+ }
+ }
+
+ private function newContainerSemanticData( $languageCode ) {
+
+ $dataItem = $this->getSemanticData()->getSubject();
+ $subobjectName = 'trans.' . $languageCode;
+
+ $subject = $this->dataItemFactory->newDIWikiPage(
+ $dataItem->getDBkey(),
+ $dataItem->getNamespace(),
+ $dataItem->getInterwiki(),
+ $subobjectName
+ );
+
+ return $this->dataItemFactory->newContainerSemanticData( $subject );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyChangePropagationNotifier.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyChangePropagationNotifier.php
new file mode 100644
index 00000000..a0834810
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyChangePropagationNotifier.php
@@ -0,0 +1,252 @@
+<?php
+
+namespace SMW;
+
+use SMW\MediaWiki\Jobs\ChangePropagationDispatchJob;
+use SMWDataItem;
+use SMWDIBlob as DIBlob;
+
+/**
+ * Before a new set of data (type, constraints etc.) is stored about a property
+ * the class tries to compare old and new specifications (values about that property)
+ * and notifies a dispatcher about a change.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class PropertyChangePropagationNotifier {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var SerializerFactory
+ */
+ private $serializerFactory;
+
+ /**
+ * @var array
+ */
+ private $propertyList = [];
+
+ /**
+ * @var boolean
+ */
+ private $hasDiff = false;
+
+ /**
+ * @var boolean
+ */
+ private $isTypePropagation = false;
+
+ /**
+ * @var boolean
+ */
+ private $isCommandLineMode = false;
+
+ /**
+ * @since 1.9
+ *
+ * @param Store $store
+ * @param SerializerFactory $serializerFactory
+ */
+ public function __construct( Store $store, SerializerFactory $serializerFactory ) {
+ $this->store = $store;
+ $this->serializerFactory = $serializerFactory;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $propertyList
+ */
+ public function setPropertyList( array $propertyList ) {
+ $this->propertyList = $propertyList;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:$wgCommandLineMode
+ * Indicates whether MW is running in command-line mode.
+ *
+ * @since 3.0
+ *
+ * @param boolean $isCommandLineMode
+ */
+ public function isCommandLineMode( $isCommandLineMode ) {
+ $this->isCommandLineMode = $isCommandLineMode;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return boolean
+ */
+ public function hasDiff() {
+ return $this->hasDiff;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ */
+ public function notify( DIWikiPage $subject ) {
+
+ $namespace = $subject->getNamespace();
+
+ if ( !$this->hasDiff() || ( $namespace !== SMW_NS_PROPERTY && $namespace !== NS_CATEGORY ) ) {
+ return false;
+ }
+
+ $params = [];
+
+ if ( $this->isTypePropagation ) {
+ $params['isTypePropagation'] = true;
+ }
+
+ return ChangePropagationDispatchJob::planAsJob( $subject, $params );
+ }
+
+ /**
+ * Compare and detect differences between the invoked semantic data
+ * and the current stored data
+ *
+ * @note Compare on extra properties from `smwgChangePropagationWatchlist`
+ * (e.g '_PLIST') to find a possible specification change
+ *
+ * @since 1.9
+ */
+ public function checkAndNotify( SemanticData &$semanticData ) {
+
+ $namespace = $semanticData->getSubject()->getNamespace();
+
+ if ( $namespace !== SMW_NS_PROPERTY && $namespace !== NS_CATEGORY ) {
+ return;
+ }
+
+ $this->hasDiff = false;
+
+ // Check the type first
+ $propertyList = array_merge(
+ [
+ '_TYPE',
+ '_CONV',
+ '_UNIT',
+ '_REDI'
+ ],
+ $this->propertyList
+ );
+
+ foreach ( $propertyList as $key ) {
+
+ // No need to keep comparing once a diff has been
+ // detected
+ if ( $this->hasDiff() ) {
+ break;
+ }
+
+ $this->doCompare( $semanticData, $key );
+ }
+
+ $this->doNotifyAndPostpone( $semanticData );
+ }
+
+ private function doCompare( $semanticData, $key ) {
+
+ $property = new DIProperty( $key );
+
+ $newValues = $semanticData->getPropertyValues( $property );
+
+ $oldValues = $this->store->getPropertyValues(
+ $semanticData->getSubject(),
+ $property
+ );
+
+ $this->setDiff( !$this->isEqual( $oldValues, $newValues ), $key );
+ }
+
+ private function setDiff( $hasDiff = true, $key ) {
+
+ if ( !$hasDiff || $this->hasDiff ) {
+ return;
+ }
+
+ $this->hasDiff = true;
+ $this->isTypePropagation = $key === '_TYPE';
+ }
+
+ /**
+ * Helper function that compares two arrays of data values to check whether
+ * they contain the same content. Returns true if the two arrays contain the
+ * same data values (irrespective of their order), false otherwise.
+ *
+ * @param SMWDataItem[] $oldDataValue
+ * @param SMWDataItem[] $newDataValue
+ *
+ * @return boolean
+ */
+ private function isEqual( array $oldDataValue, array $newDataValue ) {
+
+ // The hashes of all values of both arrays are taken, then sorted
+ // and finally concatenated, thus creating one long hash out of each
+ // of the data value arrays. These are compared.
+ $values = [];
+ foreach ( $oldDataValue as $v ) {
+ $values[] = $v->getHash();
+ }
+
+ sort( $values );
+ $oldDataValueHash = implode( '___', $values );
+
+ $values = [];
+ foreach ( $newDataValue as $v ) {
+ $values[] = $v->getHash();
+ }
+
+ sort( $values );
+ $newDataValueHash = implode( '___', $values );
+
+ return $oldDataValueHash == $newDataValueHash;
+ }
+
+ private function doNotifyAndPostpone( SemanticData &$semanticData ) {
+
+ if ( !$this->hasDiff() ) {
+ return;
+ }
+
+ $this->notify( $semanticData->getSubject() );
+
+ // If executed from the commandLine (cronJob etc.), do not
+ // suspend the update
+ if ( $this->isCommandLineMode === true ) {
+ return;
+ }
+
+ $previous = $this->store->getSemanticData(
+ $semanticData->getSubject()
+ );
+
+ $semanticDataSerializer = $this->serializerFactory->newSemanticDataSerializer();
+
+ $new = $semanticDataSerializer->serialize(
+ $semanticData
+ );
+
+ // Encode and store the new version of the SemanticData and suspend
+ // the update until ChangePropagationDispatchJob was able to select
+ // all connected entities
+ $previous->addPropertyObjectValue(
+ new DIProperty( DIProperty::TYPE_CHANGE_PROP ),
+ new DIBlob( json_encode( $new ) )
+ );
+
+ $semanticData = $previous;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyLabelFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyLabelFinder.php
new file mode 100644
index 00000000..4af23d67
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyLabelFinder.php
@@ -0,0 +1,242 @@
+<?php
+
+namespace SMW;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class PropertyLabelFinder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * Array with entries "property id" => "property label"
+ *
+ * @var string[]
+ */
+ private $languageDependentPropertyLabels = [];
+
+ /**
+ * Array with entries "property label" => "property id"
+ *
+ * @var string[]
+ */
+ private $canonicalPropertyLabels = [];
+
+ /**
+ * @var string[]
+ */
+ private $canonicalDatatypeLabels = [];
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param array $languageDependentPropertyLabels
+ * @param array $canonicalPropertyLabels
+ */
+ public function __construct( Store $store, array $languageDependentPropertyLabels = [], array $canonicalPropertyLabels = [], array $canonicalDatatypeLabels = [] ) {
+ $this->store = $store;
+ $this->languageDependentPropertyLabels = $languageDependentPropertyLabels;
+ $this->canonicalPropertyLabels = $canonicalPropertyLabels;
+ $this->canonicalDatatypeLabels = $canonicalDatatypeLabels;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getKownPredefinedPropertyLabels() {
+ return $this->languageDependentPropertyLabels;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $id
+ *
+ * @return string|boolean
+ */
+ public function findCanonicalPropertyLabelById( $id ) {
+
+ // Due to mapped lists avoid possible mismatch on dataTypes
+ // (e.g. Text -> _TEXT vs. Text -> _txt)
+ if ( ( $label = array_search( $id, $this->canonicalDatatypeLabels ) ) ) {
+ return $label;
+ }
+
+ return array_search( $id, $this->canonicalPropertyLabels );
+ }
+
+ /**
+ * @note An empty string is returned for incomplete translation (language
+ * bug) or deliberately invisible property
+ *
+ * @since 2.2
+ *
+ * @param string $id
+ *
+ * @return string
+ */
+ public function findPropertyLabelById( $id ) {
+
+ if ( array_key_exists( $id, $this->languageDependentPropertyLabels ) ) {
+ return $this->languageDependentPropertyLabels[$id];
+ }
+
+ return '';
+ }
+
+ /**
+ * @note An empty string is returned for incomplete translation (language
+ * bug) or deliberately invisible property
+ *
+ * @since 2.5
+ *
+ * @param string $id
+ * @param string $languageCode
+ *
+ * @return string
+ */
+ public function findPropertyLabelFromIdByLanguageCode( $id, $languageCode = '' ) {
+
+ if ( $languageCode === '' ) {
+ return $this->findPropertyLabelById( $id );
+ }
+
+ $lang = Localizer::getInstance()->getLang(
+ mb_strtolower( trim( $languageCode ) )
+ );
+
+ $labels = $lang->getPropertyLabels() + $lang->getDatatypeLabels();
+
+ if ( isset( $labels[$id] ) ) {
+ return $labels[$id];
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $id
+ * @param string $languageCode
+ *
+ * @return string
+ */
+ public function findPreferredPropertyLabelByLanguageCode( $id, $languageCode = '' ) {
+
+ if ( $id === '' || $id === false ) {
+ return '';
+ }
+
+ // Lookup is cached in PropertySpecificationLookup
+ $propertySpecificationLookup = ApplicationFactory::getInstance()->getPropertySpecificationLookup();
+
+ $preferredPropertyLabel = $propertySpecificationLookup->getPreferredPropertyLabelBy(
+ new DIProperty( str_replace( ' ', '_', $id ) ),
+ $languageCode
+ );
+
+ return $preferredPropertyLabel;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ * @param string $languageCode
+ *
+ * @return DIProperty[]|[]
+ */
+ public function findPropertyListFromLabelByLanguageCode( $text, $languageCode = '' ) {
+
+ if ( $text === '' ) {
+ return [];
+ }
+
+ if ( $languageCode === '' ) {
+ $languageCode = Localizer::getInstance()->getContentLanguage()->getCode();
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByProperty(
+ new DIProperty( '_PPLB' )
+ );
+
+ $dataValue->setUserValue(
+ $dataValue->getTextWithLanguageTag( $text, $languageCode )
+ );
+
+ $queryFactory = ApplicationFactory::getInstance()->getQueryFactory();
+ $descriptionFactory = $queryFactory->newDescriptionFactory();
+
+ $description = $descriptionFactory->newConjunction( [
+ $descriptionFactory->newNamespaceDescription( SMW_NS_PROPERTY ),
+ $descriptionFactory->newFromDataValue( $dataValue )
+ ] );
+
+ $propertyList = [];
+
+ $query = $queryFactory->newQuery( $description );
+ $query->setOption( $query::PROC_CONTEXT, 'PropertyLabelFinder' );
+ $query->setLimit( 100 );
+
+ $queryResult = $this->store->getQueryResult(
+ $query
+ );
+
+ if ( !$queryResult instanceof \SMWQueryResult ) {
+ return $propertyList;
+ }
+
+ foreach ( $queryResult->getResults() as $result ) {
+ $propertyList[] = DIProperty::newFromUserLabel( $result->getDBKey() );
+ }
+
+ return $propertyList;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $label
+ *
+ * @return string|false
+ */
+ public function searchPropertyIdByLabel( $label ) {
+ return array_search( $label, $this->languageDependentPropertyLabels );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $id
+ * @param string $label
+ */
+ public function registerPropertyLabel( $id, $label, $asCanonical = true ) {
+
+ // Prevent an extension from overriding an already registered
+ // canonical label that may point to a different ID
+ if ( isset( $this->canonicalPropertyLabels[$label] ) && $this->canonicalPropertyLabels[$label] !== $id ) {
+ return;
+ }
+
+ $this->languageDependentPropertyLabels[$id] = $label;
+
+ // This is done so extensions can register the property id/label as being
+ // canonical in their representation while the alias may hold translated
+ // language depedendant matches
+ if ( $asCanonical ) {
+ $this->canonicalPropertyLabels[$label] = $id;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyRegistry.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyRegistry.php
new file mode 100644
index 00000000..ab5f521a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyRegistry.php
@@ -0,0 +1,499 @@
+<?php
+
+namespace SMW;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class PropertyRegistry {
+
+ /**
+ * @var PropertyRegistry
+ */
+ private static $instance = null;
+
+ /**
+ * @var PropertyLabelFinder
+ */
+ private $propertyLabelFinder = null;
+
+ /**
+ * Array for assigning types to predefined properties. Each
+ * property is associated with an array with the following
+ * elements:
+ *
+ * * ID of datatype to be used for this property
+ *
+ * * Boolean, stating if this property is shown in Factbox, Browse, and
+ * similar interfaces; (note that this is only relevant if the
+ * property can be displayed at all, i.e. has a translated label in
+ * the wiki language; invisible properties are never shown).
+ *
+ * @var array
+ */
+ private $propertyList = [];
+
+ /**
+ * @var string[]
+ */
+ private $datatypeLabels = [];
+
+ /**
+ * @var string[]
+ */
+ private $propertyDescriptionMsgKeys = [];
+
+ /**
+ * @var PropertyAliasFinder
+ */
+ private $propertyAliasFinder;
+
+ /**
+ * @var string[]
+ */
+ private $dataTypePropertyExemptionList = [];
+
+ /**
+ * @since 2.1
+ *
+ * @return PropertyRegistry
+ */
+ public static function getInstance() {
+
+ if ( self::$instance !== null ) {
+ return self::$instance;
+ }
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $lang = Localizer::getInstance()->getLang();
+
+ $propertyAliasFinder = new PropertyAliasFinder(
+ $applicationFactory->getCache(),
+ $lang->getPropertyAliases(),
+ $lang->getCanonicalPropertyAliases()
+ );
+
+ $settings = $applicationFactory->getSettings();
+
+ self::$instance = new self(
+ DataTypeRegistry::getInstance(),
+ $applicationFactory->getPropertyLabelFinder(),
+ $propertyAliasFinder,
+ $settings->get( 'smwgDataTypePropertyExemptionList' )
+ );
+
+ self::$instance->initProperties(
+ TypesRegistry::getPropertyList(
+ $settings->isFlagSet( 'smwgCategoryFeatures', SMW_CAT_HIERARCHY )
+ )
+ );
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param DataTypeRegistry $datatypeRegistry
+ * @param PropertyLabelFinder $propertyLabelFinder
+ * @param PropertyAliasFinder $propertyAliasFinder
+ * @param array $dataTypePropertyExemptionList
+ */
+ public function __construct( DataTypeRegistry $datatypeRegistry, PropertyLabelFinder $propertyLabelFinder, PropertyAliasFinder $propertyAliasFinder, array $dataTypePropertyExemptionList = [] ) {
+
+ $this->datatypeLabels = $datatypeRegistry->getKnownTypeLabels();
+ $this->propertyLabelFinder = $propertyLabelFinder;
+ $this->propertyAliasFinder = $propertyAliasFinder;
+
+ // To get an index access
+ $this->dataTypePropertyExemptionList = array_flip( $dataTypePropertyExemptionList );
+
+ foreach ( $this->datatypeLabels as $id => $label ) {
+
+ if ( isset( $this->dataTypePropertyExemptionList[$label] ) ) {
+ continue;
+ }
+
+ $this->registerPropertyLabel( $id, $label );
+ }
+
+ foreach ( $datatypeRegistry->getKnownTypeAliases() as $alias => $id ) {
+
+ if ( isset( $this->dataTypePropertyExemptionList[$alias] ) ) {
+ continue;
+ }
+
+ $this->registerPropertyAlias( $id, $alias );
+ }
+ }
+
+ /**
+ * @since 2.1
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return array
+ */
+ public function getPropertyList() {
+ return $this->propertyList;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return array
+ */
+ public function getKnownPropertyAliases() {
+ return $this->propertyAliasFinder->getKnownPropertyAliases();
+ }
+
+ /**
+ * A method for registering/overwriting predefined properties for SMW.
+ * It should be called from within the hook 'smwInitProperties' only.
+ * IDs should start with three underscores "___" to avoid current and
+ * future confusion with SMW built-ins.
+ *
+ * @param string $id
+ * @param string $valueType SMW type id
+ * @param string|bool $label user label or false (internal property)
+ * @param boolean $isVisible only used if label is given, see isShown()
+ * @param boolean $isAnnotable
+ */
+ public function registerProperty( $id, $valueType, $label = false, $isVisible = false, $isAnnotable = true ) {
+
+ $this->propertyList[$id] = [ $valueType, $isVisible, $isAnnotable ];
+
+ if ( $label !== false ) {
+ $this->registerPropertyLabel( $id, $label );
+ }
+ }
+
+ /**
+ * Add a new alias label to an existing property ID. Note that every ID
+ * should have a primary label, either provided by SMW or registered
+ * with registerProperty().
+ *
+ * @param $id string id of a property
+ * @param $label string alias label for the property
+ *
+ * @note Always use registerProperty() for the first label. No property
+ * that has used "false" for a label on registration should have an
+ * alias.
+ */
+ public function registerPropertyAlias( $id, $label ) {
+ $this->propertyAliasFinder->registerAliasByFixedLabel( $id, $label );
+ }
+
+ /**
+ * Register an alias using a message key to allow fetching localized
+ * labels dynamically (for when the user language is changed etc).
+ *
+ * @since 2.4
+ *
+ * @param string $id
+ * @param string $msgKey
+ */
+ public function registerPropertyAliasByMsgKey( $id, $msgKey ) {
+ $this->propertyAliasFinder->registerAliasByMsgKey( $id, $msgKey );
+ }
+
+ /**
+ * Register a description message key for allowing it to be displayed in a
+ * localized context.
+ *
+ * @since 2.5
+ *
+ * @param string $id
+ * @param string $msgKey
+ */
+ public function registerPropertyDescriptionByMsgKey( $id, $msgKey ) {
+ $this->propertyDescriptionMsgKeys[$id] = $msgKey;
+ }
+
+ /**
+ * @deprecated since 3.0, use PropertyRegistry::registerPropertyDescriptionByMsgKey
+ */
+ public function registerPropertyDescriptionMsgKeyById( $id, $msgKey ) {
+ $this->registerPropertyDescriptionByMsgKey( $id, $msgKey );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $id
+ *
+ * @return string
+ */
+ public function findPropertyDescriptionMsgKeyById( $id ) {
+ return isset( $this->propertyDescriptionMsgKeys[$id] ) ? $this->propertyDescriptionMsgKeys[$id] : '';
+ }
+
+ /**
+ * Get the translated user label for a given internal property ID.
+ * Returns empty string for properties without a translation (these are
+ * usually internal, generated by SMW but not shown to the user).
+ *
+ * @note An empty string is returned for incomplete translation (language
+ * bug) or deliberately invisible property
+ *
+ * @since 2.1
+ *
+ * @param string $id
+ *
+ * @return string
+ */
+ public function findPropertyLabelById( $id ) {
+
+ // This is a hack but there is no other good way to make it work without
+ // open a whole new can of worms
+ // '__' indicates predefined properties of extensions that contain alias
+ // and translated labels and if available we want the translated label
+ if ( ( substr( $id, 0, 2 ) === '__' ) &&
+ ( $label = $this->propertyAliasFinder->findPropertyAliasById( $id ) ) ) {
+ return $label;
+ }
+
+ // core has dedicated files per language so the label is available over
+ // the invoked language
+ return $this->propertyLabelFinder->findPropertyLabelById( $id );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $id
+ *
+ * @return string
+ */
+ public function findCanonicalPropertyLabelById( $id ) {
+ return $this->propertyLabelFinder->findCanonicalPropertyLabelById( $id );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $id
+ * @param string $languageCode
+ *
+ * @return string
+ */
+ public function findPropertyLabelFromIdByLanguageCode( $id, $languageCode = '' ) {
+ return $this->propertyLabelFinder->findPropertyLabelFromIdByLanguageCode( $id, $languageCode );
+ }
+
+ /**
+ * @deprecated since 2.1 use findPropertyLabelById instead
+ */
+ public function findPropertyLabel( $id ) {
+ return $this->findPropertyLabelById( $id );
+ }
+
+ /**
+ * Get the type ID of a predefined property, or '' if the property
+ * is not predefined.
+ * The function is guaranteed to return a type ID for keys of
+ * properties where isUserDefined() returns false.
+ *
+ * @param string $id
+ *
+ * @return string
+ */
+ public function getPropertyValueTypeById( $id ) {
+
+ if ( $this->isRegistered( $id ) ) {
+ return $this->propertyList[$id][0];
+ }
+
+ return '';
+ }
+
+ /**
+ * @deprecated since 3.0, use PropertyRegistry::getPropertyValueTypeById instead
+ */
+ public function getPropertyTypeId( $id ) {
+ return $this->getPropertyValueTypeById( $id );
+ }
+
+ /**
+ * @deprecated since 2.1 use getPropertyValueTypeById instead
+ */
+ public function getPredefinedPropertyTypeId( $id ) {
+ return $this->getPropertyValueTypeById( $id );
+ }
+
+ /**
+ * Find and return the ID for the pre-defined property of the given
+ * local label. If the label does not belong to a pre-defined property,
+ * return false.
+ *
+ * @param string $label normalized property label
+ * @param boolean $useAlias determining whether to check if the label is an alias
+ *
+ * @return mixed string property ID or false
+ */
+ public function findPropertyIdByLabel( $label, $useAlias = true ) {
+
+ $id = $this->propertyLabelFinder->searchPropertyIdByLabel( $label );
+
+ if ( $id !== false ) {
+ return $id;
+ } elseif ( $useAlias && $this->propertyAliasFinder->findPropertyIdByAlias( $label ) ) {
+ return $this->propertyAliasFinder->findPropertyIdByAlias( $label );
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $label
+ * @param string $languageCode
+ *
+ * @return mixed string property ID or false
+ */
+ public function findPropertyIdFromLabelByLanguageCode( $label, $languageCode = '' ) {
+
+ $languageCode = mb_strtolower( trim( $languageCode ) );
+
+ // Match the canonical form
+ if ( $languageCode === '' ) {
+ return $this->findPropertyIdByLabel( $label );
+ }
+
+ $lang = Localizer::getInstance()->getLang(
+ $languageCode
+ );
+
+ // Language dep. stored as aliases
+ $aliases = $lang->getPropertyLabels() + $lang->getDatatypeLabels();
+
+ if ( ( $id = array_search( $label, $aliases ) ) !== false && !isset( $this->dataTypePropertyExemptionList[$label] ) ) {
+ return $id;
+ }
+
+ // Those are mostly from extension that register a msgKey as no dedicated
+ // lang. file exists; maybe this should be cached somehow?
+ foreach ( $this->propertyAliasFinder->getKnownPropertyAliasesByLanguageCode( $languageCode ) as $alias => $id ) {
+ if ( $label === $alias ) {
+ return $id;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $id
+ * @param string|null $languageCode
+ *
+ * @return string
+ */
+ public function findPreferredPropertyLabelFromIdByLanguageCode( $id, $languageCode = '' ) {
+
+ if ( $languageCode === false || $languageCode === '' ) {
+ $languageCode = Localizer::getInstance()->getUserLanguage()->getCode();
+ }
+
+ return $this->propertyLabelFinder->findPreferredPropertyLabelByLanguageCode( $id, $languageCode );
+ }
+
+ /**
+ * @deprecated since 2.1 use findPropertyIdByLabel instead
+ */
+ public function findPropertyId( $label, $useAlias = true ) {
+ return $this->findPropertyIdByLabel( $label, $useAlias );
+ }
+
+ /**
+ * @deprecated since 3.0 use isRegistered instead
+ */
+ public function isKnownPropertyId( $id ) {
+ return $this->isRegistered( $id );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $id
+ *
+ * @return boolean
+ */
+ public function isRegistered( $id ) {
+ return isset( $this->propertyList[$id] ) || array_key_exists( $id, $this->propertyList );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $id
+ *
+ * @return boolean
+ */
+ public function isVisible( $id ) {
+ return $this->isRegistered( $id ) ? $this->propertyList[$id][1] : false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ *
+ * @return boolean
+ */
+ public function isAnnotable( $id ) {
+ return $this->isRegistered( $id ) ? $this->propertyList[$id][2] : false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ *
+ * @return boolean
+ */
+ public function isDeclarative( $id ) {
+
+ if ( !$this->isRegistered( $id ) ) {
+ return false;
+ }
+
+ return isset( $this->propertyList[$id][3] ) ? $this->propertyList[$id][3] : false;
+ }
+
+ /**
+ * @note All ids must start with underscores. The translation for each ID,
+ * if any, is defined in the language files. Properties without translation
+ * cannot be entered by or displayed to users, whatever their "show" value
+ * below.
+ */
+ protected function initProperties( array $propertyList ) {
+
+ $this->propertyList = $propertyList;
+
+ foreach ( $this->datatypeLabels as $id => $label ) {
+ $this->propertyList[$id] = [ $id, true, true, false ];
+ }
+
+ // @deprecated since 2.1
+ \Hooks::run( 'smwInitProperties' );
+
+ \Hooks::run( 'SMW::Property::initProperties', [ $this ] );
+ }
+
+ private function registerPropertyLabel( $id, $label, $asCanonical = true ) {
+ $this->propertyLabelFinder->registerPropertyLabel( $id, $label, $asCanonical );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertyRestrictionExaminer.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertyRestrictionExaminer.php
new file mode 100644
index 00000000..0ab9881f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertyRestrictionExaminer.php
@@ -0,0 +1,199 @@
+<?php
+
+namespace SMW;
+
+use Title;
+use User;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PropertyRestrictionExaminer {
+
+ const CREATE_RESTRICTION = 'smw-datavalue-property-create-restriction';
+
+ /**
+ * @var array
+ */
+ private $error = [];
+
+ /**
+ * @var User|null
+ */
+ private $user;
+
+ /**
+ * @var boolean|string
+ */
+ private $createProtectionRight = false;
+
+ /**
+ * @var boolean
+ */
+ private $isQueryContext = false;
+
+ /**
+ * @var array
+ */
+ private $exists = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param User $user
+ */
+ public function setUser( User $user ) {
+ $this->user = $user;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|boolean $createProtectionRight
+ */
+ public function setCreateProtectionRight( $createProtectionRight ) {
+ $this->createProtectionRight = $createProtectionRight;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isQueryContext
+ */
+ public function isQueryContext( $isQueryContext ) {
+ $this->isQueryContext = (bool)$isQueryContext;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasRestriction() {
+ return $this->error !== [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array
+ */
+ public function getError() {
+ return $this->error;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $error
+ *
+ * @return DIProperty|null
+ */
+ public static function grepPropertyFromRestrictionErrorMsg( $errorMsg ) {
+
+ if ( strpos( $errorMsg, self::CREATE_RESTRICTION ) === false ) {
+ return null;
+ }
+
+ $error = json_decode( $errorMsg, true );
+
+ return isset( $error[2] ) ? DIProperty::newFromUserLabel( $error[2] ) : null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ * @param DIWikiPage|null $contextPage
+ */
+ public function checkRestriction( DIProperty $property, DIWikiPage $contextPage = null ) {
+
+ $this->error = [];
+
+ if ( $this->isDeclarative( $property, $contextPage ) ) {
+ return;
+ }
+
+ if ( $this->isAnnotationRestricted( $property ) ) {
+ return;
+ }
+
+ if ( $this->isCreateProtected( $property ) ) {
+ return;
+ }
+ }
+
+ private function isDeclarative( $property, $contextPage = null ) {
+
+ if ( $this->isQueryContext || $contextPage === null ) {
+ return false;
+ }
+
+ $ns = $contextPage->getNamespace();
+
+ // Property, category page are allowed to carry declarative properties
+ if ( $ns === SMW_NS_PROPERTY || $ns === NS_CATEGORY ) {
+ return false;
+ }
+
+ if ( !PropertyRegistry::getInstance()->isDeclarative( $property->getKey() ) ) {
+ return false;
+ }
+
+ return $this->error = Message::encode(
+ [
+ 'smw-datavalue-property-restricted-declarative-use',
+ $property->getLabel()
+ ],
+ Message::PARSE
+ );
+ }
+
+ private function isAnnotationRestricted( $property ) {
+
+ if ( $this->isQueryContext || $property->isUserDefined() ) {
+ return false;
+ }
+
+ if ( $property->isUserAnnotable() ) {
+ return false;
+ }
+
+ return $this->error = [
+ 'smw-datavalue-property-restricted-annotation-use',
+ $property->getLabel()
+ ];
+ }
+
+ private function isCreateProtected( $property ) {
+
+ if ( $this->user === null || $this->createProtectionRight === false ) {
+ return false;
+ }
+
+ $key = $property->getKey();
+
+ // Non-existing property?
+ if ( !isset( $this->exists[$key] ) ) {
+ $this->exists[$key] = $property->isUserDefined() && $property->getDiWikiPage()->getTitle()->exists();
+ }
+
+ if ( $this->exists[$key] || $this->user->isAllowed( $this->createProtectionRight ) ) {
+ return false;
+ }
+
+ // A user without the appropriate right cannot use a non-existing property
+ return $this->error = Message::encode(
+ [
+ self::CREATE_RESTRICTION,
+ $property->getLabel(),
+ $this->createProtectionRight
+ ],
+ Message::PARSE
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationLookup.php
new file mode 100644
index 00000000..8de435ab
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationLookup.php
@@ -0,0 +1,507 @@
+<?php
+
+namespace SMW;
+
+use Onoi\Cache\Cache;
+use RuntimeException;
+use SMW\Query\DescriptionFactory;
+use SMWDIBlob as DIBlob;
+use SMWDIBoolean as DIBoolean;
+use SMWQuery as Query;
+
+/**
+ * This class should be accessed via ApplicationFactory::getPropertySpecificationLookup
+ * to ensure a singleton instance.
+ *
+ * Changes to a property should trigger a PropertySpecificationLookup::resetCacheBy to
+ * evict all cached item store to that property.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class PropertySpecificationLookup {
+
+ /**
+ * Reference used in InMemoryPoolCache
+ */
+ const POOLCACHE_ID = 'property.specification.lookup';
+
+ /**
+ * @var CachedPropertyValuesPrefetcher
+ */
+ private $cachedPropertyValuesPrefetcher;
+
+ /**
+ * @var string
+ */
+ private $languageCode = 'en';
+
+ /**
+ * @var Cache
+ */
+ private $intermediaryMemoryCache;
+
+ /**
+ * @since 2.4
+ *
+ * @param CachedPropertyValuesPrefetcher $cachedPropertyValuesPrefetcher
+ * @param Cache $intermediaryMemoryCache
+ */
+ public function __construct( CachedPropertyValuesPrefetcher $cachedPropertyValuesPrefetcher, Cache $intermediaryMemoryCache ) {
+ $this->cachedPropertyValuesPrefetcher = $cachedPropertyValuesPrefetcher;
+ $this->intermediaryMemoryCache = $intermediaryMemoryCache;
+ $this->languageCode = Localizer::getInstance()->getContentLanguage()->getCode();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIWikiPage $subject
+ */
+ public function resetCacheBy( DIWikiPage $subject ) {
+ $this->cachedPropertyValuesPrefetcher->resetCacheBy( $subject );
+ $this->intermediaryMemoryCache->delete( $subject->getHash() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty|DIWikiPage $source
+ * @param DIProperty $target
+ *
+ * @return []|DataItem[]
+ */
+ public function getSpecification( $source, DIProperty $target ) {
+
+ if ( $source instanceof DIProperty ) {
+ $dataItem = $source->getCanonicalDiWikiPage();
+ } elseif( $source instanceof DIWikiPage ) {
+ $dataItem = $source;
+ } else {
+ throw new RuntimeException( "Invalid request instance type" );
+ }
+
+ $hash = $dataItem->getHash();
+ $key = $target->getKey();
+
+ $definition = $this->intermediaryMemoryCache->fetch( $hash );
+
+ if ( $definition === false ) {
+ $definition = [];
+ }
+
+ if ( isset( $definition[$key] ) ) {
+ return $definition[$key];
+ }
+
+ $dataItems = $this->cachedPropertyValuesPrefetcher->getPropertyValues(
+ $dataItem,
+ $target
+ );
+
+ if ( !is_array( $dataItems ) ) {
+ $dataItems = [];
+ }
+
+ $definition[$key] = $dataItems;
+ $this->intermediaryMemoryCache->save( $hash, $definition );
+
+ return $dataItems;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return false|DataItem
+ */
+ public function getFieldListBy( DIProperty $property ) {
+
+ $fieldList = false;
+ $dataItems = $this->getSpecification( $property, new DIProperty( '_LIST' ) );
+
+ if ( is_array( $dataItems ) && $dataItems !== [] ) {
+ $fieldList = end( $dataItems );
+ }
+
+ return $fieldList;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ * @param string $languageCode
+ *
+ * @return string
+ */
+ public function getPreferredPropertyLabelBy( DIProperty $property, $languageCode = '' ) {
+
+ $languageCode = $languageCode === '' ? $this->languageCode : $languageCode;
+ $key = 'ppl:' . $languageCode . ':'. $property->getKey();
+
+ // Guard against high frequency lookup
+ if ( ( $preferredPropertyLabel = $this->intermediaryMemoryCache->fetch( $key ) ) !== false ) {
+ return $preferredPropertyLabel;
+ }
+
+ $preferredPropertyLabel = $this->findPreferredPropertyLabel(
+ $property,
+ $languageCode
+ );
+
+ $this->intermediaryMemoryCache->save( $key, $preferredPropertyLabel );
+
+ return $preferredPropertyLabel;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $displayTitle
+ *
+ * @return DIProperty|false
+ */
+ public function getPropertyFromDisplayTitle( $displayTitle ) {
+
+ $descriptionFactory = new DescriptionFactory();
+
+ $description = $descriptionFactory->newSomeProperty(
+ new DIProperty( '_DTITLE' ),
+ $descriptionFactory->newValueDescription( new DIBlob( $displayTitle ) )
+ );
+
+ $query = new Query( $description );
+ $query->setLimit( 1 );
+ $query->setOption( Query::PROC_CONTEXT, 'PropertySpecificationLookup' );
+
+ $dataItems = $this->cachedPropertyValuesPrefetcher->queryPropertyValuesFor(
+ $query
+ );
+
+ if ( is_array( $dataItems ) && $dataItems !== [] ) {
+ $dataItem = end( $dataItems );
+
+ // Cache results as a linked list attached to
+ // the property so that it can be purged all together
+
+ return new DIProperty( $dataItem->getDBKey() );
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ *
+ * @return boolean
+ */
+ public function hasUniquenessConstraint( DIProperty $property ) {
+
+ $hasUniquenessConstraint = false;
+ $dataItems = $this->getSpecification( $property, new DIProperty( '_PVUC' ) );
+
+ if ( is_array( $dataItems ) && $dataItems !== [] ) {
+ $hasUniquenessConstraint = end( $dataItems )->getBoolean();
+ }
+
+ return $hasUniquenessConstraint;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ *
+ * @return DataItem|null
+ */
+ public function getPropertyGroup( DIProperty $property ) {
+
+ $dataItem = null;
+ $dataItems = $this->getSpecification( $property, new DIProperty( '_INST' ) );
+
+ if ( is_array( $dataItems ) && $dataItems !== [] ) {
+
+ foreach ( $dataItems as $dataItem ) {
+ $pv = $this->cachedPropertyValuesPrefetcher->getPropertyValues(
+ $dataItem,
+ new DIProperty( '_PPGR' )
+ );
+
+ $di = end( $pv );
+
+ if ( $di instanceof DIBoolean && $di->getBoolean() ) {
+ return $dataItem;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return DataItem|null
+ */
+ public function getExternalFormatterUri( DIProperty $property ) {
+
+ $dataItem = null;
+ $dataItems = $this->getSpecification( $property, new DIProperty( '_PEFU' ) );
+
+ if ( is_array( $dataItems ) && $dataItems !== [] ) {
+ $dataItem = end( $dataItems );
+ }
+
+ return $dataItem;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ *
+ * @return string
+ */
+ public function getAllowedPatternBy( DIProperty $property ) {
+
+ $allowsPattern = '';
+ $dataItems = $this->getSpecification( $property, new DIProperty( '_PVAP' ) );
+
+ if ( is_array( $dataItems ) && $dataItems !== [] ) {
+ $allowsPattern = end( $dataItems )->getString();
+ }
+
+ return $allowsPattern;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ *
+ * @return array
+ */
+ public function getAllowedValues( DIProperty $property ) {
+
+ $allowsValues = [];
+ $dataItems = $this->getSpecification( $property, new DIProperty( '_PVAL' ) );
+
+ if ( is_array( $dataItems ) && $dataItems !== [] ) {
+ $allowsValues = $dataItems;
+ }
+
+ return $allowsValues;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return array
+ */
+ public function getAllowedListValues( DIProperty $property ) {
+
+ $allowsListValue = [];
+ $dataItems = $this->getSpecification( $property, new DIProperty( '_PVALI' ) );
+
+ if ( is_array( $dataItems ) && $dataItems !== [] ) {
+ $allowsListValue = $dataItems;
+ }
+
+ return $allowsListValue;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ *
+ * @return integer|false
+ */
+ public function getDisplayPrecision( DIProperty $property ) {
+
+ $displayPrecision = false;
+ $dataItems = $this->getSpecification( $property, new DIProperty( '_PREC' ) );
+
+ if ( $dataItems !== false && $dataItems !== [] ) {
+ $dataItem = end( $dataItems );
+ $displayPrecision = abs( (int)$dataItem->getNumber() );
+ }
+
+ return $displayPrecision;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ *
+ * @return array
+ */
+ public function getDisplayUnits( DIProperty $property ) {
+
+ $units = [];
+
+ $dataItems = $this->cachedPropertyValuesPrefetcher->getPropertyValues(
+ $property->getCanonicalDiWikiPage(),
+ new DIProperty( '_UNIT' )
+ );
+
+ if ( $dataItems !== false && $dataItems !== [] ) {
+ foreach ( $dataItems as $dataItem ) {
+ $units = array_merge( $units, preg_split( '/\s*,\s*/u', $dataItem->getString() ) );
+ }
+ }
+
+ return $units;
+ }
+
+ /**
+ * We try to cache anything to avoid unnecessary store connections or DB
+ * lookups. For cases where a property was changed, the EventDipatcher will
+ * receive a 'property.specification.change' event (emitted as soon as the content of
+ * a property page was altered) with PropertySpecificationLookup::resetCacheBy
+ * being invoked to remove the cache entry for that specific property.
+ *
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ * @param string $languageCode
+ * @param mixed|null $linker
+ *
+ * @return string
+ */
+ public function getPropertyDescriptionByLanguageCode( DIProperty $property, $languageCode = '', $linker = null ) {
+
+ // Take the linker into account (Special vs. in page rendering etc.)
+ $languageCode = $languageCode === '' ? $this->languageCode : $languageCode;
+ $key = '--pdesc:' . $languageCode . ':' . ( $linker === null ? '0' : '1' );
+
+ $blobStore = $this->cachedPropertyValuesPrefetcher->getBlobStore();
+
+ $container = $blobStore->read(
+ $this->cachedPropertyValuesPrefetcher->getRootHashFrom( $property->getCanonicalDiWikiPage() )
+ );
+
+ if ( $container->has( $key ) ) {
+ return $container->get( $key );
+ }
+
+ $localPropertyDescription = $this->findLocalPropertyDescription(
+ $property,
+ $linker,
+ $languageCode
+ );
+
+ // If a local property description wasn't available for a predefined property
+ // the try to find a system translation
+ if ( trim( $localPropertyDescription ) === '' && !$property->isUserDefined() ) {
+ $localPropertyDescription = $this->getPredefinedPropertyDescription( $property, $linker, $languageCode );
+ }
+
+ $container->set( $key, $localPropertyDescription );
+
+ $blobStore->save(
+ $container
+ );
+
+ return $localPropertyDescription;
+ }
+
+ private function getPredefinedPropertyDescription( $property, $linker, $languageCode ) {
+
+ $description = '';
+ $key = $property->getKey();
+
+ if ( ( $msgKey = PropertyRegistry::getInstance()->findPropertyDescriptionMsgKeyById( $key ) ) === '' ) {
+ $msgKey = 'smw-property-predefined' . str_replace( '_', '-', strtolower( $key ) );
+ }
+
+ if ( !Message::exists( $msgKey ) ) {
+ return $description;
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $property
+ );
+
+ $label = $dataValue->getFormattedLabel();
+
+ $message = Message::get(
+ [ $msgKey, $label ],
+ $linker === null ? Message::ESCAPED : Message::PARSE,
+ $languageCode
+ );
+
+ return $message;
+ }
+
+ private function findLocalPropertyDescription( $property, $linker, $languageCode ) {
+
+ $text = '';
+ $descriptionProperty = new DIProperty( '_PDESC' );
+
+ $dataItems = $this->cachedPropertyValuesPrefetcher->getPropertyValues(
+ $property->getCanonicalDiWikiPage(),
+ $descriptionProperty
+ );
+
+ if ( ( $dataValue = $this->findTextValueByLanguage( $dataItems, $descriptionProperty, $languageCode ) ) !== null ) {
+ $text = $dataValue->getShortWikiText( $linker );
+ }
+
+ return $text;
+ }
+
+ private function findPreferredPropertyLabel( $property, $languageCode ) {
+
+ $text = '';
+ $preferredProperty = new DIProperty( '_PPLB' );
+
+ $dataItems = $this->cachedPropertyValuesPrefetcher->getPropertyValues(
+ $property->getCanonicalDiWikiPage(),
+ $preferredProperty
+ );
+
+ if ( ( $dataValue = $this->findTextValueByLanguage( $dataItems, $preferredProperty, $languageCode ) ) !== null ) {
+ $text = $dataValue->getShortWikiText();
+ }
+
+ return $text;
+ }
+
+ private function findTextValueByLanguage( $dataItems, $property, $languageCode ) {
+
+ if ( $dataItems === null || $dataItems === [] ) {
+ return null;
+ }
+
+ foreach ( $dataItems as $dataItem ) {
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $property
+ );
+
+ // Here a MonolingualTextValue was retunred therefore the method
+ // can be called without validation
+ $dv = $dataValue->getTextValueByLanguage( $languageCode );
+
+ if ( $dv !== null ) {
+ return $dv;
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationReqExaminer.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationReqExaminer.php
new file mode 100644
index 00000000..9f184a17
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationReqExaminer.php
@@ -0,0 +1,295 @@
+<?php
+
+namespace SMW;
+
+use SMW\PropertyAnnotators\MandatoryTypePropertyAnnotator;
+use SMW\Protection\ProtectionValidator;
+use SMWDataItem as DataItem;
+
+/**
+ * Examines codified requirements for listed types of property specifications which
+ * in case of a violation returns a message with the details of that violation.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertySpecificationReqExaminer {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var ProtectionValidator
+ */
+ private $protectionValidator;
+
+ /**
+ * @var SemanticData
+ */
+ private $semanticData;
+
+ /**
+ * @var boolean
+ */
+ private $changePropagationProtection = true;
+
+ /**
+ * @var DataItemFactory
+ */
+ private $dataItemFactory;
+
+ /**
+ * @var boolean
+ */
+ private $reqLock = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param ProtectionValidator $protectionValidator
+ */
+ public function __construct( Store $store, ProtectionValidator $protectionValidator ) {
+ $this->store = $store;
+ $this->protectionValidator = $protectionValidator;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param SemanticData|null $semanticData
+ */
+ public function setSemanticData( SemanticData $semanticData = null ) {
+ $this->semanticData = $semanticData;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $changePropagationProtection
+ */
+ public function setChangePropagationProtection( $changePropagationProtection ) {
+ $this->changePropagationProtection = (bool)$changePropagationProtection;
+ }
+
+ /**
+ * Whether a specific property requires a lock nor not.
+ *
+ * @since 3.0
+ *
+ * @param boolean
+ */
+ public function reqLock() {
+ return $this->reqLock;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return array|null
+ */
+ public function check( DIProperty $property ) {
+
+ $subject = $property->getCanonicalDiWikiPage();
+ $title = $subject->getTitle();
+
+ $semanticData = $this->store->getSemanticData( $subject );
+
+ if ( $this->semanticData === null ) {
+ $this->semanticData = $semanticData;
+ }
+
+ $this->reqLock = false;
+ $this->dataItemFactory = new DataItemFactory();
+
+ if ( $semanticData->hasProperty( new DIProperty( DIProperty::TYPE_CHANGE_PROP ) ) ) {
+ $severity = $this->changePropagationProtection ? 'error' : 'warning';
+ $this->reqLock = true;
+ return [
+ $severity,
+ 'smw-property-req-violation-change-propagation-locked-' . $severity,
+ $property->getLabel()
+ ];
+ }
+
+ if ( $this->reqLock === false && $this->protectionValidator->hasCreateProtection( $title ) ) {
+ $msg = 'smw-create-protection';
+
+ if ( $title->exists() ) {
+ $msg = 'smw-create-protection-exists';
+ }
+
+ return [
+ 'warning',
+ $msg,
+ $property->getLabel(),
+ $this->protectionValidator->getCreateProtectionRight()
+ ];
+ }
+
+ if ( $this->reqLock === false && $this->protectionValidator->hasEditProtection( $title ) ) {
+ return [
+ $property->isUserDefined() ? 'error' : 'warning',
+ 'smw-edit-protection',
+ $this->protectionValidator->getEditProtectionRight()
+ ];
+ }
+
+ if ( !$property->isUserDefined() ) {
+ return $this->checkTypeForPredefinedProperty( $property );
+ }
+
+ $type = $property->findPropertyTypeID();
+
+ if ( $type === '_ref_rec' || $type === '_rec' ) {
+ return $this->checkFieldList( $property );
+ }
+
+ if ( $type === '_eid' ) {
+ return $this->checkExternalFormatterUri( $property );
+ }
+
+ if ( $type === '_geo' ) {
+ return $this->checkMaps( $property );
+ }
+
+ if ( $this->semanticData->getOption( MandatoryTypePropertyAnnotator::IMPO_REMOVED_TYPE ) ) {
+ return $this->checkImportedVocabType( $property );
+ }
+ }
+
+ /**
+ * A violation occurs when a predefined property contains a `Has type` annotation
+ * that is incompatible with the default type.
+ */
+ private function checkTypeForPredefinedProperty( $property ) {
+
+ if ( $property->getKey() === '_EDIP' ) {
+ return $this->checkEditProtectionRight( $property );
+ }
+
+ if ( !$this->semanticData->hasProperty( $this->dataItemFactory->newDIProperty( '_TYPE' ) ) ) {
+ return;
+ }
+
+ $typeValues = $this->semanticData->getPropertyValues(
+ $this->dataItemFactory->newDIProperty( '_TYPE' )
+ );
+
+ if ( $typeValues !== [] ) {
+ list( $url, $type ) = explode( "#", end( $typeValues )->getSerialization() );
+ }
+
+ if ( DataTypeRegistry::getInstance()->isEqualByType( $type, $property->findPropertyTypeID() ) ) {
+ return;
+ }
+
+ $prop = $this->dataItemFactory->newDIProperty( $type );
+
+ return [
+ 'error',
+ 'smw-property-req-violation-predefined-type',
+ $property->getCanonicalLabel(),
+ $prop->getCanonicalLabel()
+ ];
+ }
+
+ /**
+ * Examines whether the setting `smwgEditProtectionRight` contains an appropriate
+ * value or is disabled in order for the `Is edit protected` property to function.
+ */
+ private function checkEditProtectionRight( $property ) {
+
+ if ( $this->protectionValidator->getEditProtectionRight() !== false ) {
+ return;
+ }
+
+ return [
+ 'warning',
+ 'smw-edit-protection-disabled',
+ $property->getCanonicalLabel()
+ ];
+ }
+
+ /**
+ * A violation occurs when a Reference or Record typed property does not denote
+ * a `Has fields` declaration.
+ */
+ private function checkFieldList( $property ) {
+
+ if ( $this->semanticData->hasProperty( $this->dataItemFactory->newDIProperty( '_LIST' ) ) ) {
+ return;
+ }
+
+ $prop = $this->dataItemFactory->newDIProperty( $property->findPropertyTypeID() );
+
+ return [
+ 'error',
+ 'smw-property-req-violation-missing-fields',
+ $property->getLabel(),
+ $prop->getCanonicalLabel()
+ ];
+ }
+
+ /**
+ * A violation occurs when the External Identifier typed property does not declare
+ * a `External formatter URI` declaration.
+ */
+ private function checkExternalFormatterUri( $property ) {
+
+ if ( $this->semanticData->hasProperty( $this->dataItemFactory->newDIProperty( '_PEFU' ) ) ) {
+ return;
+ }
+
+ return [
+ 'error',
+ 'smw-property-req-violation-missing-formatter-uri',
+ $property->getLabel()
+ ];
+ }
+
+ private function checkMaps( $property ) {
+
+ if ( defined( 'SM_VERSION' ) ) {
+ return;
+ }
+
+ return [
+ 'error',
+ 'smw-property-req-violation-missing-maps-extension',
+ $property->getLabel()
+ ];
+ }
+
+ /**
+ * A violation occurs when the `Imported from` property detects an incompatible
+ * `Has type` declaration.
+ */
+ private function checkImportedVocabType( $property ) {
+
+ $typeValues = $this->semanticData->getPropertyValues(
+ $this->dataItemFactory->newDIProperty( '_TYPE' )
+ );
+
+ $dataItem = $this->semanticData->getOption(
+ MandatoryTypePropertyAnnotator::IMPO_REMOVED_TYPE
+ );
+
+ if ( $dataItem instanceof DataItem && end( $typeValues )->equals( $dataItem ) ) {
+ return;
+ }
+
+ return [
+ 'warning',
+ 'smw-property-req-violation-import-type',
+ $property->getLabel()
+ ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationReqMsgBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationReqMsgBuilder.php
new file mode 100644
index 00000000..aa17253b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/PropertySpecificationReqMsgBuilder.php
@@ -0,0 +1,323 @@
+<?php
+
+namespace SMW;
+
+use Html;
+use SMW\MediaWiki\Jobs\ChangePropagationDispatchJob;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertySpecificationReqMsgBuilder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var SemanticData
+ */
+ private $semanticData;
+
+ /**
+ * @var PropertySpecificationReqExaminer
+ */
+ private $propertySpecificationReqExaminer;
+
+ /**
+ * @var array
+ */
+ private $propertyReservedNameList = [];
+
+ /**
+ * @var string
+ */
+ private $message = '';
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param PropertySpecificationReqExaminer $propertySpecificationReqExaminer
+ */
+ public function __construct( Store $store, PropertySpecificationReqExaminer $propertySpecificationReqExaminer ) {
+ $this->store = $store;
+ $this->propertySpecificationReqExaminer = $propertySpecificationReqExaminer;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param SemanticData|null $semanticData
+ */
+ public function setSemanticData( SemanticData $semanticData = null ) {
+ $this->semanticData = $semanticData;
+
+ $this->propertySpecificationReqExaminer->setSemanticData(
+ $semanticData
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $propertyReservedNameList
+ */
+ public function setPropertyReservedNameList( array $propertyReservedNameList ) {
+
+ foreach ( $propertyReservedNameList as $name ) {
+
+ if ( strpos( $name, 'smw-property-reserved' ) !== false ) {
+ $name = Message::get( $name, Message::TEXT, Message::CONTENT_LANGUAGE );
+ }
+
+ $this->propertyReservedNameList[$name] = true;
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean
+ */
+ public function reqLock() {
+ return $this->propertySpecificationReqExaminer->reqLock();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string
+ */
+ public function getMessage() {
+ return $this->message;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ */
+ public function check( DIProperty $property ) {
+
+ $subject = $property->getCanonicalDiWikiPage();
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $property
+ );
+
+ $propertyName = $dataValue->getFormattedLabel();
+
+ $this->message = $this->checkUniqueness( $property, $propertyName );
+
+ if ( isset( $this->propertyReservedNameList[$propertyName] ) ) {
+ $this->message .= $this->createMessage(
+ [
+ 'error',
+ 'smw-property-name-reserved',
+ $propertyName
+ ]
+ );
+ }
+
+ $this->message .= $this->createMessage(
+ $this->propertySpecificationReqExaminer->check( $property )
+ );
+
+ if ( $this->propertySpecificationReqExaminer->reqLock() === false && ChangePropagationDispatchJob::hasPendingJobs( $subject ) ) {
+ $this->message .= $this->createMessage(
+ [
+ 'warning',
+ 'smw-property-req-violation-change-propagation-pending',
+ ChangePropagationDispatchJob::getPendingJobsCount( $subject )
+ ]
+ );
+ }
+
+ if ( $this->semanticData !== null && $this->semanticData->hasProperty( new DIProperty( '_ERRC' ) ) ) {
+ $this->message .= $this->findErrorMessages();
+ }
+
+ if ( $this->semanticData !== null && ( $props = $this->semanticData->getPropertyValues( new DIProperty( '_TYPE' ) ) ) && count( $props ) > 1 ) {
+ $this->message .= $this->createMessage(
+ [
+ 'warning',
+ 'smw-property-req-violation-type'
+ ]
+ );
+ }
+
+ if ( $property->isUserDefined() && wfMessage( 'smw-property-introductory-message-user' )->exists() ) {
+ $this->message .= $this->createIntroductoryMessage( 'smw-property-introductory-message-user', $propertyName );
+ }
+
+ if ( !$property->isUserDefined() && wfMessage( 'smw-property-introductory-message-special' )->exists() ) {
+ $this->message .= $this->createIntroductoryMessage( 'smw-property-introductory-message-special', $propertyName );
+ }
+
+ if ( wfMessage( 'smw-property-introductory-message' )->exists() ) {
+ $this->message .= $this->createIntroductoryMessage( 'smw-property-introductory-message', $propertyName );
+ }
+
+ if ( $property->isUserDefined() && $this->store->getPropertyTableInfoFetcher()->isFixedTableProperty( $property ) ) {
+ $this->message .= $this->createFixedTableMessage( $propertyName );
+ }
+
+ if ( !$property->isUserDefined() ) {
+ $this->message .= $this->createPredefinedPropertyMessage( $property, $propertyName );
+ }
+
+ $label = mb_strtolower( str_replace( ' ', '-', $propertyName ) );
+
+ if ( wfMessage( "smw-property-message-$label" )->exists() ) {
+ $this->message .= $this->createIntroductoryMessage( "smw-property-message-$label", $propertyName, false );
+ }
+ }
+
+ private function createMessage( $messsage ) {
+
+ if ( !is_array( $messsage ) ) {
+ return '';
+ }
+
+ $type = array_shift( $messsage );
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => $messsage[0],
+ 'class' => 'plainlinks ' . ( $type !== '' ? 'smw-callout smw-callout-'. $type : '' )
+ ],
+ Message::get( $messsage, Message::PARSE, Message::USER_LANGUAGE )
+ );
+ }
+
+ private function createIntroductoryMessage( $msgKey, $propertyName, $class = true ) {
+
+ $message = wfMessage( $msgKey, $propertyName )->parse();
+
+ if ( $message === '' ) {
+ return '';
+ }
+
+ if ( $class === true ) {
+ $class = 'smw-callout smw-callout-info';
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => "$msgKey",
+ 'class' => "plainlinks $msgKey " . $class
+ ],
+ $message
+ );
+ }
+
+ private function createFixedTableMessage( $propertyName ) {
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'smw-property-content-fixedtable-message',
+ 'class' => 'plainlinks smw-callout smw-callout-info'
+ ],
+ wfMessage( 'smw-property-userdefined-fixedtable', $propertyName )->parse()
+ );
+ }
+
+ /**
+ * Returns an introductory text for a predefined property
+ *
+ * @note In order to enable a more detailed description for a specific
+ * predefined property a concatenated message key can be used (e.g
+ * 'smw-property-predefined' + <internal property key> => '_asksi' ) but
+ * because translatewiki.net doesn't handle `_` well, convert `_` to `-`
+ * resulting in 'smw-property-predefined-asksi' as translatable key
+ */
+ private function createPredefinedPropertyMessage( $property, $propertyName ) {
+
+ $key = $property->getKey();
+ $message = '';
+
+ if ( $property->isUserDefined() ) {
+ return $message;
+ }
+
+ if ( ( $messageKey = PropertyRegistry::getInstance()->findPropertyDescriptionMsgKeyById( $key ) ) !== '' ) {
+ $messageKeyLong = $messageKey . '-long';
+ } else {
+ $messageKey = 'smw-property-predefined' . str_replace( '_', '-', strtolower( $key ) );
+ $messageKeyLong = 'smw-property-predefined-long' . str_replace( '_', '-', strtolower( $key ) );
+ }
+
+ if ( wfMessage( $messageKey )->exists() ) {
+ $message .= wfMessage( $messageKey, $propertyName )->parse();
+ } else {
+ $message .= wfMessage( 'smw-property-predefined-default', $propertyName )->parse();
+ }
+
+ if ( wfMessage( $messageKeyLong )->exists() ) {
+ $message .= ' ' . wfMessage( $messageKeyLong )->parse();
+ }
+
+ $message .= ' ' . wfMessage( 'smw-property-predefined-common' )->parse();
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'smw-property-content-predefined-message',
+ 'class' => 'smw-property-predefined-intro plainlinks'
+ ],
+ $message
+ );
+ }
+
+ private function findErrorMessages() {
+
+ $pv = $this->semanticData->getPropertyValues( new DIProperty( '_ERRC' ) );
+ $errors = [];
+
+ foreach ( $pv as $v ) {
+ $subSemanticData = $this->semanticData->findSubSemanticData(
+ $v->getSubobjectName()
+ );
+
+ foreach ( $subSemanticData->getPropertyValues( new DIProperty( '_ERRT' ) ) as $error ) {
+ $errors[] = Message::decode( $error->getString(), Message::PARSE, Message::USER_LANGUAGE );
+ }
+ }
+
+ if ( $errors === [] ) {
+ return '';
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'smw-property-error-list',
+ 'class' => 'plainlinks smw-callout smw-callout-error'
+ ],
+ count( $errors ) > 1 ? '<ul><li>' . implode( '</li><li>', $errors ) . '</li></ul>' : implode( '', $errors )
+ );
+ }
+
+ private function checkUniqueness( DIProperty $property, $propertyName ) {
+
+ if ( $this->store->getObjectIds()->isUnique( $property ) ) {
+ return '';
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => 'smw-property-uniqueness',
+ 'class' => 'smw-callout smw-callout-error plainlinks'
+ ],
+ Message::get( [ 'smw-property-label-uniqueness', $propertyName ], Message::PARSE, Message::USER_LANGUAGE )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Protection/EditProtectionUpdater.php b/www/wiki/extensions/SemanticMediaWiki/src/Protection/EditProtectionUpdater.php
new file mode 100644
index 00000000..c5336abd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Protection/EditProtectionUpdater.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace SMW\Protection;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use SMW\DIProperty;
+use SMW\MediaWiki\Hooks\ArticleProtectComplete;
+use SMW\Message;
+use SMW\PropertyAnnotators\EditProtectedPropertyAnnotator;
+use SMW\SemanticData;
+use User;
+use WikiPage;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class EditProtectionUpdater implements LoggerAwareInterface {
+
+ /**
+ * @var WikiPage
+ */
+ private $wikiPage;
+
+ /**
+ * @var User
+ */
+ private $user;
+
+ /**
+ * @var boolean
+ */
+ private $isRestrictedUpdate = false;
+
+ /**
+ * @var boolean|string
+ */
+ private $editProtectionRight = false;
+
+ /**
+ * LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @since 2.5
+ *
+ * @param WikiPage $wikiPage
+ * @param User|null $user
+ */
+ public function __construct( WikiPage $wikiPage, User $user = null ) {
+ $this->wikiPage = $wikiPage;
+ $this->user = $user;
+
+ if ( $this->user === null ) {
+ $this->user = $GLOBALS['wgUser'];
+ }
+ }
+
+ /**
+ * @see LoggerAwareInterface::setLogger
+ *
+ * @since 2.5
+ *
+ * @param LoggerInterface $logger
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|boolean $editProtectionRight
+ */
+ public function setEditProtectionRight( $editProtectionRight ) {
+ $this->editProtectionRight = $editProtectionRight;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function isRestrictedUpdate() {
+ return $this->isRestrictedUpdate;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param SemanticData $semanticData
+ */
+ public function doUpdateFrom( SemanticData $semanticData ) {
+
+ // Do nothing
+ if ( $this->editProtectionRight === false ) {
+ return;
+ }
+
+ list( $isEditProtected, $isAnnotationBySystem ) = $this->fetchEditProtectedInfo( $semanticData );
+
+ $title = $this->wikiPage->getTitle();
+
+ if ( $title === null ) {
+ return;
+ }
+
+ $restrictions = array_flip( $title->getRestrictions( 'edit' ) );
+
+ // No `Is edit protected` was found and the restriction doesn't contain
+ // a matchable `editProtectionRight`
+ if ( $isEditProtected === null && !isset( $restrictions[$this->editProtectionRight] ) ) {
+ return $this->log( __METHOD__ . ' no update required' );
+ }
+
+ if ( $isEditProtected && !isset( $restrictions[$this->editProtectionRight] ) && !$isAnnotationBySystem ) {
+ return $this->doUpdateRestrictions( $isEditProtected );
+ }
+
+ if ( $isEditProtected && $title->isProtected( 'edit' ) || !$isEditProtected && !$title->isProtected( 'edit' ) ) {
+ return $this->log( __METHOD__ . ' Status already set, no update required' );
+ }
+
+ $this->doUpdateRestrictions( $isEditProtected );
+ }
+
+ private function fetchEditProtectedInfo( $semanticData ) {
+
+ // Whether or not the update was invoked by the ArticleProtectComplete hook
+ $this->isRestrictedUpdate = $semanticData->getOption( ArticleProtectComplete::RESTRICTED_UPDATE ) === true;
+ $property = new DIProperty( '_EDIP' );
+
+ $isEditProtected = null;
+ $isAnnotationBySystem = false;
+
+ $dataItems = $semanticData->getPropertyValues(
+ $property
+ );
+
+ if ( $dataItems !== [] ) {
+ $isEditProtected = false;
+
+ // In case of two competing values, true always wins
+ foreach ( $dataItems as $dataItem ) {
+
+ $isEditProtected = $dataItem->getBoolean();
+
+ if ( $isEditProtected ) {
+ break;
+ }
+ }
+
+ $isAnnotationBySystem = $dataItem->getOption( EditProtectedPropertyAnnotator::SYSTEM_ANNOTATION );
+ }
+
+ return [ $isEditProtected, $isAnnotationBySystem ];
+ }
+
+ private function doUpdateRestrictions( $isEditProtected ) {
+
+ $protections = [];
+ $expiry = [];
+
+ if ( $isEditProtected ) {
+ $this->log( __METHOD__ . ' add protection on edit, move' );
+
+ $protections = [
+ 'edit' => $this->editProtectionRight,
+ 'move' => $this->editProtectionRight
+ ];
+
+ $expiry = [
+ 'edit' => 'infinity',
+ 'move' => 'infinity'
+ ];
+ } else {
+ $this->log( __METHOD__ . ' remove protection on edit, move' );
+ $protections = [];
+ $expiry = [];
+ }
+
+ $reason = Message::get( 'smw-edit-protection-auto-update' );
+ $cascade = false;
+
+ $status = $this->wikiPage->doUpdateRestrictions(
+ $protections,
+ $expiry,
+ $cascade,
+ $reason,
+ $this->user
+ );
+ }
+
+ private function log( $message, $context = [] ) {
+
+ if ( $this->logger === null ) {
+ return;
+ }
+
+ $this->logger->info( $message, $context );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Protection/ProtectionValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/Protection/ProtectionValidator.php
new file mode 100644
index 00000000..ff8103ea
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Protection/ProtectionValidator.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace SMW\Protection;
+
+use Onoi\Cache\Cache;
+use SMW\CachedPropertyValuesPrefetcher;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\RequestOptions;
+use Title;
+
+/**
+ * Handles protection validation.
+ *
+ * The lookup is cached using the `CachedPropertyValuesPrefetcher` to avoid a
+ * continued access to the Store or DB layer.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ProtectionValidator {
+
+ /**
+ * Reference used in InMemoryPoolCache
+ */
+ const POOLCACHE_ID = 'protection.validator';
+
+ /**
+ * @var CachedPropertyValuesPrefetcher
+ */
+ private $cachedPropertyValuesPrefetcher;
+
+ /**
+ * @var Cache
+ */
+ private $intermediaryMemoryCache;
+
+ /**
+ * @var boolean|string
+ */
+ private $editProtectionRight = false;
+
+ /**
+ * @var boolean|string
+ */
+ private $createProtectionRight = false;
+
+ /**
+ * @var boolean|string
+ */
+ private $changePropagationProtection = true;
+
+ /**
+ * @since 2.5
+ *
+ * @param CachedPropertyValuesPrefetcher $cachedPropertyValuesPrefetcher
+ * @param Cache $intermediaryMemoryCache
+ */
+ public function __construct( CachedPropertyValuesPrefetcher $cachedPropertyValuesPrefetcher, Cache $intermediaryMemoryCache ) {
+ $this->cachedPropertyValuesPrefetcher = $cachedPropertyValuesPrefetcher;
+ $this->intermediaryMemoryCache = $intermediaryMemoryCache;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|boolean $editProtectionRight
+ */
+ public function setEditProtectionRight( $editProtectionRight ) {
+ $this->editProtectionRight = $editProtectionRight;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string|false
+ */
+ public function getEditProtectionRight() {
+ return $this->editProtectionRight;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|boolean $createProtectionRight
+ */
+ public function setCreateProtectionRight( $createProtectionRight ) {
+ $this->createProtectionRight = $createProtectionRight;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string|false
+ */
+ public function getCreateProtectionRight() {
+ return $this->createProtectionRight;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $changePropagationProtection
+ */
+ public function setChangePropagationProtection( $changePropagationProtection ) {
+ $this->changePropagationProtection = (bool)$changePropagationProtection;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ */
+ private function resetCacheBy( DIWikiPage $subject ) {
+ $this->cachedPropertyValuesPrefetcher->resetCacheBy( $subject );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ *
+ * @return boolean
+ */
+ public function hasEditProtectionOnNamespace( Title $title ) {
+ return $this->editProtectionRight && $this->checkProtection( DIWikiPage::newFromTitle( $title )->asBase() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ *
+ * @return boolean
+ */
+ public function hasChangePropagationProtection( Title $title ) {
+
+ $subject = DIWikiPage::newFromTitle( $title )->asBase();
+ $namespace = $subject->getNamespace();
+
+ if ( ( $namespace !== SMW_NS_PROPERTY && $namespace !== NS_CATEGORY ) || $this->changePropagationProtection === false ) {
+ return false;
+ }
+
+ return $this->checkProtection( $subject, new DIProperty( '_CHGPRO' ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Title $title
+ *
+ * @return boolean
+ */
+ public function hasProtection( Title $title ) {
+ return $this->checkProtection( DIWikiPage::newFromTitle( $title )->asBase() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Title $title
+ * @param Title $title
+ *
+ * @return boolean
+ */
+ public function hasCreateProtection( Title $title ) {
+ return $this->createProtectionRight && !$title->userCan( 'edit' );
+ }
+
+ /**
+ * @note There is not direct validation of the permission within this method,
+ * it is done by the Title::userCan when probing against the User and hooks
+ * that carry out the permission check including the validation provided by
+ * SMW's `PermissionPthValidator`.
+ *
+ * @since 2.5
+ *
+ * @param Title $title
+ *
+ * @return boolean
+ */
+ public function hasEditProtection( Title $title ) {
+ return !$title->userCan( 'edit' ) && $this->checkProtection( DIWikiPage::newFromTitle( $title )->asBase() );
+ }
+
+ private function checkProtection( $subject, $property = null ) {
+
+ if ( $property === null ) {
+ $property = new DIProperty( '_EDIP' );
+ }
+
+ $key = $subject->getHash() . $property->getKey();
+ $hasProtection = false;
+
+ if ( $this->intermediaryMemoryCache->contains( $key ) ) {
+ return $this->intermediaryMemoryCache->fetch( $key );
+ }
+
+ // Set editProtectionRight to influence the key to detect changes
+ // before the cache is evicted
+ $requestOptions = new RequestOptions();
+ $requestOptions->addExtraCondition( $this->editProtectionRight );
+
+ $dataItems = $this->cachedPropertyValuesPrefetcher->getPropertyValues(
+ $subject,
+ $property,
+ $requestOptions
+ );
+
+ if ( $dataItems !== null && $dataItems !== [] ) {
+ $hasProtection = $property->getKey() === '_EDIP' ? end( $dataItems )->getBoolean() : true;
+ }
+
+ $this->intermediaryMemoryCache->save( $key, $hasProtection );
+
+ return $hasProtection;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/DebugFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/DebugFormatter.php
new file mode 100644
index 00000000..d9c84dbf
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/DebugFormatter.php
@@ -0,0 +1,261 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\ProcessingErrorMsgHandler;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class DebugFormatter {
+
+ const JSON_FORMAT = 'json';
+
+ /**
+ * @var boolean
+ */
+ private static $explainFormat = '';
+
+ /**
+ * @since 3.0
+ *
+ * @param string $explainFormat
+ */
+ public static function setExplainFormat( $explainFormat ) {
+ if ( $explainFormat === self::JSON_FORMAT ) {
+ self::$explainFormat = $explainFormat;
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return string
+ */
+ public static function getFormat( $type ) {
+
+ $format = '';
+
+ // Use a more expressive explain output
+ // https://dev.mysql.com/doc/refman/5.6/en/explain.html
+ // https://mariadb.com/kb/en/mariadb/explain-formatjson-in-mysql/
+ if ( $type === 'mysql' && self::$explainFormat === self::JSON_FORMAT ) {
+ $format = 'FORMAT=json';
+ }
+
+ return $format;
+ }
+
+ /**
+ * Generate textual debug output that shows an arbitrary list of informative
+ * fields. Used for formatting query debug output.
+ *
+ * @note All strings given must be usable and safe in wiki and HTML
+ * contexts.
+ *
+ * @param $storeName string name of the storage backend for which this is generated
+ * @param $entries array of name => value of informative entries to display
+ * @param $query SMWQuery or null, if given add basic data about this query as well
+ *
+ * @return string
+ */
+ public static function getStringFrom( $storeName, array $entries, Query $query = null ) {
+
+ if ( $query instanceof Query ) {
+ $preEntries = [];
+ $preEntries['ASK Query'] = '<div class="smwpre">' . str_replace( '[', '&#91;', $query->getDescription()->getQueryString() ) . '</div>';
+ $entries = array_merge( $preEntries, $entries );
+ $entries['Query Metrics'] = 'Query-Size:' . $query->getDescription()->getSize() . '<br />' .
+ 'Query-Depth:' . $query->getDescription()->getDepth();
+ $errors = '';
+
+ $queryErrors = ProcessingErrorMsgHandler::normalizeAndDecodeMessages(
+ $query->getErrors()
+ );
+
+ foreach ( $queryErrors as $error ) {
+ $errors .= $error . '<br />';
+ }
+
+ if ( $errors === '' ) {
+ $errors = 'None';
+ }
+
+ $entries['Errors and Warnings'] = $errors;
+ }
+
+ $result = '<div class="smw-debug" style="border: 5px dotted #ffcc00; background: #FFF0BD; padding: 20px; margin-bottom: 10px;">' .
+ "<div class='smw-column-header'><big>$storeName debug output</big></div>";
+
+ foreach ( $entries as $header => $information ) {
+ $result .= "<div class='smw-column-header'>$header</div>";
+
+ if ( $information !== '' ) {
+ $result .= "$information";
+ }
+ }
+
+ $result .= '</div>';
+
+ return $result;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $type
+ * @param array $rows
+ *
+ * @return string
+ */
+ public static function prettifyExplain( $type, $res ) {
+
+ $output = '';
+
+ // https://dev.mysql.com/doc/refman/5.0/en/explain-output.html
+ if ( $type === 'mysql' ) {
+ $output .= '<div class="smwpre" style="word-break:normal;">' .
+ '<table class="" style="border-spacing: 5px;"><tr>' .
+ '<th style="text-align: left;">ID</th>'.
+ '<th style="text-align: left;">select_type</th>'.
+ '<th style="text-align: left;">table</th>'.
+ '<th style="text-align: left;">type</th>'.
+ '<th style="text-align: left;">possible_keys</th>'.
+ '<th style="text-align: left;">key</th>'.
+ '<th style="text-align: left;">key_len</th>'.
+ '<th style="text-align: left;">ref</th>'.
+ '<th style="text-align: left;">rows</th>'.
+ '<th style="text-align: left;">Extra</th></tr>';
+
+ foreach ( $res as $row ) {
+
+ if ( isset( $row->EXPLAIN ) ) {
+ return '<div class="smwpre">' . $row->EXPLAIN . '</div>';
+ }
+
+ $output .= "<tr><td>" . $row->id .
+ "</td><td>" . $row->select_type .
+ "</td><td>" . $row->table .
+ "</td><td>" . $row->type .
+ "</td><td>" . $row->possible_keys .
+ "</td><td>" . $row->key .
+ "</td><td>" . $row->key_len .
+ "</td><td>" . $row->ref .
+ "</td><td>" . $row->rows .
+ "</td><td>" . $row->Extra . "</td></tr>";
+ }
+
+ $output .= '</table></div>';
+ }
+
+ if ( $type === 'postgres' ) {
+ $output .= '<div class="smwpre">';
+
+ foreach ( $res as $row ) {
+ foreach ( $row as $key => $value ) {
+ $output .= str_replace( [ ' ', '->' ], [ '&nbsp;', '└── ' ], $value ) .'<br>';
+ }
+ }
+
+ $output .= '</div>';
+ }
+
+ // SQlite doesn't support this
+ if ( $type === 'sqlite' ) {
+ $output .= 'Not supported.';
+ }
+
+ return $output;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $sparql
+ *
+ * @return string
+ */
+ public static function prettifySparql( $sparql ) {
+
+ $sparql = str_replace(
+ [
+ '[',
+ ':',
+ ' ',
+ '<',
+ '>'
+ ],
+ [
+ '&#91;',
+ '&#x003A;',
+ '&#x0020;',
+ '&#x3C;',
+ '&#x3E;'
+ ],
+ $sparql
+ );
+
+ return '<div class="smwpre">' . $sparql . '</div>';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $sql
+ * @param string $alias
+ *
+ * @return string
+ */
+ public static function prettifySql( $sql, $alias ) {
+
+ $sql = str_replace(
+ [
+ "SELECT DISTINCT",
+ "FROM",
+ "INNER JOIN",
+ "LEFT OUTER JOIN",
+ "LEFT JOIN",
+ "RIGHT JOIN",
+ "WHERE",
+ "ORDER BY",
+ "GROUP BY",
+ "LIMIT",
+ "OFFSET",
+ "AND $alias.smw_",
+ ",$alias.smw_",
+ "AND (",
+ "))",
+ "(("
+ ],
+ [
+ "SELECT DISTINCT<br>&nbsp;",
+ "<br>FROM<br>&nbsp;",
+ "<br>INNER JOIN<br>&nbsp;",
+ "<br>LEFT OUTER JOIN<br>&nbsp;",
+ "<br>LEFT JOIN<br>&nbsp;",
+ "<br>RIGHT JOIN<br>&nbsp;",
+ "<br>WHERE<br>&nbsp;",
+ "<br>ORDER BY<br>&nbsp;",
+ "<br>GROUP BY<br>&nbsp;",
+ "<br>LIMIT<br>&nbsp;",
+ "<br>OFFSET<br>&nbsp;",
+ "<br>&nbsp;&nbsp;AND $alias.smw_",
+ ",<br>&nbsp;&nbsp;$alias.smw_",
+ "<br>&nbsp;&nbsp;&nbsp;AND (",
+ ")<br>&nbsp;&nbsp;)",
+ "(<br>&nbsp;&nbsp;&nbsp;("
+ ],
+ $sql
+ );
+
+ return '<div class="smwpre">' . $sql . '</div>';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Deferred.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Deferred.php
new file mode 100644
index 00000000..23ed1382
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Deferred.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace SMW\Query;
+
+use Html;
+use ParserOutput;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Deferred {
+
+ /**
+ * Identifies the showMode
+ */
+ const SHOW_MODE = 'dq.showmode';
+
+ /**
+ * Identifies unparsed parameters
+ */
+ const QUERY_PARAMETERS = 'dq.parameters';
+
+ /**
+ * Identifies the @control element
+ */
+ const CONTROL_ELEMENT = 'dq.control';
+
+ /**
+ * @since 3.0
+ *
+ * @param ParserOutput $parserOutput
+ */
+ public static function registerResources( ParserOutput $parserOutput ) {
+ $parserOutput->addModuleStyles( 'ext.smw.deferred.styles' );
+ $parserOutput->addModules( 'ext.smw.deferred' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ *
+ * @return string
+ */
+ public static function buildHTML( Query $query ) {
+
+ $isShowMode = $query->getOption( self::SHOW_MODE );
+ $params = $query->getOption( 'query.params' );
+
+ // Ensures that a generated string can appear next to another text
+ $element = $isShowMode ? 'span' : 'div';
+
+ $result = Html::rawElement(
+ $element,
+ [
+ 'class' => 'smw-deferred-query' . ( isset( $params['class'] ) ? ' ' . $params['class'] : '' ),
+ 'data-query' => json_encode(
+ [
+ 'query' => trim( $query->getOption( self::QUERY_PARAMETERS ) ),
+ 'params' => $params,
+ 'limit' => $query->getLimit(),
+ 'offset' => $query->getOffset(),
+ 'max' => $GLOBALS['smwgQMaxInlineLimit'],
+ 'cmd' => $isShowMode ? 'show' : 'ask'
+ ]
+ )
+ ],
+ Html::rawElement(
+ $element,
+ [
+ 'id' => 'deferred-control',
+ 'data-control' => $isShowMode ? '' : $query->getOption( self::CONTROL_ELEMENT )
+ ]
+ ) . Html::rawElement(
+ $element,
+ [
+ 'id' => 'deferred-output',
+ 'class' => 'smw-loading-image-dots'
+ ]
+ )
+ );
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/DescriptionFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/DescriptionFactory.php
new file mode 100644
index 00000000..2a294abe
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/DescriptionFactory.php
@@ -0,0 +1,144 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Query\Language\ClassDescription;
+use SMW\Query\Language\ConceptDescription;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\NamespaceDescription;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ThingDescription;
+use SMW\Query\Language\ValueDescription;
+use SMWDataItem as DataItem;
+use SMWDataValue as DataValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class DescriptionFactory {
+
+ /**
+ * @since 2.4
+ *
+ * @param DataItem $dataItem
+ * @param DIProperty|null $property = null
+ * @param integer $comparator
+ *
+ * @return ValueDescription
+ */
+ public function newValueDescription( DataItem $dataItem, DIProperty $property = null, $comparator = SMW_CMP_EQ ) {
+ return new ValueDescription( $dataItem, $property, $comparator );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ * @param Description $description
+ *
+ * @return SomeProperty
+ */
+ public function newSomeProperty( DIProperty $property, Description $description ) {
+ return new SomeProperty( $property, $description );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return ThingDescription
+ */
+ public function newThingDescription() {
+ return new ThingDescription();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Description[] $descriptions
+ *
+ * @return Disjunction
+ */
+ public function newDisjunction( $descriptions = [] ) {
+ return new Disjunction( $descriptions );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Description[] $descriptions
+ *
+ * @return Conjunction
+ */
+ public function newConjunction( $descriptions = [] ) {
+ return new Conjunction( $descriptions );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $ns
+ *
+ * @return NamespaceDescription
+ */
+ public function newNamespaceDescription( $ns ) {
+ return new NamespaceDescription( $ns );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIWikiPage|[] $category
+ *
+ * @return ClassDescription
+ */
+ public function newClassDescription( $category ) {
+ return new ClassDescription( $category );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIWikiPage $concept
+ *
+ * @return ConceptDescription
+ */
+ public function newConceptDescription( DIWikiPage $concept ) {
+ return new ConceptDescription( $concept );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DataValue $dataValue
+ *
+ * @return Description
+ */
+ public function newFromDataValue( DataValue $dataValue ) {
+
+ if ( !$dataValue->isValid() ) {
+ return $this->newThingDescription();
+ }
+
+ // Avoid circular reference when called from outside of the DV context
+ $dataValue->setOption( DataValue::OPT_QUERY_CONTEXT, true );
+
+ $description = $dataValue->getQueryDescription( $dataValue->getWikiValue() );
+
+ if ( $dataValue->getProperty() === null ) {
+ return $description;
+ }
+
+ return $this->newSomeProperty(
+ $dataValue->getProperty(),
+ $description
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Exception/FingerprintNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Exception/FingerprintNotFoundException.php
new file mode 100644
index 00000000..903516c7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Exception/FingerprintNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Query\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class FingerprintNotFoundException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Exception/ResultFormatNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Exception/ResultFormatNotFoundException.php
new file mode 100644
index 00000000..06040ed9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Exception/ResultFormatNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\Query\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ResultFormatNotFoundException extends RuntimeException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Excerpts.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Excerpts.php
new file mode 100644
index 00000000..d16f5e8f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Excerpts.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\DIWikiPage;
+
+/**
+ * Record excerpts for query results that support an excerpt retrieval function.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Excerpts {
+
+ /**
+ * @var []
+ */
+ protected $excerpts = [];
+
+ /**
+ * @var boolean
+ */
+ protected $noHighlight = false;
+
+ /**
+ * @var boolean
+ */
+ protected $hasHighlight = false;
+
+ /**
+ * @var boolean
+ */
+ protected $stripTags = true;
+
+ /**
+ * @since 3.0
+ */
+ public function noHighlight() {
+ $this->noHighlight = true;
+ }
+
+ /**
+ * @note The hash is expected to be equivalent to DIWikiPage::getHash to
+ * easily match result subjects available in an QueryResult instance.
+ *
+ * @since 3.0
+ *
+ * @param DIWikiPage|string $hash
+ * @param string|integer $score
+ */
+ public function addExcerpt( $hash, $excerpt ) {
+
+ if ( $hash instanceof DIWikiPage ) {
+ $hash = $hash->getHash();
+ }
+
+ $this->excerpts[] = [ $hash, $excerpt ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage|string $hash
+ *
+ * @return string|integer|false
+ */
+ public function getExcerpt( $hash ) {
+
+ if ( $hash instanceof DIWikiPage ) {
+ $hash = $hash->getHash();
+ }
+
+ foreach ( $this->excerpts as $map ) {
+ if ( $map[0] === $hash ) {
+ return $map[1];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getExcerpts() {
+ return $this->excerpts;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasHighlight() {
+ return $this->hasHighlight;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ExportPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ExportPrinter.php
new file mode 100644
index 00000000..bb4e3871
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ExportPrinter.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace SMW\Query;
+
+use SMWQueryResult as QueryResult;
+
+/**
+ * Interface for SMW export related result printers
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+interface ExportPrinter extends ResultPrinter {
+
+ /**
+ * Outputs the result as file.
+ *
+ * @since 1.8
+ *
+ * @param QueryResult $queryResult
+ * @param array $params
+ */
+ public function outputAsFile( QueryResult $queryResult, array $params );
+
+ /**
+ * Some printers do not mainly produce embeddable HTML or Wikitext, but
+ * produce stand-alone files. An example is RSS or iCalendar. This function
+ * returns the mimetype string that this file would have, or FALSE if no
+ * standalone files are produced.
+ *
+ * If this function returns something other than FALSE, then the printer will
+ * not be regarded as a printer that displays in-line results. This is used to
+ * determine if a file output should be generated in Special:Ask.
+ *
+ * @since 1.8
+ *
+ * @param QueryResult $queryResult
+ *
+ * @return string
+ */
+ public function getMimeType( QueryResult $queryResult );
+
+ /**
+ * Some printers can produce not only embeddable HTML or Wikitext, but
+ * can also produce stand-alone files. An example is RSS or iCalendar.
+ * This function returns a filename that is to be sent to the caller
+ * in such a case (the default filename is created by browsers from the
+ * URL, and it is often not pretty).
+ *
+ * @param QueryResult $queryResult
+ *
+ * @return string|boolean
+ */
+ public function getFileName( QueryResult $queryResult );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ClassDescription.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ClassDescription.php
new file mode 100644
index 00000000..273193ac
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ClassDescription.php
@@ -0,0 +1,210 @@
+<?php
+
+namespace SMW\Query\Language;
+
+use Exception;
+use SMW\DataValueFactory;
+use SMW\DIWikiPage;
+use SMW\Localizer;
+
+/**
+ * Description of a single class as given by a wiki category, or of a
+ * disjunction of such classes. Corresponds to (disjunctions of) atomic classes
+ * in OWL and to (unions of) classes in RDF.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class ClassDescription extends Description {
+
+ /**
+ * @var array of DIWikiPage
+ */
+ protected $m_diWikiPages;
+
+ /**
+ * @var integer|null
+ */
+ protected $hierarchyDepth;
+
+ /**
+ * Constructor.
+ *
+ * @param mixed $content DIWikiPage or array of DIWikiPage
+ *
+ * @throws Exception
+ */
+ public function __construct( $content ) {
+ if ( $content instanceof DIWikiPage ) {
+ $this->m_diWikiPages = [ $content ];
+ } elseif ( is_array( $content ) ) {
+ $this->m_diWikiPages = $content;
+ } else {
+ throw new Exception( "ClassDescription::__construct(): parameter must be an DIWikiPage object or an array of such objects." );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $hierarchyDepth
+ */
+ public function setHierarchyDepth( $hierarchyDepth ) {
+
+ if ( $hierarchyDepth > $GLOBALS['smwgQSubcategoryDepth'] ) {
+ $hierarchyDepth = $GLOBALS['smwgQSubcategoryDepth'];
+ }
+
+ $this->hierarchyDepth = $hierarchyDepth;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return integer|null
+ */
+ public function getHierarchyDepth() {
+ return $this->hierarchyDepth;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ClassDescription $description
+ *
+ * @return boolean
+ */
+ public function isMergableDescription( ClassDescription $description ) {
+
+ if ( isset( $this->isNegation ) && isset( $description->isNegation ) ) {
+ return true;
+ }
+
+ if ( !isset( $this->isNegation ) && !isset( $description->isNegation ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $dataItem
+ */
+ public function addClass( DIWikiPage $dataItem ) {
+ $this->m_diWikiPages[] = $dataItem;
+ }
+
+ /**
+ * @param ClassDescription $description
+ */
+ public function addDescription( ClassDescription $description ) {
+ $this->m_diWikiPages = array_merge( $this->m_diWikiPages, $description->getCategories() );
+ }
+
+ /**
+ * @see Description::getFingerprint
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getFingerprint() {
+
+ $hash = [];
+
+ foreach ( $this->m_diWikiPages as $subject ) {
+ $hash[$subject->getHash()] = true;
+ }
+
+ ksort( $hash );
+ $extra = ( isset( $this->isNegation ) ? '|' . $this->isNegation : '' );
+
+ return 'Cl:' . md5( implode( '|', array_keys( $hash ) ) . $this->hierarchyDepth . $extra );
+ }
+
+ /**
+ * @return array of DIWikiPage
+ */
+ public function getCategories() {
+ return $this->m_diWikiPages;
+ }
+
+ public function getQueryString( $asValue = false ) {
+
+ $first = true;
+ $namespaceText = Localizer::getInstance()->getNamespaceTextById( NS_CATEGORY );
+
+ foreach ( $this->m_diWikiPages as $wikiPage ) {
+ $wikiValue = DataValueFactory::getInstance()->newDataValueByItem( $wikiPage, null );
+ if ( $first ) {
+ $result = '[[' . $namespaceText . ':' . ( isset( $this->isNegation ) ? '!' : '' ) . $wikiValue->getText();
+ $first = false;
+ } else {
+ $result .= '||' . ( isset( $this->isNegation ) ? '!' : '' ) . $wikiValue->getText();
+ }
+ }
+
+ if ( $this->hierarchyDepth !== null ) {
+ $result .= '|+depth=' . $this->hierarchyDepth;
+ }
+
+ $result .= ']]';
+
+ if ( $asValue ) {
+ return ' <q>' . $result . '</q> ';
+ }
+
+ return $result;
+ }
+
+ public function isSingleton() {
+ return false;
+ }
+
+ public function getSize() {
+
+ if ( $GLOBALS['smwgQSubcategoryDepth'] > 0 ) {
+ return 1; // disj. of cats should not cause much effort if we compute cat-hierarchies anyway!
+ }
+
+ return count( $this->m_diWikiPages );
+ }
+
+ public function getQueryFeatures() {
+
+ if ( count( $this->m_diWikiPages ) > 1 ) {
+ return SMW_CATEGORY_QUERY | SMW_DISJUNCTION_QUERY;
+ }
+
+ return SMW_CATEGORY_QUERY;
+ }
+
+ public function prune( &$maxsize, &$maxdepth, &$log ) {
+
+ if ( $maxsize >= $this->getSize() ) {
+ $maxsize = $maxsize - $this->getSize();
+ return $this;
+ } elseif ( $maxsize <= 0 ) {
+ $log[] = $this->getQueryString();
+ $result = new ThingDescription();
+ } else {
+ $result = new ClassDescription( array_slice( $this->m_diWikiPages, 0, $maxsize ) );
+ $rest = new ClassDescription( array_slice( $this->m_diWikiPages, $maxsize ) );
+
+ $result->setHierarchyDepth(
+ $this->getHierarchyDepth()
+ );
+
+ $log[] = $rest->getQueryString();
+ $maxsize = 0;
+ }
+
+ $result->setPrintRequests( $this->getPrintRequests() );
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ConceptDescription.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ConceptDescription.php
new file mode 100644
index 00000000..482c04d7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ConceptDescription.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace SMW\Query\Language;
+
+use SMW\DataValueFactory;
+use SMW\DIWikiPage;
+
+/**
+ * Description of a single class as described by a concept page in the wiki.
+ * Corresponds to classes in (the EL fragment of) OWL DL, and to some extent to
+ * tree-shaped queries in SPARQL.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class ConceptDescription extends Description {
+
+ /**
+ * @var DIWikiPage
+ */
+ private $concept;
+
+ /**
+ * @param DIWikiPage $concept
+ */
+ public function __construct( DIWikiPage $concept ) {
+ $this->concept = $concept;
+ }
+
+ /**
+ * @see Description::getFingerprint
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getFingerprint() {
+ return 'Co:' . md5( $this->concept->getHash() );
+ }
+
+ /**
+ * @return DIWikiPage
+ */
+ public function getConcept() {
+ return $this->concept;
+ }
+
+ public function getQueryString( $asValue = false ) {
+
+ $pageValue = DataValueFactory::getInstance()->newDataValueByItem( $this->concept, null );
+ $result = '[[' . $pageValue->getPrefixedText() . ']]';
+
+ if ( $asValue ) {
+ return ' <q>' . $result . '</q> ';
+ }
+
+ return $result;
+ }
+
+ public function isSingleton() {
+ return false;
+ }
+
+ public function getQueryFeatures() {
+ return SMW_CONCEPT_QUERY;
+ }
+
+ ///NOTE: getSize and getDepth /could/ query the store to find the real size
+ /// of the concept. But it is not clear if this is desirable anyway, given that
+ /// caching structures may be established for retrieving concepts more quickly.
+ /// Inspecting those would require future requests to the store, and be very
+ /// store specific.
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Conjunction.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Conjunction.php
new file mode 100644
index 00000000..be5e8dbe
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Conjunction.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace SMW\Query\Language;
+
+/**
+ * Description of a collection of many descriptions, all of which
+ * must be satisfied (AND, conjunction).
+ *
+ * Corresponds to conjunction in OWL and SPARQL. Not available in RDFS.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class Conjunction extends Description {
+
+ /**
+ * @var Description[]
+ */
+ protected $descriptions = [];
+
+ /**
+ * @since 1.6
+ *
+ * @param array $descriptions
+ */
+ public function __construct( array $descriptions = [] ) {
+ foreach ( $descriptions as $description ) {
+ $this->addDescription( $description );
+ }
+ }
+
+ /**
+ * @see Description::getFingerprint
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getFingerprint() {
+
+ if ( $this->fingerprint !== null ) {
+ return $this->fingerprint;
+ }
+
+ $fingerprint = [];
+
+ // Filter equal signatures
+ foreach ( $this->descriptions as $description ) {
+ $fingerprint[$description->getFingerprint()] = true;
+ }
+
+ // Sorting to generate a constant fingerprint independent of its
+ // position within a conjunction ( [Foo]][[Bar]], [[Bar]][[Foo]])
+ ksort( $fingerprint );
+
+ return $this->fingerprint = 'C:' . md5( implode( '|', array_keys( $fingerprint ) ) );
+ }
+
+ public function getDescriptions() {
+ return $this->descriptions;
+ }
+
+ public function addDescription( Description $description ) {
+
+ $this->fingerprint = null;
+
+ if ( ! ( $description instanceof ThingDescription ) ) {
+ if ( $description instanceof Conjunction ) { // absorb sub-conjunctions
+ foreach ( $description->getDescriptions() as $subdesc ) {
+ $this->descriptions[$subdesc->getFingerprint()] = $subdesc;
+ }
+ } else {
+ $this->descriptions[$description->getFingerprint()] = $description;
+ }
+
+ // move print descriptions downwards
+ ///TODO: This may not be a good solution, since it does modify $description and since it does not react to future changes
+ $this->m_printreqs = array_merge( $this->m_printreqs, $description->getPrintRequests() );
+ $description->setPrintRequests( [] );
+ }
+
+ $fingerprint = $this->getFingerprint();
+
+ foreach ( $this->descriptions as $description ) {
+ $description->setMembership( $fingerprint );
+ }
+ }
+
+ public function getQueryString( $asvalue = false ) {
+ $result = '';
+
+ foreach ( $this->descriptions as $desc ) {
+ $result .= ( $result ? ' ' : '' ) . $desc->getQueryString( false );
+ }
+
+ if ( $result === '' ) {
+ return $asvalue ? '+' : '';
+ }
+
+ // <q> not needed for stand-alone conjunctions (AND binds stronger than OR)
+ return $asvalue ? " <q>{$result}</q> " : $result;
+ }
+
+ public function isSingleton() {
+ foreach ( $this->descriptions as $d ) {
+ if ( $d->isSingleton() ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public function getSize() {
+ $size = 0;
+
+ foreach ( $this->descriptions as $desc ) {
+ $size += $desc->getSize();
+ }
+
+ return $size;
+ }
+
+ public function getDepth() {
+ $depth = 0;
+
+ foreach ( $this->descriptions as $desc ) {
+ $depth = max( $depth, $desc->getDepth() );
+ }
+
+ return $depth;
+ }
+
+ public function getQueryFeatures() {
+ $result = SMW_CONJUNCTION_QUERY;
+
+ foreach ( $this->descriptions as $desc ) {
+ $result = $result | $desc->getQueryFeatures();
+ }
+
+ return $result;
+ }
+
+ public function prune( &$maxsize, &$maxdepth, &$log ) {
+ if ( $maxsize <= 0 ) {
+ $log[] = $this->getQueryString();
+ return new ThingDescription();
+ }
+
+ $prunelog = [];
+ $newdepth = $maxdepth;
+ $result = new Conjunction();
+
+ foreach ( $this->descriptions as $desc ) {
+ $restdepth = $maxdepth;
+ $result->addDescription( $desc->prune( $maxsize, $restdepth, $prunelog ) );
+ $newdepth = min( $newdepth, $restdepth );
+ }
+
+ if ( count( $result->getDescriptions() ) > 0 ) {
+ $log = array_merge( $log, $prunelog );
+ $maxdepth = $newdepth;
+
+ if ( count( $result->getDescriptions() ) == 1 ) { // simplify unary conjunctions!
+ $descriptions = $result->getDescriptions();
+ $result = array_shift( $descriptions );
+ }
+
+ $result->setPrintRequests( $this->getPrintRequests() );
+
+ return $result;
+ } else {
+ $log[] = $this->getQueryString();
+
+ $result = new ThingDescription();
+ $result->setPrintRequests( $this->getPrintRequests() );
+
+ return $result;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Description.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Description.php
new file mode 100644
index 00000000..5e5a0ffc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Description.php
@@ -0,0 +1,207 @@
+<?php
+
+namespace SMW\Query\Language;
+
+use SMW\Query\Exception\FingerprintNotFoundException;
+use SMW\Query\PrintRequest;
+
+/**
+ * Abstract base class for all descriptions
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+abstract class Description {
+
+ /**
+ * @var PrintRequest[]
+ */
+ protected $m_printreqs = [];
+
+ /**
+ * @var string|null
+ */
+ protected $fingerprint = null;
+
+ /**
+ * @var string
+ */
+ private $membership = '';
+
+ /**
+ * Get the (possibly empty) array of all print requests that
+ * exist for the entities that fit this description.
+ *
+ * @return PrintRequest[]
+ */
+ public function getPrintRequests() {
+ return $this->m_printreqs;
+ }
+
+ /**
+ * Set the array of print requests completely.
+ *
+ * @param PrintRequest[] $printRequests
+ */
+ public function setPrintRequests( array $printRequests ) {
+ $this->m_printreqs = $printRequests;
+ }
+
+ /**
+ * Add a single SMW\Query\PrintRequest.
+ *
+ * @param PrintRequest $printRequest
+ */
+ public function addPrintRequest( PrintRequest $printRequest ) {
+ $this->m_printreqs[] = $printRequest;
+ }
+
+ /**
+ * Add a new print request, but at the beginning of the list of requests
+ * (thus it will be printed first).
+ *
+ * @param PrintRequest $printRequest
+ */
+ public function prependPrintRequest( PrintRequest $printRequest ) {
+ array_unshift( $this->m_printreqs, $printRequest );
+ }
+
+ /**
+ * Returns a compound signature that identifies the canonized
+ * description. It builds a fingerrint so that [[Foo::123]][[Bar::abc]]
+ * returns the same signature as for [[Bar::abc]][[Foo::123]].
+ *
+ * @note An extension to a description should not rely on the query string
+ * as sole representation for a fingerprint.
+ *
+ * @since 2.5
+ *
+ * @return string
+ * @throws FingerprintNotFoundException
+ */
+ public function getFingerprint() {
+
+ if ( $this->fingerprint !== null ) {
+ return $this->fingerprint;
+ }
+
+ throw new FingerprintNotFoundException( "Missing a fingerprint, a signature was expected for the current description instance." );
+ }
+
+ /**
+ * Identifies an arbitrary membership to a wider circle of descriptions that
+ * mostly occurs in connection with a Conjunction, Disjunction, or
+ * SomeProperty.
+ *
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getMembership() {
+ return $this->membership;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $membership
+ */
+ public function setMembership( $membership ) {
+ $this->membership = $membership;
+ }
+
+ /**
+ * Return a string expressing this query.
+ * Some descriptions have different syntax in property value positions. The
+ * parameter $asvalue specifies whether the serialisation should take that into
+ * account.
+ *
+ * Example: The SMWValueDescription [[Paris]] returns the single result "Paris"
+ * but can also be used as value in [[has location::Paris]] which is preferred
+ * over the canonical [[has location::\<q\>[[Paris]]\</q\>]].
+ *
+ * The result should be a plain query string that SMW is able to parse,
+ * without any kind of HTML escape sequences.
+ *
+ * @param boolean $asValue
+ *
+ * @return string
+ */
+ abstract public function getQueryString( $asValue = false );
+
+ /**
+ * Return true if the description is required to encompass at most a single
+ * result, independently of the knowledge base.
+ *
+ * @return boolean
+ */
+ abstract public function isSingleton();
+
+ /**
+ * Compute the size of the description. Default is 1.
+ *
+ * @return integer
+ */
+ public function getSize() {
+ return 1;
+ }
+
+ /**
+ * Compute the depth of the description. Default is 0.
+ *
+ * @return integer
+ */
+ public function getDepth() {
+ return 0;
+ }
+
+ /**
+ * Report on query features used in description. Return values are (sums of)
+ * query feature constants such as SMW_PROPERTY_QUERY.
+ */
+ public function getQueryFeatures() {
+ return 0;
+ }
+
+ /**
+ * Recursively restrict query to a maximal size and depth as given.
+ * Returns a possibly changed description that should be used as a replacement.
+ * Reduce values of parameters to account for the returned descriptions size.
+ * Default implementation for non-nested descriptions of size 1.
+ * The parameter $log contains a list of all pruned conditions, updated when some
+ * description was reduced.
+ *
+ * @note Objects must not do changes on $this during pruning, since $this can be
+ * reused in multiple places of one or many queries. Make new objects to reflect
+ * changes!
+ */
+ public function prune( &$maxsize, &$maxDepth, &$log ) {
+
+ if ( ( $maxsize < $this->getSize() ) || ( $maxDepth < $this->getDepth() ) ) {
+ $log[] = $this->getQueryString();
+
+ $result = new ThingDescription();
+ $result->setPrintRequests( $this->getPrintRequests() );
+
+ return $result;
+ }
+
+ $maxsize = $maxsize - $this->getSize();
+ $maxDepth = $maxDepth - $this->getDepth();
+
+ return $this;
+ }
+
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->getQueryString();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Disjunction.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Disjunction.php
new file mode 100644
index 00000000..04fddc15
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/Disjunction.php
@@ -0,0 +1,239 @@
+<?php
+
+namespace SMW\Query\Language;
+
+/**
+ * Description of a collection of many descriptions, at least one of which
+ * must be satisfied (OR, disjunction).
+ *
+ * Corresponds to disjunction in OWL and SPARQL. Not available in RDFS.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class Disjunction extends Description {
+
+ /**
+ * @var Description[]
+ */
+ private $descriptions = [];
+
+ /**
+ * contains a single class description if any such disjunct was given;
+ * disjunctive classes are aggregated therei
+ * n
+ * @var null|ClassDescription
+ */
+ private $classDescription = null;
+
+ /**
+ * Used if disjunction is trivially true already
+ *
+ * @var boolean
+ */
+ private $isTrue = false;
+
+ public function __construct( array $descriptions = [] ) {
+ foreach ( $descriptions as $desc ) {
+ $this->addDescription( $desc );
+ }
+ }
+
+ /**
+ * @see Description::getFingerprint
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getFingerprint() {
+
+ // Avoid a recursive tree
+ if ( $this->fingerprint !== null ) {
+ return $this->fingerprint;
+ }
+
+ $fingerprint = [];
+
+ foreach ( $this->descriptions as $description ) {
+ $fingerprint[$description->getFingerprint()] = true;
+ }
+
+ ksort( $fingerprint );
+
+ return $this->fingerprint = 'D:' . md5( implode( '|', array_keys( $fingerprint ) ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $hierarchyDepth
+ */
+ public function setHierarchyDepth( $hierarchyDepth ) {
+
+ $this->fingerprint = null;
+
+ if ( $this->classDescription !== null ) {
+ $this->classDescription->setHierarchyDepth( $hierarchyDepth );
+ }
+
+ foreach ( $this->descriptions as $key => $description ) {
+ if ( $description instanceof SomeProperty ) {
+ $description->setHierarchyDepth( $hierarchyDepth );
+ }
+ }
+ }
+
+ public function getDescriptions() {
+ return $this->descriptions;
+ }
+
+ public function addDescription( Description $description ) {
+
+ $this->fingerprint = null;
+ $fingerprint = $description->getFingerprint();
+
+ if ( $description instanceof ThingDescription ) {
+ $this->isTrue = true;
+ $this->descriptions = []; // no conditions any more
+ $this->classDescription = null;
+ }
+
+ if ( !$this->isTrue ) {
+ // Combine class descriptions only when those describe the same state
+ if ( $description instanceof ClassDescription ) {
+ if ( is_null( $this->classDescription ) ) { // first class description
+ $this->classDescription = $description;
+ $this->descriptions[$description->getFingerprint()] = $description;
+ } elseif ( $this->classDescription->isMergableDescription( $description ) ) {
+ $this->classDescription->addDescription( $description );
+ } else {
+ $this->descriptions[$description->getFingerprint()] = $description;
+ }
+ } elseif ( $description instanceof Disjunction ) { // absorb sub-disjunctions
+ foreach ( $description->getDescriptions() as $subdesc ) {
+ $this->descriptions[$subdesc->getFingerprint()] = $subdesc;
+ }
+ // } elseif ($description instanceof SMWSomeProperty) {
+ ///TODO: use subdisjunct. for multiple SMWSomeProperty descs with same property
+ } else {
+ $this->descriptions[$fingerprint] = $description;
+ }
+ }
+
+ // move print descriptions downwards
+ ///TODO: This may not be a good solution, since it does modify $description and since it does not react to future cahges
+ $this->m_printreqs = array_merge( $this->m_printreqs, $description->getPrintRequests() );
+ $description->setPrintRequests( [] );
+ }
+
+ public function getQueryString( $asValue = false ) {
+
+ if ( $this->isTrue ) {
+ return '+';
+ }
+
+ $result = '';
+ $sep = $asValue ? '||':' OR ';
+
+ foreach ( $this->descriptions as $desc ) {
+ $subdesc = $desc->getQueryString( $asValue );
+
+ if ( $desc instanceof SomeProperty ) { // enclose in <q> for parsing
+ if ( $asValue ) {
+ $subdesc = ' <q>[[' . $subdesc . ']]</q> ';
+ } else {
+ $subdesc = ' <q>' . $subdesc . '</q> ';
+ }
+ }
+
+ $result .= ( $result ? $sep:'' ) . $subdesc;
+ }
+
+ if ( $asValue ) {
+ return $result;
+ }
+
+ return ' <q>' . $result . '</q> ';
+ }
+
+ public function isSingleton() {
+ /// NOTE: this neglects the unimportant case where several disjuncts describe the same object.
+ if ( count( $this->descriptions ) != 1 ) {
+ return false;
+ }
+
+ return $this->descriptions[0]->isSingleton();
+ }
+
+ public function getSize() {
+ $size = 0;
+
+ foreach ( $this->descriptions as $desc ) {
+ $size += $desc->getSize();
+ }
+
+ return $size;
+ }
+
+ public function getDepth() {
+ $depth = 0;
+
+ foreach ( $this->descriptions as $desc ) {
+ $depth = max( $depth, $desc->getDepth() );
+ }
+
+ return $depth;
+ }
+
+ public function getQueryFeatures() {
+ $result = SMW_DISJUNCTION_QUERY;
+
+ foreach ( $this->descriptions as $desc ) {
+ $result = $result | $desc->getQueryFeatures();
+ }
+
+ return $result;
+ }
+
+ public function prune( &$maxsize, &$maxdepth, &$log ) {
+
+ if ( $maxsize <= 0 ) {
+ $log[] = $this->getQueryString();
+ return new ThingDescription();
+ }
+
+ $prunelog = [];
+ $newdepth = $maxdepth;
+ $result = new Disjunction();
+
+ foreach ( $this->descriptions as $desc ) {
+ $restdepth = $maxdepth;
+ $result->addDescription( $desc->prune( $maxsize, $restdepth, $prunelog ) );
+ $newdepth = min( $newdepth, $restdepth );
+ }
+
+ if ( count( $result->getDescriptions() ) > 0 ) {
+ $log = array_merge( $log, $prunelog );
+ $maxdepth = $newdepth;
+
+ if ( count( $result->getDescriptions() ) == 1 ) { // simplify unary disjunctions!
+ $descriptions = $result->getDescriptions();
+ $result = array_shift( $descriptions );
+ }
+
+ $result->setPrintRequests( $this->getPrintRequests() );
+
+ return $result;
+ }
+
+ $log[] = $this->getQueryString();
+
+ $result = new ThingDescription();
+ $result->setPrintRequests( $this->getPrintRequests() );
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/NamespaceDescription.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/NamespaceDescription.php
new file mode 100644
index 00000000..405f8a40
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/NamespaceDescription.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace SMW\Query\Language;
+
+use SMW\Localizer;
+
+/**
+ * Description of all pages within a given wiki namespace, given by a numerical
+ * constant. Corresponds to a class restriction with a special class that
+ * characterises the given namespace (or at least that is how one could map
+ * this to OWL etc.).
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class NamespaceDescription extends Description {
+
+ /**
+ * @var integer
+ */
+ private $namespace;
+
+ /**
+ * @param integer $namespace
+ */
+ public function __construct( $namespace ) {
+ $this->namespace = $namespace;
+ }
+
+ /**
+ * @see Description::getFingerprint
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getFingerprint() {
+ // Avoid a simple `int` which may interfere with an associative array
+ // when compounding hash strings from different descriptions
+ return 'N:' . md5( $this->namespace );
+ }
+
+ /**
+ * @return integer
+ */
+ public function getNamespace() {
+ return $this->namespace;
+ }
+
+ public function getQueryString( $asValue = false ) {
+
+ $localizedNamespaceText = Localizer::getInstance()->getNamespaceTextById( $this->namespace );
+
+ $prefix = $this->namespace == NS_CATEGORY ? ':' : '';
+
+ if ( $asValue ) {
+ return ' <q>[[' . $prefix . $localizedNamespaceText . ':+]]</q> ';
+ }
+
+ return '[[' . $prefix . $localizedNamespaceText . ':+]]';
+ }
+
+ public function isSingleton() {
+ return false;
+ }
+
+ public function getQueryFeatures() {
+ return SMW_NAMESPACE_QUERY;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/SomeProperty.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/SomeProperty.php
new file mode 100644
index 00000000..7260c48d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/SomeProperty.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace SMW\Query\Language;
+
+use SMW\DIProperty;
+
+/**
+ * Description of a set of instances that have an attribute with some value
+ * that fits another (sub)description.
+ *
+ * Corresponds to existential quantification ("SomeValuesFrom" restriction) on
+ * properties in OWL. In conjunctive queries (OWL) and SPARQL (RDF), it is
+ * represented by using variables in the object part of such properties.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class SomeProperty extends Description {
+
+ /**
+ * @var Description
+ */
+ protected $description;
+
+ /**
+ * @var DIProperty
+ */
+ protected $property;
+
+ /**
+ * @var integer|null
+ */
+ protected $hierarchyDepth;
+
+ /**
+ * @since 1.6
+ *
+ * @param DIProperty $property
+ * @param Description $description
+ */
+ public function __construct( DIProperty $property, Description $description ) {
+ $this->property = $property;
+ $this->description = $description;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $hierarchyDepth
+ */
+ public function setHierarchyDepth( $hierarchyDepth ) {
+
+ if ( $hierarchyDepth > $GLOBALS['smwgQSubpropertyDepth'] ) {
+ $hierarchyDepth = $GLOBALS['smwgQSubpropertyDepth'];
+ }
+
+ $this->hierarchyDepth = $hierarchyDepth;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return integer|null
+ */
+ public function getHierarchyDepth() {
+ return $this->hierarchyDepth;
+ }
+
+ /**
+ * @see Description::getFingerprint
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getFingerprint() {
+
+ // Avoid a recursive tree
+ if ( $this->fingerprint !== null ) {
+ return $this->fingerprint;
+ }
+
+ $subDescription = $this->description;
+ $property = $this->property->getSerialization();
+
+ // Resolve property.chains and connect its members
+ while ( $subDescription instanceof SomeProperty ) {
+ $subDescription = $subDescription->getDescription();
+ $subDescription->setMembership( $property );
+ }
+
+ // During a recursive chain use the hash from a stored
+ // member to distinguish Foo.Bar.Foobar.Bam from Foo.Bar.Foobar
+ $membership = $this->getMembership() . $subDescription->getMembership();
+
+ return $this->fingerprint = 'S:' . md5( $property . '|' . $membership . '|' . $this->description->getFingerprint() . $this->hierarchyDepth );
+ }
+
+ /**
+ * @return DIProperty
+ */
+ public function getProperty() {
+ return $this->property;
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @return Description
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @return string
+ */
+ public function getQueryString( $asValue = false ) {
+ $subDescription = $this->description;
+
+ // Use the canonical label to ensure that conditions contain
+ // language indep. references
+ $propertyChainString = $this->property->getCanonicalLabel();
+ $propertyname = $propertyChainString;
+ $final = '';
+
+ while ( ( $propertyname !== '' ) && ( $subDescription instanceof SomeProperty ) ) { // try to use property chain syntax
+ $propertyname = $subDescription->getProperty()->getCanonicalLabel();
+
+ if ( $propertyname !== '' ) {
+ $propertyChainString .= '.' . $propertyname;
+ $subDescription = $subDescription->getDescription();
+ }
+ }
+
+ if ( $this->hierarchyDepth !== null ) {
+ $final = '|+depth=' . $this->hierarchyDepth;
+ }
+
+ if ( $asValue ) {
+ return '<q>[[' . $propertyChainString . '::' . $subDescription->getQueryString( true ) . $final . ']]</q>';
+ }
+
+ return '[[' . $propertyChainString . '::' . $subDescription->getQueryString( true ) . $final . ']]';
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @return boolean
+ */
+ public function isSingleton() {
+ return false;
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @return integer
+ */
+ public function getSize() {
+ return 1 + $this->getDescription()->getSize();
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @return integer
+ */
+ public function getDepth() {
+ return 1 + $this->getDescription()->getDepth();
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @return integer
+ */
+ public function getQueryFeatures() {
+ return SMW_PROPERTY_QUERY | $this->description->getQueryFeatures();
+ }
+
+ /**
+ * @since 1.6
+ *
+ * @return SomeProperty
+ */
+ public function prune( &$maxsize, &$maxdepth, &$log ) {
+
+ if ( ( $maxsize <= 0 ) || ( $maxdepth <= 0 ) ) {
+ $log[] = $this->getQueryString();
+ return new ThingDescription();
+ }
+
+ $maxsize--;
+ $maxdepth--;
+
+ $result = new SomeProperty(
+ $this->property,
+ $this->description->prune( $maxsize, $maxdepth, $log )
+ );
+
+ $result->setHierarchyDepth( $this->getHierarchyDepth() );
+ $result->setPrintRequests( $this->getPrintRequests() );
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ThingDescription.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ThingDescription.php
new file mode 100644
index 00000000..3419df19
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ThingDescription.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace SMW\Query\Language;
+
+/**
+ * A dummy description that describes any object. Corresponds to
+ * owl:thing, the class of all abstract objects. Note that it is
+ * not used for datavalues of attributes in order to support type
+ * hinting in the API: descriptions of data are always
+ * ValueDescription objects.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class ThingDescription extends Description {
+
+ public function getQueryString( $asValue = false ) {
+ return $asValue ? ( isset( $this->isNegation ) ? '!' : '' ) . '+' : '';
+ }
+
+ public function isSingleton() {
+ return false;
+ }
+
+ public function getSize() {
+ return 0; // no real condition, no size or depth
+ }
+
+ public function prune( &$maxsize, &$maxdepth, &$log ) {
+ return $this;
+ }
+
+ /**
+ * @see Description::getFingerprint
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getFingerprint() {
+ // Avoid a simple 0 which may interfere with an associative array
+ // when compounding hash strings from different descriptions
+ return 'T:' . md5( 0 ) . ( isset( $this->isNegation ) ? '!' : '' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ValueDescription.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ValueDescription.php
new file mode 100644
index 00000000..be0161d2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Language/ValueDescription.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace SMW\Query\Language;
+
+use SMW\DataValueFactory;
+use SMw\DIProperty;
+use SMW\Query\QueryComparator;
+use SMWDataItem as DataItem;
+use SMWNumberValue as NumberValue;
+use SMWURIValue as UriValue;
+
+/**
+ * Description of one data value, or of a range of data values.
+ *
+ * Technically this usually corresponds to nominal predicates or to unary
+ * concrete domain predicates in OWL which are parametrised by one constant
+ * from the concrete domain.
+ * In RDF, concrete domain predicates that define ranges (like "greater or
+ * equal to") are not directly available.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class ValueDescription extends Description {
+
+ /**
+ * @var DataItem
+ */
+ private $dataItem;
+
+ /**
+ * @var integer element in the SMW_CMP_ enum
+ */
+ private $comparator;
+
+ /**
+ * @var null|DIProperty
+ */
+ private $property = null;
+
+ /**
+ * @param DataItem $dataItem
+ * @param null|DIProperty $property
+ * @param integer $comparator
+ */
+ public function __construct( DataItem $dataItem, DIProperty $property = null, $comparator = SMW_CMP_EQ ) {
+ $this->dataItem = $dataItem;
+ $this->comparator = $comparator;
+ $this->property = $property;
+ }
+
+ /**
+ * @see Description::getFingerprint
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getFingerprint() {
+
+ $property = null;
+
+ if ( $this->property !== null ) {
+ $property = $this->property->getSerialization();
+ }
+
+ // A change to the order does also change the signature and renders a
+ // different query ID
+ return 'V:' . md5( $this->comparator . '|' . $this->dataItem->getHash() . '|' . $property );
+ }
+
+ /**
+ * @deprecated Use getDataItem() and \SMW\DataValueFactory::getInstance()->newDataValueByItem() if needed. Vanishes before SMW 1.7
+ * @return DataItem
+ */
+ public function getDataValue() {
+ // FIXME: remove
+ return $this->dataItem;
+ }
+
+ /**
+ * @return DataItem
+ */
+ public function getDataItem() {
+ return $this->dataItem;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return DIProperty|null
+ */
+ public function getProperty() {
+ return $this->property;
+ }
+
+ /**
+ * @return integer
+ */
+ public function getComparator() {
+ return $this->comparator;
+ }
+
+ /**
+ * @param bool $asValue
+ *
+ * @return string
+ */
+ public function getQueryString( $asValue = false ) {
+
+ $comparator = QueryComparator::getInstance()->getStringForComparator(
+ $this->comparator
+ );
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $this->dataItem,
+ $this->property
+ );
+
+ // Set option to ensure that the output doesn't alter the display
+ // characteristics of a value
+ $dataValue->setOption( UriValue::VALUE_RAW, true );
+ $dataValue->setOption( NumberValue::NO_DISP_PRECISION_LIMIT, true );
+
+ if ( $asValue ) {
+ return $comparator . $dataValue->getWikiValue();
+ }
+
+ // this only is possible for values of Type:Page
+ if ( $comparator === '' ) { // some extra care for Category: pages
+ return '[[:' . $dataValue->getWikiValue() . ']]';
+ }
+
+ return '[[' . $comparator . $dataValue->getWikiValue() . ']]';
+ }
+
+ public function isSingleton() {
+ return $this->comparator == SMW_CMP_EQ;
+ }
+
+ public function getSize() {
+ return 1;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser.php
new file mode 100644
index 00000000..d34f6905
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\Query\Language\Description;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+interface Parser {
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty|string $property
+ * @param string $value
+ *
+ * @return string
+ */
+ public function createCondition( $property, $value );
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getErrors();
+
+ /**
+ * Describes a processed description instance in terms of the existence of
+ * a self reference in connection with the context page a query is
+ * embedded.
+ *
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function containsSelfReference();
+
+ /**
+ * @since 3.0
+ *
+ * @param string $condition
+ *
+ * @return Description
+ */
+ public function getQueryDescription( $condition );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/DescriptionProcessor.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/DescriptionProcessor.php
new file mode 100644
index 00000000..45af6deb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/DescriptionProcessor.php
@@ -0,0 +1,307 @@
+<?php
+
+namespace SMW\Query\Parser;
+
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Message;
+use SMW\Query\DescriptionFactory;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\ValueDescription;
+use SMW\Site;
+use SMWDataValue as DataValue;
+use SMW\Query\QueryComparator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class DescriptionProcessor {
+
+ /**
+ * @var DataValueFactory
+ */
+ private $dataValueFactory;
+
+ /**
+ * @var DescriptionFactory
+ */
+ private $descriptionFactory;
+
+ /**
+ * @var integer
+ */
+ private $queryFeatures;
+
+ /**
+ * @var DIWikiPage|null
+ */
+ private $contextPage;
+
+ /**
+ * @var boolean
+ */
+ private $selfReference = false;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $queryFeatures
+ */
+ public function __construct( $queryFeatures = false ) {
+ $this->queryFeatures = $queryFeatures === false ? $GLOBALS['smwgQFeatures'] : $queryFeatures;
+ $this->dataValueFactory = DataValueFactory::getInstance();
+ $this->descriptionFactory = new DescriptionFactory();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIWikiPage|null $contextPage
+ */
+ public function setContextPage( DIWikiPage $contextPage = null ) {
+ $this->contextPage = $contextPage;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function clear() {
+ $this->errors = [];
+ $this->selfReference = false;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function containsSelfReference() {
+ return $this->selfReference;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array|string $error
+ */
+ public function addError( $error ) {
+
+ if ( !is_array( $error ) ) {
+ $error = (array)$error;
+ }
+
+ if ( $error !== [] ) {
+ $this->errors[] = Message::encode( $error );
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $msgKey
+ */
+ public function addErrorWithMsgKey( $msgKey /*...*/ ) {
+ $this->errors[] = Message::encode( func_get_args() );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ * @param string $chunk
+ *
+ * @return Description|null
+ */
+ public function newDescriptionForPropertyObjectValue( DIProperty $property, $chunk ) {
+
+ $dataValue = $this->dataValueFactory->newDataValueByProperty( $property );
+ $dataValue->setContextPage( $this->contextPage );
+
+ // Indicates whether a value is being used by a query condition or not which
+ // can lead to a modified validation of a value.
+ $dataValue->setOption( DataValue::OPT_QUERY_CONTEXT, true );
+ $dataValue->setOption( 'isCapitalLinks', Site::isCapitalLinks() );
+
+ $description = $dataValue->getQueryDescription( $chunk );
+ $this->addError( $dataValue->getErrors() );
+
+ return $description;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $chunk
+ *
+ * @return Description|null
+ */
+ public function newDescriptionForWikiPageValueChunk( $chunk ) {
+
+ // Only create a simple WpgValue to initiate the query description target
+ // operation. If the chunk contains something like "≤Issue/1220" then the
+ // WpgValue would return with an error as it cannot parse ≤ as/ legal
+ // character, the chunk itself is processed by
+ // DataValue::getQueryDescription hence no need to use it as input for
+ // the factory instance
+ $dataValue = $this->dataValueFactory->newTypeIDValue( '_wpg', 'QP_WPG_TITLE' );
+ $dataValue->setContextPage( $this->contextPage );
+
+ $dataValue->setOption( DataValue::OPT_QUERY_CONTEXT, true );
+
+ // #3587
+ // Requesting capital links is influenced by two factors, `wgCapitalLinks`
+ // is enabled sitewide and the `WikiPageValue` condition is identified
+ // as SMW_CMP_EQ/NEQ (e.g. [[Foo]], [[!Foo]]) with other expressions
+ // (e.g. [[~foo*]]) to remain in the form of the user input
+ $queryComparator = QueryComparator::getInstance();
+
+ if ( Site::isCapitalLinks() && (
+ $queryComparator->containsComparator( $chunk, SMW_CMP_EQ ) ||
+ $queryComparator->containsComparator( $chunk, SMW_CMP_NEQ ) ) ) {
+ $dataValue->setOption( 'isCapitalLinks', true );
+ }
+
+ $description = null;
+
+ $description = $dataValue->getQueryDescription( $chunk );
+ $this->addError( $dataValue->getErrors() );
+
+ if ( !$this->selfReference && $this->contextPage !== null && $description instanceof ValueDescription ) {
+ $this->selfReference = $description->getDataItem()->equals( $this->contextPage );
+ }
+
+ return $description;
+ }
+
+ /**
+ * The method was supposed to be named just `or` and `and` but this works
+ * only on PHP 7.1 therefore ...
+ */
+
+ /**
+ * @since 2.4
+ *
+ * @param Description|null $currentDescription
+ * @param Description|null $newDescription
+ *
+ * @return Description|null
+ */
+ public function asOr( Description $currentDescription = null, Description $newDescription = null ) {
+ return $this->newCompoundDescription( $currentDescription, $newDescription, SMW_DISJUNCTION_QUERY );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Description|null $currentDescription
+ * @param Description|null $newDescription
+ *
+ * @return Description|null
+ */
+ public function asAnd( Description $currentDescription = null, Description $newDescription = null ) {
+ return $this->newCompoundDescription( $currentDescription, $newDescription, SMW_CONJUNCTION_QUERY );
+ }
+
+ /**
+ * Extend a given description by a new one, either by adding the new description
+ * (if the old one is a container description) or by creating a new container.
+ * The parameter $conjunction determines whether the combination of both descriptions
+ * should be a disjunction or conjunction.
+ *
+ * In the special case that the current description is NULL, the new one will just
+ * replace the current one.
+ *
+ * The return value is the expected combined description. The object $currentDescription will
+ * also be changed (if it was non-NULL).
+ */
+ private function newCompoundDescription( Description $currentDescription = null, Description $newDescription = null, $compoundType = SMW_CONJUNCTION_QUERY ) {
+
+ $notallowedmessage = 'smw_noqueryfeature';
+
+ if ( $newDescription instanceof SomeProperty ) {
+ $allowed = $this->queryFeatures & SMW_PROPERTY_QUERY;
+ } elseif ( $newDescription instanceof ClassDescription ) {
+ $allowed = $this->queryFeatures & SMW_CATEGORY_QUERY;
+ } elseif ( $newDescription instanceof ConceptDescription ) {
+ $allowed = $this->queryFeatures & SMW_CONCEPT_QUERY;
+ } elseif ( $newDescription instanceof Conjunction ) {
+ $allowed = $this->queryFeatures & SMW_CONJUNCTION_QUERY;
+ $notallowedmessage = 'smw_noconjunctions';
+ } elseif ( $newDescription instanceof Disjunction ) {
+ $allowed = $this->queryFeatures & SMW_DISJUNCTION_QUERY;
+ $notallowedmessage = 'smw_nodisjunctions';
+ } else {
+ $allowed = true;
+ }
+
+ if ( !$allowed ) {
+ $this->addErrorWithMsgKey( $notallowedmessage, $newDescription->getQueryString() );
+ return $currentDescription;
+ }
+
+ if ( $newDescription === null ) {
+ return $currentDescription;
+ } elseif ( $currentDescription === null ) {
+ return $newDescription;
+ } else { // we already found descriptions
+ return $this->newCompoundDescriptionByType( $compoundType, $currentDescription, $newDescription );
+ }
+ }
+
+ private function newCompoundDescriptionByType( $compoundType, $currentDescription, $newDescription ) {
+
+ if ( ( ( $compoundType & SMW_CONJUNCTION_QUERY ) != 0 && ( $currentDescription instanceof Conjunction ) ) ||
+ ( ( $compoundType & SMW_DISJUNCTION_QUERY ) != 0 && ( $currentDescription instanceof Disjunction ) ) ) { // use existing container
+ $currentDescription->addDescription( $newDescription );
+ return $currentDescription;
+ } elseif ( ( $compoundType & SMW_CONJUNCTION_QUERY ) != 0 ) { // make new conjunction
+ return $this->newConjunction( $currentDescription, $newDescription );
+ } elseif ( ( $compoundType & SMW_DISJUNCTION_QUERY ) != 0 ) { // make new disjunction
+ return $this->newDisjunction( $currentDescription, $newDescription );
+ }
+ }
+
+ private function newConjunction( $currentDescription, $newDescription ) {
+
+ if ( $this->queryFeatures & SMW_CONJUNCTION_QUERY ) {
+ return $this->descriptionFactory->newConjunction( [ $currentDescription, $newDescription ] );
+ }
+
+ $this->addErrorWithMsgKey( 'smw_noconjunctions', $newDescription->getQueryString() );
+
+ return $currentDescription;
+ }
+
+ private function newDisjunction( $currentDescription, $newDescription ) {
+
+ if ( $this->queryFeatures & SMW_DISJUNCTION_QUERY ) {
+ return $this->descriptionFactory->newDisjunction( [ $currentDescription, $newDescription ] );
+ }
+
+ $this->addErrorWithMsgKey( 'smw_nodisjunctions', $newDescription->getQueryString() );
+
+ return $currentDescription;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/LegacyParser.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/LegacyParser.php
new file mode 100644
index 00000000..fa3c6096
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/LegacyParser.php
@@ -0,0 +1,847 @@
+<?php
+
+namespace SMW\Query\Parser;
+
+use SMW\DataTypeRegistry;
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Localizer;
+use SMW\Query\DescriptionFactory;
+use SMW\Query\Language\ClassDescription;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Parser;
+use SMW\Query\QueryToken;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Markus Krötzsch
+ */
+class LegacyParser implements Parser {
+
+ /**
+ * @var DescriptionProcessor
+ */
+ private $descriptionProcessor;
+
+ /**
+ * @var QueryToken
+ */
+ private $queryToken;
+
+ /**
+ * @var Tokenizer
+ */
+ private $tokenizer;
+
+ /**
+ * @var DescriptionFactory
+ */
+ private $descriptionFactory;
+
+ /**
+ * @var DataTypeRegistry
+ */
+ private $dataTypeRegistry;
+
+ /**
+ * Description of the default namespace restriction, or NULL if not used
+ *
+ * @var array|null
+ */
+ private $defaultNamespace;
+
+ /**
+ * List of open blocks ("parentheses") that need closing at current step
+ *
+ * @var array
+ */
+ private $separatorStack = [];
+
+ /**
+ * Remaining string to be parsed (parsing eats query string from the front)
+ *
+ * @var string
+ */
+ private $currentString;
+
+ /**
+ * Cache label of category namespace . ':'
+ *
+ * @var string
+ */
+ private $categoryPrefix;
+
+ /**
+ * Cache label of concept namespace . ':'
+ *
+ * @var string
+ */
+ private $conceptPrefix;
+
+ /**
+ * Cache canonnical label of category namespace . ':'
+ *
+ * @var string
+ */
+ private $categoryPrefixCannonical;
+
+ /**
+ * Cache canonnical label of concept namespace . ':'
+ *
+ * @var string
+ */
+ private $conceptPrefixCannonical;
+
+ /**
+ * @var DIWikiPage|null
+ */
+ private $contextPage;
+
+ /**
+ * @var boolean
+ */
+ private $selfReference = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param DescriptionProcessor $descriptionProcessor
+ * @param Tokenizer $tokenizer
+ * @param QueryToken $queryToken
+ */
+ public function __construct( DescriptionProcessor $descriptionProcessor, Tokenizer $tokenizer, QueryToken $queryToken ) {
+ $this->descriptionProcessor = $descriptionProcessor;
+ $this->tokenizer = $tokenizer;
+ $this->queryToken = $queryToken;
+ $this->descriptionFactory = new DescriptionFactory();
+ $this->dataTypeRegistry = DataTypeRegistry::getInstance();
+ $this->setDefaultPrefix();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage|null $contextPage
+ */
+ public function setContextPage( DIWikiPage $contextPage = null ) {
+ $this->contextPage = $contextPage;
+ }
+
+ /**
+ * Provide an array of namespace constants that are used as default restrictions.
+ * If NULL is given, no such default restrictions will be added (faster).
+ *
+ * @since 1.6
+ */
+ public function setDefaultNamespaces( $namespaces ) {
+ $this->defaultNamespace = null;
+
+ if ( !is_array( $namespaces ) ) {
+ return;
+ }
+
+ foreach ( $namespaces as $namespace ) {
+ $this->defaultNamespace = $this->descriptionProcessor->asOr(
+ $this->defaultNamespace,
+ $this->descriptionFactory->newNamespaceDescription( $namespace )
+ );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|null $languageCode
+ */
+ public function setDefaultPrefix( $languageCode = null ) {
+
+ $localizer = Localizer::getInstance();
+
+ if ( $languageCode === null ) {
+ $language = $localizer->getContentLanguage();
+ } else {
+ $language = $localizer->getLanguage( $languageCode );
+ }
+
+ $this->categoryPrefix = $language->getNsText( NS_CATEGORY ) . ':';
+ $this->conceptPrefix = $language->getNsText( SMW_NS_CONCEPT ) . ':';
+
+ $this->categoryPrefixCannonical = 'Category:';
+ $this->conceptPrefixCannonical = 'Concept:';
+
+ $this->tokenizer->setDefaultPattern(
+ [
+ $this->categoryPrefix,
+ $this->conceptPrefix,
+ $this->categoryPrefixCannonical,
+ $this->conceptPrefixCannonical
+ ]
+ );
+ }
+
+ /**
+ * Return array of error messages (possibly empty).
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->descriptionProcessor->getErrors();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function containsSelfReference() {
+
+ if ( $this->selfReference ) {
+ return true;
+ }
+
+ return $this->descriptionProcessor->containsSelfReference();
+ }
+
+ /**
+ * Return error message or empty string if no error occurred.
+ *
+ * @return string
+ */
+ public function getErrorString() {
+ throw new \RuntimeException( "Shouldnot be used, remove getErrorString usage!" );
+ return smwfEncodeMessages( $this->getErrors() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return QueryToken
+ */
+ public function getQueryToken() {
+ return $this->queryToken;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function createCondition( $property, $value ) {
+
+ if ( $property instanceOf DIProperty ) {
+ $property = $property->getLabel();
+ }
+
+ return "[[$property::$value]]";
+ }
+
+ /**
+ * Compute an SMWDescription from a query string. Returns whatever descriptions could be
+ * wrestled from the given string (the most general result being SMWThingDescription if
+ * no meaningful condition was extracted).
+ *
+ * @param string $queryString
+ *
+ * @return Description
+ */
+ public function getQueryDescription( $queryString ) {
+
+ if ( $queryString === '' ) {
+ $this->descriptionProcessor->addErrorWithMsgKey(
+ 'smw-query-condition-empty'
+ );
+
+ return $this->descriptionFactory->newThingDescription();
+ }
+
+ $this->descriptionProcessor->clear();
+ $this->descriptionProcessor->setContextPage( $this->contextPage );
+
+ $this->currentString = $queryString;
+ $this->separatorStack = [];
+
+ $this->selfReference = false;
+ $setNS = false;
+
+ $description = $this->getSubqueryDescription( $setNS );
+
+ // add default namespaces if applicable
+ if ( !$setNS ) {
+ $description = $this->descriptionProcessor->asAnd(
+ $this->defaultNamespace,
+ $description
+ );
+ }
+
+ // parsing went wrong, no default namespaces
+ if ( $description === null ) {
+ $description = $this->descriptionFactory->newThingDescription();
+ }
+
+ return $description;
+ }
+
+ /**
+ * Compute an SMWDescription for current part of a query, which should
+ * be a standalone query (the main query or a subquery enclosed within
+ * "\<q\>...\</q\>". Recursively calls similar methods and returns NULL upon error.
+ *
+ * The call-by-ref parameter $setNS is a boolean. Its input specifies whether
+ * the query should set the current default namespace if no namespace restrictions
+ * were given. If false, the calling super-query is happy to set the required
+ * NS-restrictions by itself if needed. Otherwise the subquery has to impose the defaults.
+ * This is so, since outermost queries and subqueries of disjunctions will have to set
+ * their own default restrictions.
+ *
+ * The return value of $setNS specifies whether or not the subquery has a namespace
+ * specification in place. This might happen automatically if the query string imposes
+ * such restrictions. The return value is important for those callers that otherwise
+ * set up their own restrictions.
+ *
+ * Note that $setNS is no means to switch on or off default namespaces in general,
+ * but just controls query generation. For general effect, the default namespaces
+ * should be set to NULL.
+ *
+ * @return Description|null
+ */
+ private function getSubqueryDescription( &$setNS ) {
+
+ $conjunction = null; // used for the current inner conjunction
+ $disjuncts = []; // (disjunctive) array of subquery conjunctions
+
+ $hasNamespaces = false; // does the current $conjnuction have its own namespace restrictions?
+ $mustSetNS = $setNS; // must NS restrictions be set? (may become true even if $setNS is false)
+
+ $continue = ( $chunk = $this->readChunk() ) !== ''; // skip empty subquery completely, thorwing an error
+
+ while ( $continue ) {
+ $setsubNS = false;
+
+ switch ( $chunk ) {
+ case '[[': // start new link block
+ $ld = $this->getLinkDescription( $setsubNS );
+
+ if ( !is_null( $ld ) ) {
+ $conjunction = $this->descriptionProcessor->asAnd( $conjunction, $ld );
+ }
+ break;
+ case 'AND':
+ case '<q>': // enter new subquery, currently irrelevant but possible
+ $this->pushDelimiter( '</q>' );
+ $conjunction = $this->descriptionProcessor->asAnd( $conjunction, $this->getSubqueryDescription( $setsubNS ) );
+ break;
+ case 'OR':
+ case '||':
+ case '':
+ case '</q>': // finish disjunction and maybe subquery
+ if ( !is_null( $this->defaultNamespace ) ) { // possibly add namespace restrictions
+ if ( $hasNamespaces && !$mustSetNS ) {
+ // add NS restrictions to all earlier conjunctions (all of which did not have them yet)
+ $mustSetNS = true; // enforce NS restrictions from now on
+ $newdisjuncts = [];
+
+ foreach ( $disjuncts as $conj ) {
+ $newdisjuncts[] = $this->descriptionProcessor->asAnd( $conj, $this->defaultNamespace );
+ }
+
+ $disjuncts = $newdisjuncts;
+ } elseif ( !$hasNamespaces && $mustSetNS ) {
+ // add ns restriction to current result
+ $conjunction = $this->descriptionProcessor->asAnd( $conjunction, $this->defaultNamespace );
+ }
+ }
+
+ $disjuncts[] = $conjunction;
+ // start anew
+ $conjunction = null;
+ $hasNamespaces = false;
+
+ // finish subquery?
+ if ( $chunk == '</q>' ) {
+ if ( $this->popDelimiter( '</q>' ) ) {
+ $continue = false; // leave the loop
+ } else {
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_toomanyclosing', $chunk );
+ return null;
+ }
+ } elseif ( $chunk === '' ) {
+ $continue = false;
+ }
+ break;
+ case '+': // "... AND true" (ignore)
+ break;
+ default: // error: unexpected $chunk
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $chunk );
+ // return null; // Try to go on, it can only get better ...
+ }
+
+ if ( $setsubNS ) { // namespace restrictions encountered in current conjunct
+ $hasNamespaces = true;
+ }
+
+ if ( $continue ) { // read on only if $continue remained true
+ $chunk = $this->readChunk();
+ }
+ }
+
+ if ( count( $disjuncts ) > 0 ) { // make disjunctive result
+ $result = null;
+
+ foreach ( $disjuncts as $d ) {
+ if ( is_null( $d ) ) {
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_emptysubquery' );
+ $setNS = false;
+ return null;
+ } else {
+ $result = $this->descriptionProcessor->asOr( $result, $d );
+ }
+ }
+ } else {
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_emptysubquery' );
+ $setNS = false;
+ return null;
+ }
+
+ // NOTE: also false if namespaces were given but no default NS descs are available
+ $setNS = $mustSetNS;
+
+ return $result;
+ }
+
+ /**
+ * Compute an SMWDescription for current part of a query, which should
+ * be the content of "[[ ... ]]". Returns NULL upon error.
+ *
+ * Parameters $setNS has the same use as in getSubqueryDescription().
+ */
+ private function getLinkDescription( &$setNS ) {
+ // This method is called when we encountered an opening '[['. The following
+ // block could be a Category-statement, fixed object, or property statement.
+
+ // NOTE: untrimmed, initial " " escapes prop. chains
+ $chunk = $this->readChunk( '', true, false );
+
+ if ( $this->hasClassPrefix( $chunk ) ) {
+ return $this->getClassDescription( $setNS, $this->isClass( $chunk ) );
+ }
+
+ // fixed subject, namespace restriction, property query, or subquery
+
+ // Do not consume hit, "look ahead"
+ $sep = $this->readChunk( '', false );
+
+ if ( ( $sep == '::' ) || ( $sep == ':=' ) ) {
+ if ( $chunk{0} != ':' ) { // property statement
+ return $this->getPropertyDescription( $chunk, $setNS );
+ } else { // escaped article description, read part after :: to get full contents
+ $chunk .= $this->readChunk( '\[\[|\]\]|\|\||\|' );
+ return $this->getArticleDescription( trim( $chunk ), $setNS );
+ }
+ }
+
+ // Fixed article/namespace restriction. $sep should be ]] or ||
+ return $this->getArticleDescription( trim( $chunk ), $setNS );
+ }
+
+ /**
+ * Parse a category description (the part of an inline query that
+ * is in between "[[Category:" and the closing "]]" and create a
+ * suitable description.
+ */
+ private function getClassDescription( &$setNS, $category = true ) {
+
+ // No subqueries allowed here, inline disjunction allowed, wildcards allowed
+ $description = null;
+ $continue = true;
+ $invalidName = false;
+
+ while ( $continue ) {
+ $chunk = $this->readChunk();
+
+ if ( $chunk == '+' ) {
+ $desc = $this->descriptionFactory->newNamespaceDescription( $category ? NS_CATEGORY : SMW_NS_CONCEPT );
+ $description = $this->descriptionProcessor->asOr( $description, $desc );
+ } else { // assume category/concept title
+ $isNegation = false;
+
+ // [[Category:!Foo]]
+ // Only the ElasticStore does actively support this construct
+ if ( $chunk{0} === '!' ) {
+ $chunk = substr( $chunk, 1 );
+ $isNegation = true;
+ }
+
+ // We add a prefix to prevent problems with, e.g., [[Category:Template:Test]]
+ $prefix = $category ? $this->categoryPrefix : $this->conceptPrefix;
+ $title = Title::newFromText( $prefix . $chunk );
+
+ // Something like [[Category::Foo]] doesn't produce any meaningful
+ // results
+ if ( strpos( $prefix . $chunk, '::' ) !== false ) {
+ $invalidName .= "{$prefix}{$chunk}";
+ } elseif ( $invalidName ) {
+ $invalidName .= "||{$chunk}";
+ }
+
+ if ( $title !== null ) {
+ $diWikiPage = new DIWikiPage( $title->getDBkey(), $title->getNamespace(), '' );
+
+ if ( !$this->selfReference && $this->contextPage !== null ) {
+ $this->selfReference = $diWikiPage->equals( $this->contextPage );
+ }
+
+ $desc = $category ? $this->descriptionFactory->newClassDescription( $diWikiPage ) : $this->descriptionFactory->newConceptDescription( $diWikiPage );
+
+ if ( $isNegation ) {
+ $desc->isNegation = $isNegation;
+ }
+
+ $description = $this->descriptionProcessor->asOr( $description, $desc );
+ }
+ }
+
+ $chunk = $this->readChunk();
+
+ // Disjunctions only for categories
+ $continue = ( $chunk == '||' ) && $category;
+ }
+
+ if ( $invalidName ) {
+ return $this->descriptionProcessor->addErrorWithMsgKey( 'smw-category-invalid-value-assignment', "[[{$invalidName}]]" );
+ }
+
+ return $this->finishLinkDescription( $chunk, false, $description, $setNS );
+ }
+
+ /**
+ * Parse a property description (the part of an inline query that
+ * is in between "[[Some property::" and the closing "]]" and create a
+ * suitable description. The "::" is the first chunk on the current
+ * string.
+ */
+ private function getPropertyDescription( $propertyName, &$setNS ) {
+
+ // Consume separator ":=" or "::"
+ $this->readChunk();
+ $dataValueFactory = DataValueFactory::getInstance();
+
+ // First process property chain syntax (e.g. "property1.property2::value"),
+ // escaped by initial " ":
+ $propertynames = ( $propertyName{0} == ' ' ) ? [ $propertyName ] : explode( '.', $propertyName );
+ $propertyValueList = [];
+
+ $typeid = '_wpg';
+ $inverse = false;
+
+ // After iteration, $property and $typeid correspond to last value
+ foreach ( $propertynames as $name ) {
+
+ // Non-final property in chain was no wikipage: not allowed
+ if ( !$this->isPagePropertyType( $typeid ) ) {
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_valuesubquery', $name );
+
+ // TODO: read some more chunks and try to finish [[ ]]
+ return null;
+ }
+
+ $propertyValue = $dataValueFactory->newPropertyValueByLabel( $name );
+
+ // Illegal property identifier
+ if ( !$propertyValue->isValid() ) {
+ $this->descriptionProcessor->addError( $propertyValue->getErrors() );
+
+ // TODO: read some more chunks and try to finish [[ ]]
+ return null;
+ }
+
+ // Set context to allow evading restriction checks for specific
+ // entities that handle the context such as pre-defined properties
+ // (Has query, Modification date etc.)
+ $propertyValue->setOption( $propertyValue::OPT_QUERY_CONTEXT, true );
+
+ // Check restriction
+ if ( $propertyValue->isRestricted() ) {
+ $this->descriptionProcessor->addError( $propertyValue->getRestrictionError() );
+ return null;
+ }
+
+ $property = $propertyValue->getDataItem();
+ $propertyValueList[] = $propertyValue;
+
+ $typeid = $property->findPropertyTypeID();
+ $inverse = $property->isInverse();
+ }
+
+ $innerdesc = null;
+ $continue = true;
+
+ while ( $continue ) {
+ $chunk = $this->readChunk();
+
+ switch ( $chunk ) {
+ // !+
+ case '!+':
+ $desc = $this->descriptionFactory->newThingDescription();
+ $desc->isNegation = true;
+ $innerdesc = $this->descriptionProcessor->asOr( $innerdesc, $desc );
+ $chunk = $this->readChunk();
+ break;
+ // wildcard, add namespaces for page-type properties
+ case '+':
+ if ( !is_null( $this->defaultNamespace ) && ( $this->isPagePropertyType( $typeid ) || $inverse ) ) {
+ $innerdesc = $this->descriptionProcessor->asOr( $innerdesc, $this->defaultNamespace );
+ } else {
+ $innerdesc = $this->descriptionProcessor->asOr( $innerdesc, $this->descriptionFactory->newThingDescription() );
+ }
+ $chunk = $this->readChunk();
+ break;
+ // subquery, set default namespaces
+ case '<q>':
+ if ( $this->isPagePropertyType( $typeid ) || $inverse ) {
+ $this->pushDelimiter( '</q>' );
+ $setsubNS = true;
+ $innerdesc = $this->descriptionProcessor->asOr( $innerdesc, $this->getSubqueryDescription( $setsubNS ) );
+ } else { // no subqueries allowed for non-pages
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_valuesubquery', end( $propertynames ) );
+ $innerdesc = $this->descriptionProcessor->asOr( $innerdesc, $this->descriptionFactory->newThingDescription() );
+ }
+ $chunk = $this->readChunk();
+ break;
+ // normal object value
+ default:
+ // read value(s), possibly with inner [[...]]
+ $open = 1;
+ $value = $chunk;
+ $continue2 = true;
+ // read value with inner [[, ]], ||
+ while ( ( $open > 0 ) && ( $continue2 ) ) {
+ $chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
+ switch ( $chunk ) {
+ case '[[': // open new [[ ]]
+ $open++;
+ break;
+ case ']]': // close [[ ]]
+ $open--;
+ break;
+ case '|':
+ case '||': // terminates only outermost [[ ]]
+ if ( $open == 1 ) {
+ $open = 0;
+ }
+ break;
+ case '': ///TODO: report error; this is not good right now
+ $continue2 = false;
+ break;
+ }
+ if ( $open != 0 ) {
+ $value .= $chunk;
+ }
+ } ///NOTE: at this point, we normally already read one more chunk behind the value
+ $outerDesription = $this->descriptionProcessor->newDescriptionForPropertyObjectValue(
+ $propertyValue->getDataItem(),
+ $value
+ );
+
+ $this->queryToken->addFromDesciption( $outerDesription );
+ $innerdesc = $this->descriptionProcessor->asOr(
+ $innerdesc,
+ $outerDesription
+ );
+
+ }
+ $continue = ( $chunk == '||' );
+ }
+
+ // No description, make a wildcard search
+ if ( $innerdesc === null ) {
+ if ( $this->defaultNamespace !== null && $this->isPagePropertyType( $typeid ) ) {
+ $innerdesc = $this->descriptionProcessor->asOr( $innerdesc, $this->defaultNamespace );
+ } else {
+ $innerdesc = $this->descriptionProcessor->asOr( $innerdesc, $this->descriptionFactory->newThingDescription() );
+ }
+
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_propvalueproblem', $propertyValue->getWikiValue() );
+ }
+
+ $propertyValueList = array_reverse( $propertyValueList );
+
+ foreach ( $propertyValueList as $propertyValue ) {
+ $innerdesc = $this->descriptionFactory->newSomeProperty( $propertyValue->getDataItem(), $innerdesc );
+ }
+
+ $result = $innerdesc;
+
+ return $this->finishLinkDescription( $chunk, false, $result, $setNS );
+ }
+
+ /**
+ * Parse an article description (the part of an inline query that
+ * is in between "[[" and the closing "]]" assuming it is not specifying
+ * a category or property) and create a suitable description.
+ * The first chunk behind the "[[" has already been read and is
+ * passed as a parameter.
+ */
+ private function getArticleDescription( $firstChunk, &$setNS ) {
+
+ $chunk = $firstChunk;
+ $description = null;
+
+ $continue = true;
+ $localizer = Localizer::getInstance();
+
+ while ( $continue ) {
+
+ // No subqueries of the form [[<q>...</q>]] (not needed)
+ if ( $chunk == '<q>' ) {
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_misplacedsubquery' );
+ return null;
+ }
+
+ // ":Category:Foo" "User:bar" ":baz" ":+"
+ $list = preg_split( '/:/', $chunk, 3 );
+
+ if ( ( $list[0] === '' ) && ( count( $list ) == 3 ) ) {
+ $list = array_slice( $list, 1 );
+ }
+
+ // try namespace restriction
+ if ( ( count( $list ) == 2 ) && ( $list[1] == '+' ) ) {
+
+ $idx = $localizer->getNamespaceIndexByName( $list[0] );
+
+ if ( $idx !== false ) {
+ $description = $this->descriptionProcessor->asOr(
+ $description,
+ $this->descriptionFactory->newNamespaceDescription( $idx )
+ );
+ }
+ } else {
+ $outerDesription = $this->descriptionProcessor->newDescriptionForWikiPageValueChunk(
+ $chunk
+ );
+
+ $this->queryToken->addFromDesciption( $outerDesription );
+
+ $description = $this->descriptionProcessor->asOr(
+ $description,
+ $outerDesription
+ );
+ }
+
+ $chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
+
+ if ( $chunk == '||' ) {
+ $chunk = $this->readChunk( '\[\[|\]\]|\|\||\|' );
+ $continue = true;
+ } else {
+ $continue = false;
+ }
+ }
+
+ return $this->finishLinkDescription( $chunk, true, $description, $setNS );
+ }
+
+ private function finishLinkDescription( $chunk, $hasNamespaces, $description, &$setNS ) {
+
+ if ( is_null( $description ) ) { // no useful information or concrete error found
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $chunk ); // was smw_badqueryatom
+ } elseif ( !$hasNamespaces && $setNS && !is_null( $this->defaultNamespace ) ) {
+ $description = $this->descriptionProcessor->asAnd( $description, $this->defaultNamespace );
+ $hasNamespaces = true;
+ }
+
+ $setNS = $hasNamespaces;
+
+ if ( $chunk == '|' ) { // skip content after single |, but report a warning
+ // Note: Using "|label" in query atoms used to be a way to set the mainlabel in SMW <1.0; no longer supported now
+ $chunk = $this->readChunk( '\]\]' );
+ $labelpart = '|';
+ $hasError = true;
+
+ // Set an individual hierarchy depth
+ if ( strpos( $chunk, '+depth=' ) !== false ) {
+ list( $k, $depth ) = explode( '=', $chunk, 2 );
+
+ if ( $description instanceOf ClassDescription || $description instanceOf SomeProperty || $description instanceOf Disjunction ) {
+ $description->setHierarchyDepth( $depth );
+ }
+
+ $chunk = $this->readChunk( '\]\]' );
+ $hasError = false;
+ }
+
+ if ( $chunk != ']]' ) {
+ $labelpart .= $chunk;
+ $chunk = $this->readChunk( '\]\]' );
+ }
+
+ if ( $hasError ) {
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_unexpectedpart', $labelpart );
+ }
+ }
+
+ if ( $chunk != ']]' ) {
+ // What happended? We found some chunk that could not be processed as
+ // link content (as in [[Category:Test<q>]]), or the closing ]] are
+ // just missing entirely.
+ if ( $chunk !== '' ) {
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_misplacedsymbol', $chunk );
+
+ // try to find a later closing ]] to finish this misshaped subpart
+ $chunk = $this->readChunk( '\]\]' );
+
+ if ( $chunk != ']]' ) {
+ $chunk = $this->readChunk( '\]\]' );
+ }
+ }
+ if ( $chunk === '' ) {
+ $this->descriptionProcessor->addErrorWithMsgKey( 'smw_noclosingbrackets' );
+ }
+ }
+
+ return $description;
+ }
+
+ /**
+ * @see Tokenizer::read
+ */
+ private function readChunk( $stoppattern = '', $consume = true, $trim = true ) {
+ return $this->tokenizer->getToken( $this->currentString, $stoppattern, $consume, $trim );
+ }
+
+ /**
+ * Enter a new subblock in the query, which must at some time be terminated by the
+ * given $endstring delimiter calling popDelimiter();
+ */
+ private function pushDelimiter( $endstring ) {
+ array_push( $this->separatorStack, $endstring );
+ }
+
+ /**
+ * Exit a subblock in the query ending with the given delimiter.
+ * If the delimiter does not match the top-most open block, false
+ * will be returned. Otherwise return true.
+ */
+ private function popDelimiter( $endstring ) {
+ $topdelim = array_pop( $this->separatorStack );
+ return ( $topdelim == $endstring );
+ }
+
+ private function isPagePropertyType( $typeid ) {
+ return $typeid == '_wpg' || $this->dataTypeRegistry->isSubDataType( $typeid );
+ }
+
+ private function hasClassPrefix( $chunk ) {
+ return in_array( smwfNormalTitleText( $chunk ), [ $this->categoryPrefix, $this->conceptPrefix, $this->categoryPrefixCannonical, $this->conceptPrefixCannonical ] );
+ }
+
+ private function isClass( $chunk ) {
+ return smwfNormalTitleText( $chunk ) == $this->categoryPrefix || smwfNormalTitleText( $chunk ) == $this->categoryPrefixCannonical;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/TermParser.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/TermParser.php
new file mode 100644
index 00000000..96b889a3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/TermParser.php
@@ -0,0 +1,253 @@
+<?php
+
+namespace SMW\Query\Parser;
+
+/**
+ * The term parser uses a simplified string to build an #ask conform query
+ * string, for example:
+ * - `in:foo bar || (phrase:bar && not:foo)` becomes `[[in:
+ * foo bar]] || <q>[[phrase:bar]] && [[not:foo]]</q>`
+ * - `in:(foo && bar)`becomes [[in:foo]] && [[in:bar]]
+ *
+ * A custom prefix map allows to create assignments between a custom prefix and
+ * a property set and hereby simplifies the search input process.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TermParser {
+
+ /**
+ * @var []
+ */
+ private $standard_prefix = [ 'in:', 'phrase:', 'not:', 'has:', 'category:' ];
+
+ /**
+ * @var []
+ */
+ private static $cache = [];
+
+ /**
+ * The `prefix_map` is expected to contain assignments of prefixes that link
+ * to a collection of properties. The prefix is used as short-cut to cover a
+ * range of disjunctive query declarations to simplify the creation of a
+ * query construct such as:
+ *
+ * - Prefix map: `'keyword' => [ 'Has keyword', 'Keyword' ]`
+ * - Input: `keyword:foo bar`
+ * - Output: `([[Has keyword::foo bar]] || [[Keyword::foo bar]])`
+ *
+ * @var []
+ */
+ private $prefix_map = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param array $prefix_map
+ */
+ public function __construct( array $prefix_map = [] ) {
+ $this->prefix_map = $prefix_map;
+ }
+
+ /**
+ * @param string $term
+ *
+ * @return string
+ */
+ public function parse( $term ) {
+
+ $hash = md5( $term );
+
+ if ( isset( self::$cache[$hash] ) ) {
+ return self::$cache[$hash];
+ }
+
+ $pattern = '';
+ $custom_prefix = [];
+
+ foreach ( array_keys( $this->prefix_map ) as $p ) {
+
+ // Just in case, `in:`, `phrase:`, `has:`, and `not:` are not
+ // permitted to be overridden by a prefix assignment, `category:`
+ // can.
+ if ( in_array( $p, [ 'in', 'phrase', 'not', 'has' ] ) ) {
+ continue;
+ }
+
+ $pattern .= '|(' . $p . ':)';
+ $custom_prefix[] = "$p:";
+ }
+
+ // in:(A&&b)-> in:A && in:b
+ $this->normalize_compact_form( 'in', $pattern, $term );
+
+ // has:(A&&b) -> has:A && has:b
+ $this->normalize_compact_form( 'has', $pattern, $term );
+
+ // Simplify the processing by normalizing expressions
+ $term = str_replace( [ '<q>', '</q>' ], [ '(', ')' ], $term );
+
+ $terms = preg_split(
+ "/(in:)|(phrase:)|(not:)|(has:)|(category:)$pattern|(&&)|(AND)|(OR)|(\|\|)|(\()|(\)|(\[\[))/",
+ $term,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
+ );
+
+ $affix = array_merge(
+ [ '&&', 'AND', '||', 'OR', '(', ')', '[[' ],
+ $this->standard_prefix,
+ $custom_prefix
+ );
+
+ $term = '';
+ $custom = '';
+ $prefix = '';
+ $k = 0;
+
+ while ( key( $terms ) !== null ) {
+
+ $t_term = current( $terms );
+ $new = trim( $t_term );
+
+ $continue = true;
+ $space = $t_term{0} == ' ' ? ' ' : '';
+
+ // Look ahead
+ $next = next( $terms );
+ $last = substr( $term, -2 );
+
+ if ( $new === '' ) {
+ continue;
+ }
+
+ if ( $new === '[[' && $next === '[[' ) {
+ continue;
+ }
+
+ if ( in_array( $new, $custom_prefix ) ) {
+ $custom = "[[$new";
+ $prefix = $new;
+ } elseif( in_array( $new, $this->standard_prefix ) ) {
+ $term .= "[[$new";
+ } elseif ( $custom !== '' ) {
+ $custom .= $new;
+ $last = substr( $new, -2 );
+ } else {
+ $term .= "{$space}{$new}";
+ }
+
+ // has:Property Foo -> [[Property Foo::+]]
+ if ( $new === 'has:' ) {
+ $next = trim( $next );
+ $last = ']]';
+ // Already using the next element to set the property,
+ // skip in case other terms are to be found
+ $continue = !next( $terms );
+ $term = str_replace( 'has:', "$next::+$last", $term );
+ }
+
+ if ( $continue && $last === ']]' || $new === '(' || $new === '||' ) {
+ continue;
+ }
+
+ // Check next element, close expression in case of a matching
+ // affix
+ if ( $k > 0 && in_array( $next, $affix ) ) {
+ $term .= $this->close( $custom, $prefix );
+ }
+
+ // Last element
+ if ( $next === false && !in_array( $last, [ '&&', 'AND', '||', 'OR', ']]' ] ) ) {
+ if ( $custom === '' && mb_substr_count( $term, '[[' ) > mb_substr_count( $term, ']]' ) ) {
+ $term .= $this->close( $custom, $prefix );
+ } elseif ( $custom !== '' ) {
+ $term .= $this->close( $custom, $prefix );
+ }
+ }
+
+ $k++;
+ }
+
+ return self::$cache[$hash] = $this->normalize( $term );
+ }
+
+ private function close( &$custom, $prefix ) {
+
+ // Standard closing
+ if ( $custom === '' ) {
+ return "]]";
+ }
+
+ $term = "$custom]]";
+ $custom = '';
+ $terms = [];
+ $p_map = str_replace( ':', '', $prefix );
+
+ if ( !isset( $this->prefix_map[$p_map] ) ) {
+ return $term;
+ }
+
+ // A custom prefix adds additional disjunctive conditions to broaden the
+ // search radius for all its assigned properties.
+ foreach ( $this->prefix_map[$p_map] as $val ) {
+ $terms[] = str_replace( $prefix, $val . '::', $term );
+ }
+
+ // `keyword:foo bar` -> ([[Has keyword::foo bar]] || [[Keyword::foo bar]])
+ return '(' . implode( '||', $terms ) . ')';
+ }
+
+ private function normalize( $term ) {
+ return str_replace(
+ [ ')[[', ']](', '(', ')', '||', '&&', 'AND', 'OR', ']][[', '[[[[', ']]]]', ' ' ],
+ [ ') [[', ']] (', '<q>', '</q>', ' || ', ' && ', ' AND ', ' OR ', ']] [[', '[[', ']]', ' ' ],
+ $term
+ );
+ }
+
+ private function normalize_compact_form( $exp, $pattern, &$term ) {
+
+ if ( strpos( $term, "$exp:(" ) === false ) {
+ return;
+ }
+
+ preg_match_all("/$exp:\((.*?)\)/", $term, $matches );
+
+ foreach ( $matches[0] as $match ) {
+ $orig = $match;
+ $match = str_replace( "$exp:(", '', $match );
+
+ if ( substr( $match, -1 ) === ')' ) {
+ $match = substr( $match, 0, -1 );
+ }
+
+ $terms = preg_split(
+ "/(in:)|(phrase:)|(not:)|(has:)|(category:)$pattern|(&&)|(AND)|(OR)|(\|\|)|(\()|(\)|(\[\[))/",
+ $match,
+ -1,
+ PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
+ );
+
+ $replace = '';
+
+ foreach ( $terms as $t ) {
+ $t = trim( $t );
+
+ if ( in_array( $t, [ '&&', 'AND', '||', 'OR' ] ) ) {
+ $replace .= " $t ";
+ } elseif ( $t === ')' ) {
+ $replace .= "$t";
+ } else {
+ $replace .= "$exp:$t";
+ }
+ }
+
+ $term = str_replace( $orig, $replace, $term );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/Tokenizer.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/Tokenizer.php
new file mode 100644
index 00000000..101812fa
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Parser/Tokenizer.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace SMW\Query\Parser;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Markus Krötzsch
+ */
+class Tokenizer {
+
+ /**
+ * @var string
+ */
+ private $defaultPattern = '';
+
+ /**
+ * @since 3.0
+ *
+ * @param array $prefixes
+ */
+ public function setDefaultPattern( array $prefixes ) {
+
+ $pattern = '';
+
+ foreach ( $prefixes as $pref ) {
+ $pattern .= '|^' . $pref;
+ }
+
+ $this->defaultPattern = '\[\[|\]\]|::|:=|<q>|<\/q>' . $pattern . '|\|\||\|';
+ }
+
+ /**
+ * Get the next unstructured string chunk from the query string.
+ * Chunks are delimited by any of the special strings used in inline queries
+ * (such as [[, ]], <q>, ...). If the string starts with such a delimiter,
+ * this delimiter is returned. Otherwise the first string in front of such a
+ * delimiter is returned.
+ * Trailing and initial spaces are ignored if $trim is true, and chunks
+ * consisting only of spaces are not returned.
+ * If there is no more qurey string left to process, the empty string is
+ * returned (and in no other case).
+ *
+ * The stoppattern can be used to customise the matching, especially in order to
+ * overread certain special symbols.
+ *
+ * $consume specifies whether the returned chunk should be removed from the
+ * query string.
+ *
+ * @param string $currentString
+ * @param string $stoppattern
+ * @param boolean $consume
+ * @param boolean $trim
+ *
+ * @return string
+ */
+ public function getToken( &$currentString, $stoppattern = '', $consume = true, $trim = true ) {
+
+ if ( $stoppattern === '' ) {
+ $stoppattern = $this->defaultPattern;
+ }
+
+ $chunks = preg_split( '/[\s]*(' . $stoppattern . ')/iu', $currentString, 2, PREG_SPLIT_DELIM_CAPTURE );
+
+ if ( count( $chunks ) == 1 ) { // no matches anymore, strip spaces and finish
+ if ( $consume ) {
+ $currentString = '';
+ }
+
+ return $trim ? trim( $chunks[0] ) : $chunks[0];
+ } elseif ( count( $chunks ) == 3 ) { // this should generally happen if count is not 1
+ if ( $chunks[0] === '' ) { // string started with delimiter
+ if ( $consume ) {
+ $currentString = $chunks[2];
+ }
+
+ return $trim ? trim( $chunks[1] ) : $chunks[1];
+ } else {
+ if ( $consume ) {
+ $currentString = $chunks[1] . $chunks[2];
+ }
+
+ return $trim ? trim( $chunks[0] ) : $chunks[0];
+ }
+ }
+
+ // should never happen
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest.php
new file mode 100644
index 00000000..24542c92
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest.php
@@ -0,0 +1,362 @@
+<?php
+
+namespace SMW\Query;
+
+use InvalidArgumentException;
+use SMW\DataValues\PropertyChainValue;
+use SMW\Localizer;
+use SMW\Query\PrintRequest\Deserializer;
+use SMW\Query\PrintRequest\Formatter;
+use SMW\Query\PrintRequest\Serializer;
+use SMWDataValue;
+use SMWPropertyValue as PropertyValue;
+use Title;
+
+/**
+ * Container class for request for printout, as used in queries to
+ * obtain additional information for the retrieved results.
+ *
+ * @ingroup SMWQuery
+ * @author Markus Krötzsch
+ */
+class PrintRequest {
+
+ /**
+ * Query mode to print all direct categories of the current element.
+ */
+ const PRINT_CATS = 0;
+
+ /**
+ * Query mode to print all property values of a certain attribute of the
+ * current element.
+ */
+ const PRINT_PROP = 1;
+
+ /**
+ * Query mode to print the current element (page in result set).
+ */
+ const PRINT_THIS = 2;
+
+ /**
+ * Query mode to print whether current element is in given category
+ * (Boolean printout).
+ */
+ const PRINT_CCAT = 3;
+
+ /**
+ * Query mode indicating a chainable property value entity, with the last
+ * element to represent the printable output
+ */
+ const PRINT_CHAIN = 4;
+
+ protected $m_mode; // type of print request
+
+ protected $m_label; // string for labelling results, contains no markup
+
+ protected $m_data; // data entries specifyin gwhat was requested (mixed type)
+
+ protected $m_typeid = false; // id of the datatype of the printed objects, if applicable
+
+ protected $m_outputformat; // output format string for formatting results, if applicable
+
+ protected $m_hash = false; // cache your hash (currently useful since SMWQueryResult accesses the hash many times, might be dropped at some point)
+
+ protected $m_params = [];
+
+ /**
+ * Identifies whether this instance was used/added and is diconnected to
+ * the original query where it was added.
+ *
+ * Mostly used in cases where QueryProcessor::addThisPrintout was executed.
+ */
+ private $isDisconnected = false;
+
+ /**
+ * Whether the label was marked with an extra `#` identifier.
+ */
+ private $labelMarker = false;
+
+ /**
+ * Create a print request.
+ *
+ * @param integer $mode a constant defining what to printout
+ * @param string $label the string label to describe this printout
+ * @param mixed $data optional data for specifying some request, might be a property object, title, or something else; interpretation depends on $mode
+ * @param mixed $outputformat optional string for specifying an output format, e.g. an output unit
+ * @param array|null $params optional array of further, named parameters for the print request
+ */
+ public function __construct( $mode, $label, $data = null, $outputformat = false, array $params = null ) {
+ if ( ( ( $mode == self::PRINT_CATS || $mode == self::PRINT_THIS ) &&
+ !is_null( $data ) ) ||
+ ( $mode == self::PRINT_PROP &&
+ ( !( $data instanceof PropertyValue ) || !$data->isValid() ) ) ||
+ ( $mode == self::PRINT_CHAIN &&
+ ( !( $data instanceof PropertyChainValue ) || !$data->isValid() ) ) ||
+ ( $mode == self::PRINT_CCAT &&
+ !( $data instanceof Title ) )
+ ) {
+ throw new InvalidArgumentException( 'Data provided for print request does not fit the type of printout.' );
+ }
+
+ $this->m_mode = $mode;
+ $this->m_data = $data;
+ $this->m_outputformat = $outputformat;
+
+ if ( $mode == self::PRINT_CCAT && !$outputformat ) {
+ $this->m_outputformat = 'x'; // changed default for Boolean case
+ }
+
+ $this->setLabel( $label );
+
+ if ( $params !== null ) {
+ $this->m_params = $params;
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isDisconnected
+ */
+ public function isDisconnected( $isDisconnected ) {
+ $this->isDisconnected = (bool)$isDisconnected;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ */
+ public function markThisLabel( $text ) {
+
+ if ( $this->m_mode !== self::PRINT_THIS ) {
+ return;
+ }
+
+ $this->labelMarker = $text !== '' && $text{0} === '#';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function hasLabelMarker() {
+ return $this->labelMarker;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $mode
+ *
+ * @return boolean
+ */
+ public function isMode( $mode ) {
+ return $this->m_mode === $mode;
+ }
+
+ public function getMode() {
+ return $this->m_mode;
+ }
+
+ public function getLabel() {
+ return $this->m_label;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getCanonicalLabel() {
+
+ if ( $this->m_mode === self::PRINT_PROP ) {
+ return $this->m_data->getDataItem()->getCanonicalLabel();
+ } elseif ( $this->m_mode === self::PRINT_CHAIN ) {
+ return $this->m_data->getDataItem()->getString();
+ } elseif ( $this->m_mode === self::PRINT_CATS ) {
+ return Localizer::getInstance()->getNamespaceTextById( NS_CATEGORY );
+ } elseif ( $this->m_mode === self::PRINT_CCAT ) {
+ return $this->m_data->getPrefixedText();
+ }
+
+ return $this->m_label;
+ }
+
+ /**
+ * Obtain an HTML-formatted representation of the label.
+ * The $linker is a Linker object used for generating hyperlinks.
+ * If it is NULL, no links will be created.
+ */
+ public function getHTMLText( $linker = null ) {
+ return Formatter::format( $this, $linker, Formatter::FORMAT_HTML );
+ }
+
+ /**
+ * Obtain a Wiki-formatted representation of the label.
+ */
+ public function getWikiText( $linker = false ) {
+ return Formatter::format( $this, $linker, Formatter::FORMAT_WIKI );
+ }
+
+ /**
+ * Convenience method for accessing the text in either HTML or Wiki format.
+ */
+ public function getText( $outputMode, $linker = null ) {
+ return Formatter::format( $this, $linker, $outputMode );
+ }
+
+ /**
+ * Return additional data related to the print request. The result might be
+ * an object of class PropertyValue or Title, or simply NULL if no data
+ * is required for the given type of printout.
+ */
+ public function getData() {
+ return $this->m_data;
+ }
+
+ public function getOutputFormat() {
+ return $this->m_outputformat;
+ }
+
+ /**
+ * If this print request refers to some property, return the type id of this property.
+ * Otherwise return '_wpg' since all other types of print request return wiki pages.
+ *
+ * @return string
+ */
+ public function getTypeID() {
+
+ if ( $this->m_typeid !== false ) {
+ return $this->m_typeid;
+ }
+
+ if ( $this->m_mode == self::PRINT_PROP ) {
+ $this->m_typeid = $this->m_data->getDataItem()->findPropertyTypeID();
+ } elseif ( $this->m_mode == self::PRINT_CHAIN ) {
+ $this->m_typeid = $this->m_data->getLastPropertyChainValue()->getDataItem()->findPropertyTypeID();
+ } else {
+ $this->m_typeid = '_wpg';
+ }
+
+ return $this->m_typeid;
+ }
+
+ /**
+ * Return a hash string that is used to eliminate duplicate
+ * print requests. The hash also includes the chosen label,
+ * so it is possible to print the same date with different
+ * labels.
+ *
+ * @return string
+ */
+ public function getHash() {
+
+ if ( $this->m_hash !== false ) {
+ return $this->m_hash;
+ }
+
+ $this->m_hash = $this->m_mode . ':' . $this->m_label . ':';
+
+ if ( $this->m_data instanceof Title ) {
+ $this->m_hash .= $this->m_data->getPrefixedText() . ':';
+ }
+ elseif ( $this->m_data instanceof SMWDataValue ) {
+ $this->m_hash .= $this->m_data->getHash() . ':';
+ }
+
+ $this->m_hash .= $this->m_outputformat . ':' . implode( '|', $this->m_params );
+
+ return $this->m_hash;
+ }
+
+ /**
+ * Serialise this object like print requests given in \#ask.
+ *
+ * @param $params boolean that sets if the serialization should
+ * include the extra print request parameters
+ */
+ public function getSerialisation( $showparams = false ) {
+
+ // In case of disconnected instance (QueryProcessor::addThisPrintout as
+ // part of a post-processing) return an empty serialization when the
+ // mainLabel is available to avoid an extra `?...`
+ if ( $this->isMode( self::PRINT_THIS ) && $this->isDisconnected ) {
+ return '';
+ }
+
+ return Serializer::serialize( $this, $showparams );
+ }
+
+ /**
+ * Returns the value of a named parameter.
+ *
+ * @param $key string the name of the parameter key
+ *
+ * @return string Value of the paramer, if set (else FALSE)
+ */
+ public function getParameter( $key ) {
+ return array_key_exists( $key, $this->m_params ) ? $this->m_params[$key] : false;
+ }
+
+ /**
+ * Returns the array of parameters, where a string is mapped to a string.
+ *
+ * @return array Map of parameter names to values.
+ */
+ public function getParameters() {
+ return $this->m_params;
+ }
+
+ /**
+ * Sets a print request parameter.
+ *
+ * @param $key string Name of the parameter
+ * @param $value string Value for the parameter
+ */
+ public function setParameter( $key, $value ) {
+ $this->m_params[$key] = $value;
+ }
+
+ /**
+ * Removes a request parameter
+ *
+ * @since 3.0
+ *
+ * @param string $key
+ */
+ public function removeParameter( $key ) {
+ unset( $this->m_params[$key] );
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @note $this->m_data = clone $data; // we assume that the caller denotes
+ * the object ot us; else he needs provide us with a clone
+ *
+ * @param string $label
+ */
+ public function setLabel( $label ) {
+ $this->m_label = $label;
+
+ if ( $this->m_data instanceof SMWDataValue ) {
+ $this->m_data->setCaption( $label );
+ }
+ }
+
+ /**
+ * @see Deserializer::deserialize
+ * @since 2.4
+ *
+ * @param string $text
+ * @param $showMode = false
+ *
+ * @return PrintRequest|null
+ */
+ public static function newFromText( $text, $showMode = false ) {
+ return Deserializer::deserialize( $text, $showMode );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Deserializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Deserializer.php
new file mode 100644
index 00000000..480d6a77
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Deserializer.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace SMW\Query\PrintRequest;
+
+use InvalidArgumentException;
+use SMW\DataValueFactory;
+use SMW\DataValues\PropertyChainValue;
+use SMW\Localizer;
+use SMW\Query\PrintRequest;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class Deserializer {
+
+ /**
+ * Create an PrintRequest object from a string description as one
+ * would normally use in #ask and related inputs. The string must start
+ * with a "?" and may contain label and formatting parameters after "="
+ * or "#", respectively. However, further parameters, given in #ask by
+ * "|+param=value" are not allowed here; they must be added
+ * individually.
+ *
+ * @since 2.5
+ *
+ * @param string $text
+ * @param boolean $showMode = false
+ *
+ * @return PrintRequest|null
+ */
+ public static function deserialize( $text, $showMode = false ) {
+
+ list( $parts, $outputFormat, $printRequestLabel ) = self::getPartsFromText(
+ $text
+ );
+
+ $data = null;
+
+ if ( $printRequestLabel === '' ) { // print "this"
+ $printmode = PrintRequest::PRINT_THIS;
+
+ // default
+ $label = '';
+
+ // Distinguish the case of an empty format
+ if ( $outputFormat === '' ) {
+ $outputFormat = null;
+ }
+
+ } elseif ( self::isCategory( $printRequestLabel ) ) { // print categories
+ $printmode = PrintRequest::PRINT_CATS;
+ $label = $showMode ? '' : Localizer::getInstance()->getNamespaceTextById( NS_CATEGORY ); // default
+ } elseif ( PropertyChainValue::isChained( $printRequestLabel ) ) {
+
+ $data = DataValueFactory::getInstance()->newDataValueByType( PropertyChainValue::TYPE_ID );
+ $data->setUserValue( $printRequestLabel );
+
+ $printmode = PrintRequest::PRINT_CHAIN;
+ $label = $showMode ? '' : $data->getLastPropertyChainValue()->getWikiValue(); // default
+
+ } else { // print property or check category
+ $title = Title::newFromText( $printRequestLabel, SMW_NS_PROPERTY ); // trim needed for \n
+
+ // not a legal property/category name; give up
+ if ( $title === null ) {
+ return null;
+ }
+
+ if ( $title->getNamespace() == NS_CATEGORY ) {
+ $printmode = PrintRequest::PRINT_CCAT;
+ $data = $title;
+ $label = $showMode ? '' : $title->getText(); // default
+ } else { // enforce interpretation as property (even if it starts with something that looks like another namespace)
+ $printmode = PrintRequest::PRINT_PROP;
+ $data = DataValueFactory::getInstance()->newPropertyValueByLabel( $printRequestLabel );
+ if ( !$data->isValid() ) { // not a property; give up
+ return null;
+ }
+ $label = $showMode ? '' : $data->getWikiValue(); // default
+ }
+ }
+
+ // "plain printout"
+ // @docu mentions that `?foo#` is equal to `?foo#-` and avoid an
+ // empty string to distinguish it from "false"
+ if ( $outputFormat === '' ) {
+ $outputFormat = '-';
+ }
+
+ // label found, use this instead of default
+ if ( count( $parts ) > 1 ) {
+ $label = trim( $parts[1] );
+ }
+
+ if ( $printmode === PrintRequest::PRINT_THIS ) {
+
+ // Cover the case of `?#Test=#-`
+ if ( strrpos( $label, '#' ) !== false ) {
+ list( $label, $outputFormat ) = explode( '#', $label );
+
+ // `?#=foo#` is equal to `?#=foo#-`
+ if ( $outputFormat === '' ) {
+ $outputFormat = '-';
+ }
+ }
+ }
+
+ try {
+ $printRequest = new PrintRequest( $printmode, $label, $data, trim( $outputFormat ) );
+ $printRequest->markThisLabel( $text );
+ } catch ( InvalidArgumentException $e ) {
+ // something still went wrong; give up
+ $printRequest = null;
+ }
+
+ return $printRequest;
+ }
+
+ private static function isCategory( $text ) {
+
+ // Check for the canonical form (singular, plural)
+ if ( $text == 'Category' || $text == 'Categories' ) {
+ return true;
+ }
+
+ return Localizer::getInstance()->getNamespaceTextById( NS_CATEGORY ) == mb_convert_case( $text, MB_CASE_TITLE );
+ }
+
+ private static function getPartsFromText( $text ) {
+
+ // #1464
+ // Temporary encode "=" within a <> entity (<span>...</span>)
+ $text = preg_replace_callback( "/(<(.*?)>(.*?)>)/u", function( $matches ) {
+ foreach ( $matches as $match ) {
+ return str_replace( [ '=' ], [ '-3D' ], $match );
+ }
+ }, $text );
+
+ $parts = explode( '=', $text, 2 );
+
+ // Restore temporary encoding
+ $parts[0] = str_replace( [ '-3D' ], [ '=' ], $parts[0] );
+
+ if ( isset( $parts[1] ) ) {
+ $parts[1] = str_replace( [ '-3D' ], [ '=' ], $parts[1] );
+ }
+
+ $propparts = explode( '#', $parts[0], 2 );
+ $printRequestLabel = trim( $propparts[0] );
+ $outputFormat = isset( $propparts[1] ) ? trim( $propparts[1] ) : false;
+
+ return [ $parts, $outputFormat, $printRequestLabel ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Formatter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Formatter.php
new file mode 100644
index 00000000..49ad3e9d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Formatter.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace SMW\Query\PrintRequest;
+
+use Linker;
+use SMW\Query\PrintRequest;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class Formatter {
+
+ const FORMAT_WIKI = SMW_OUTPUT_WIKI;
+ const FORMAT_HTML = SMW_OUTPUT_HTML;
+
+ /**
+ * Obtain an HTML-formatted or Wiki-formatted representation of the label.
+ * The $linker is a Linker object used for generating hyperlinks.
+ * If it is NULL, no links will be created.
+ *
+ * @since 2.5
+ *
+ * @param PrintRequest $printRequest
+ * @param Linker|null $linker
+ * @param integer|null $outputType
+ *
+ * @return string
+ */
+ public static function format( PrintRequest $printRequest, $linker = null, $outputType = null ) {
+
+ if ( $outputType === self::FORMAT_WIKI || $outputType === SMW_OUTPUT_WIKI ) {
+ return self::getWikiText( $printRequest, $linker );
+ }
+
+ return self::getHTMLText( $printRequest, $linker );
+ }
+
+ private static function getHTMLText( $printRequest, $linker = null ) {
+
+ $label = htmlspecialchars( $printRequest->getLabel() );
+
+ if ( $linker === null || $linker === false || $label === '' ) {
+ return $label;
+ }
+
+ switch ( $printRequest->getMode() ) {
+ case PrintRequest::PRINT_CATS:
+ return Linker::link( Title::newFromText( 'Categories', NS_SPECIAL ), $label );
+ case PrintRequest::PRINT_CCAT:
+ return Linker::link( $printRequest->getData(), $label );
+ case PrintRequest::PRINT_CHAIN:
+ case PrintRequest::PRINT_PROP:
+ return $printRequest->getData()->getShortHTMLText( $linker );
+ case PrintRequest::PRINT_THIS:
+ default:
+ return $label;
+ }
+ }
+
+ private static function getWikiText( $printRequest, $linker = false ) {
+
+ $label = $printRequest->getLabel();
+
+ if ( $linker === null || $linker === false || $label === '' ) {
+ return $label;
+ }
+
+ switch ( $printRequest->getMode() ) {
+ case PrintRequest::PRINT_CATS:
+ return '[[:' . 'Special:Categories' . '|' . $label . ']]';
+ case PrintRequest::PRINT_CHAIN:
+ case PrintRequest::PRINT_PROP:
+ return $printRequest->getData()->getShortWikiText( $linker );
+ case PrintRequest::PRINT_CCAT:
+ return '[[:' . $printRequest->getData()->getPrefixedText() . '|' . $label . ']]';
+ case PrintRequest::PRINT_THIS:
+ default:
+ return $label;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Serializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Serializer.php
new file mode 100644
index 00000000..85523c9c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequest/Serializer.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace SMW\Query\PrintRequest;
+
+use SMW\Localizer;
+use SMW\Query\PrintRequest;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class Serializer {
+
+ /**
+ * @since 2.5
+ *
+ * @param PrintRequest $printRequest
+ * @param boolean $showparams that sets if the serialization should include
+ * the extra print request parameters
+ *
+ * @return string
+ */
+ public static function serialize( PrintRequest $printRequest, $showparams = false ) {
+ $parameters = '';
+
+ if ( $showparams ) {
+
+ // #2037 index is required as helper parameter during the result
+ // display but is not part of the original request
+ if ( $printRequest->getParameter( 'lang' ) ) {
+ $printRequest->removeParameter( 'index' );
+ };
+
+ foreach ( $printRequest->getParameters() as $key => $value ) {
+ $parameters .= "|+" . $key . "=" . $value;
+ }
+ }
+
+ switch ( $printRequest->getMode() ) {
+ case PrintRequest::PRINT_CATS:
+ return self::doSerializeCat( $printRequest, $parameters );
+ case PrintRequest::PRINT_CCAT:
+ return self::doSerializeCcat( $printRequest, $parameters );
+ case PrintRequest::PRINT_CHAIN:
+ case PrintRequest::PRINT_PROP:
+ return self::doSerializeProp( $printRequest, $parameters );
+ case PrintRequest::PRINT_THIS:
+ return self::doSerializeThis( $printRequest, $parameters );
+ default:
+ return '';
+ }
+
+ return ''; // no current serialisation
+ }
+
+ private static function doSerializeCat( $printRequest, $parameters ) {
+
+ $catlabel = Localizer::getInstance()->getNamespaceTextById( NS_CATEGORY );
+ $result = '?' . $catlabel;
+
+ if ( $printRequest->getLabel() != $catlabel ) {
+ $result .= '=' . $printRequest->getLabel();
+ }
+
+ return $result . $parameters;
+ }
+
+ private static function doSerializeCcat( $printRequest, $parameters ) {
+
+ $printname = $printRequest->getData()->getPrefixedText();
+ $result = '?' . $printname;
+
+ if ( $printRequest->getOutputFormat() != 'x' ) {
+ $result .= '#' . $printRequest->getOutputFormat();
+ }
+
+ if ( $printRequest->getLabel() != $printname ) {
+ $result .= '=' . $printRequest->getLabel();
+ }
+
+ return $result . $parameters;
+ }
+
+ private static function doSerializeProp( $printRequest, $parameters ) {
+
+ $printname = '';
+
+ $label = $printRequest->getLabel();
+ $data = $printRequest->getData();
+
+ if ( $data->isVisible() ) {
+ // #1564
+ // Use the canonical form for predefined properties to ensure
+ // that local representations are for display but points to
+ // the correct property
+ if ( $printRequest->isMode( PrintRequest::PRINT_CHAIN ) ) {
+ $printname = $data->getDataItem()->getString();
+ // If the preferred label and invoked label are the same
+ // then no additional label is required as the label is
+ // recognized as being available by the system
+ if ( $label === $data->getLastPropertyChainValue()->getDataItem()->getPreferredLabel() ) {
+ $label = $printname;
+ }
+ } else {
+
+ $printname = $data->getDataItem()->getCanonicalLabel();
+
+ if ( $label === $data->getDataItem()->getPreferredLabel() ) {
+ $label = $printname;
+ }
+
+ // Don't carry a localized label for a predefined property
+ // (fetched via the wikiValue)
+ if ( !$data->getDataItem()->isUserDefined() && $label === $data->getWikiValue() ) {
+ $label = $data->getDataItem()->getCanonicalLabel();
+ }
+ }
+ }
+
+ $result = '?' . $printname;
+
+ if ( $printRequest->getOutputFormat() !== '' ) {
+ $result .= '#' . $printRequest->getOutputFormat();
+ }
+
+ if ( $printname != $label && $label !== '' ) {
+ $result .= '=' . $label;
+ }
+
+ return $result . $parameters;
+ }
+
+ private static function doSerializeThis( $printRequest, $parameters ) {
+
+ $result = '?';
+
+ // Has leading ?#
+ if ( $printRequest->hasLabelMarker() ) {
+ $result .= '#';
+ }
+
+ if ( $printRequest->getLabel() !== '' ) {
+ $result .= '=' . $printRequest->getLabel();
+ }
+
+ $outputFormat = $printRequest->getOutputFormat();
+
+ if ( $outputFormat !== '' && $outputFormat !== false && $outputFormat !== null ) {
+
+ // Handle ?, ?#- vs. ?#Foo=#-
+ if ( $printRequest->getLabel() !== '' ) {
+ $result .= '#';
+ }
+
+ $result .= $outputFormat;
+ }
+
+ return $result . $parameters;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequestFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequestFactory.php
new file mode 100644
index 00000000..2d60b6f6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/PrintRequestFactory.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMWPropertyValue as PropertyValue;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class PrintRequestFactory {
+
+ /**
+ * @since 2.1
+ *
+ * @param DIProperty $property
+ *
+ * @return PrintRequest
+ */
+ public function newFromProperty( DIProperty $property ) {
+
+ $propertyValue = DataValueFactory::getInstance()->newDataValueByType( PropertyValue::TYPE_ID );
+ $propertyValue->setDataItem( $property );
+
+ $instance = new PrintRequest(
+ PrintRequest::PRINT_PROP,
+ $propertyValue->getWikiValue(),
+ $propertyValue
+ );
+
+ return $instance;
+ }
+
+ /**
+ * @see PrintRequest::newFromText
+ *
+ * @since 2.4
+ *
+ * @param string $text
+ * @param $showMode = false
+ *
+ * @return PrintRequest|null
+ */
+ public function newFromText( $text, $showMode = false ) {
+ return PrintRequest::newFromText( $text, $showMode );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $label
+ * @param array $parameters
+ *
+ * @return PrintRequest
+ */
+ public function newThisPrintRequest( $label = '', array $parameters = [] ) {
+ return new PrintRequest( PrintRequest::PRINT_THIS, $label, null, false, $parameters );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/DefaultParamDefinition.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/DefaultParamDefinition.php
new file mode 100644
index 00000000..6f02b379
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/DefaultParamDefinition.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace SMW\Query\Processor;
+
+use SMW\Query\ResultPrinter;
+use SMW\Message;
+use ParamProcessor\ParamDefinition;
+use SMW\Query\QueryContext;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DefaultParamDefinition {
+
+ /**
+ * Produces a list of allowed parameters of a query using any specific format.
+ *
+ * @since 3.0
+ *
+ * @param integer|null $context
+ * @param ResultPrinter|null $resultPrinter
+ *
+ * @return IParamDefinition[]
+ */
+ public static function getParamDefinitions( $context = null, ResultPrinter $resultPrinter = null ) {
+ return self::buildParamDefinitions( $GLOBALS, $context, $resultPrinter );
+ }
+
+ /**
+ * @private
+ *
+ * Give grep a chance to find the msg usages:
+ *
+ * smw-paramdesc-format, smw-paramdesc-source, smw-paramdesc-limit,
+ * smw-paramdesc-offset, smw-paramdesc-link, smw-paramdesc-sort,
+ * smw-paramdesc-order, smw-paramdesc-headers, smw-paramdesc-mainlabel,
+ * smw-paramdesc-intro, smw-paramdesc-outro, smw-paramdesc-searchlabel,
+ * smw-paramdesc-default
+ *
+ * @since 3.0
+ *
+ * @param integer|null $context
+ * @param ResultPrinter|null $resultPrinter
+ *
+ * @return IParamDefinition[]
+ */
+ public static function buildParamDefinitions( $vars, $context = null, ResultPrinter $resultPrinter = null ) {
+ $params = [];
+
+ $allowedFormats = $vars['smwgResultFormats'];
+
+ foreach ( $vars['smwgResultAliases'] as $aliases ) {
+ $allowedFormats += $aliases;
+ }
+
+ $allowedFormats[] = 'auto';
+
+ $params['format'] = [
+ 'type' => 'smwformat',
+ 'default' => 'auto',
+ ];
+
+ // TODO $params['format']->setToLower( true );
+ // TODO $allowedFormats
+
+ $params['source'] = self::getSourceParam( $vars );
+
+ $params['limit'] = [
+ 'type' => 'integer',
+ 'default' => $vars['smwgQDefaultLimit'],
+ 'negatives' => false,
+ ];
+
+ $params['offset'] = [
+ 'type' => 'integer',
+ 'default' => 0,
+ 'negatives' => false,
+ 'upperbound' => $vars['smwgQUpperbound'],
+ ];
+
+ $params['link'] = [
+ 'default' => 'all',
+ 'values' => [ 'all', 'subject', 'none' ],
+ ];
+
+ // The empty string represents the page itself, which should be sorted by default.
+ $params['sort'] = [
+ 'islist' => true,
+ 'default' => [ '' ]
+ ];
+
+ $params['order'] = [
+ 'islist' => true,
+ 'default' => [],
+ 'values' => [ 'descending', 'desc', 'asc', 'ascending', 'rand', 'random' ],
+ ];
+
+ $params['headers'] = [
+ 'default' => 'show',
+ 'values' => [ 'show', 'hide', 'plain' ],
+ ];
+
+ $params['mainlabel'] = [
+ 'default' => false,
+ ];
+
+ $params['intro'] = [
+ 'default' => '',
+ ];
+
+ $params['outro'] = [
+ 'default' => '',
+ ];
+
+ $params['searchlabel'] = [
+ 'default' => Message::get( 'smw_iq_moreresults', Message::TEXT, Message::USER_LANGUAGE )
+ ];
+
+ $params['default'] = [
+ 'default' => '',
+ ];
+
+ if ( $context === QueryContext::DEFERRED_QUERY ) {
+ $params['@control'] = [
+ 'default' => '',
+ 'values' => [ 'slider' ],
+ ];
+ }
+
+ if ( !( $resultPrinter instanceof ResultPrinter ) || $resultPrinter->supportsRecursiveAnnotation() ) {
+ $params['import-annotation'] = [
+ 'message' => 'smw-paramdesc-import-annotation',
+ 'type' => 'boolean',
+ 'default' => false
+ ];
+ }
+
+ foreach ( $params as $name => &$param ) {
+ if ( is_array( $param ) ) {
+ $param['message'] = 'smw-paramdesc-' . $name;
+ }
+ }
+
+ return ParamDefinition::getCleanDefinitions( $params );
+ }
+
+ private static function getSourceParam( $vars ) {
+ $sourceValues = is_array( $vars['smwgQuerySources'] ) ? array_keys( $vars['smwgQuerySources'] ) : [];
+
+ return [
+ 'default' => array_key_exists( 'default', $sourceValues ) ? 'default' : '',
+ 'values' => $sourceValues,
+ ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/ParamListProcessor.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/ParamListProcessor.php
new file mode 100644
index 00000000..d7c0b22d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/ParamListProcessor.php
@@ -0,0 +1,263 @@
+<?php
+
+namespace SMW\Query\Processor;
+
+use SMW\Query\PrintRequestFactory;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ParamListProcessor {
+
+ /**
+ * Format type
+ */
+ const FORMAT_LEGACY = 'format.legacy';
+
+ /**
+ * Identify the PrintThis instance
+ */
+ const PRINT_THIS = 'print.this';
+
+ /**
+ * @var PrintRequestFactory
+ */
+ private $printRequestFactory;
+
+ /**
+ * @since 3.0
+ *
+ * @param PrintRequestFactory|null $printRequestFactory
+ */
+ public function __construct( PrintRequestFactory $printRequestFactory = null ) {
+ $this->printRequestFactory = $printRequestFactory;
+
+ if ( $this->printRequestFactory === null ) {
+ $this->printRequestFactory = new PrintRequestFactory();
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $paramList
+ * @param string $type
+ *
+ * @return array
+ */
+ public function format( array $paramList, $type ) {
+
+ if ( $type === self::FORMAT_LEGACY ) {
+ return $this->legacy_format( $paramList );
+ }
+
+ return $paramList;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ * @param boolean $showMode
+ *
+ * @return array
+ */
+ public function preprocess( array $parameters, $showMode = false ) {
+
+ $previousPrintout = null;
+
+ $serialization = [
+ 'showMode' => $showMode,
+ 'templateArgs' => false,
+ 'query' => '',
+ 'this' => [],
+ 'printouts' => [],
+ 'parameters' => []
+ ];
+
+ foreach ( $parameters as $name => $param ) {
+
+ // special handling for arrays - this can happen if the
+ // parameter came from a checkboxes input in Special:Ask:
+ if ( is_array( $param ) ) {
+ $param = implode( ',', array_keys( $param ) );
+ }
+
+ $param = $this->encodeEq( $param );
+
+ // #1258 (named_args -> named args)
+ // accept 'name' => 'value' just as '' => 'name=value':
+ if ( is_string( $name ) && ( $name !== '' ) ) {
+ $param = str_replace( "_", " ", $name ) . '=' . $param;
+ }
+
+ // Find out whether this is a mainlabel, and if so store related
+ // parameters separate since QueryProcessor::addThisPrintout is
+ // added in isolation !!??!!
+ // $isMainlabel = strpos( $param, 'mainlabel=' ) !== false;
+
+ // mainlable=Foo |+with=200 ... is currently not support
+ // use
+ // |?=Foo |+width=200 ...
+ // |mainlabel=-
+ $isMainlabel = false;
+
+ if ( $param === '' ) {
+ } elseif ( $isMainlabel ) {
+ $this->addThisPrintRequest( $name, $param, $previousPrintout, $serialization );
+ } elseif ( $param[0] == '?' ) {
+ $this->addPrintRequest( $name, $param, $previousPrintout, $serialization );
+ } elseif ( $param[0] == '+' ) {
+ $this->addPrintRequestParameter( $name, $param, $previousPrintout, $serialization );
+ } else {
+ $this->addOtherParameters( $name, $param, $serialization, $showMode );
+ }
+ }
+
+ $serialization['query'] = str_replace(
+ [ '&lt;', '&gt;', '0x003D' ],
+ ['<', '>', '=' ],
+ $serialization['query']
+ );
+
+ if ( $showMode ) {
+ $serialization['query'] = '[[:' . $serialization['query'] . ']]';
+ }
+
+ return $serialization;
+ }
+
+ private function legacy_format( array $paramList ) {
+
+ $printouts = [];
+
+ foreach ( $paramList['printouts'] as $k => $request ) {
+
+ if ( !isset( $request['label'] ) ) {
+ continue;
+ }
+
+ // #502
+ // In case of template arguments suppress the showMode to allow for
+ // labels to be generated and to be transfered to the invoked template
+ // otherwise labels will be empty and not be accessible in a template
+ $showMode = $paramList['templateArgs'] ? false : $paramList['showMode'];
+
+ $printRequest = $this->printRequestFactory->newFromText(
+ $request['label'],
+ $showMode
+ );
+
+ if ( $printRequest === null ) {
+ continue;
+ }
+
+ foreach ( $request['params'] as $key => $value ) {
+ $printRequest->setParameter( $key, $value );
+ }
+
+ $printouts[] = $printRequest;
+ }
+
+ return [
+ $paramList['query'],
+ $paramList['parameters'],
+ $printouts
+ ];
+ }
+
+ private function encodeEq ( $param ) {
+ // Bug 32955 / #640
+ // Modify (e.g. replace `=`) a condition string only if enclosed by
+ // [[ ... ]]
+ //
+ // #3560
+ // Instead of `-3D` as temporary replacement, use the UTF representation
+ // to decode the `=` sign and eliminate possible collisions with a search
+ // request that contains `-3D` string
+ return preg_replace_callback(
+ '/\[\[([^\[\]]*)\]\]/xu',
+ function( array $matches ) {
+ return str_replace( [ '=' ], [ '0x003D' ], $matches[0] );
+ },
+ $param
+ );
+ }
+
+ private function addPrintRequest( $name, $param, &$previousPrintout, array &$serialization ) {
+
+ $param = substr( $param, 1 );
+
+ // Currently we don't filter any duplicates hence the additional
+ // $name is added to distinguish printouts with the same configuration
+ $hash = md5( json_encode( $param ) . $name );
+ $previousPrintout = $hash;
+
+ $serialization['printouts'][$hash] = [
+ 'label' => $param,
+ 'params' => []
+ ];
+ }
+
+ private function addThisPrintRequest( $name, $param, &$previousPrintout, array &$serialization ) {
+
+ $param = substr( $param, 1 );
+
+ $parts = explode( '=', $param, 2 );
+ $serialization['parameters']['mainlabel'] = count( $parts ) >= 2 ? $parts[1] : null;
+ $previousPrintout = self::PRINT_THIS;
+ }
+
+ private function addPrintRequestParameter( $name, $param, $previousPrintout, array &$serialization ) {
+
+ if ( $previousPrintout === null ) {
+ return;
+ }
+
+ $param = substr( $param, 1 );
+ $parts = explode( '=', $param, 2 );
+
+ if ( $previousPrintout === self::PRINT_THIS ) {
+ if ( count( $parts ) == 2 ) {
+ $serialization['this'] = [ trim( $parts[0] ) => $parts[1] ];
+ } else {
+ $serialization['this'] = [ trim( $parts[0] ) => null ];
+ }
+ } else {
+ if ( count( $parts ) == 2 ) {
+ $serialization['printouts'][$previousPrintout]['params'][trim( $parts[0] )] = $parts[1];
+ } else {
+ $serialization['printouts'][$previousPrintout]['params'][trim( $parts[0] )] = null;
+ }
+ }
+ }
+
+ private function addOtherParameters( $name, $param, array &$serialization, $showMode ) {
+
+ // #1645
+ $parts = $showMode && $name == 0 ? $param : explode( '=', $param, 2 );
+
+ if ( is_array( $parts ) && count( $parts ) >= 2 ) {
+ $p = strtolower( trim( $parts[0] ) );
+
+ if ( $p === 'template' ) {
+ $serialization['templateArgs'] = true;
+ }
+
+ // Don't trim here, some parameters care for " "
+ //
+ // #3196
+ // Ensure to decode `0x003D` from encodeEq to support things like
+ // `|intro=[[File:Foo.png|link=Bar]]`
+ $serialization['parameters'][$p] = str_replace( [ '0x003D' ], [ '=' ], $parts[1] );
+ } else {
+ $serialization['query'] .= $param;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/QueryCreator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/QueryCreator.php
new file mode 100644
index 00000000..7d2ac2be
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Processor/QueryCreator.php
@@ -0,0 +1,252 @@
+<?php
+
+namespace SMW\Query\Processor;
+
+use SMW\DataValueFactory;
+use SMW\Localizer;
+use SMW\Query\QueryContext;
+use SMW\QueryFactory;
+use SMWPropertyValue as PropertyValue;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class QueryCreator implements QueryContext {
+
+ /**
+ * @var QueryFactory
+ */
+ private $queryFactory;
+
+ /**
+ * @var array
+ */
+ private $params = [];
+
+ /**
+ * @see smwgQDefaultNamespaces
+ * @var null|array
+ */
+ private $defaultNamespaces = null;
+
+ /**
+ * @see smwgQDefaultLimit
+ * @var integer
+ */
+ private $defaultLimit = 0;
+
+ /**
+ * @see smwgQFeatures
+ * @var integer
+ */
+ private $queryFeatures = 0;
+
+ /**
+ * @see smwgQConceptFeatures
+ * @var integer
+ */
+ private $conceptFeatures = 0;
+
+ /**
+ * @since 2.5
+ *
+ * @param QueryFactory $queryFactory
+ * @param array|null $defaultNamespaces
+ * @param integer $defaultLimit
+ */
+ public function __construct( QueryFactory $queryFactory, $defaultNamespaces = null, $defaultLimit = 50 ) {
+ $this->queryFactory = $queryFactory;
+ $this->defaultNamespaces = $defaultNamespaces;
+ $this->defaultLimit = $defaultLimit;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $queryFeatures
+ */
+ public function setQFeatures( $queryFeatures ) {
+ $this->queryFeatures = $queryFeatures;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $conceptFeatures
+ */
+ public function setQConceptFeatures( $conceptFeatures ) {
+ $this->conceptFeatures = $conceptFeatures;
+ }
+
+ /**
+ * Parse a query string given in SMW's query language to create an Query.
+ * Parameters are given as key-value-pairs in the given array. The parameter
+ * $context defines in what context the query is used, which affects certaim
+ * general settings.
+ *
+ * @since 2.5
+ *
+ * @param string $queryString
+ * @param array $params
+ *
+ * @return Query
+ */
+ public function create( $queryString, array $params = [] ) {
+
+ $this->params = $params;
+ $context = $this->getParam( 'context', self::INLINE_QUERY );
+
+ $queryParser = $this->queryFactory->newQueryParser(
+ $context == self::CONCEPT_DESC ? $this->conceptFeatures : $this->queryFeatures
+ );
+
+ $contextPage = $this->getParam( 'contextPage', null );
+ $queryMode = $this->getParam( 'queryMode', self::MODE_INSTANCES );
+
+ $queryParser->setContextPage( $contextPage );
+ $queryParser->setDefaultNamespaces( $this->defaultNamespaces );
+
+ $query = $this->queryFactory->newQuery(
+ $queryParser->getQueryDescription( $queryString ),
+ $context
+ );
+
+ $query->setQueryToken( $queryParser->getQueryToken() );
+ $query->setQueryString( $queryString );
+ $query->setContextPage( $contextPage );
+ $query->setQueryMode( $queryMode );
+
+ $query->setExtraPrintouts(
+ $this->getParam( 'extraPrintouts', [] )
+ );
+
+ $query->setMainLabel(
+ $this->getParam( 'mainLabel', '' )
+ );
+
+ $query->setQuerySource(
+ $this->getParam( 'source', null )
+ );
+
+ $query->setOption(
+ 'self.reference',
+ $queryParser->containsSelfReference()
+ );
+
+ // keep parsing or other errors for later output
+ $query->addErrors(
+ $queryParser->getErrors()
+ );
+
+ // set sortkeys, limit, and offset
+ $query->setOffset(
+ max( 0, trim( $this->getParam( 'offset', 0 ) ) + 0 )
+ );
+
+ $query->setLimit(
+ max( 0, trim( $this->getParam( 'limit', $this->defaultLimit ) ) + 0 ),
+ $queryMode != self::MODE_COUNT
+ );
+
+ $sortKeys = $this->getSortKeys(
+ $this->getParam( 'sort', [] ),
+ $this->getParam( 'order', [] ),
+ $this->getParam( 'defaultSort', 'ASC' )
+ );
+
+ $query->addErrors(
+ $sortKeys['errors']
+ );
+
+ $query->setSortKeys(
+ $sortKeys['keys']
+ );
+
+ return $query;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $sortParameters
+ * @param array $orderParameters
+ * @param string $defaultSort
+ *
+ * @return array ( keys => array(), errors => array() )
+ */
+ private function getSortKeys( array $sortParameters, array $orderParameters, $defaultSort ) {
+
+ $sortKeys = [];
+ $sortErros = [];
+
+ $orders = $this->normalize_order( $orderParameters );
+
+ foreach ( $sortParameters as $sort ) {
+ $sortKey = false;
+
+ // An empty string indicates we mean the page, such as element 0 on the next line.
+ // sort=,Some property
+ if ( trim( $sort ) === '' ) {
+ $sortKey = '';
+ } else {
+
+ $propertyValue = DataValueFactory::getInstance()->newDataValueByType( PropertyValue::TYPE_ID );
+ $propertyValue->setOption( PropertyValue::OPT_QUERY_CONTEXT, true );
+
+ $propertyValue->setUserValue(
+ $this->normalize_sort( trim( $sort ) )
+ );
+
+ if ( $propertyValue->isValid() ) {
+ $sortKey = $propertyValue->getDataItem()->getKey();
+ } else {
+ $sortErros = array_merge( $sortErros, $propertyValue->getErrors() );
+ }
+ }
+
+ if ( $sortKey !== false ) {
+ $order = empty( $orders ) ? $defaultSort : array_shift( $orders );
+ $sortKeys[$sortKey] = $order;
+ }
+ }
+
+ // If more sort arguments are provided then properties, assume the first one is for the page.
+ // TODO: we might want to add errors if there is more then one.
+ if ( !array_key_exists( '', $sortKeys ) && !empty( $orders ) ) {
+ $sortKeys[''] = array_shift( $orders );
+ }
+
+ return [ 'keys' => $sortKeys, 'errors' => $sortErros ];
+ }
+
+ private function normalize_order( $orderParameters ) {
+ $orders = [];
+
+ foreach ( $orderParameters as $key => $order ) {
+ $order = strtolower( trim( $order ) );
+ if ( ( $order == 'descending' ) || ( $order == 'reverse' ) || ( $order == 'desc' ) ) {
+ $orders[$key] = 'DESC';
+ } elseif ( ( $order == 'random' ) || ( $order == 'rand' ) ) {
+ $orders[$key] = 'RANDOM';
+ } else {
+ $orders[$key] = 'ASC';
+ }
+ }
+
+ return $orders;
+ }
+
+ private function normalize_sort( $sort ) {
+ return Localizer::getInstance()->getNamespaceTextById( NS_CATEGORY ) == mb_convert_case( $sort, MB_CASE_TITLE ) ? '_INST' : $sort;
+ }
+
+ private function getParam( $key, $default ) {
+ return isset( $this->params[$key] ) ? $this->params[$key] : $default;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotator.php
new file mode 100644
index 00000000..6390af5d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotator.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\PropertyAnnotator;
+
+/**
+ * Specifying the ProfileAnnotator interface
+ *
+ * @ingroup SMW
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+interface ProfileAnnotator extends PropertyAnnotator {
+
+ /**
+ * Returns the query meta data property
+ *
+ * @since 1.9
+ *
+ * @return DIProperty
+ */
+ public function getProperty();
+
+ /**
+ * Returns the query meta data container
+ *
+ * @since 1.9
+ *
+ * @return DIContainer
+ */
+ public function getContainer();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotatorFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotatorFactory.php
new file mode 100644
index 00000000..52f1e618
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotatorFactory.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\DIWikiPage;
+use SMW\Query\ProfileAnnotators\DescriptionProfileAnnotator;
+use SMW\Query\ProfileAnnotators\DurationProfileAnnotator;
+use SMW\Query\ProfileAnnotators\FormatProfileAnnotator;
+use SMW\Query\ProfileAnnotators\NullProfileAnnotator;
+use SMW\Query\ProfileAnnotators\ParametersProfileAnnotator;
+use SMW\Query\ProfileAnnotators\SourceProfileAnnotator;
+use SMW\Query\ProfileAnnotators\StatusCodeProfileAnnotator;
+use SMW\Query\ProfileAnnotators\SchemaLinkProfileAnnotator;
+use SMWContainerSemanticData as ContainerSemanticData;
+use SMWDIContainer as DIContainer;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class ProfileAnnotatorFactory {
+
+ /**
+ * @since 2.1
+ *
+ * @param Query $query
+ * @param string $format
+ *
+ * @return ProfileAnnotator
+ */
+ public function newProfileAnnotator( Query $query, $format ) {
+
+ $profileAnnotator = $this->newDescriptionProfileAnnotator(
+ $query
+ );
+
+ $profileAnnotator = $this->newFormatProfileAnnotator(
+ $profileAnnotator,
+ $format
+ );
+
+ $profileAnnotator = $this->newParametersProfileAnnotator(
+ $profileAnnotator,
+ $query
+ );
+
+ $profileAnnotator = $this->newDurationProfileAnnotator(
+ $profileAnnotator,
+ $query->getOption( Query::PROC_QUERY_TIME )
+ );
+
+ $profileAnnotator = $this->newSourceProfileAnnotator(
+ $profileAnnotator,
+ $query->getQuerySource()
+ );
+
+ $profileAnnotator = $this->newStatusCodeProfileAnnotator(
+ $profileAnnotator,
+ $query->getOption( Query::PROC_STATUS_CODE )
+ );
+
+ $profileAnnotator = $this->newSchemaLinkProfileAnnotator(
+ $profileAnnotator,
+ $query->getOption( 'schema_link' )
+ );
+
+ return $profileAnnotator;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Query $query
+ *
+ * @return DescriptionProfileAnnotator
+ */
+ public function newDescriptionProfileAnnotator( Query $query ) {
+
+ $profileAnnotator = new NullProfileAnnotator(
+ $this->newDIContainer( $query )
+ );
+
+ $profileAnnotator = new DescriptionProfileAnnotator(
+ $profileAnnotator,
+ $query->getDescription()
+ );
+
+ return $profileAnnotator;
+ }
+
+ private function newFormatProfileAnnotator( $profileAnnotator, $format ) {
+ return new FormatProfileAnnotator( $profileAnnotator, $format );
+ }
+
+ private function newParametersProfileAnnotator( $profileAnnotator, $query ) {
+
+ if ( $query->getOption( Query::OPT_PARAMETERS ) === false ) {
+ return $profileAnnotator;
+ }
+
+ return new ParametersProfileAnnotator( $profileAnnotator, $query );
+ }
+
+ private function newDurationProfileAnnotator( $profileAnnotator, $duration ) {
+
+ if ( $duration == 0 ) {
+ return $profileAnnotator;
+ }
+
+ return new DurationProfileAnnotator( $profileAnnotator, $duration );
+ }
+
+ private function newSourceProfileAnnotator( $profileAnnotator, $querySource ) {
+
+ if ( $querySource === '' || $querySource === null ) {
+ return $profileAnnotator;
+ }
+
+ return new SourceProfileAnnotator( $profileAnnotator, $querySource );
+ }
+
+ private function newStatusCodeProfileAnnotator( $profileAnnotator, $statusCodes ) {
+
+ if ( $statusCodes === false || $statusCodes === null || $statusCodes === [] ) {
+ return $profileAnnotator;
+ }
+
+ return new StatusCodeProfileAnnotator( $profileAnnotator, $statusCodes );
+ }
+
+ private function newSchemaLinkProfileAnnotator( $profileAnnotator, $schemaLink ) {
+
+ if ( $schemaLink === false || $schemaLink === null ) {
+ return $profileAnnotator;
+ }
+
+ return new SchemaLinkProfileAnnotator( $profileAnnotator, $schemaLink );
+ }
+
+ /**
+ * #1416 create container manually to avoid any issues that may arise from
+ * a failed Title::makeTitleSafe.
+ */
+ private function newDIContainer( Query $query ) {
+
+ $subject = $query->getContextPage();
+
+ if ( $subject === null ) {
+ $containerSemanticData = ContainerSemanticData::makeAnonymousContainer();
+ } else {
+ $subject = new DIWikiPage(
+ $subject->getDBkey(),
+ $subject->getNamespace(),
+ $subject->getInterwiki(),
+ $query->getQueryId()
+ );
+
+ $containerSemanticData = new ContainerSemanticData( $subject );
+ }
+
+ return new DIContainer(
+ $containerSemanticData
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/DescriptionProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/DescriptionProfileAnnotator.php
new file mode 100644
index 00000000..6823eeec
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/DescriptionProfileAnnotator.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\DIProperty;
+use SMW\Query\Language\Description;
+use SMW\Query\ProfileAnnotator;
+use SMWDIBlob as DIBlob;
+use SMWDINumber as DINumber;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class DescriptionProfileAnnotator extends ProfileAnnotatorDecorator {
+
+ /**
+ * @var Description
+ */
+ private $description;
+
+ /**
+ * @since 1.9
+ *
+ * @param ProfileAnnotator $profileAnnotator
+ * @param Description $description
+ */
+ public function __construct( ProfileAnnotator $profileAnnotator, Description $description ) {
+ parent::__construct( $profileAnnotator );
+ $this->description = $description;
+ }
+
+ /**
+ * ProfileAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+ $this->addQueryString( $this->description->getQueryString() );
+ $this->addQuerySize( $this->description->getSize() );
+ $this->addQueryDepth( $this->description->getDepth() );
+ }
+
+ private function addQueryString( $queryString ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_ASKST' ),
+ new DIBlob( $queryString )
+ );
+ }
+
+ private function addQuerySize( $size ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_ASKSI' ),
+ new DINumber( $size )
+ );
+ }
+
+ private function addQueryDepth( $depth ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_ASKDE' ),
+ new DINumber( $depth )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/DurationProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/DurationProfileAnnotator.php
new file mode 100644
index 00000000..cf237c23
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/DurationProfileAnnotator.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\DIProperty;
+use SMW\Query\ProfileAnnotator;
+use SMWDINumber as DINumber;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class DurationProfileAnnotator extends ProfileAnnotatorDecorator {
+
+ /**
+ * @var integer
+ */
+ private $duration;
+
+ /**
+ * @since 1.9
+ *
+ * @param ProfileAnnotator $profileAnnotator
+ * @param integer $duration
+ */
+ public function __construct( ProfileAnnotator $profileAnnotator, $duration ) {
+ parent::__construct( $profileAnnotator );
+ $this->duration = $duration;
+ }
+
+ /**
+ * ProfileAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+ if ( $this->duration > 0 ) {
+ $this->addGreaterThanZeroQueryDuration( $this->duration );
+ }
+ }
+
+ private function addGreaterThanZeroQueryDuration( $duration ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_ASKDU' ),
+ new DINumber( $duration )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/FormatProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/FormatProfileAnnotator.php
new file mode 100644
index 00000000..8ba1ced4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/FormatProfileAnnotator.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\DIProperty;
+use SMW\Query\ProfileAnnotator;
+use SMWDIBlob as DIBlob;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class FormatProfileAnnotator extends ProfileAnnotatorDecorator {
+
+ /**
+ * @var string
+ */
+ private $format;
+
+ /**
+ * @since 1.9
+ *
+ * @param ProfileAnnotator $profileAnnotator
+ * @param string $format
+ */
+ public function __construct( ProfileAnnotator $profileAnnotator, $format ) {
+ parent::__construct( $profileAnnotator );
+ $this->format = $format;
+ }
+
+ /**
+ * ProfileAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+ $this->addQueryFormat( $this->format );
+ }
+
+ private function addQueryFormat( $format ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_ASKFO' ),
+ new DIBlob( $format )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/NullProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/NullProfileAnnotator.php
new file mode 100644
index 00000000..bd5b686a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/NullProfileAnnotator.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\DIProperty;
+use SMW\Query\ProfileAnnotator;
+use SMWDIContainer as DIContainer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class NullProfileAnnotator implements ProfileAnnotator {
+
+ /**
+ * @var DIContainer
+ */
+ private $container;
+
+ /**
+ * @since 1.9
+ *
+ * @param DIContainer $container
+ */
+ public function __construct( DIContainer $container ) {
+ $this->container = $container;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->getSemanticData()->getErrors();
+ }
+
+ /**
+ * ProfileAnnotator::getProperty
+ *
+ * @since 1.9
+ *
+ * @return array
+ */
+ public function getProperty() {
+ return new DIProperty( '_ASK' );
+ }
+
+ /**
+ * ProfileAnnotator::getContainer
+ *
+ * @since 1.9
+ *
+ * @return DIContainer
+ */
+ public function getContainer() {
+ return $this->container;
+ }
+
+ /**
+ * ProfileAnnotator::getSemanticData
+ *
+ * @since 1.9
+ *
+ * @return SemanticData
+ */
+ public function getSemanticData() {
+ return $this->container->getSemanticData();
+ }
+
+ /**
+ * ProfileAnnotator::addAnnotation
+ *
+ * @since 1.9
+ */
+ public function addAnnotation() {
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/ParametersProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/ParametersProfileAnnotator.php
new file mode 100644
index 00000000..0f59128e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/ParametersProfileAnnotator.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\DIProperty;
+use SMW\Query\ProfileAnnotator;
+use SMWDIBlob as DIBlob;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ParametersProfileAnnotator extends ProfileAnnotatorDecorator {
+
+ /**
+ * @var Query
+ */
+ private $query;
+
+ /**
+ * @since 2.5
+ *
+ * @param ProfileAnnotator $profileAnnotator
+ * @param Query $query
+ */
+ public function __construct( ProfileAnnotator $profileAnnotator, Query $query ) {
+ parent::__construct( $profileAnnotator );
+ $this->query = $query;
+ }
+
+ /**
+ * ProfileAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+
+ list( $sort, $order ) = $this->doSerializeSortKeys( $this->query );
+
+ $options = [
+ 'limit' => $this->query->getLimit(),
+ 'offset' => $this->query->getOffset(),
+ 'sort' => $sort,
+ 'order' => $order,
+ 'mode' => $this->query->getQueryMode()
+ ];
+
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_ASKPA' ),
+ new DIBlob( json_encode( $options ) )
+ );
+ }
+
+ private function doSerializeSortKeys( $query ) {
+
+ $sort = [];
+ $order = [];
+
+ if ( $query->getSortKeys() === null ) {
+ return [ $sort, $order ];
+ }
+
+ foreach ( $query->getSortKeys() as $key => $value ) {
+ $sort[] = $key;
+ $order[] = strtolower( $value );
+ }
+
+ return [ $sort, $order ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/ProfileAnnotatorDecorator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/ProfileAnnotatorDecorator.php
new file mode 100644
index 00000000..ef607550
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/ProfileAnnotatorDecorator.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\Query\ProfileAnnotator;
+use SMW\SemanticData;
+
+/**
+ * Decorator implementing the ProfileAnnotator interface
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+abstract class ProfileAnnotatorDecorator implements ProfileAnnotator {
+
+ /**
+ * @var ProfileAnnotator
+ */
+ protected $profileAnnotator;
+
+ /**
+ * @since 1.9
+ *
+ * @param ProfileAnnotator $profileAnnotator
+ */
+ public function __construct( ProfileAnnotator $profileAnnotator ) {
+ $this->profileAnnotator = $profileAnnotator;
+ }
+
+ /**
+ * ProfileAnnotator::getProperty
+ *
+ * @since 1.9
+ *
+ * @return DIProperty
+ */
+ public function getProperty() {
+ return $this->profileAnnotator->getProperty();
+ }
+
+ /**
+ * ProfileAnnotator::getContainer
+ *
+ * @since 1.9
+ *
+ * @return DIContainer
+ */
+ public function getContainer() {
+ return $this->profileAnnotator->getContainer();
+ }
+
+ /**
+ * @see ProfileAnnotator::getSemanticData
+ *
+ * @since 1.9
+ *
+ * @return SemanticData
+ */
+ public function getSemanticData() {
+ return $this->profileAnnotator->getSemanticData();
+ }
+
+ /**
+ * ProfileAnnotator::addAnnotation
+ *
+ * @since 1.9
+ *
+ * @return ProfileAnnotator
+ */
+ public function addAnnotation() {
+ $this->profileAnnotator->addAnnotation();
+ $this->addPropertyValues();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param SemanticData $semanticData
+ */
+ public function pushAnnotationsTo( SemanticData $semanticData ) {
+
+ $this->addAnnotation();
+
+ $semanticData->addPropertyObjectValue(
+ $this->profileAnnotator->getProperty(),
+ $this->profileAnnotator->getContainer()
+ );
+ }
+
+ /**
+ * @since 1.9
+ */
+ protected abstract function addPropertyValues();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/SchemaLinkProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/SchemaLinkProfileAnnotator.php
new file mode 100644
index 00000000..111d79a0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/SchemaLinkProfileAnnotator.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Query\ProfileAnnotator;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SchemaLinkProfileAnnotator extends ProfileAnnotatorDecorator {
+
+ /**
+ * @var string
+ */
+ private $schemaLink = '';
+
+ /**
+ * @since 3.0
+ *
+ * @param ProfileAnnotator $profileAnnotator
+ * @param string $SchemaLink
+ */
+ public function __construct( ProfileAnnotator $profileAnnotator, $schemaLink ) {
+ parent::__construct( $profileAnnotator );
+ $this->schemaLink = $schemaLink;
+ }
+
+ /**
+ * ProfileAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+
+ if ( $this->schemaLink === '' ) {
+ return;
+ }
+
+ if ( !is_string( $this->schemaLink ) ) {
+ throw new RuntimeException( "Expected a string as `Schema link` value!" );
+ }
+
+ $this->addSchemaLinkAnnotation( $this->schemaLink );
+ }
+
+ private function addSchemaLinkAnnotation( $schemaLink ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_SCHEMA_LINK' ),
+ new DIWikiPage( $schemaLink, SMW_NS_SCHEMA )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/SourceProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/SourceProfileAnnotator.php
new file mode 100644
index 00000000..31837010
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/SourceProfileAnnotator.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\DIProperty;
+use SMW\Query\ProfileAnnotator;
+use SMWDIBlob as DIBlob;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SourceProfileAnnotator extends ProfileAnnotatorDecorator {
+
+ /**
+ * @var string
+ */
+ private $querySource;
+
+ /**
+ * @since 2.5
+ *
+ * @param ProfileAnnotator $profileAnnotator
+ * @param string $querySource
+ */
+ public function __construct( ProfileAnnotator $profileAnnotator, $querySource = '' ) {
+ parent::__construct( $profileAnnotator );
+ $this->querySource = $querySource;
+ }
+
+ /**
+ * ProfileAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+ if ( $this->querySource !== '' ) {
+ $this->addQuerySource( $this->querySource );
+ }
+ }
+
+ private function addQuerySource( $querySource ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_ASKSC' ),
+ new DIBlob( $querySource )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/StatusCodeProfileAnnotator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/StatusCodeProfileAnnotator.php
new file mode 100644
index 00000000..8afc5ce4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ProfileAnnotators/StatusCodeProfileAnnotator.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace SMW\Query\ProfileAnnotators;
+
+use SMW\DIProperty;
+use SMW\Query\ProfileAnnotator;
+use SMWDINumber as DINumber;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class StatusCodeProfileAnnotator extends ProfileAnnotatorDecorator {
+
+ /**
+ * @var array
+ */
+ private $statusCodes = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param ProfileAnnotator $profileAnnotator
+ * @param array $statusCodes
+ */
+ public function __construct( ProfileAnnotator $profileAnnotator, array $statusCodes = [] ) {
+ parent::__construct( $profileAnnotator );
+ $this->statusCodes = $statusCodes;
+ }
+
+ /**
+ * ProfileAnnotatorDecorator::addPropertyValues
+ */
+ protected function addPropertyValues() {
+ if ( $this->statusCodes !== [] ) {
+ foreach ( $this->statusCodes as $statusCode ) {
+ $this->addStatusCodeAnnotation( $statusCode );
+ }
+ }
+ }
+
+ private function addStatusCodeAnnotation( $statusCode ) {
+ $this->getSemanticData()->addPropertyObjectValue(
+ new DIProperty( '_ASKCO' ),
+ new DINumber( $statusCode )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryComparator.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryComparator.php
new file mode 100644
index 00000000..f2fb168f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryComparator.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace SMW\Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.5.3
+ *
+ * @author mwjames
+ * @author Jeroen De Dauw
+ */
+class QueryComparator {
+
+ /**
+ * @var QueryComparator
+ */
+ private static $instance = null;
+
+ /**
+ * @var array
+ */
+ private $comparators = null;
+
+ /**
+ * @var array
+ */
+ private $reverseCache = [];
+
+ /**
+ * @since 2.3
+ *
+ * @param string $comparatorList
+ * @param boolean $strictComparators
+ */
+ public function __construct( $comparatorList, $strictComparators ) {
+ $this->comparators = $this->getEnabledComparators( $comparatorList, $strictComparators );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return self
+ */
+ public static function getInstance() {
+
+ if ( self::$instance === null ) {
+ self::$instance = new self(
+ $GLOBALS['smwgQComparators'],
+ $GLOBALS['smwStrictComparators']
+ );
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * @since 2.3
+ */
+ public static function clear() {
+ self::$instance = null;
+ }
+
+ /**
+ * Gets an array with all supported comparator strings.
+ * The string for SMW_CMP_EQ, which is an empty string, is not in this list.
+ *
+ * @since 1.5.3
+ *
+ * @return array
+ */
+ public function getComparatorStrings() {
+ return array_keys( $this->comparators );
+ }
+
+ /**
+ * Gets the SMW_CMP_ for a string comparator, falling back to the
+ * $defaultComparator when none is found.
+ *
+ * @since 1.5.3
+ *
+ * @param string $string
+ * @param integer $defaultComparator Item of the SMW_CMP_ enum
+ *
+ * @return integer Item of the SMW_CMP_ enum
+ */
+ public function getComparatorFromString( $string, $defaultComparator = SMW_CMP_EQ ) {
+
+ if ( $string === '' ) {
+ return SMW_CMP_EQ;
+ }
+
+ return array_key_exists( $string, $this->comparators ) ? $this->comparators[$string] : $defaultComparator;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $value
+ * @param integer $comparator
+ *
+ * @return boolean
+ */
+ public function containsComparator( $value, $comparator = SMW_CMP_EQ ) {
+ return $this->extractComparatorFromString( $value ) === $comparator;
+ }
+
+ /**
+ * Extract possible comparators from a value and alter it to consist
+ * only of the remaining effective value string (without the comparator).
+ *
+ * @since 2.4
+ *
+ * @param $value
+ *
+ * @return integer
+ */
+ public function extractComparatorFromString( &$value ) {
+
+ $comparator = SMW_CMP_EQ;
+
+ foreach ( $this->getComparatorStrings() as $string ) {
+ if ( strpos( $value, $string ) === 0 ) {
+ $comparator = $this->getComparatorFromString( substr( $value, 0, strlen( $string ) ) );
+ $value = substr( $value, strlen( $string ) );
+ break;
+ }
+ }
+
+ return $comparator;
+ }
+
+ /**
+ * Gets the comparator string for a comparator.
+ *
+ * @since 1.5.3
+ *
+ * @param $comparator
+ *
+ * @return string
+ */
+ public function getStringForComparator( $comparator ) {
+
+ if ( $this->reverseCache === [] ) {
+ $this->reverseCache = array_flip( $this->comparators );
+ }
+
+ if ( $comparator == SMW_CMP_EQ ) {
+ return '';
+ } elseif ( array_key_exists( $comparator, $this->reverseCache ) ) {
+ return $this->reverseCache[$comparator];
+ }
+
+ throw new Exception( "Comparator $comparator does not have a string representatation" );
+ }
+
+ private function getEnabledComparators( $comparatorList, $strictComparators ) {
+
+ // Note: Comparators that contain other comparators at the beginning of
+ // the string need to be at beginning of the array.
+ $comparators = [
+ 'like:' => SMW_CMP_PRIM_LIKE,
+ 'nlike:' => SMW_CMP_PRIM_NLKE,
+ 'in:' => SMW_CMP_IN,
+ 'not:' => SMW_CMP_NOT,
+ 'phrase:' => SMW_CMP_PHRASE,
+ '!~' => SMW_CMP_NLKE,
+ '<<' => SMW_CMP_LESS,
+ '>>' => SMW_CMP_GRTR,
+ '<' => $strictComparators ? SMW_CMP_LESS : SMW_CMP_LEQ,
+ '>' => $strictComparators ? SMW_CMP_GRTR : SMW_CMP_GEQ,
+ '≤' => SMW_CMP_LEQ,
+ '≥' => SMW_CMP_GEQ,
+ '!' => SMW_CMP_NEQ,
+ '~' => SMW_CMP_LIKE,
+ ];
+
+ if ( strpos( $comparatorList, '|' ) === false ) {
+ return $comparators;
+ }
+
+ $allowedComparators = explode( '|', $comparatorList );
+
+ // Remove the comparators that are not allowed.
+ foreach ( $comparators as $string => $comparator ) {
+ if ( !in_array( $string, $allowedComparators ) ) {
+ unset( $comparators[$string] );
+ }
+ }
+
+ return $comparators;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryContext.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryContext.php
new file mode 100644
index 00000000..936c15ed
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryContext.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace SMW\Query;
+
+/**
+ * "Query contexts" define restrictions during query parsing and
+ * are used to preconfigure query (e.g. special pages show no further
+ * results link)
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author Markus Krötzsch
+ */
+interface QueryContext {
+
+ /**
+ * Query for special page
+ */
+ const SPECIAL_PAGE = 1000;
+
+ /**
+ * Query for inline use
+ */
+ const INLINE_QUERY = 1001;
+
+ /**
+ * Deferred query definition
+ */
+ const DEFERRED_QUERY = 1002;
+
+ /**
+ * Query for concept definition
+ */
+ const CONCEPT_DESC = 1003;
+
+ /**
+ * normal instance retrieval
+ */
+ const MODE_INSTANCES = 1;
+
+ /**
+ * find result count only
+ */
+ const MODE_COUNT = 2;
+
+ /**
+ * prepare query, but show debug data instead of executing it
+ */
+ const MODE_DEBUG = 3;
+
+ /**
+ * do nothing with the query
+ */
+ const MODE_NONE = 4;
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryLinker.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryLinker.php
new file mode 100644
index 00000000..e7bb42d5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryLinker.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\Message;
+use SMW\DIProperty;
+use SMWInfolink as Infolink;
+use SMWQuery as Query;
+
+/**
+ * Representing a Special:Ask query link to further query results
+ *
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class QueryLinker {
+
+ /**
+ * @since 2.4
+ *
+ * @param Query $query
+ * @param array $parameters
+ *
+ * @return Infolink
+ */
+ public static function get( Query $query, array $parameters = [] ) {
+
+ $link = Infolink::newInternalLink( '', ':Special:Ask', false, [] );
+ $link->setCompactLink( $GLOBALS['smwgCompactLinkSupport'] );
+
+ foreach ( $parameters as $key => $value ) {
+
+ if ( !is_string( $key ) ) {
+ continue;
+ }
+
+ $link->setParameter( $value, $key );
+ }
+
+ $params = self::getParameters( $query );
+
+ foreach ( $params as $key => $param ) {
+ $link->setParameter( $param, is_string( $key ) ? $key : false );
+ }
+
+ $link->setCaption(
+ ' ' . Message::get( 'smw_iq_moreresults', Message::TEXT, Message::USER_LANGUAGE )
+ );
+
+ return $link;
+ }
+
+ private static function getParameters( $query ) {
+
+ $params = [ trim( $query->getQueryString( true ) ) ];
+
+ foreach ( $query->getExtraPrintouts() as /* PrintRequest */ $printout ) {
+ if ( ( $serialisation = $printout->getSerialisation( true ) ) !== '' ) {
+ $params[] = $serialisation;
+ }
+ }
+
+ if ( $query->getMainLabel() !== false ) {
+ $params['mainlabel'] = $query->getMainLabel();
+ }
+
+ if ( $query->getQuerySource() !== '' ) {
+ $params['source'] = $query->getQuerySource();
+ }
+
+ $params['offset'] = $query->getOffset();
+
+ if ( $params['offset'] === 0 ) {
+ unset( $params['offset'] );
+ }
+
+ if ( $query->getLimit() > 0 ) {
+ $params['limit'] = $query->getLimit();
+ }
+
+ $sortKeys = $query->getSortKeys();
+ $count = count( $sortKeys );
+
+ if ( $count == 0 ) {
+ return $params;
+ }
+
+ $order = [];
+ $sort = [];
+
+ foreach ( $sortKeys as $key => $order_by ) {
+
+ $order_by = strtolower( $order_by );
+
+ // Default mode, skip
+ if ( $count == 1 && $key === '' && $order_by === 'asc' ) {
+ continue;
+ }
+
+ // Avoid predefined properties to appear as key as in _MDAT
+ if ( $key !== '' && $key{0} === '_' ) {
+ $key = DIProperty::newFromUserLabel( $key )->getLabel();
+ } else {
+ $key = str_replace( '_', ' ', $key );
+ }
+
+ $order[] = $order_by;
+ $sort[] = $key;
+ }
+
+ if ( $sort !== [] ) {
+ $params['order'] = implode( ',', $order );
+ $params['sort'] = implode( ',', $sort );
+ }
+
+ return $params;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/QuerySourceFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/QuerySourceFactory.php
new file mode 100644
index 00000000..8c4a4f0c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/QuerySourceFactory.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace SMW\Query;
+
+use RuntimeException;
+use SMW\QueryEngine;
+use SMW\Store;
+use SMW\StoreAware;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class QuerySourceFactory {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var array
+ */
+ private $querySources = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param array $querySources
+ */
+ public function __construct( Store $store, $querySources = [] ) {
+ $this->store = $store;
+ $this->querySources = $querySources;
+
+ // Standard store
+ $this->querySources['sql_store'] = 'SMW\SQLStore\SQLStore';
+ }
+
+ /**
+ * @see DefaultSettings::$smwgQuerySources
+ *
+ * @since 2.5
+ *
+ * @param string|null $source
+ *
+ * @return QueryEngine|Store
+ * @throws RuntimeException
+ */
+ public function get( $source = null ) {
+
+ $params = [];
+
+ if ( $source !== '' && isset( $this->querySources[$source] ) ) {
+
+ $querySource = $this->querySources[$source];
+
+ // [ '\SMW\FooHandler', ... parameters ],
+ if ( is_array( $querySource ) ) {
+ $source = array_shift( $querySource );
+ $params = $querySource;
+ } else {
+ $source = $this->querySources[$source];
+ }
+ }
+
+ // Fallback to the default store
+ if ( $source === null || !class_exists( $source ) ) {
+ $source = $this->store;
+ } elseif ( $params !== [] ) {
+ $source = new $source( $params );
+ } else {
+ $source = new $source;
+ }
+
+ if ( !$source instanceof QueryEngine && !$source instanceof Store ) {
+ throw new RuntimeException( get_class( $source ) . " does not match the expected QueryEngine interface." );
+ }
+
+ if ( $source instanceof StoreAware ) {
+ $source->setStore( $this->store );
+ }
+
+ return $source;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|null $source
+ *
+ * @return string
+ */
+ public function toString( $source = null ) {
+
+ if ( $source === 'sql_store' ) {
+ return 'SMWSQLStore';
+ }
+
+ if ( $source !== '' && $source !== null ) {
+ return $source;
+ }
+
+ return json_encode( $this->store->getInfo( 'store' ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryStringifier.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryStringifier.php
new file mode 100644
index 00000000..9f1c1ee1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryStringifier.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace SMW\Query;
+
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class QueryStringifier {
+
+ /**
+ * @since 2.5
+ *
+ * @param Query $query
+ *
+ * @return string
+ */
+ public static function rawUrlEncode( Query $query ) {
+ return rawurlencode( self::toString( $query ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ * @param boolean $printParameters
+ *
+ * @return string
+ */
+ public static function toArray( Query $query, $printParameters = false ) {
+
+ $serialized = [];
+ $serialized['conditions'] = $query->getQueryString();
+
+ $serialized['parameters'] = [
+ 'limit' => $query->getLimit(),
+ 'offset' => $query->getOffset(),
+ 'mainlabel' => $query->getMainlabel()
+ ];
+
+ if ( $query->getQuerySource() !== null && $query->getQuerySource() !== '' ) {
+ $serialized['parameters']['source'] = $query->getQuerySource();
+ }
+
+ list( $serialized['sort'], $serialized['order'] ) = self::sortKeys(
+ $query
+ );
+
+ if ( $serialized['sort'] !== [] ) {
+ $serialized['parameters']['sort'] = implode( ',', $serialized['sort'] );
+ }
+
+ if ( $serialized['order'] !== [] ) {
+ $serialized['parameters']['order'] = implode( ',', $serialized['order'] );
+ }
+
+ unset( $serialized['sort'] );
+ unset( $serialized['order'] );
+
+ $serialized['printouts'] = self::printouts(
+ $query,
+ $printParameters
+ );
+
+ return $serialized;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Query $query
+ *
+ * @return string
+ */
+ public static function toJson( Query $query, $printParameters = false ) {
+ return json_encode( self::toArray( $query, $printParameters ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Query $query
+ *
+ * @return string
+ */
+ public static function toString( Query $query, $printParameters = false ) {
+
+ $serialized = self::toArray( $query, $printParameters );
+
+ $string = $serialized['conditions'];
+
+ if ( $serialized['printouts'] !== [] ) {
+ $string .= '|' . implode( '|', $serialized['printouts'] );
+ }
+
+ foreach ( $serialized['parameters'] as $key => $value ) {
+ $string .= "|$key=$value";
+ }
+
+ return $string;
+ }
+
+ private static function printouts( $query, $showParams = false ) {
+
+ $printouts = [];
+
+ if ( $query->getExtraPrintouts() === null ) {
+ return $printouts;
+ }
+
+ foreach ( $query->getExtraPrintouts() as $printout ) {
+ if ( ( $serialisation = $printout->getSerialisation( $showParams ) ) !== '' ) {
+ $printouts[] = $serialisation;
+ }
+ }
+
+ return $printouts;
+ }
+
+ private static function sortKeys( $query ) {
+
+ $sort = [];
+ $order = [];
+
+ if ( $query->getSortKeys() === null ) {
+ return [ $sort, $order ];
+ }
+
+ foreach ( $query->getSortKeys() as $key => $value ) {
+
+ if ( $key === '' ) {
+ continue;
+ }
+
+ $sort[] = str_replace( '_', ' ', $key );
+ $order[] = strtolower( $value );
+ }
+
+ return [ $sort, $order ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryToken.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryToken.php
new file mode 100644
index 00000000..d6deef12
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/QueryToken.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\DIWikiPage;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ValueDescription;
+use SMW\Utils\Tokenizer;
+use SMWDIBlob as DIBlob;
+
+/**
+ * For a wildcard search, build tokens from the query string, and allow to highlight
+ * them in the result set.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class QueryToken {
+
+ // TokensHighlighter
+ // QueryTokensHighlighter
+
+ /**
+ * Highlighter marker type
+ */
+ const HL_WIKI = 'HL_WIKI';
+ const HL_BOLD = 'HL_BOLD';
+ const HL_SPAN = 'HL_SPAN';
+ const HL_UNDERLINE = 'HL_UNDERLINE';
+
+ /**
+ * @var array
+ */
+ private $tokens = [];
+
+ /**
+ * @var array
+ */
+ private $minHighlightTokenLength = 4;
+
+ /**
+ * @var array
+ */
+ private $highlightType = 4;
+
+ /**
+ * @var string
+ */
+ private $outputFormat;
+
+ /**
+ * @since 2.5
+ *
+ * @param array $tokens
+ */
+ public function __construct( array $tokens = [] ) {
+ $this->tokens = $tokens;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getTokens() {
+ return $this->tokens;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Description $description
+ */
+ public function addFromDesciption( Description $description ) {
+
+ if ( $description instanceof Conjunction ) {
+ foreach ( $description->getDescriptions() as $desc ) {
+ return $this->addFromDesciption( $desc );
+ }
+ }
+
+ if ( $description instanceof SomeProperty ) {
+ return $this->addFromDesciption( $description->getDescription() );
+ }
+
+ if ( !$description instanceof ValueDescription ) {
+ return;
+ }
+
+ $isProximate = $description->getComparator() === SMW_CMP_LIKE || $description->getComparator() === SMW_CMP_PRIM_LIKE;
+
+ // [[SomeProperty::~*Foo*]] / [[SomeProperty::like:*Foo*]]
+ if ( $isProximate && $description->getDataItem() instanceof DIBlob ) {
+ return $this->addTokensFromText( $description->getDataItem()->getString() );
+ }
+
+ // [[~~* ... *]]
+ if ( $description->getDataItem() instanceof DIWikiPage && strpos( $description->getDataItem()->getDBKey(), '~' ) !== false ) {
+ return $this->addTokensFromText( $description->getDataItem()->getDBKey() );
+ }
+ }
+
+ /**
+ * Sets format information (|?Foo#-hl) from a result printer
+ *
+ * @since 2.5
+ *
+ * @param string $outputFormat
+ */
+ public function setOutputFormat( $outputFormat ) {
+ $this->outputFormat = $outputFormat;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ * @param type $text
+ *
+ * @return string
+ */
+ public function highlight( $text, $type = self::HL_BOLD ) {
+
+ if ( $this->tokens === [] || strpos( strtolower( $this->outputFormat ), '-hl' ) === false ) {
+ return $text;
+ }
+
+ return $this->doHighlight( $text, $type, array_keys( $this->tokens ) );
+ }
+
+ private function doHighlight( $text, $type, $tokens ) {
+
+ if ( $type === self::HL_BOLD ) {
+ $replacement = "<b>$0</b>";
+ } elseif ( $type === self::HL_UNDERLINE ) {
+ $replacement = "<u>$0</u>";
+ } elseif ( $type === self::HL_SPAN ) {
+ $replacement = "<span class='smw-query-token'>$0</span>";
+ } else {
+ $replacement = "'''$0'''";
+ }
+
+ // Match all tokens except those within [ ... ] to avoid breaking links
+ // and annotations
+ $pattern = '/(' . implode( '|', $tokens ) . ')+(?![^\[]*\])/iu';
+
+ return preg_replace( $pattern, $replacement, $text );
+ }
+
+ private function addTokensFromText( $text ) {
+
+ // Remove query related chars
+ $text = str_replace(
+ [ '*', '"', '~', '_', '+', '-' ],
+ [ '', '', '', ' ', '', '' ],
+ $text
+ );
+
+ return $this->tokens += array_flip( Tokenizer::tokenize( $text ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/RemoteRequest.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/RemoteRequest.php
new file mode 100644
index 00000000..325d0c7b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/RemoteRequest.php
@@ -0,0 +1,336 @@
+<?php
+
+namespace SMW\Query;
+
+use Html;
+use Onoi\HttpRequest\CachedCurlRequest;
+use Onoi\HttpRequest\CurlRequest;
+use Onoi\HttpRequest\HttpRequest;
+use RuntimeException;
+use SMW\ApplicationFactory;
+use SMW\Message;
+use SMW\Query\Result\StringResult;
+use SMW\QueryEngine;
+use SMW\Site;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class RemoteRequest implements QueryEngine {
+
+ /**
+ * Send by a remote source when remote access has been disabled.
+ */
+ const SOURCE_DISABLED = "\x7fsmw-remote-response-disabled\x7f";
+
+ /**
+ * Identifies a source to support a remote request.
+ */
+ const REQUEST_ID = "\x7fsmw-remote-request\x7f";
+
+ /**
+ * @var []
+ */
+ private $parameters = [];
+
+ /**
+ * @var HttpRequest
+ */
+ private $httpRequest;
+
+ /**
+ * @var []
+ */
+ private $features = [];
+
+ /**
+ * @var []
+ */
+ private static $isConnected;
+
+ /**
+ * @since 3.0
+ *
+ * @param array $parameters
+ * @param HttpRequest|null $httpRequest
+ */
+ public function __construct( array $parameters = [], HttpRequest $httpRequest = null ) {
+ $this->parameters = $parameters;
+ $this->httpRequest = $httpRequest;
+ $this->features = $GLOBALS['smwgRemoteReqFeatures'];
+
+ if ( isset( $this->parameters['smwgRemoteReqFeatures'] ) ) {
+ $this->features = $this->parameters['smwgRemoteReqFeatures'];
+ }
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function clear() {
+ self::$isConnected = null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $flag
+ *
+ * @return boolean
+ */
+ public function hasFeature( $flag ) {
+ return ( ( (int)$this->features & $flag ) == $flag );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Query $query
+ *
+ * @return StringResult|string
+ */
+ public function getQueryResult( Query $query ) {
+
+ if ( $query->isEmbedded() && $query->getLimit() == 0 ) {
+ return $this->further_link( $query );
+ }
+
+ if ( !isset( $this->parameters['url'] ) ) {
+ throw new RuntimeException( "Missing a remote URL for $source" );
+ }
+
+ $source = $query->getQuerySource();
+ $this->init();
+
+ if ( !$this->canConnect( $this->parameters['url'] ) ) {
+ return $this->error( 'smw-remote-source-unavailable', $this->parameters['url'] );
+ }
+
+ $result = $this->fetch( $query );
+
+ $isFromCache = false;
+ $isDisabled = false;
+
+ if ( $this->httpRequest instanceof CachedCurlRequest ) {
+ $isFromCache = $this->httpRequest->isFromCache();
+ }
+
+ if ( $result === self::SOURCE_DISABLED ) {
+ $result = $this->error( 'smw-remote-source-disabled', $source );
+ $isDisabled = true;
+ }
+
+ // Find out whether the source has send an ID and hereby produces an output
+ // that can be used by the `RemoteRequest`
+ if ( strpos( $result, self::REQUEST_ID ) === false ) {
+ $result = $this->error( 'smw-remote-source-unmatched-id', $source );
+ $isDisabled = true;
+ } else {
+ $result = str_replace( self::REQUEST_ID, '', $result );
+ }
+
+ // Add an information note depending on the context before the actual output
+ $callback = function( $result, array $options ) use( $isFromCache, $isDisabled, $source ) {
+
+ $options['source'] = $source;
+ $options['is.cached'] = $isFromCache;
+ $options['is.disabled'] = $isDisabled;
+
+ return $this->format_result( $result, $options );
+ };
+
+ $stringResult = new StringResult( $result, $query );
+ $stringResult->setPreOutputCallback( $callback );
+ $stringResult->setFromCache( $isFromCache );
+
+ if ( $query->getQueryMode() === Query::MODE_COUNT ) {
+ $stringResult->setCountValue( $result );
+ }
+
+ return $stringResult;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $result
+ * @param array $options
+ *
+ * @return string
+ */
+ public function format_result( $result, array $options ) {
+
+ // No changes to any export related output
+ if ( ( isset( $options['is.disabled'] ) && $options['is.disabled'] ) || !$this->hasFeature( SMW_REMOTE_REQ_SHOW_NOTE ) ) {
+ return $result;
+ }
+
+ if ( ( isset( $options['is.exportformat'] ) && $options['is.exportformat'] ) ) {
+ return $result;
+ }
+
+ $msg = $options['is.cached'] ? 'smw-remote-request-note-cached' : 'smw-remote-request-note';
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-note smw-remote-query',
+ 'style' => 'margin-top:12px;'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-icon-info',
+ 'style' => 'margin-left: -5px; padding: 10px 12px 12px 12px;'
+ ]
+ ) . Message::get( [ $msg, $options['source'] ], Message::PARSE, Message::USER_LANGUAGE )
+ ) . $result;
+ }
+
+ private function further_link( $query ) {
+
+ $link = QueryLinker::get( $query );
+
+ // Find remaining parameters, format, template etc.
+ $extraParameters = $query->getOption( 'query.params' );
+
+ foreach ( $extraParameters as $key => $value ) {
+
+ if ( $key === 'limit' || $value === '' ) {
+ continue;
+ }
+
+ if ( is_array( $value ) ) {
+ $value = implode( ',', $value );
+ }
+
+ $link->setParameter( $value, $key );
+ }
+
+ return $link->getText( SMW_OUTPUT_WIKI );
+ }
+
+ private function init() {
+
+ if ( $this->httpRequest === null && isset( $this->parameters['cache'] ) ) {
+ $this->httpRequest = new CachedCurlRequest(
+ curl_init(),
+ ApplicationFactory::getInstance()->getCache()
+ );
+
+ $this->httpRequest->setOption(
+ ONOI_HTTP_REQUEST_RESPONSECACHE_TTL,
+ $this->parameters['cache']
+ );
+
+ $this->httpRequest->setOption(
+ ONOI_HTTP_REQUEST_RESPONSECACHE_PREFIX,
+ Site::id( 'smw:query:remote:' )
+ );
+ }
+
+ if ( $this->httpRequest === null ) {
+ $this->httpRequest = new CurlRequest( curl_init() );
+ }
+ }
+
+ private function error() {
+ $params = func_get_args();
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'id' => $params[0],
+ 'class' => 'smw-callout smw-callout-error'
+ ],
+ Message::get( $params, Message::PARSE, Message::USER_LANGUAGE )
+ );
+ }
+
+ private function canConnect( $url ) {
+
+ $this->httpRequest->setOption( CURLOPT_URL, $url );
+
+ if ( self::$isConnected === null ) {
+ self::$isConnected = $this->httpRequest->ping();
+ }
+
+ return self::$isConnected;
+ }
+
+ private function fetch( $query ) {
+
+ $parameters = $query->toArray();
+ $default = '';
+ $params = [ 'title' => 'Special:Ask', 'q' => '', 'po' => '', 'p' => [] ];
+
+ if ( isset( $parameters['conditions'] ) ) {
+ $params['q'] = $parameters['conditions'];
+ }
+
+ if ( isset( $parameters['printouts'] ) ) {
+ $params['po'] = implode( '|', $parameters['printouts'] );
+ }
+
+ if ( !isset( $parameters['parameters'] ) ) {
+ $parameters['parameters'] = [];
+ }
+
+ // Find remaining parameters, format, template etc.
+ $extraParameters = $query->getOption( 'query.params' );
+
+ if ( is_array( $extraParameters ) ) {
+ $parameters['parameters'] = array_merge( $parameters['parameters'], $extraParameters );
+ }
+
+ foreach ( $parameters['parameters'] as $key => $value ) {
+
+ if ( $key === 'default' ) {
+ $default = $value;
+ }
+
+ if ( $value === '' ) {
+ continue;
+ }
+
+ if ( is_array( $value ) ) {
+ $value = implode( ',', $value );
+ }
+
+ $params['p'][] = "$key=$value";
+ }
+
+ $params['request_type'] = $query->isEmbedded() ? 'embed' : 'special_page';
+ $output = '';
+
+ $options = [
+ CURLOPT_SSL_VERIFYPEER => false,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => http_build_query( $params ),
+ CURLOPT_RETURNTRANSFER => 1
+ ];
+
+ foreach ( $options as $key => $value ) {
+ $this->httpRequest->setOption( $key, $value );
+ }
+
+ $output = $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastError() !== '' ) {
+ $output = $this->httpRequest->getLastError();
+ }
+
+ // The remote Special:Ask doesn't return a default output hence it is done
+ // at this point
+ if ( $output === '' ) {
+ $output = $default;
+ }
+
+ return $output;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/CachedQueryResultPrefetcher.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/CachedQueryResultPrefetcher.php
new file mode 100644
index 00000000..673c5aeb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/CachedQueryResultPrefetcher.php
@@ -0,0 +1,552 @@
+<?php
+
+namespace SMW\Query\Result;
+
+use Onoi\BlobStore\BlobStore;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\QueryEngine;
+use SMW\QueryFactory;
+use SMW\Store;
+use SMW\Utils\BufferedStatsdCollector;
+use SMW\Utils\Timer;
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+
+/**
+ * The prefetcher only caches the subject list from a computed a query
+ * condition. The result is processed before an individual query printer has
+ * access to the query result hence it does not interfere with the final string
+ * output manipulation.
+ *
+ * The main objective is to avoid unnecessary computing of results for queries
+ * that have the same query signature. PrintRequests as part of a QueryResult
+ * object are not cached and are not part of a query signature.
+ *
+ * Cache eviction is carried out either manually (action=purge) or executed
+ * through the QueryDepedencyLinksStore.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class CachedQueryResultPrefetcher implements QueryEngine, LoggerAwareInterface {
+
+ /**
+ * Update this version number when the serialization format
+ * changes.
+ */
+ const VERSION = '1';
+
+ /**
+ * Namespace occupied by the BlobStore
+ */
+ const CACHE_NAMESPACE = 'smw:query:store';
+
+ /**
+ * ID used by the bufferedStatsdCollector, requires to be changed in case
+ * the data schema is modified
+ *
+ * PHP 5.6 can do self::CACHE_NAMESPACE . ':' . self::VERSION
+ */
+ const STATSD_ID = 'smw:query:store:1:d:';
+
+ /**
+ * ID for the tempCache
+ */
+ const POOLCACHE_ID = 'queryresult.prefetcher';
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var QueryFactory
+ */
+ private $queryFactory;
+
+ /**
+ * @var BlobStore
+ */
+ private $blobStore;
+
+ /**
+ * @var QueryEngine
+ */
+ private $queryEngine;
+
+ /**
+ * @var BufferedStatsdCollector
+ */
+ private $bufferedStatsdCollector;
+
+ /**
+ * @var integer|boolean
+ */
+ private $nonEmbeddedCacheLifetime = false;
+
+ /**
+ * @var boolean
+ */
+ private $enabledCache = true;
+
+ /**
+ * @var loggerInterface
+ */
+ private $logger;
+
+ /**
+ * Keep a temp cache to hold on query results that aren't stored yet.
+ *
+ * If for example the retrieval is executed in deferred mode then a request
+ * may occur in the same transaction cycle without being stored to the actual
+ * back-end, yet queries with the same signature may have been retrieved
+ * already therefore allow to recall the result from tempCache.
+ *
+ * @var InMemoryCache
+ */
+ private $tempCache;
+
+ /**
+ * An internal change to the query execution may occur without being detected
+ * by the Description hash (which is the desired behaviour) and to avoid a
+ * stalled cache on an altered execution plan, use this modifier to generate
+ * a new hash.
+ *
+ * @var string/integer
+ */
+ private $dependantHashIdExtension = '';
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param QueryFactory $queryFactory
+ * @param BlobStore $blobStore
+ * @param BufferedStatsdCollector $bufferedStatsdCollector
+ */
+ public function __construct( Store $store, QueryFactory $queryFactory, BlobStore $blobStore, BufferedStatsdCollector $bufferedStatsdCollector ) {
+ $this->store = $store;
+ $this->queryFactory = $queryFactory;
+ $this->blobStore = $blobStore;
+ $this->bufferedStatsdCollector = $bufferedStatsdCollector;
+ $this->tempCache = ApplicationFactory::getInstance()->getInMemoryPoolCache()->getPoolCacheById( self::POOLCACHE_ID );
+
+ $this->initStats( date( 'Y-m-d H:i:s' ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getStats() {
+
+ $stats = array_filter( $this->bufferedStatsdCollector->getStats(), function( $key ) {
+ return $key !== false;
+ } );
+
+ if ( !isset( $stats['misses'] ) || ! isset( $stats['hits'] ) ) {
+ return $stats;
+ }
+
+ $misses = $this->sum( 0, $stats['misses'] );
+ $hits = $this->sum( 0, $stats['hits'] );
+
+ $stats['ratio'] = [];
+ $stats['ratio']['hit'] = $hits > 0 ? round( $hits / ( $hits + $misses ), 4 ) : 0;
+ $stats['ratio']['miss'] = $hits > 0 ? round( 1 - $stats['ratio']['hit'], 4 ) : 1;
+
+ // Move to last
+ $meta = $stats['meta'];
+ unset( $stats['meta'] );
+ $stats['meta'] = $meta;
+
+ return $stats;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|integer $dependantHashIdExtension
+ */
+ public function setDependantHashIdExtension( $dependantHashIdExtension ) {
+ $this->dependantHashIdExtension = $dependantHashIdExtension;
+ }
+
+ /**
+ * @see LoggerAwareInterface::setLogger
+ *
+ * @since 2.5
+ *
+ * @param LoggerInterface $logger
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param QueryEngine $queryEngine
+ */
+ public function setQueryEngine( QueryEngine $queryEngine ) {
+ $this->queryEngine = $queryEngine;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean
+ */
+ public function isEnabled() {
+ return $this->blobStore->canUse();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param QueryEngine $queryEngine
+ */
+ public function disableCache() {
+ $this->enabledCache = false;
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function recordStats() {
+ $this->bufferedStatsdCollector->recordStats();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer|boolean $nonEmbeddedCacheLifetime
+ */
+ public function setNonEmbeddedCacheLifetime( $nonEmbeddedCacheLifetime ) {
+ $this->nonEmbeddedCacheLifetime = $nonEmbeddedCacheLifetime;
+ }
+
+ /**
+ * @see QueryEngine::getQueryResult
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getQueryResult( Query $query ) {
+
+ if ( !$this->queryEngine instanceof QueryEngine ) {
+ throw new RuntimeException( "Missing a QueryEngine instance." );
+ }
+
+ if ( !$this->canUse( $query ) || $query->getLimit() < 1 || $query->getOption( Query::NO_CACHE ) === true ) {
+ $this->bufferedStatsdCollector->incr( $this->getNoCacheId( $query ) );
+ return $this->queryEngine->getQueryResult( $query );
+ }
+
+ Timer::start( __CLASS__ );
+
+ $queryId = $this->getHashFrom( $query->getQueryId() );
+
+ $container = $this->blobStore->read(
+ $queryId
+ );
+
+ if ( $this->tempCache->contains( $queryId ) || $container->has( 'results' ) ) {
+ return $this->newQueryResultFromCache( $queryId, $query, $container );
+ }
+
+ $queryResult = $this->queryEngine->getQueryResult( $query );
+
+ $this->tempCache->save(
+ $queryId,
+ $queryResult
+ );
+
+ $this->log(
+ __METHOD__ . ' from backend in (sec): ' . Timer::getElapsedTime( __CLASS__, 5 ) . " ($queryId)"
+ );
+
+ if ( $this->canUse( $query ) && $queryResult instanceof QueryResult ) {
+ $this->addQueryResultToCache( $queryResult, $queryId, $container, $query );
+ }
+
+ return $queryResult;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIWikiPage|array $items
+ * @param string $context
+ */
+ public function resetCacheBy( $items, $context = '' ) {
+
+ if ( !$this->blobStore->canUse() ) {
+ return;
+ }
+
+ if ( !is_array( $items ) ) {
+ $items = [ $items ];
+ }
+
+ $recordStats = false;
+ $context = $context === '' ? 'Undefined' : $context;
+
+ if ( is_array( $context ) ) {
+ $context = implode( '.', $context );
+ }
+
+ foreach ( $items as $item ) {
+ $id = $this->getHashFrom( $item );
+ $this->tempCache->delete( $id );
+
+ if ( $this->blobStore->exists( $id ) ) {
+ $recordStats = true;
+ $this->bufferedStatsdCollector->incr( 'deletes.on' . $context );
+ $this->blobStore->delete( $id );
+ }
+ }
+
+ if ( $recordStats ) {
+ $this->bufferedStatsdCollector->recordStats();
+ }
+ }
+
+ private function canUse( $query ) {
+ return $this->enabledCache && $this->blobStore->canUse() && ( $query->getContextPage() !== null || ( $query->getContextPage() === null && $this->nonEmbeddedCacheLifetime > 0 ) );
+ }
+
+ private function newQueryResultFromCache( $queryId, $query, $container ) {
+
+ $results = [];
+ $incrStats = 'hits.Undefined';
+ $resolverJournal = null;
+
+ if ( ( $context = $query->getOption( Query::PROC_CONTEXT ) ) === false ) {
+ $context = 'Undefined';
+ }
+
+ // Check if the tempCache is available for result that have not yet been
+ // stored to the cache back-end
+ if ( ( $queryResult = $this->tempCache->fetch( $queryId ) ) !== false ) {
+ $this->log( __METHOD__ . ' using tempCache ' . "($queryId)" );
+
+ if ( !$queryResult instanceof QueryResult ) {
+ return $queryResult;
+ }
+
+ $incrStats = 'hits.tempCache.' . ( $query->getContextPage() !== null ? 'embedded' : 'nonEmbedded' );
+
+ $queryResult->reset();
+ $results = $queryResult->getResults();
+
+ $hasFurtherResults = $queryResult->hasFurtherResults();
+ $countValue = $queryResult->getCountValue();
+ $resolverJournal = $queryResult->getResolverJournal();
+ } else {
+
+ $incrStats = ( $query->getContextPage() !== null ? 'hits.embedded.' : 'hits.nonEmbedded.' ) . $context;
+
+ foreach ( $container->get( 'results' ) as $hash ) {
+ $results[] = DIWikiPage::doUnserialize( $hash );
+ }
+
+ $hasFurtherResults = $container->get( 'continue' );
+ $countValue = $container->get( 'count' );
+ }
+
+ $queryResult = $this->queryFactory->newQueryResult(
+ $this->store,
+ $query,
+ $results,
+ $hasFurtherResults
+ );
+
+ $queryResult->setCountValue( $countValue );
+ $queryResult->setFromCache( true );
+
+ if ( $resolverJournal !== null ) {
+ $queryResult->setResolverJournal( $resolverJournal );
+ }
+
+ $time = Timer::getElapsedTime( __CLASS__, 5 );
+
+ $this->bufferedStatsdCollector->incr( $incrStats );
+
+ $this->bufferedStatsdCollector->calcMedian(
+ 'medianRetrievalResponseTime.cached',
+ $time
+ );
+
+ $this->log( __METHOD__ . ' (sec): ' . $time . " ($queryId)" );
+
+ return $queryResult;
+ }
+
+ private function addQueryResultToCache( $queryResult, $queryId, $container, $query ) {
+
+ if ( ( $context = $query->getOption( Query::PROC_CONTEXT ) ) === false ) {
+ $context = 'Undefined';
+ }
+
+ $this->bufferedStatsdCollector->incr(
+ ( $query->getContextPage() !== null ? 'misses.embedded.' : 'misses.nonEmbedded.' ) . $context
+ );
+
+ $this->bufferedStatsdCollector->calcMedian(
+ 'medianRetrievalResponseTime.uncached',
+ Timer::getElapsedTime( __CLASS__, 5 )
+ );
+
+ $callback = function() use( $queryResult, $queryId, $container, $query ) {
+ $this->doCacheQueryResult( $queryResult, $queryId, $container, $query );
+ };
+
+ $deferredTransactionalUpdate = ApplicationFactory::getInstance()->newDeferredTransactionalCallableUpdate(
+ $callback
+ );
+
+ $deferredTransactionalUpdate->setOrigin( __METHOD__ );
+ $deferredTransactionalUpdate->setFingerprint( __METHOD__ . $queryId );
+ $deferredTransactionalUpdate->waitOnTransactionIdle();
+
+ // Make sure that in any event the collector is executed after
+ // the process has finished
+ $deferredTransactionalUpdate->addPostCommitableCallback(
+ BufferedStatsdCollector::class,
+ [ $this, 'recordStats' ]
+ );
+
+ $deferredTransactionalUpdate->pushUpdate();
+ }
+
+ private function doCacheQueryResult( $queryResult, $queryId, $container, $query ) {
+
+ $results = [];
+
+ // Keep the simple string representation to avoid unnecessary data cruft
+ // during using PHP serialize( ... )
+ foreach ( $queryResult->getResults() as $dataItem ) {
+ $results[] = $dataItem->getSerialization();
+ }
+
+ $container->set( 'results', $results );
+ $container->set( 'continue', $queryResult->hasFurtherResults() );
+ $container->set( 'count', $queryResult->getCountValue() );
+
+ $queryResult->reset();
+ $contextPage = $query->getContextPage();
+
+ if ( $contextPage === null ) {
+ $container->setExpiryInSeconds( $this->nonEmbeddedCacheLifetime );
+ $hash = 'nonEmbedded';
+ } else {
+ $this->addToLinkedList( $contextPage, $queryId );
+ $hash = $contextPage->getHash();
+ }
+
+ $this->blobStore->save(
+ $container
+ );
+
+ $this->tempCache->delete( $queryId );
+
+ $this->log(
+ __METHOD__ . ' cache storage (sec): ' . Timer::getElapsedTime( __CLASS__, 5 ) . " ($queryId)"
+ );
+
+ return $queryResult;
+ }
+
+ private function addToLinkedList( $contextPage, $queryId ) {
+
+ // Ensure that without QueryDependencyLinksStore being enabled recorded
+ // subjects related to a query can be discoverable and purged separately
+ $container = $this->blobStore->read(
+ $this->getHashFrom( $contextPage )
+ );
+
+ // If a subject gets purged then the linked list of queries associated
+ // with that subject allows for an immediate associated removal
+ $container->addToLinkedList( $queryId );
+
+ $this->blobStore->save(
+ $container
+ );
+ }
+
+ private function getHashFrom( $subject ) {
+
+ if ( $subject instanceof DIWikiPage ) {
+ // In case the we detect a _QUERY subobject, use it directly
+ if ( ( $subobjectName = $subject->getSubobjectName() ) !== '' && strpos( $subobjectName, Query::ID_PREFIX ) !== false ) {
+ $subject = $subobjectName;
+ } else {
+ $subject = $subject->asBase()->getHash();
+ }
+ }
+
+ return md5( $subject . self::VERSION . $this->dependantHashIdExtension );
+ }
+
+ private function log( $message, $context = [] ) {
+
+ if ( $this->logger === null ) {
+ return;
+ }
+
+ $this->logger->info( $message, $context );
+ }
+
+ private function getNoCacheId( $query ) {
+
+ $id = 'noCache.misc';
+
+ if ( !$this->canUse( $query ) ) {
+ $id = 'noCache.disabled';
+ }
+
+ if ( $query->getLimit() < 1 ) {
+ $id = 'noCache.byLimit';
+ }
+
+ if ( $query->getOption( Query::NO_CACHE ) === true ) {
+ $id = 'noCache.byOption';
+ }
+
+ if ( ( $context = $query->getOption( Query::PROC_CONTEXT ) ) !== false ) {
+ $id .= '.' . $context;
+ }
+
+ return $id;
+ }
+
+ private function initStats( $date ) {
+
+ $this->bufferedStatsdCollector->shouldRecord( $this->isEnabled() );
+
+ $this->bufferedStatsdCollector->init( 'misses', [] );
+ $this->bufferedStatsdCollector->init( 'hits', [] );
+ $this->bufferedStatsdCollector->init( 'deletes', [] );
+ $this->bufferedStatsdCollector->init( 'noCache', [] );
+ $this->bufferedStatsdCollector->init( 'medianRetrievalResponseTime', [] );
+ $this->bufferedStatsdCollector->set( 'meta.version', self::VERSION );
+ $this->bufferedStatsdCollector->set( 'meta.cacheLifetime.embedded', $GLOBALS['smwgQueryResultCacheLifetime'] );
+ $this->bufferedStatsdCollector->set( 'meta.cacheLifetime.nonEmbedded', $GLOBALS['smwgQueryResultNonEmbeddedCacheLifetime'] );
+ $this->bufferedStatsdCollector->init( 'meta.collectionDate.start', $date );
+ $this->bufferedStatsdCollector->set( 'meta.collectionDate.update', $date );
+ }
+
+ // http://stackoverflow.com/questions/3777995/php-array-recursive-sum
+ private static function sum( $value, $container ) {
+ return $value + ( is_array( $container ) ? array_reduce( $container, 'self::sum' ) : $container );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/ResolverJournal.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/ResolverJournal.php
new file mode 100644
index 00000000..d306a353
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/ResolverJournal.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace SMW\Query\Result;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMWDataItem as DataItem;
+
+/**
+ * This class records selected entities used in a QueryResult by the time the
+ * ResultArray creates an object instance which avoids unnecessary work in the
+ * QueryResultDependencyListResolver (in terms of recursive processing of the
+ * QueryResult) to find related "column" entities (those related to a
+ * printrequest).
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class ResolverJournal {
+
+ /**
+ * @var array
+ */
+ private $dataItems = [];
+
+ /**
+ * @var array
+ */
+ private $properties = [];
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getEntityList() {
+ return $this->dataItems;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getPropertyList() {
+ return $this->properties;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function prune() {
+ $this->dataItems = [];
+ $this->properties = [];
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DataItem $dataItem
+ */
+ public function recordItem( DataItem $dataItem ) {
+ if ( $dataItem instanceof DIWikiPage ) {
+ $this->dataItems[$dataItem->getHash()] = $dataItem;
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty|null $property
+ */
+ public function recordProperty( DIProperty $property = null ) {
+ if ( $property !== null ) {
+ $this->properties[$property->getKey()] = $property;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/ResultFieldMatchFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/ResultFieldMatchFinder.php
new file mode 100644
index 00000000..46e87508
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/ResultFieldMatchFinder.php
@@ -0,0 +1,357 @@
+<?php
+
+namespace SMW\Query\Result;
+
+use SMW\DataValueFactory;
+use SMW\DataValues\MonolingualTextValue;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Parser\InTextAnnotationParser;
+use SMW\Query\PrintRequest;
+use SMW\Query\QueryToken;
+use SMW\RequestOptions;
+use SMW\Store;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+use SMWDIBoolean as DIBoolean;
+
+/**
+ * Returns the result content (DI objects) for a single PrintRequest, representing
+ * as cell of the intersection between a subject row and a print column.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author mwjames
+ */
+class ResultFieldMatchFinder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PrintRequest
+ */
+ private $printRequest;
+
+ /**
+ * @var QueryToken
+ */
+ private $queryToken;
+
+ /**
+ * @var boolean|array
+ */
+ private static $catCacheObj = false;
+
+ /**
+ * @var boolean|array
+ */
+ private static $catCache = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param PrintRequest $printRequest
+ */
+ public function __construct( Store $store, PrintRequest $printRequest ) {
+ $this->printRequest = $printRequest;
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param QueryToken|null $queryToken
+ */
+ public function setQueryToken( QueryToken $queryToken = null ) {
+
+ if ( $queryToken === null ) {
+ return;
+ }
+
+ $this->queryToken = $queryToken;
+
+ $this->queryToken->setOutputFormat(
+ $this->printRequest->getOutputFormat()
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DataItem $dataItem
+ *
+ * @param DataItem[]|[]
+ */
+ public function findAndMatch( DataItem $dataItem ) {
+
+ $content = [];
+
+ // Request the current element (page in result set).
+ // The limit is ignored here.
+ if ( $this->printRequest->isMode( PrintRequest::PRINT_THIS ) ) {
+ return [ $dataItem ];
+ }
+
+ // Request all direct categories of the current element
+ // Always recompute cache here to ensure output format is respected.
+ if ( $this->printRequest->isMode( PrintRequest::PRINT_CATS ) ) {
+ self::$catCache = $this->store->getPropertyValues(
+ $dataItem,
+ new DIProperty( '_INST' ),
+ $this->getRequestOptions( false )
+ );
+
+ self::$catCacheObj = $dataItem->getHash();
+
+ $limit = $this->printRequest->getParameter( 'limit' );
+
+ return ( $limit === false ) ? ( self::$catCache ) : array_slice( self::$catCache, 0, $limit );
+ }
+
+ // Request to whether current element is in given category (Boolean printout).
+ // The limit is ignored here.
+ if ( $this->printRequest->isMode( PrintRequest::PRINT_CCAT ) ) {
+ if ( self::$catCacheObj !== $dataItem->getHash() ) {
+ self::$catCache = $this->store->getPropertyValues(
+ $dataItem,
+ new DIProperty( '_INST' )
+ );
+ self::$catCacheObj = $dataItem->getHash();
+ }
+
+ $found = false;
+ $prkey = $this->printRequest->getData()->getDBkey();
+
+ foreach ( self::$catCache as $cat ) {
+ if ( $cat->getDBkey() == $prkey ) {
+ $found = true;
+ break;
+ }
+ }
+
+ return [ new DIBoolean( $found ) ];
+ }
+
+ // Request all property values of a certain attribute of the current element.
+ if ( $this->printRequest->isMode( PrintRequest::PRINT_PROP ) || $this->printRequest->isMode( PrintRequest::PRINT_CHAIN ) ) {
+ return $this->getResultsForProperty( $dataItem );
+ }
+
+ return $content;
+ }
+
+ /**
+ * 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 RequestOptions|null
+ */
+ public function getRequestOptions( $useLimit = true ) {
+ $limit = $useLimit ? $this->printRequest->getParameter( 'limit' ) : false;
+ $order = trim( $this->printRequest->getParameter( 'order' ) );
+ $options = null;
+
+ // Important: use "!=" for order, since trim() above does never return "false", use "!==" for limit since "0" is meaningful here.
+ if ( ( $limit !== false ) || ( $order != false ) ) {
+ $options = new RequestOptions();
+
+ if ( $limit !== false ) {
+ $options->limit = trim( $limit );
+ }
+
+ // Expecting a natural sort behaviour (n-asc, n-desc)?
+ if ( strpos( $order, 'n-' ) !== false ) {
+ $order = str_replace( 'n-', '', $order );
+ $options->natural = true;
+ }
+
+ if ( ( $order == 'descending' ) || ( $order == 'reverse' ) || ( $order == 'desc' ) ) {
+ $options->sort = true;
+ $options->ascending = false;
+ } elseif ( ( $order == 'ascending' ) || ( $order == 'asc' ) ) {
+ $options->sort = true;
+ $options->ascending = true;
+ }
+ }
+
+ return $options;
+ }
+
+ private function getResultsForProperty( $dataItem ) {
+
+ $content = $this->getResultContent(
+ $dataItem
+ );
+
+ if ( !$this->isMultiValueWithParameter( 'index' ) && !$this->isMultiValueWithParameter( 'lang' ) ) {
+ return $content;
+ }
+
+ // Print one component of a multi-valued string.
+ //
+ // Known limitation: the printrequest still is of type _rec, so if
+ // printers check for this then they will not recognize that it returns
+ // some more concrete type.
+ if ( $this->printRequest->isMode( PrintRequest::PRINT_CHAIN ) ) {
+ $propertyValue = $this->printRequest->getData()->getLastPropertyChainValue();
+ } else {
+ $propertyValue = $this->printRequest->getData();
+ }
+
+ $index = $this->printRequest->getParameter( 'index' );
+ $lang = $this->printRequest->getParameter( 'lang' );
+ $newcontent = [];
+
+ // Replace content with specific content from a Container/MultiValue
+ foreach ( $content as $diContainer ) {
+
+ /* AbstractMultiValue */
+ $multiValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $diContainer,
+ $propertyValue->getDataItem()
+ );
+
+ $multiValue->setOption( $multiValue::OPT_QUERY_CONTEXT, true );
+
+ if ( $multiValue instanceof MonolingualTextValue && $lang !== false && ( $textValue = $multiValue->getTextValueByLanguage( $lang ) ) !== null ) {
+
+ // Return the text representation without a language reference
+ // (tag) since the value has been filtered hence only matches
+ // that language
+ $newcontent[] = $this->applyContentManipulation( $textValue->getDataItem() );
+
+ // Set the index so ResultArray::getNextDataValue can
+ // find the correct PropertyDataItem (_TEXT;_LCODE) position
+ // to match the DI
+ $this->printRequest->setParameter( 'index', 1 );
+ } elseif ( $lang === false && $index !== false && ( $dataItemByRecord = $multiValue->getDataItemByIndex( $index ) ) !== null ) {
+ $newcontent[] = $this->applyContentManipulation( $dataItemByRecord );
+ }
+ }
+
+ $content = $newcontent;
+ unset( $newcontent );
+
+ return $content;
+ }
+
+ private function isMultiValueWithParameter( $parameter ) {
+ return strpos( $this->printRequest->getTypeID(), '_rec' ) !== false && $this->printRequest->getParameter( $parameter ) !== false;
+ }
+
+ private function getResultContent( DataItem $dataItem ) {
+
+ $dataValue = $this->printRequest->getData();
+ $dataItems = [ $dataItem ];
+
+ if ( !$dataValue->isValid() ) {
+ return [];
+ }
+
+ // If it is a chain then try to find a connected DIWikiPage subject that
+ // matches the property on the chained PrintRequest.
+ // For example, Number.Date.SomeThing will not return any meaningful results
+ // because Number will return a DINumber object and not a DIWikiPage.
+ // If on the other hand Has page.Number (with Number being the Last and
+ // `Has page` is of type Page) then the iteration will lookup on results
+ // for `Has page` and try to match a Number annotation on the results
+ // retrieved from `Has page`.
+ if ( $this->printRequest->isMode( PrintRequest::PRINT_CHAIN ) ) {
+
+ // Output of the previous iteration is the input for the next iteration
+ foreach ( $dataValue->getPropertyChainValues() as $pv ) {
+ $dataItems = $this->doFetchPropertyValues( $dataItems, $pv );
+
+ // If the results return empty then it means that for this element
+ // the chain has no matchable items hence we stop
+ if ( $dataItems === [] ) {
+ return [];
+ }
+ }
+
+ $dataValue = $dataValue->getLastPropertyChainValue();
+ }
+
+ return $this->doFetchPropertyValues( $dataItems, $dataValue );
+ }
+
+ private function doFetchPropertyValues( $dataItems, $dataValue ) {
+
+ $propertyValues = [];
+
+ foreach ( $dataItems as $dataItem ) {
+
+ if ( !$dataItem instanceof DIWikiPage ) {
+ continue;
+ }
+
+ $pv = $this->store->getPropertyValues(
+ $dataItem,
+ $dataValue->getDataItem(),
+ $this->getRequestOptions()
+ );
+
+ if ( $pv instanceof \Iterator ) {
+ $pv = iterator_to_array( $pv );
+ }
+
+ $propertyValues = array_merge( $propertyValues, $pv );
+ unset( $pv );
+ }
+
+ array_walk( $propertyValues, function( &$dataItem ) {
+ $dataItem = $this->applyContentManipulation( $dataItem );
+ } );
+
+ return $propertyValues;
+ }
+
+ private function applyContentManipulation( $dataItem ) {
+
+ if ( !$dataItem instanceof DIBlob ) {
+ return $dataItem;
+ }
+
+ $type = $this->printRequest->getTypeID();
+
+ // Avoid `_cod`, `_eid` or similar types that use the DIBlob as storage
+ // object
+ if ( $type !== '_txt' && strpos( $type, '_rec' ) === false ) {
+ return $dataItem;
+ }
+
+ $outputFormat = $this->printRequest->getOutputFormat();
+
+ // #2325
+ // Output format marked with -raw are allowed to retain a possible [[ :: ]]
+ // annotation
+ // '-ia' is deprecated use `-raw`
+ if ( strpos( $outputFormat, '-raw' ) !== false || strpos( $outputFormat, '-ia' ) !== false ) {
+ return $dataItem;
+ }
+
+ // #1314
+ $string = InTextAnnotationParser::removeAnnotation(
+ $dataItem->getString()
+ );
+
+ // #2253
+ if ( $this->queryToken !== null ) {
+ $string = $this->queryToken->highlight( $string );
+ }
+
+ return new DIBlob( $string );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/StringResult.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/StringResult.php
new file mode 100644
index 00000000..69bd055a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/Result/StringResult.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace SMW\Query\Result;
+
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class StringResult extends QueryResult {
+
+ /**
+ * @var array
+ */
+ private $result = '';
+
+ /**
+ * @var Query
+ */
+ private $query;
+
+ /**
+ * @var callable
+ */
+ private $preOutputCallback;
+
+ /**
+ * @var array
+ */
+ private $options = [
+ 'noparse' => true,
+ 'isHTML' => true
+ ];
+
+ /**
+ * @since 3.0
+ *
+ * @param string $result
+ */
+ public function __construct( $result = '', Query $query ) {
+ $this->result = $result;
+ $this->query = $query;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function setOption( $key, $value ) {
+ $this->options[$key] = $value;
+ }
+
+ /**
+ * Manipulate or transform the result before the actual output.
+ *
+ * @since 3.0
+ *
+ * @param callable $preOutputCallback
+ */
+ public function setPreOutputCallback( callable $preOutputCallback ) {
+ $this->preOutputCallback = $preOutputCallback;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getResults() {
+
+ $result = $this->result;
+
+ if ( is_callable( $this->preOutputCallback ) ) {
+ $result = call_user_func_array( $this->preOutputCallback, [ $result, $this->options ] );
+ }
+
+ // Inline representation requires a different handling for results already
+ // being parsed by for example a remote request.
+ if ( $this->query->isEmbedded() ) {
+ return [ $result, 'noparse' => $this->options['noparse'], 'isHTML' => $this->options['isHTML'] ];
+ }
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinter.php
new file mode 100644
index 00000000..c7d06ef4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinter.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace SMW\Query;
+
+use SMWQueryResult as QueryResult;
+
+/**
+ * Interface for SMW result printers.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author Markus Krötzsch
+ */
+interface ResultPrinter {
+
+ // Constructor restriction:
+ // Needs to have exactly one required argument $formatName.
+ // Is allowed to have additional optional arguments.
+
+ /**
+ * Main entry point: takes an QueryResult and parameters given as key-value-pairs in an array,
+ * and returns the serialised version of the results, formatted as HTML or Wiki or whatever is
+ * specified. Normally this is not overwritten by subclasses.
+ *
+ * If the outputmode is SMW_OUTPUT_WIKI, then the function will return something that is suitable
+ * for being used in a MediaWiki parser function, i.e. a wikitext strong *or* an array with flags
+ * and the string as entry 0. See Parser::setFunctionHook() for documentation on this. In all other
+ * cases, the function returns just a string.
+ *
+ * For outputs SMW_OUTPUT_WIKI and SMW_OUTPUT_HTML, error messages or standard "further results" links
+ * are directly generated and appended. For SMW_OUTPUT_FILE, only the plain generated text is returned.
+ *
+ * @note A note on recursion: some query printers may return wiki code that comes from other pages,
+ * e.g. from templates that are used in formatting or from embedded result pages. Both kinds of pages
+ * may contain \#ask queries that do again use new pages, so we must care about recursion. We do so
+ * by simply counting how often this method starts a subparse and stopping at depth 2. There is one
+ * special case: if this method is called outside parsing, and the concrete printer returns wiki text,
+ * and wiki text is requested, then we may return wiki text with sub-queries to the caller. If the
+ * caller parses this (which is likely) then this will again call us in parse-context and all recursion
+ * checks catch. Only the first level of parsing is done outside and thus not counted. Thus you
+ * effectively can get down to level 3. The basic maximal depth of 2 can be changed by setting the
+ * variable SMWResultPrinter::$maxRecursionDepth (in LocalSettings.php, after enableSemantics()).
+ * Do this at your own risk.
+ *
+ * @param $results QueryResult
+ * @param $fullParams array
+ * @param $outputMode integer
+ *
+ * @return string
+ */
+ public function getResult( QueryResult $results, array $fullParams, $outputMode );
+
+ /**
+ * This function determines the query mode that is to be used for this printer in
+ * various contexts. The query mode influences how queries to that printer should
+ * be processed to obtain a result. Possible values are SMWQuery::MODE_INSTANCES
+ * (retrieve instances), SMWQuery::MODE_NONE (do nothing), SMWQuery::MODE_COUNT
+ * (get number of results), SMWQuery::MODE_DEBUG (return debugging text).
+ * Possible values for context are SMWQueryProcessor::SPECIAL_PAGE,
+ * SMWQueryProcessor::INLINE_QUERY, SMWQueryProcessor::CONCEPT_DESC.
+ *
+ * The default implementation always returns SMWQuery::MODE_INSTANCES. File exports
+ * like RSS will use MODE_INSTANCES on special pages (so that instances are
+ * retrieved for the export) and MODE_NONE otherwise (displaying just a download link).
+ *
+ * @param $context
+ *
+ * @return integer
+ */
+ public function getQueryMode( $context );
+
+ /**
+ * Get a human readable label for this printer. The default is to
+ * return just the format identifier. Concrete implementations may
+ * refer to messages here. The format name is normally not used in
+ * wiki text but only in forms etc. hence the user language should be
+ * used when retrieving messages.
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Set whether errors should be shown. By default they are.
+ *
+ * @param boolean $show
+ */
+ public function setShowErrors( $show );
+
+ /**
+ * Takes a list of parameter definitions and adds those supported by this
+ * result printer. Most result printers should override this method.
+ *
+ * @since 1.8
+ *
+ * @param ParamDefinition[] $definitions
+ *
+ * @return array
+ */
+ public function getParamDefinitions( array $definitions );
+
+ /**
+ * Returns if the format is an export format.
+ *
+ * @since 1.8
+ *
+ * @return boolean
+ */
+ public function isExportFormat();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/CategoryResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/CategoryResultPrinter.php
new file mode 100644
index 00000000..63acdf6f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/CategoryResultPrinter.php
@@ -0,0 +1,322 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use SMW\MediaWiki\Collator;
+use SMWDataItem as DataItem;
+use SMWQueryResult as QueryResult;
+use SMW\Utils\HtmlColumns;
+use SMW\ApplicationFactory;
+
+/**
+ * Print query results in alphabetic groups displayed in columns, a la the
+ * standard Category pages.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author David Loomer
+ * @author Yaron Koren
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class CategoryResultPrinter extends ResultPrinter {
+
+ /**
+ * @var string
+ */
+ private $delim;
+
+ /**
+ * @var string
+ */
+ private $template;
+
+ /**
+ * @var string
+ */
+ private $userParam;
+
+ /**
+ * @var integer
+ */
+ private $numColumns;
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return wfMessage( 'smw_printername_' . $this->mFormat )->text();
+ }
+
+ /**
+ * @see ResultPrinter::isDeferrable
+ *
+ * {@inheritDoc}
+ */
+ public function isDeferrable() {
+ return true;
+ }
+
+ /**
+ * @see ResultPrinter::supportsRecursiveAnnotation
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function supportsRecursiveAnnotation() {
+ return true;
+ }
+
+ /**
+ * @see ResultPrinter::getParamDefinitions
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $definitions = parent::getParamDefinitions( $definitions );
+
+ $definitions[] = [
+ 'name' => 'columns',
+ 'type' => 'integer',
+ 'message' => 'smw-paramdesc-columns',
+ 'negatives' => false,
+ 'default' => 3,
+ ];
+
+ $definitions[] = [
+ 'name' => 'delim',
+ 'message' => 'smw-paramdesc-category-delim',
+ 'default' => '',
+ ];
+
+ $definitions[] = [
+ 'name' => 'template',
+ 'message' => 'smw-paramdesc-category-template',
+ 'default' => '',
+ ];
+
+ $definitions[] = [
+ 'name' => 'userparam',
+ 'message' => 'smw-paramdesc-category-userparam',
+ 'default' => '',
+ ];
+
+ $definitions[] = [
+ 'name' => 'named args',
+ 'type' => 'boolean',
+ 'message' => 'smw-paramdesc-named_args',
+ 'default' => false,
+ ];
+
+ return $definitions;
+ }
+
+ /**
+ * @see ResultPrinter::handleParameters
+ *
+ * {@inheritDoc}
+ */
+ protected function handleParameters( array $params, $outputmode ) {
+ parent::handleParameters( $params, $outputmode );
+
+ $this->userParam = isset( $params['userparam'] ) ? trim( $params['userparam'] ) : '';
+ $this->delim = isset( $params['delim'] ) ? trim( $params['delim'] ) : '';
+ $this->numColumns = isset( $params['columns'] ) ? $params['columns'] : 3;
+ $this->template = isset( $params['template'] ) ? $params['template'] : '';
+ }
+
+ /**
+ * @since 3.0
+ */
+ protected function initServices() {
+ $mwCollaboratorFactory = ApplicationFactory::getInstance()->newMwCollaboratorFactory();
+
+ $this->htmlColumns = new HtmlColumns();
+ $this->templateRenderer = $mwCollaboratorFactory->newWikitextTemplateRenderer();
+ $this->collator = Collator::singleton();
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * {@inheritDoc}
+ */
+ protected function getResultText( QueryResult $res, $outputMode ) {
+
+ $this->initServices();
+ $contents = $this->getContents( $res, $outputMode );
+
+ if ( $contents === [] ) {
+ return $res->addErrors( [ 'smw-qp-empty-data' ] );
+ }
+
+ $this->htmlColumns->setContinueAbbrev( wfMessage( 'listingcontinuesabbrev' )->text() );
+ $this->htmlColumns->setColumns( $this->numColumns );
+
+ // 0 indicates to use responsive columns
+ if ( $this->params['columns'] == 0 ) {
+ $this->htmlColumns->setColumnClass( 'smw-column-responsive' );
+ $this->htmlColumns->setColumns( 1 );
+ }
+
+ $this->htmlColumns->addContents( $contents, HtmlColumns::INDX_CONTENT );
+
+ return $this->htmlColumns->getHtml();
+ }
+
+ private function getContents( QueryResult $res, $outputMode ) {
+ $contents = [];
+
+ // Print all result rows:
+ $rowindex = 0;
+ $row = $res->getNext();
+
+ while ( $row !== false ) {
+ $nextrow = $res->getNext(); // look ahead
+
+ if ( !isset( $row[0] ) ) {
+ $row = $nextrow;
+ continue;
+ }
+
+ $content = $row[0]->getContent();
+
+ if ( !isset( $content[0] ) || !( $content[0] instanceof DataItem ) ) {
+ $row = $nextrow;
+ continue;
+ }
+
+ $first_letter = $this->first_letter( $res, $content[0] );
+
+ if ( !isset( $contents[$first_letter] ) ) {
+ $contents[$first_letter] = [];
+ $last_letter = $first_letter;
+ }
+
+ if ( $this->template !== '' ) { // build template code
+
+ $first_col = true;
+ $this->hasTemplates = true;
+
+ if ( $this->userParam ) {
+ $this->templateRenderer->addField( 'userparam', $this->userParam );
+ }
+
+ $this->row_to_template( $row, $res, $first_col );
+
+ $this->templateRenderer->addField( '#', $rowindex );
+ $this->templateRenderer->packFieldsForTemplate( $this->template );
+
+ // str_replace('|', '&#x007C;', // encode '|' for use in templates (templates fail otherwise) --
+ // this is not the place for doing this, since even DV-Wikitexts contain proper "|"!
+ $contents[$first_letter][] = $this->templateRenderer->render();
+ } else { // build simple list
+ $first_col = true;
+ $contents[$first_letter][] = $this->row_to_contents( $row, $first_col );
+ }
+
+ $row = $nextrow;
+ $rowindex++;
+ }
+
+ // Make label for finding further results
+ if ( $this->linkFurtherResults( $res ) ) {
+ $contents[$last_letter][] = $this->getFurtherResultsLink( $res, $outputMode )->getText( SMW_OUTPUT_WIKI, $this->mLinker );
+ }
+
+ return $contents;
+ }
+
+ private function first_letter( QueryResult $res, DataItem $dataItem ) {
+
+ $sortKey = $dataItem->getSortKey();
+
+ if ( $dataItem->getDIType() === DataItem::TYPE_WIKIPAGE ) {
+ $sortKey = $res->getStore()->getWikiPageSortKey( $dataItem );
+ }
+
+ return $this->collator->getFirstLetter( $sortKey );
+ }
+
+ private function row_to_contents( $row, &$first_col ) {
+
+ // has anything but the first column been printed?
+ $found_values = false;
+ $result = '';
+
+ foreach ( $row as $field ) {
+ $first_value = true;
+ $fieldValues = [];
+
+ while ( ( $text = $field->getNextText( SMW_OUTPUT_WIKI, $this->getLinker( $first_col ) ) ) !== false ) {
+
+ // first values after first column
+ if ( !$first_col && !$found_values ) {
+ $result .= '(';
+ $found_values = true;
+ }
+
+ // first value in any column, print header
+ if ( $first_value ) {
+ $first_value = false;
+ $printRequest = $field->getPrintRequest();
+
+ if ( $this->mShowHeaders && ( $printRequest->getLabel() !== '' ) ) {
+ $linker = $this->mShowHeaders === SMW_HEADERS_PLAIN ? null : $this->mLinker;
+ $result .= $printRequest->getText( SMW_OUTPUT_WIKI, $linker );
+ $result .= ' ';
+ }
+ }
+
+ $fieldValues[] = $text;
+ }
+
+ $first_col = false;
+
+ // Always sort the column value list in the same order
+ natsort( $fieldValues );
+ $result .= implode( ( $this->delim ? $this->delim : ',' ) . ' ', $fieldValues ) . ' ';
+ }
+
+ if ( $found_values ) {
+ $result = trim( $result ) . ')';
+ }
+
+ return $result;
+ }
+
+ private function row_to_template( $row, $res, &$first_col ) {
+
+ // explicitly number parameters for more robust parsing (values may contain "=")
+ $i = 0;
+
+ foreach ( $row as $field ) {
+ $i++;
+
+ $fieldName = '';
+
+ if ( $this->params['named args'] ) {
+ $fieldName = $field->getPrintRequest()->getLabel();
+ }
+
+ if ( $fieldName === '' || $fieldName === '?' ) {
+ $fieldName = $fieldName . $i;
+ }
+
+ $fieldValues = [];
+
+ while ( ( $text = $field->getNextText( SMW_OUTPUT_WIKI, $this->getLinker( $first_col ) ) ) !== false ) {
+ $fieldValues[] = $text;
+ }
+
+ natsort( $fieldValues );
+
+ $this->templateRenderer->addField( $fieldName, implode( $this->delim . ' ', $fieldValues ) );
+ $first_col = false;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/CsvFileExportPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/CsvFileExportPrinter.php
new file mode 100644
index 00000000..1f67f12c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/CsvFileExportPrinter.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use Sanitizer;
+use SMW\Utils\Csv;
+use SMWQueryResult as QueryResult;
+
+/**
+ * CSV export support
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Nathan R. Yergler
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class CsvFileExportPrinter extends FileExportPrinter {
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return $this->msg( 'smw_printername_csv' )->text();
+ }
+
+ /**
+ * @see FileExportPrinter::getMimeType
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getMimeType( QueryResult $queryResult ) {
+ return 'text/csv';
+ }
+
+ /**
+ * @see FileExportPrinter::getFileName
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFileName( QueryResult $queryResult ) {
+ return $this->params['filename'];
+ }
+
+ /**
+ * @see ResultPrinter::getParamDefinitions
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $params = parent::getParamDefinitions( $definitions );
+
+ $definitions['searchlabel']->setDefault(
+ $this->msg( 'smw_csv_link' )->inContentLanguage()->text()
+ );
+
+ $params[] = [
+ 'name' => 'sep',
+ 'message' => 'smw-paramdesc-csv-sep',
+ 'default' => ',',
+ ];
+
+ $params['valuesep'] = [
+ 'message' => 'smw-paramdesc-csv-valuesep',
+ 'default' => ',',
+ ];
+
+ $params['showsep'] = [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'message' => 'smw-paramdesc-showsep',
+ ];
+
+ $params[] = [
+ 'name' => 'filename',
+ 'message' => 'smw-paramdesc-filename',
+ 'default' => 'result.csv',
+ ];
+
+ $params['merge'] = [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'message' => 'smw-paramdesc-csv-merge',
+ ];
+
+ $params['bom'] = [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'message' => 'smw-paramdesc-csv-bom',
+ ];
+
+ return $params;
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * {@inheritDoc}
+ */
+ protected function getResultText( QueryResult $res, $outputMode ) {
+
+ // Always return a link for when the output mode is not a file request,
+ // a file request is normally only initiated when resolving the query
+ // via Special:Ask
+ if ( $outputMode !== SMW_OUTPUT_FILE ) {
+ return $this->getCsvLink( $res, $outputMode );
+ }
+
+ $csv = new Csv(
+ $this->params['showsep'],
+ $this->params['bom']
+ );
+
+ return $this->getCsv( $csv, $res );
+ }
+
+ private function getCsvLink( QueryResult $res, $outputMode ) {
+
+ // Can be viewed as HTML if requested, no more parsing needed
+ $this->isHTML = $outputMode == SMW_OUTPUT_HTML;
+
+ $link = $this->getLink(
+ $res,
+ $outputMode
+ );
+
+ return $link->getText( $outputMode, $this->mLinker );
+ }
+
+ private function getCsv( Csv $csv, $res ) {
+
+ $sep = str_replace( '_', ' ', $this->params['sep'] );
+ $vsep = str_replace( '_', ' ', $this->params['valuesep'] );
+
+ $header = [];
+ $rows = [];
+
+ if ( $this->mShowHeaders ) {
+ foreach ( $res->getPrintRequests() as $pr ) {
+ $header[] = $pr->getLabel();
+ }
+ }
+
+ while ( $row = $res->getNext() ) {
+ $row_items = [];
+
+ foreach ( $row as /* SMWResultArray */ $field ) {
+ $growing = [];
+
+ while ( ( $object = $field->getNextDataValue() ) !== false ) {
+ $growing[] = Sanitizer::decodeCharReferences( $object->getWikiValue() );
+ }
+
+ $row_items[] = implode( $vsep, $growing );
+ }
+
+ $rows[] = $row_items;
+ }
+
+ if ( $this->params['merge'] === true ) {
+ $rows = $csv->merge( $rows, $vsep );
+ }
+
+ return $csv->toString(
+ $header,
+ $rows,
+ $sep
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/FeedExportPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/FeedExportPrinter.php
new file mode 100644
index 00000000..ece089d2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/FeedExportPrinter.php
@@ -0,0 +1,453 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use FeedItem;
+use ParserOptions;
+use Sanitizer;
+use SMW\DataValueFactory;
+use SMW\DIWikiPage;
+use SMW\Query\ExportPrinter;
+use SMW\Query\Result\StringResult;
+use SMW\Site;
+use SMWQueryResult as QueryResult;
+use TextContent;
+use Title;
+use WikiPage;
+
+/**
+ * Result printer that exports query results as RSS/Atom feed
+ *
+ * @since 1.8
+ *
+ * @license GNU GPL v2 or later
+ * @author mwjames
+ */
+final class FeedExportPrinter extends ResultPrinter implements ExportPrinter {
+
+ /**
+ * @var boolean
+ */
+ private $httpHeader = true;
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return $this->msg( 'smw-printername-feed' )->text();
+ }
+
+ /**
+ * @see ExportPrinter::isExportFormat
+ *
+ * {@inheritDoc}
+ */
+ public function isExportFormat() {
+ return true;
+ }
+
+ /**
+ * @see 3.0
+ */
+ public function disableHttpHeader() {
+ $this->httpHeader = false;
+ }
+
+ /**
+ * @see ExportPrinter::getMimeType
+ *
+ * {@inheritDoc}
+ */
+ public function getMimeType( QueryResult $queryResult ) {
+ return $this->params['type'] === 'atom' ? 'application/atom+xml' : 'application/rss+xml';
+ }
+
+ /**
+ * @see ExportPrinter::getFileName
+ *
+ * {@inheritDoc}
+ */
+ public function getFileName( QueryResult $queryResult ) {
+ return false;
+ }
+
+ /**
+ * @see ExportPrinter::outputAsFile
+ *
+ * {@inheritDoc}
+ */
+ public function outputAsFile( QueryResult $queryResult, array $params ) {
+ $result = $this->getResult( $queryResult, $params, SMW_OUTPUT_FILE );
+
+ if ( Site::isCommandLineMode() || $queryResult instanceof StringResult ) {
+
+ if ( $this->httpHeader ) {
+ header( 'Content-type: ' . 'text/xml' . '; charset=UTF-8' );
+ }
+
+ echo $result;
+ }
+ }
+
+ /**
+ * The export uses MODE_INSTANCES on special pages (so that instances are
+ * retrieved for the export) otherwise use MODE_NONE (displaying just a
+ * download link).
+ *
+ * @param $mode
+ *
+ * @return integer
+ */
+ public function getQueryMode( $mode ) {
+
+ if ( $mode == \SMWQueryProcessor::SPECIAL_PAGE ) {
+ return \SMWQuery::MODE_INSTANCES;
+ }
+ return \SMWQuery::MODE_NONE;
+
+ }
+
+ /**
+ * @see ResultPrinter::getParamDefinitions
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $params = parent::getParamDefinitions( $definitions );
+
+ $params['searchlabel']->setDefault( $this->msg( 'smw-label-feed-link' )->inContentLanguage()->text() );
+
+ $params['type'] = [
+ 'type' => 'string',
+ 'default' => 'rss',
+ 'message' => 'smw-paramdesc-feedtype',
+ 'values' => [ 'rss', 'atom' ],
+ ];
+
+ $params['title'] = [
+ 'message' => 'smw-paramdesc-feedtitle',
+ 'default' => '',
+ 'aliases' => [ 'rsstitle' ],
+ ];
+
+ $params['description'] = [
+ 'message' => 'smw-paramdesc-feeddescription',
+ 'default' => '',
+ 'aliases' => [ 'rssdescription' ],
+ ];
+
+ $params['page'] = [
+ 'message' => 'smw-paramdesc-feedpagecontent',
+ 'default' => 'none',
+ 'values' => [ 'none', 'full', 'abstract' ],
+ ];
+
+ return $params;
+ }
+
+ /**
+ * @since 2.5
+ * @see ResultPrinter::getDefaultSort
+ *
+ * {@inheritDoc}
+ */
+ public function getDefaultSort() {
+ return 'DESC';
+ }
+
+ /**
+ * Returns a string that is to be sent to the caller
+ *
+ * @param QueryResult $res
+ * @param integer $outputMode
+ *
+ * @return string
+ */
+ protected function getResultText( QueryResult $res, $outputMode ) {
+
+ if ( $outputMode !== SMW_OUTPUT_FILE ) {
+ return $this->getFeedLink( $res, $outputMode );
+ }
+
+ if ( $res->getCount() == 0 ){
+ $res->addErrors( [ $this->msg( 'smw_result_noresults' )->inContentLanguage()->text() ] );
+ }
+
+ return $this->getFeed( $res, $this->params['type'] );
+ }
+
+ /**
+ * Build a feed
+ *
+ * @since 1.8
+ *
+ * @param QueryResult $results
+ * @param $type
+ *
+ * @return string
+ */
+ protected function getFeed( QueryResult $results, $type ) {
+ global $wgFeedClasses;
+
+ if( !isset( $wgFeedClasses[$type] ) ) {
+ $results->addErrors( [ $this->msg( 'feed-invalid' )->inContentLanguage()->text() ] );
+ return '';
+ }
+
+ /**
+ * @var \ChannelFeed $feed
+ */
+ $feed = new $wgFeedClasses[$type](
+ $this->feedTitle(),
+ $this->feedDescription(),
+ $this->feedURL()
+ );
+
+ // Create feed header
+ if ( $this->httpHeader ) {
+ $feed->outHeader();
+ }
+
+ // Create feed items
+ while ( $row = $results->getNext() ) {
+ $feed->outItem( $this->feedItem( $row ) );
+ }
+
+ // Create feed footer
+ $feed->outFooter();
+ }
+
+ /**
+ * Returns feed title
+ *
+ * @since 1.8
+ *
+ * @return string
+ */
+ protected function feedTitle() {
+
+ if ( $this->params['title'] === '' ) {
+ return $GLOBALS['wgSitename'];
+ }
+
+ return $this->params['title'];
+ }
+
+ /**
+ * Returns feed description
+ *
+ * @since 1.8
+ *
+ * @return string
+ */
+ protected function feedDescription() {
+
+ if ( $this->params['description'] !== '' ) {
+ return $this->msg( 'smw-label-feed-description', $this->params['description'], $this->params['type'] )->text();
+ }
+
+ return $this->msg( 'tagline' )->text();
+ }
+
+ /**
+ * Returns feed URL
+ *
+ * @since 1.8
+ *
+ * @return string
+ */
+ protected function feedURL() {
+
+ if ( $GLOBALS['wgTitle'] instanceof Title ) {
+ return $GLOBALS['wgTitle']->getFullUrl();
+ }
+
+ return Title::newFromText( 'Feed' )->getFullUrl();
+ }
+
+ /**
+ * Returns feed item
+ *
+ * @since 1.8
+ *
+ * @param array $row
+ *
+ * @return array
+ */
+ protected function feedItem( array $row ) {
+
+ $rowItems = [];
+ $subject = false;
+
+ /**
+ * Loop over all properties within a row
+ *
+ * @var \SMWResultArray $field
+ * @var \SMWDataValue $object
+ */
+ foreach ( $row as $field ) {
+ $itemSegments = [];
+
+ $subject = $field->getResultSubject()->getTitle();
+
+ // Loop over all values for the property.
+ while ( ( $dataValue = $field->getNextDataValue() ) !== false ) {
+ if ( $dataValue->getDataItem() instanceof DIWikiPage ) {
+
+ $linker = null;
+
+ if ( $dataValue->getDataItem()->getSubobjectName() === '' && $this->params['link'] !== 'none' ) {
+ $linker = smwfGetLinker();
+ }
+
+ $itemSegments[] = Sanitizer::decodeCharReferences( $dataValue->getLongWikiText( $linker ) );
+ } else {
+ $itemSegments[] = Sanitizer::decodeCharReferences( $dataValue->getWikiValue() );
+ }
+ }
+
+ // Join all property values into a single string, separated by a comma
+ if ( $itemSegments !== [] ) {
+ $rowItems[] = $this->parse( $subject, implode( ', ', $itemSegments ) );
+ }
+ }
+
+ if ( $subject instanceof Title ) {
+ return $this->newFeedItem( $subject, $rowItems );
+ }
+
+ return [];
+ }
+
+ /**
+ * Returns page content
+ *
+ * @since 1.8
+ *
+ * @param WikiPage $wikiPage
+ *
+ * @return string
+ */
+ protected function getPageContent( WikiPage $wikiPage ) {
+
+ if ( !in_array( $this->params['page'], [ 'abstract', 'full' ] ) ) {
+ return '';
+ }
+
+ if ( method_exists( $wikiPage, 'getContent' ) ) {
+ $content = $wikiPage->getContent();
+
+ if ( $content instanceof TextContent ) {
+ $text = $content->getNativeData();
+ } else {
+ return '';
+ }
+ } else {
+ $text = $wikiPage->getText();
+ }
+
+ return $this->parse( $wikiPage->getTitle(), $text );
+ }
+
+ /**
+ * Feed item description and property value output manipulation
+ *
+ * @note FeedItem will do an FeedItem::xmlEncode therefore no need
+ * to be overly cautious here
+ *
+ * @since 1.8
+ *
+ * @param array $items
+ * @param string $pageContent
+ *
+ * @return string
+ */
+ protected function feedItemDescription( $items, $pageContent ) {
+
+ $text = FeedItem::stripComment( implode( '', $items ) ) . FeedItem::stripComment( $pageContent );
+
+ // Abstract of the first 200 chars
+ if ( $this->params['page'] === 'abstract' ) {
+ $text = preg_replace('/\s+?(\S+)?$/', '', substr( $text, 0, 201 ) ) . ' ...';
+ }
+
+ return $text;
+ }
+
+ /**
+ * According to MW documentation, the comment field is only implemented for RSS
+ *
+ * @since 1.8
+ *
+ * @return string
+ */
+ protected function feedItemComments( ) {
+ return '';
+ }
+
+ private function newFeedItem( $title, $rowItems ) {
+ $wikiPage = WikiPage::newFromID( $title->getArticleID() );
+
+ if ( $wikiPage !== null && $wikiPage->exists() ){
+
+ // #1741
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem(
+ DIWikipage::newFromTitle( $title )
+ );
+
+ // Ensures that the namespace prefix (Help:...) is used in cases where
+ // no display title is available.
+ $dataValue->setOption( 'prefixed.preferred.caption', true );
+
+ $feedItem = new FeedItem(
+ $dataValue->getPreferredCaption(),
+ $this->feedItemDescription( $rowItems, $this->getPageContent( $wikiPage ) ),
+ $title->getFullURL(),
+ $wikiPage->getTimestamp(),
+ $wikiPage->getUserText(),
+ $this->feedItemComments()
+ );
+ } else {
+ // #1562
+ $feedItem = new FeedItem(
+ $title->getPrefixedText(),
+ '',
+ $title->getFullURL()
+ );
+ }
+
+ return $feedItem;
+ }
+
+ private function parse( Title $title = null, $text ) {
+
+ if ( $title === null ) {
+ return $text;
+ }
+
+ $parserOptions = new ParserOptions();
+
+ // FIXME: Remove the if block once compatibility with MW <1.31 is dropped
+ if ( ! defined( '\ParserOutput::SUPPORTS_STATELESS_TRANSFORMS' ) || \ParserOutput::SUPPORTS_STATELESS_TRANSFORMS !== 1 ) {
+ $parserOptions->setEditSection( false );
+ }
+
+ return $GLOBALS['wgParser']->parse( $text, $title, $parserOptions )->getText( [ 'enableSectionEditLinks' => false ] );
+ }
+
+ private function getFeedLink( QueryResult $res, $outputMode ) {
+
+ // Can be viewed as HTML if requested, no more parsing needed
+ $this->isHTML = $outputMode == SMW_OUTPUT_HTML;
+
+ $link = $this->getLink(
+ $res,
+ $outputMode
+ );
+
+ return $link->getText( $outputMode, $this->mLinker );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/FileExportPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/FileExportPrinter.php
new file mode 100644
index 00000000..b58e62ad
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/FileExportPrinter.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use SMW\Query\ExportPrinter;
+use SMWQuery;
+use SMWQueryProcessor;
+use SMWQueryResult;
+
+/**
+ * Base class for file export result printers
+ *
+ * @since 1.8
+ * @license GNU GPL v2+
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+abstract class FileExportPrinter extends ResultPrinter implements ExportPrinter {
+
+ /**
+ * @var boolean
+ */
+ private $httpHeader = true;
+
+ /**
+ * @see ExportPrinter::isExportFormat
+ *
+ * @since 1.8
+ *
+ * @return boolean
+ */
+ public function isExportFormat() {
+ return true;
+ }
+
+ /**
+ * @see 3.0
+ */
+ public function disableHttpHeader() {
+ $this->httpHeader = false;
+ }
+
+ /**
+ * @see ExportPrinter::outputAsFile
+ *
+ * @since 1.8
+ *
+ * @param SMWQueryResult $queryResult
+ * @param array $params
+ */
+ public function outputAsFile( SMWQueryResult $queryResult, array $params ) {
+ $result = $this->getResult( $queryResult, $params, SMW_OUTPUT_FILE );
+
+ if ( $this->httpHeader ) {
+ header( 'Content-type: ' . $this->getMimeType( $queryResult ) . '; charset=UTF-8' );
+ }
+
+ $fileName = $this->getFileName( $queryResult );
+
+ if ( $fileName !== false ) {
+ $utf8Name = rawurlencode( $fileName );
+ $fileName = iconv( "UTF-8", "ASCII//TRANSLIT", $fileName );
+
+ if ( $this->httpHeader ) {
+ header( "content-disposition: attachment; filename=\"$fileName\"; filename*=UTF-8''$utf8Name;" );
+ }
+ }
+
+ echo $result;
+ }
+
+ /**
+ * @see ExportPrinter::getFileName
+ *
+ * @since 1.8
+ *
+ * @param SMWQueryResult $queryResult
+ *
+ * @return string|boolean
+ */
+ public function getFileName( SMWQueryResult $queryResult ) {
+ return false;
+ }
+
+ /**
+ * File exports use MODE_INSTANCES on special pages (so that instances are
+ * retrieved for the export) and MODE_NONE otherwise (displaying just a download link).
+ *
+ * @param $mode
+ *
+ * @return integer
+ */
+ public function getQueryMode( $mode ) {
+ return $mode == SMWQueryProcessor::SPECIAL_PAGE ? SMWQuery::MODE_INSTANCES : SMWQuery::MODE_NONE;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter.php
new file mode 100644
index 00000000..6c24bc68
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use ParamProcessor\ParamDefinition;
+use SMW\Message;
+use SMW\Query\ResultPrinters\ListResultPrinter\ListResultBuilder;
+use SMWQueryResult;
+
+/**
+ * Print query results in lists.
+ *
+ * @license GNU GPL v2+
+ *
+ * @author Markus Krötzsch
+ */
+
+/**
+ * SMW's printer for results in lists.
+ * The implementation covers comma-separated lists, ordered and unordered lists.
+ * List items may be formatted using templates.
+ *
+ * In the code below, one list item (with all extra information displayed for
+ * it) is called a "row", while one entry in this row is called a "field".
+ * Every field may in turn contain many "values".
+ */
+class ListResultPrinter extends ResultPrinter {
+
+ /**
+ * Get a human readable label for this printer.
+ *
+ * @return string
+ */
+ public function getName() {
+ // Give grep a chance to find the usages:
+ // smw_printername_list, smw_printername_ol,smw_printername_ul, smw_printername_plainlist, smw_printername_template
+ return Message::get( 'smw_printername_' . $this->mFormat, Message::TEXT, Message::USER_LANGUAGE );
+ }
+
+ /**
+ * @see ResultPrinter::isDeferrable
+ *
+ * {@inheritDoc}
+ */
+ public function isDeferrable() {
+ return true;
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * @param SMWQueryResult $queryResult
+ * @param $outputMode
+ *
+ * @return string
+ */
+ protected function getResultText( SMWQueryResult $queryResult, $outputMode ) {
+
+ $builder = $this->getBuilder( $queryResult );
+
+ $this->hasTemplates = $this->hasTemplates();
+
+ return $builder->getResultText() . $this->getFurtherResultsText( $queryResult, $outputMode );
+ }
+
+ /**
+ * @param SMWQueryResult $queryResult
+ *
+ * @return ListResultBuilder
+ */
+ private function getBuilder( SMWQueryResult $queryResult ) {
+
+ $builder = new ListResultBuilder( $queryResult, $this->mLinker );
+
+ $builder->set( $this->params );
+
+ $builder->set( [
+ 'link-first' => $this->mLinkFirst,
+ 'link-others' => $this->mLinkOthers,
+ 'show-headers' => $this->mShowHeaders,
+ ] );
+
+ if ( $this->params[ 'template' ] !== '' && isset( $this->fullParams[ 'sep' ] ) && $this->fullParams[ 'sep' ]->wasSetToDefault() === true ) {
+ $builder->set( 'sep', '' );
+ }
+
+ return $builder;
+ }
+
+ /**
+ * @return bool
+ */
+ private function hasTemplates() {
+ return $this->params[ 'template' ] !== '' || $this->params[ 'introtemplate' ] !== '' || $this->params[ 'outrotemplate' ] !== '';
+ }
+
+
+ /**
+ * Get text for further results link. Used only during getResultText().
+ *
+ * @since 1.9
+ * @param SMWQueryResult $res
+ * @param integer $outputMode
+ * @return string
+ */
+ private function getFurtherResultsText( SMWQueryResult $res, $outputMode ) {
+
+ if ( $this->linkFurtherResults( $res) ) {
+
+ $link = $this->getFurtherResultsLink( $res, $outputMode );
+ return $link->getText( SMW_OUTPUT_WIKI, $this->mLinker );
+
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function supportsRecursiveAnnotation() {
+ return true;
+ }
+
+ /**
+ * @see SMWIResultPrinter::getParamDefinitions
+ *
+ * @since 3.0
+ *
+ * @param ParamDefinition[] $definitions
+ *
+ * @return ParamDefinition[]
+ * @throws \Exception
+ */
+ public function getParamDefinitions( array $definitions ) {
+
+ $listFormatDefinitions = [
+
+ 'propsep' => [
+ 'message' => 'smw-paramdesc-propsep',
+ 'default' => Message::get( 'smw-format-list-property-separator' ),
+ ],
+
+ 'valuesep' => [
+ 'message' => 'smw-paramdesc-valuesep',
+ 'default' => Message::get( 'smw-format-list-value-separator' ),
+ ],
+
+ 'template' => [
+ 'message' => 'smw-paramdesc-template',
+ 'default' => '',
+ 'trim' => true,
+ ],
+
+ 'named args' => [
+ 'type' => 'boolean',
+ 'message' => 'smw-paramdesc-named_args',
+ 'default' => false,
+ ],
+
+ 'userparam' => [
+ 'message' => 'smw-paramdesc-userparam',
+ 'default' => '',
+ ],
+
+ 'class' => [
+ 'message' => 'smw-paramdesc-class',
+ 'default' => '',
+ ],
+
+ 'introtemplate' => [
+ 'message' => 'smw-paramdesc-introtemplate',
+ 'default' => '',
+ ],
+
+ 'outrotemplate' => [
+ 'message' => 'smw-paramdesc-outrotemplate',
+ 'default' => '',
+ ],
+
+ ];
+
+ if ( $this->mFormat !== 'ul' && $this->mFormat !== 'ol' ) {
+
+ $listFormatDefinitions[ 'sep' ] =
+ [
+ 'message' => 'smw-paramdesc-sep',
+ 'default' => ', ',
+ ];
+ }
+
+ return array_merge( $definitions, ParamDefinition::getCleanDefinitions( $listFormatDefinitions ) );
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ListResultBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ListResultBuilder.php
new file mode 100644
index 00000000..6cf2bfc1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ListResultBuilder.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace SMW\Query\ResultPrinters\ListResultPrinter;
+
+use Linker;
+use SMW\Message;
+use SMWQueryResult;
+
+/**
+ * Class ListResultBuilder
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Stephan Gambke
+ */
+class ListResultBuilder {
+
+ private static $defaultConfigurations = [
+ '*' => [
+ 'value-open-tag' => '<span class="smw-value">',
+ 'value-close-tag' => '</span>',
+ 'field-open-tag' => '<span class="smw-field">',
+ 'field-close-tag' => '</span>',
+ 'field-label-open-tag' => '<span class="smw-field-label">',
+ 'field-label-close-tag' => '</span>',
+ 'field-label-separator' => ': ',
+ 'other-fields-open' => ' (',
+ 'other-fields-close' => ')',
+ ],
+ 'list' => [
+ 'row-open-tag' => '<span class="smw-row">',
+ 'row-close-tag' => '</span>',
+ 'result-open-tag' => '<span class="smw-format list-format $CLASS$">',
+ 'result-close-tag' => '</span>',
+ ],
+ 'ol' => [
+ 'row-open-tag' => "<li class=\"smw-row\">",
+ 'row-close-tag' => '</li>',
+ 'result-open-tag' => '<ol class="smw-format ol-format $CLASS$" start="$START$">',
+ 'result-close-tag' => '</ol>',
+ ],
+ 'ul' => [
+ 'row-open-tag' => '<li class="smw-row">',
+ 'row-close-tag' => '</li>',
+ 'result-open-tag' => '<ul class="smw-format ul-format $CLASS$">',
+ 'result-close-tag' => '</ul>',
+ ],
+ 'plainlist' => [
+ 'value-open-tag' => '',
+ 'value-close-tag' => '',
+ 'field-open-tag' => '',
+ 'field-close-tag' => '',
+ 'field-label-open-tag' => '',
+ 'field-label-close-tag' => '',
+ 'row-open-tag' => '',
+ 'row-close-tag' => '',
+ 'result-open-tag' => '',
+ 'result-close-tag' => '',
+ ],
+ ];
+
+ /** @var Linker|null */
+ private $linker = null;
+
+ /** @var SMWQueryResult */
+ private $queryResult;
+
+ /** @var ParameterDictionary */
+ private $configuration;
+
+ private $templateRendererFactory;
+
+ /**
+ * ListResultBuilder constructor.
+ *
+ * @param SMWQueryResult $queryResult
+ * @param Linker $linker
+ */
+ public function __construct( SMWQueryResult $queryResult, Linker $linker ) {
+ $this->linker = $linker;
+ $this->queryResult = $queryResult;
+ $this->configuration = new ParameterDictionary();
+ }
+
+ /**
+ * @return string
+ */
+ public function getResultText() {
+
+ $this->prepareBuilt();
+
+ return
+ $this->getTemplateCall( 'introtemplate' ) .
+ $this->get( 'result-open-tag' ) .
+
+ join( $this->get( 'sep' ), $this->getRowTexts() ) .
+
+ $this->get( 'result-close-tag' ) .
+ $this->getTemplateCall( 'outrotemplate' );
+ }
+
+ private function prepareBuilt() {
+
+ $format = $this->getEffectiveFormat();
+
+ $this->configuration->setDefault(
+ array_merge(
+ self::$defaultConfigurations[ '*' ],
+ self::$defaultConfigurations[ $format ],
+ $this->getDefaultsFromI18N() )
+ );
+
+ if ( $this->get( 'template' ) !== '' ) {
+
+ $this->set( [ 'value-open-tag' => '', 'value-close-tag' => '' ] );
+
+ }
+
+ $this->set( 'result-open-tag', $this->replaceVariables( $this->get( 'result-open-tag' ) ) );
+ }
+
+ /**
+ * @return string
+ */
+ private function getEffectiveFormat() {
+
+ $format = $this->get( 'format' );
+
+ if ( in_array( $format, [ 'ol', 'ul', 'plainlist' ] ) ) {
+ return $format;
+ }
+
+ if ( $this->get( 'template' ) !== '' ) {
+ return 'plainlist';
+ }
+
+ return 'list';
+ }
+
+ /**
+ * @param string $setting
+ * @param string $default
+ *
+ * @return mixed
+ */
+ protected function get( $setting, $default = '' ) {
+ return $this->configuration->get( $setting, $default );
+ }
+
+ /**
+ * @param string|string[] $setting
+ * @param string|null $value
+ */
+ public function set( $setting, $value = null ) {
+ $this->configuration->set( $setting, $value );
+ }
+
+ /**
+ * @return string[]
+ */
+ private function getDefaultsFromI18N() {
+ return [
+ 'field-label-separator' => Message::get( 'smw-format-list-field-label-separator' ),
+ 'other-fields-open' => Message::get( 'smw-format-list-other-fields-open' ),
+ 'other-fields-close' => Message::get( 'smw-format-list-other-fields-close' ),
+ ];
+ }
+
+ /**
+ * @param string $subject
+ *
+ * @return string
+ */
+ private function replaceVariables( $subject ) {
+ return str_replace( [ '$START$', '$CLASS$' ], [ htmlspecialchars( $this->get( 'offset' ) + 1 ), htmlspecialchars( $this->get( 'class' ) ) ], $subject );
+ }
+
+ /**
+ * @param string $param
+ *
+ * @return string
+ */
+ private function getTemplateCall( $param ) {
+
+ $templatename = $this->get( $param );
+
+ if ( $templatename === '' ) {
+ return '';
+ }
+
+ $templateRenderer = $this->getTemplateRendererFactory()->getTemplateRenderer();
+ $templateRenderer->packFieldsForTemplate( $templatename );
+
+ return $templateRenderer->render();
+
+ }
+
+ /**
+ * @return TemplateRendererFactory
+ */
+ private function getTemplateRendererFactory() {
+
+ if ( $this->templateRendererFactory === null ) {
+ $this->templateRendererFactory = new TemplateRendererFactory( $this->getQueryResult() );
+ $this->templateRendererFactory->setUserparam( $this->get( 'userparam' ) );
+ }
+
+ return $this->templateRendererFactory;
+ }
+
+ /**
+ * @return SMWQueryResult
+ */
+ private function getQueryResult() {
+ return $this->queryResult;
+ }
+
+ /**
+ * @return string[]
+ */
+ private function getRowTexts() {
+
+ $queryResult = $this->getQueryResult();
+ $queryResult->reset();
+
+ $rowTexts = [];
+ $num = $queryResult->getQuery()->getOffset();
+ $rowBuilder = $this->getRowBuilder();
+
+ while ( ( $row = $queryResult->getNext() ) !== false ) {
+
+ $rowTexts[] =
+ $this->get( 'row-open-tag' ) .
+ $rowBuilder->getRowText( $row, $num ) .
+ $this->get( 'row-close-tag' );
+
+ $num++;
+ }
+
+ return $rowTexts;
+ }
+
+ /**
+ * @return RowBuilder
+ */
+ private function getRowBuilder() {
+
+ if ( $this->get( 'template' ) === '' ) {
+ $rowBuilder = new SimpleRowBuilder();
+ $rowBuilder->setLinker( $this->linker );
+ } else {
+ $rowBuilder = new TemplateRowBuilder( $this->getTemplateRendererFactory() );
+ }
+
+ $valueTextsBuilder = new ValueTextsBuilder();
+ $valueTextsBuilder->setLinker( $this->linker );
+ $valueTextsBuilder->setConfiguration( $this->configuration );
+
+ $rowBuilder->setValueTextsBuilder( $valueTextsBuilder );
+ $rowBuilder->setConfiguration( $this->configuration );
+
+ return $rowBuilder;
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ParameterDictionary.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ParameterDictionary.php
new file mode 100644
index 00000000..0f25baed
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ParameterDictionary.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace SMW\Query\ResultPrinters\ListResultPrinter;
+
+/**
+ * Class ParameterDictionary
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Stephan Gambke
+ */
+class ParameterDictionary {
+
+ private $configuration = [];
+
+ /**
+ * @param string|string[] $setting
+ * @param mixed $value
+ */
+ public function set( $setting, $value = null ) {
+
+ if ( !is_array( $setting ) ) {
+ $setting = [ $setting => $value ];
+ }
+
+ $this->configuration = array_replace( $this->configuration, $setting );
+ }
+
+ /**
+ * @param string $setting
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function get( $setting, $default = '' ) {
+ return isset( $this->configuration[ $setting ] ) ? $this->configuration[ $setting ] : $default;
+ }
+
+ /**
+ * @param string|string[] $setting
+ * @param mixed $value
+ */
+ public function setDefault( $setting, $value = null ) {
+
+ if ( !is_array( $setting ) ) {
+ $setting = [ $setting => $value ];
+ }
+
+ $this->configuration = array_replace( $setting, $this->configuration );
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ParameterDictionaryUser.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ParameterDictionaryUser.php
new file mode 100644
index 00000000..1ba8588d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ParameterDictionaryUser.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace SMW\Query\ResultPrinters\ListResultPrinter;
+
+/**
+ * Class RowBuilder
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Stephan Gambke
+ */
+trait ParameterDictionaryUser {
+
+ /** @var ParameterDictionary */
+ private $configuration;
+
+ /**
+ * @param ParameterDictionary $configuration
+ */
+ public function setConfiguration( ParameterDictionary &$configuration ) {
+ $this->configuration = $configuration;
+ }
+
+ /**
+ * @param string $setting
+ * @param string $default
+ *
+ * @return mixed
+ */
+ protected function get( $setting, $default = '' ) {
+ return $this->configuration->get( $setting, $default );
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/RowBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/RowBuilder.php
new file mode 100644
index 00000000..c0f5b14c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/RowBuilder.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace SMW\Query\ResultPrinters\ListResultPrinter;
+
+/**
+ * Class RowBuilder
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Stephan Gambke
+ */
+abstract class RowBuilder {
+
+ use ParameterDictionaryUser;
+
+ private $valueTextsBuilder;
+
+ /**
+ * @param \SMWResultArray[] $fields
+ *
+ * @param int $rownum
+ *
+ * @return string
+ */
+ abstract public function getRowText( array $fields, $rownum = 0 );
+
+ /**
+ * @return mixed
+ */
+ protected function getValueTextsBuilder() {
+ return $this->valueTextsBuilder;
+ }
+
+ /**
+ * @param mixed $valueTextsBuilder
+ */
+ public function setValueTextsBuilder( $valueTextsBuilder ) {
+ $this->valueTextsBuilder = $valueTextsBuilder;
+ }
+
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/SimpleRowBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/SimpleRowBuilder.php
new file mode 100644
index 00000000..fc37c46a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/SimpleRowBuilder.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace SMW\Query\ResultPrinters\ListResultPrinter;
+
+use Linker;
+use SMWResultArray;
+
+/**
+ * Class SimpleRowBuilder
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Stephan Gambke
+ */
+class SimpleRowBuilder extends RowBuilder {
+
+ private $linker;
+
+ /**
+ * @param \SMWResultArray[] $fields
+ *
+ * @param int $rownum
+ *
+ * @return string
+ */
+ public function getRowText( array $fields, $rownum = 0 ) {
+
+ $fieldTexts = $this->getFieldTexts( $fields );
+
+ $firstFieldText = array_shift( $fieldTexts );
+
+ if ( $firstFieldText === null ) {
+ return '';
+ }
+
+ if ( count( $fieldTexts ) > 0 ) {
+
+ $otherFieldsText =
+ $this->get( 'other-fields-open' ) .
+ join( $this->get( 'propsep' ), $fieldTexts ) .
+ $this->get( 'other-fields-close' );
+
+ } else {
+ $otherFieldsText = '';
+ }
+
+ return
+ $firstFieldText .
+ $otherFieldsText;
+ }
+
+ /**
+ * @param string[] $fields
+ *
+ * @return array
+ */
+ private function getFieldTexts( array $fields ) {
+
+ $columnNumber = 0;
+ $fieldTexts = [];
+
+ foreach ( $fields as $field ) {
+
+ $valuesText = $this->getValueTextsBuilder()->getValuesText( $field, $columnNumber );
+
+ if ( $valuesText !== '' ) {
+ $fieldTexts[] =
+ $this->get( 'field-open-tag' ) .
+ $this->getFieldLabel( $field ) .
+ $valuesText .
+ $this->get( 'field-close-tag' );
+ }
+
+ $columnNumber++;
+ }
+
+ return $fieldTexts;
+ }
+
+ /**
+ * @param SMWResultArray $field
+ *
+ * @return string
+ */
+ private function getFieldLabel( SMWResultArray $field ) {
+
+ $showHeaders = $this->get( 'show-headers' );
+
+ if ( $showHeaders === SMW_HEADERS_HIDE || $field->getPrintRequest()->getLabel() === '' ) {
+ return '';
+ }
+
+ $linker = $showHeaders === SMW_HEADERS_PLAIN ? null : $this->getLinker();
+
+ return
+ $this->get( 'field-label-open-tag' ) .
+ $field->getPrintRequest()->getText( SMW_OUTPUT_WIKI, $linker ) .
+ $this->get( 'field-label-close-tag' ) .
+ $this->get( 'field-label-separator' );
+
+ }
+
+ /**
+ * @return Linker
+ */
+ protected function getLinker() {
+ return $this->linker;
+ }
+
+ /**
+ * @param Linker $linker
+ */
+ public function setLinker( Linker $linker ) {
+ $this->linker = $linker;
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/TemplateRendererFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/TemplateRendererFactory.php
new file mode 100644
index 00000000..ce689032
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/TemplateRendererFactory.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace SMW\Query\ResultPrinters\ListResultPrinter;
+
+use SMW\ApplicationFactory;
+use SMW\MediaWiki\Renderer\WikitextTemplateRenderer;
+
+/**
+ * Class TemplateRendererFactory
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Stephan Gambke
+ */
+class TemplateRendererFactory {
+
+ private $templateRenderer;
+
+ private $queryResult;
+ private $numberOfPages;
+ private $userparam = '';
+
+ /**
+ * TemplateRendererFactory constructor.
+ *
+ * @param $queryResult
+ */
+ public function __construct( $queryResult ) {
+ $this->queryResult = $queryResult;
+ }
+
+ /**
+ * @param mixed $userparam
+ */
+ public function setUserparam( $userparam ) {
+ $this->userparam = $userparam;
+ }
+
+ /**
+ * @return WikitextTemplateRenderer
+ */
+ public function getTemplateRenderer() {
+
+ if ( $this->templateRenderer === null ) {
+ $this->templateRenderer = ApplicationFactory::getInstance()->newMwCollaboratorFactory()->newWikitextTemplateRenderer();
+ $this->addCommonTemplateFields( $this->templateRenderer );
+ }
+
+ return clone( $this->templateRenderer );
+ }
+
+ /**
+ * @param WikitextTemplateRenderer $templateRenderer
+ */
+ private function addCommonTemplateFields( WikitextTemplateRenderer $templateRenderer ) {
+
+ if ( $this->userparam !== '' ) {
+
+ $templateRenderer->addField(
+ '#userparam',
+ $this->userparam
+ );
+ }
+
+ $query = $this->getQueryResult()->getQuery();
+
+ $templateRenderer->addField(
+ '#querycondition',
+ $query->getQueryString()
+ );
+
+ $templateRenderer->addField(
+ '#querylimit',
+ $query->getLimit()
+ );
+
+ $templateRenderer->addField(
+ '#resultoffset',
+ $query->getOffset()
+ );
+
+ $templateRenderer->addField(
+ '#rowcount',
+ $this->getRowCount()
+ //$query->getCount() // FIXME: Re-activate if another query takes too long.
+ );
+ }
+
+ /**
+ * @return \SMWQueryResult
+ */
+ private function getQueryResult() {
+ return $this->queryResult;
+ }
+
+ /**
+ * @return int
+ */
+ private function getRowCount() {
+
+ if ( $this->numberOfPages === null ) {
+
+ $queryResult = $this->getQueryResult();
+
+ $countQuery = \SMWQueryProcessor::createQuery( $queryResult->getQueryString(), \SMWQueryProcessor::getProcessedParams( [] ) );
+ $countQuery->querymode = \SMWQuery::MODE_COUNT;
+
+ $countQueryResult = $queryResult->getStore()->getQueryResult( $countQuery );
+
+ $this->numberOfPages = $countQueryResult instanceof \SMWQueryResult ? $countQueryResult->getCountValue() : $countQueryResult;
+ }
+
+ return $this->numberOfPages;
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/TemplateRowBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/TemplateRowBuilder.php
new file mode 100644
index 00000000..68abca19
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/TemplateRowBuilder.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace SMW\Query\ResultPrinters\ListResultPrinter;
+
+use SMWResultArray;
+
+/**
+ * Class TemplateRowBuilder
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Stephan Gambke
+ */
+class TemplateRowBuilder extends RowBuilder {
+
+ private $templateRendererFactory;
+
+ /**
+ * TemplateRowBuilder constructor.
+ *
+ * @param TemplateRendererFactory $templateRendererFactory
+ */
+ public function __construct( TemplateRendererFactory $templateRendererFactory ) {
+ $this->templateRendererFactory = $templateRendererFactory;
+ }
+
+ /**
+ * Returns text for one result row, formatted as a template call.
+ *
+ * @param \SMWResultArray[] $fields
+ *
+ * @param int $rownum
+ *
+ * @return string
+ */
+ public function getRowText( array $fields, $rownum = 0 ) {
+
+ $templateRenderer = $this->templateRendererFactory->getTemplateRenderer();
+
+ foreach ( $fields as $column => $field ) {
+
+ $fieldLabel = $this->getFieldLabel( $field, $column );
+ $fieldText = $this->getValueTextsBuilder()->getValuesText( $field, $column );
+
+ $templateRenderer->addField( $fieldLabel, $fieldText );
+ }
+
+ /** @deprecated since SMW 3.0 */
+ $templateRenderer->addField( '#', $rownum );
+
+ $templateRenderer->addField( '#rownumber', $rownum + 1 );
+ $templateRenderer->packFieldsForTemplate( $this->get( 'template' ) );
+
+ return $templateRenderer->render();
+
+ }
+
+ /**
+ * @param SMWResultArray $field
+ * @param int $column
+ *
+ * @return string
+ */
+ private function getFieldLabel( SMWResultArray $field, $column ) {
+
+ if ( $this->get( 'named args' ) === false ) {
+ return intval( $column + 1 );
+ }
+
+ $label = $field->getPrintRequest()->getLabel();
+
+ if ( $label === '' ) {
+ return intval( $column + 1 );
+ }
+
+ return $label;
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ValueTextsBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ValueTextsBuilder.php
new file mode 100644
index 00000000..f328772d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ListResultPrinter/ValueTextsBuilder.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace SMW\Query\ResultPrinters\ListResultPrinter;
+
+use Linker;
+use SMWDataValue;
+use SMWResultArray;
+
+/**
+ * Class ValueTextsBuilder
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Stephan Gambke
+ */
+class ValueTextsBuilder {
+
+ use ParameterDictionaryUser;
+
+ private $linker;
+
+ /**
+ * @param SMWResultArray $field
+ * @param int $column
+ *
+ * @return string
+ */
+ public function getValuesText( SMWResultArray $field, $column = 0 ) {
+
+ $valueTexts = $this->getValueTexts( $field, $column );
+
+ return join( $this->get( 'valuesep' ), $valueTexts );
+
+ }
+
+ /**
+ * @param SMWResultArray $field
+ * @param int $column
+ *
+ * @return string[]
+ */
+ private function getValueTexts( SMWResultArray $field, $column ) {
+
+ $valueTexts = [];
+
+ $field->reset();
+
+ while ( ( $dataValue = $field->getNextDataValue() ) !== false ) {
+
+ $valueTexts[] =
+ $this->get( 'value-open-tag' ) .
+ $this->getValueText( $dataValue, $column ) .
+ $this->get( 'value-close-tag' );
+ }
+
+ return $valueTexts;
+ }
+
+ /**
+ * @param SMWDataValue $value
+ * @param int $column
+ *
+ * @return string
+ */
+ private function getValueText( SMWDataValue $value, $column = 0 ) {
+
+ $text = $value->getShortText( SMW_OUTPUT_WIKI, $this->getLinkerForColumn( $column ) );
+
+ return $this->sanitizeValueText( $text );
+ }
+
+ /**
+ * Depending on current linking settings, returns a linker object
+ * for making hyperlinks or NULL if no links should be created.
+ *
+ * @param int $columnNumber Column number
+ *
+ * @return \Linker|null
+ */
+ private function getLinkerForColumn( $columnNumber ) {
+
+ if ( ( $columnNumber === 0 && $this->get( 'link-first' ) ) ||
+ ( $columnNumber > 0 && $this->get( 'link-others' ) ) ) {
+ return $this->getLinker();
+ }
+
+ return null;
+ }
+
+ /**
+ * @return Linker
+ */
+ protected function getLinker() {
+ return $this->linker;
+ }
+
+ /**
+ * @param Linker $linker
+ */
+ public function setLinker( Linker $linker ) {
+ $this->linker = $linker;
+ }
+
+ /**
+ * @param $text
+ *
+ * @return string
+ */
+ private function sanitizeValueText( $text ) {
+
+ if ( $this->isSimpleList() ) {
+ return $text;
+ }
+
+ return \Sanitizer::removeHTMLtags( $text, null, [], [], [ 'table', 'tr', 'th', 'td', 'dl', 'dd', 'ul', 'li', 'ol' ] );
+ }
+
+ /**
+ * @return bool
+ */
+ private function isSimpleList() {
+ $format = $this->get( 'format' );
+ return $format !== 'ul' && $format !== 'ol';
+ }
+
+} \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/NullResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/NullResultPrinter.php
new file mode 100644
index 00000000..66f2ba3f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/NullResultPrinter.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use SMWQueryResult as QueryResult;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class NullResultPrinter extends ResultPrinter {
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return 'null';
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * {@inheritDoc}
+ */
+ protected function getResultText( QueryResult $queryResult, $outputMode ) {
+ return '';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ResultPrinter.php
new file mode 100644
index 00000000..55845919
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/ResultPrinter.php
@@ -0,0 +1,749 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use Linker;
+use ParamProcessor\ParamDefinition;
+use ParserOptions;
+use Sanitizer;
+use SMW\Message;
+use SMW\Parser\RecursiveTextProcessor;
+use SMW\Query\Result\StringResult;
+use SMW\Query\ResultPrinter as IResultPrinter;
+use SMWInfolink;
+use SMWOutputs as ResourceManager;
+use SMWQuery;
+use SMWQueryResult as QueryResult;
+use Title;
+
+/**
+ * Abstract base class for SMW's novel query printing mechanism. It implements
+ * part of the former functionality of SMWInlineQuery (everything related to
+ * output formatting and the corresponding parameters) and is subclassed by concrete
+ * printers that provide the main formatting functionality.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author mwjames
+ */
+abstract class ResultPrinter implements IResultPrinter {
+
+ /**
+ * Individual printers can decide what sort of deferrable mode is used for
+ * the output. `DEFERRED_DATA` signals that the format expects only the data
+ * component to be loaded from the backend.
+ */
+ const DEFERRED_DATA = 'deferred.data';
+
+ /**
+ * List of parameters, set by handleParameters.
+ * param name (lower case, trimmed) => param value (mixed)
+ *
+ * @since 1.7
+ *
+ * @var array
+ */
+ protected $params;
+
+ /**
+ * List of parameters, set by handleParameters.
+ * param name (lower case, trimmed) => IParam object
+ *
+ * @since 1.8
+ *
+ * @var \IParam[]
+ */
+ protected $fullParams;
+
+ /**
+ * @since 1.8
+ *
+ * @var
+ */
+ protected $outputMode;
+
+ /**
+ * The query result being displayed.
+ *
+ * @since 1.8
+ *
+ * @var QueryResult
+ */
+ protected $results;
+
+ /**
+ * Text to print *before* the output in case it is *not* empty; assumed to be wikitext.
+ * Normally this is handled in SMWResultPrinter and can be ignored by subclasses.
+ */
+ protected $mIntro = '';
+
+ /**
+ * Text to print *after* the output in case it is *not* empty; assumed to be wikitext.
+ * Normally this is handled in SMWResultPrinter and can be ignored by subclasses.
+ */
+ protected $mOutro = '';
+
+ /**
+ * Text to use for link to further results, or empty if link should not be shown.
+ * Unescaped! Use @see SMWResultPrinter::getSearchLabel()
+ * and @see SMWResultPrinter::linkFurtherResults()
+ * instead of accessing this directly.
+ */
+ protected $mSearchlabel = null;
+
+ /** Default return value for empty queries. Unescaped. Normally not used in sub-classes! */
+ protected $mDefault = '';
+
+ // parameters relevant for printers in general:
+ protected $mFormat; // a string identifier describing a valid format
+ protected $mLinkFirst; // should article names of the first column be linked?
+ protected $mLinkOthers; // should article names of other columns (besides the first) be linked?
+ protected $mShowHeaders = SMW_HEADERS_SHOW; // should the headers (property names) be printed?
+ protected $mShowErrors = true; // should errors possibly be printed?
+ protected $mInline; // is this query result "inline" in some page (only then a link to unshown results is created, error handling may also be affected)
+ protected $mLinker; // Linker object as needed for making result links. Might come from some skin at some time.
+
+ /**
+ * List of errors that occurred while processing the parameters.
+ *
+ * @since 1.6
+ *
+ * @var array
+ */
+ protected $mErrors = [];
+
+ /**
+ * If set, treat result as plain HTML. Can be used by printer classes if wiki mark-up is not enough.
+ * This setting is used only after the result text was generated.
+ * @note HTML query results cannot be used as parameters for other templates or in any other way
+ * in combination with other wiki text. The result will be inserted on the page literally.
+ */
+ protected $isHTML = false;
+
+ /**
+ * If set, take the necessary steps to make sure that things like {{templatename| ...}} are properly
+ * processed if they occur in the result. Clearly, this is only relevant if the output is not HTML, i.e.
+ * it is ignored if SMWResultPrinter::$is_HTML is true. This setting is used only after the result
+ * text was generated.
+ * @note This requires extra processing and may make the result less useful for being used as a
+ * parameter for further parser functions. Use only if required.
+ */
+ protected $hasTemplates = false;
+ /// Incremented while expanding templates inserted during printout; stop expansion at some point
+ private static $mRecursionDepth = 0;
+ /// This public variable can be set to higher values to allow more recursion; do this at your own risk!
+ /// This can be set in LocalSettings.php, but only after enableSemantics().
+ public static $maxRecursionDepth = 2;
+
+ /**
+ * @var RecursiveTextProcessor
+ */
+ protected $recursiveTextProcessor;
+
+ /**
+ * @var boolean
+ */
+ private $recursiveAnnotation = false;
+
+ /**
+ * For certaing activities (embedded pages etc.) make sure that annotations
+ * are not tranclucded (imported) into the target page when resolving a
+ * query.
+ *
+ * @var boolean
+ */
+ protected $transcludeAnnotation = true;
+
+ /**
+ * Return serialised results in specified format.
+ * Implemented by subclasses.
+ */
+ abstract protected function getResultText( QueryResult $res, $outputMode );
+
+ /**
+ * Constructor. The parameter $format is a format string
+ * that may influence the processing details.
+ *
+ * Do not override in deriving classes.
+ *
+ * @param string $format
+ * @param boolean $inline Optional since 1.9
+ */
+ public function __construct( $format, $inline = true ) {
+ global $smwgQDefaultLinking;
+
+ $this->mFormat = $format;
+ $this->mInline = $inline;
+ $this->mLinkFirst = ( $smwgQDefaultLinking != 'none' );
+ $this->mLinkOthers = ( $smwgQDefaultLinking == 'all' );
+ $this->mLinker = new Linker(); ///TODO: how can we get the default or user skin here (depending on context)?
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $queryContext
+ */
+ public function setQueryContext( $queryContext ) {
+ $this->mInline = $queryContext != QueryContext::SPECIAL_PAGE;
+ }
+
+ /**
+ * This method is added temporary measures to avoid breaking those that relied
+ * on the removed ContextSource interface.
+ *
+ * @since 3.0
+ *
+ * @return Message
+ */
+ public function msg() {
+ return wfMessage( func_get_args() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param RecursiveTextProcessor $recursiveTextProcessor
+ */
+ public function setRecursiveTextProcessor( RecursiveTextProcessor $recursiveTextProcessor ) {
+ $this->recursiveTextProcessor = $recursiveTextProcessor;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $feature
+ *
+ * @return boolean
+ */
+ public function isEnabledFeature( $feature ) {
+ return ( (int)$GLOBALS['smwgResultFormatsFeatures'] & $feature ) != 0;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ public function expandTemplates( $text ) {
+ return $this->recursiveTextProcessor->expandTemplates( $text );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $modules
+ * @param array $styleModules
+ */
+ public function registerResources( array $modules = [], array $styleModules = [] ) {
+
+ foreach ( $modules as $module ) {
+ ResourceManager::requireResource( $module );
+ }
+
+ foreach ( $styleModules as $styleModule ) {
+ ResourceManager::requireStyle( $styleModule );
+ }
+ }
+
+ /**
+ * @see IResultPrinter::getResult
+ *
+ * @note: since 1.8 this method is final, since it's the entry point.
+ * Most logic has been moved out to buildResult, which you can override.
+ *
+ * @param $results QueryResult
+ * @param $fullParams array
+ * @param $outputMode integer
+ *
+ * @return string
+ */
+ public final function getResult( QueryResult $results, array $fullParams, $outputMode ) {
+ $this->outputMode = $outputMode;
+ $this->results = $results;
+
+ $params = [];
+ $modules = [];
+ $styles = [];
+
+ /**
+ * @var \ParamProcessor\Param $param
+ */
+ foreach ( $fullParams as $param ) {
+ $params[$param->getName()] = $param->getValue();
+ }
+
+ $this->params = $params;
+ $this->fullParams = $fullParams;
+
+ $this->postProcessParameters();
+ $this->handleParameters( $this->params, $outputMode );
+
+ $resources = $this->getResources();
+
+ if ( isset( $resources['modules'] ) ) {
+ $modules = $resources['modules'];
+ }
+
+ if ( isset( $resources['styles'] ) ) {
+ $styles = $resources['styles'];
+ }
+
+ // Register possible default modules at this point to allow for content
+ // retrieved from a remote source to use required JS/CSS modules from the
+ // local entry point
+ $this->registerResources( $modules, $styles );
+
+ if ( $results instanceof StringResult ) {
+ $results->setOption( 'is.exportformat', $this->isExportFormat() );
+ return $results->getResults();
+ }
+
+ return $this->buildResult( $results );
+ }
+
+ /**
+ * Build and return the HTML result.
+ *
+ * @since 1.8
+ *
+ * @param QueryResult $results
+ *
+ * @return string
+ */
+ protected function buildResult( QueryResult $results ) {
+ $this->isHTML = false;
+ $this->hasTemplates = false;
+
+ $outputMode = $this->outputMode;
+
+ // Default output for normal printers:
+ if ( $outputMode !== SMW_OUTPUT_FILE && $results->getCount() == 0 ) {
+ if ( !$results->hasFurtherResults() ) {
+ return $this->escapeText( $this->mDefault, $outputMode )
+ . $this->getErrorString( $results );
+ } elseif ( $this->mInline && $this->isDeferrable() !== self::DEFERRED_DATA ) {
+
+ if ( !$this->linkFurtherResults( $results ) ) {
+ return '';
+ }
+
+ return $this->getFurtherResultsLink( $results, $outputMode )->getText( $outputMode, $this->mLinker )
+ . $this->getErrorString( $results );
+ }
+ }
+
+ // Get output from printer:
+ $result = $this->getResultText( $results, $outputMode );
+
+ if ( $outputMode !== SMW_OUTPUT_FILE ) {
+ $result = $this->handleNonFileResult( $result, $results, $outputMode );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Continuation of getResult that only gets executed for non file outputs.
+ *
+ * @since 1.6
+ *
+ * @param string $result
+ * @param QueryResult $results
+ * @param integer $outputmode
+ *
+ * @return string
+ */
+ protected function handleNonFileResult( $result, QueryResult $results, $outputmode ) {
+
+ // append errors
+ $result .= $this->getErrorString( $results );
+
+ // Should not happen, used as fallback which in case the parser state
+ // relies on the $GLOBALS['wgParser']
+ if ( $this->recursiveTextProcessor === null ) {
+ $this->recursiveTextProcessor = new RecursiveTextProcessor();
+ }
+
+ $this->recursiveTextProcessor->uniqid();
+
+ $this->recursiveTextProcessor->setMaxRecursionDepth(
+ self::$maxRecursionDepth
+ );
+
+ $this->recursiveTextProcessor->transcludeAnnotation(
+ $this->transcludeAnnotation
+ );
+
+ $this->recursiveTextProcessor->setRecursiveAnnotation(
+ $this->recursiveAnnotation
+ );
+
+ // Apply intro parameter
+ if ( ( $this->mIntro ) && ( $results->getCount() > 0 ) ) {
+ if ( $outputmode == SMW_OUTPUT_HTML ) {
+ $result = Message::get( [ 'smw-parse', $this->mIntro ], Message::PARSE ) . $result;
+ } elseif ( $outputmode !== SMW_OUTPUT_RAW ) {
+ $result = $this->mIntro . $result;
+ }
+ }
+
+ // Apply outro parameter
+ if ( ( $this->mOutro ) && ( $results->getCount() > 0 ) ) {
+ if ( $outputmode == SMW_OUTPUT_HTML ) {
+ $result = $result . Message::get( [ 'smw-parse', $this->mOutro ], Message::PARSE );
+ } elseif ( $outputmode !== SMW_OUTPUT_RAW ) {
+ $result = $result . $this->mOutro;
+ }
+ }
+
+ // Preprocess embedded templates if needed
+ if ( ( !$this->isHTML ) && ( $this->hasTemplates ) ) {
+ $result = $this->recursiveTextProcessor->recursivePreprocess( $result );
+ }
+
+ if ( ( $this->isHTML ) && ( $outputmode == SMW_OUTPUT_WIKI ) ) {
+ $result = [ $result, 'isHTML' => true ];
+ } elseif ( ( !$this->isHTML ) && ( $outputmode == SMW_OUTPUT_HTML ) ) {
+ $result = $this->recursiveTextProcessor->recursiveTagParse( $result );
+ }
+
+ if ( $this->mShowErrors && $this->recursiveTextProcessor->getError() !== [] ) {
+ $result .= Message::get( $this->recursiveTextProcessor->getError(), Message::TEXT, Message::USER_LANGUAGE );
+ }
+
+ $this->recursiveTextProcessor->releaseAnnotationBlock();
+
+ return $result;
+ }
+
+ /**
+ * Does any additional parameter handling that needs to be done before the
+ * actual result is build. This includes cleaning up parameter values
+ * and setting class fields.
+ *
+ * Since 1.6 parameter handling should happen via validator based on the parameter
+ * definitions returned in getParameters. Therefore this method should likely
+ * not be used in any new code. It's mainly here for legacy reasons.
+ *
+ * @since 1.6
+ *
+ * @param array $params
+ * @param $outputMode
+ */
+ protected function handleParameters( array $params, $outputMode ) {
+ // No-op
+ }
+
+ /**
+ * Similar to handleParameters.
+ *
+ * @since 1.8
+ */
+ protected function postProcessParameters() {
+ $params = $this->params;
+
+ $this->mIntro = isset( $params['intro'] ) ? str_replace( '_', ' ', $params['intro'] ) : '';
+ $this->mOutro = isset( $params['outro'] ) ? str_replace( '_', ' ', $params['outro'] ) : '';
+
+ $this->mSearchlabel = !isset( $params['searchlabel'] ) || $params['searchlabel'] === false ? null : $params['searchlabel'];
+ $link = isset( $params['link'] ) ? $params['link'] : '';
+
+ switch ( $link ) {
+ case 'head': case 'subject':
+ $this->mLinkFirst = true;
+ $this->mLinkOthers = false;
+ break;
+ case 'all':
+ $this->mLinkFirst = true;
+ $this->mLinkOthers = true;
+ break;
+ case 'none':
+ $this->mLinkFirst = false;
+ $this->mLinkOthers = false;
+ break;
+ }
+
+ $this->mDefault = isset( $params['default'] ) ? str_replace( '_', ' ', $params['default'] ) : '';
+ $headers = isset( $params['headers'] ) ? $params['headers'] : '';
+
+ if ( $headers == 'hide' ) {
+ $this->mShowHeaders = SMW_HEADERS_HIDE;
+ } elseif ( $headers == 'plain' ) {
+ $this->mShowHeaders = SMW_HEADERS_PLAIN;
+ } else {
+ $this->mShowHeaders = SMW_HEADERS_SHOW;
+ }
+
+ $this->recursiveAnnotation = isset( $params['import-annotation'] ) ? $params['import-annotation'] : false;
+ }
+
+ /**
+ * Depending on current linking settings, returns a linker object
+ * for making hyperlinks or NULL if no links should be created.
+ *
+ * @param boolean $firstcol True of this is the first result column (having special linkage settings).
+ * @return Linker|null
+ */
+ protected function getLinker( $firstcol = false ) {
+ if ( ( $firstcol && $this->mLinkFirst ) || ( !$firstcol && $this->mLinkOthers ) ) {
+ return $this->mLinker;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets a SMWInfolink object that allows linking to a display of the query result.
+ *
+ * @since 1.8
+ *
+ * @param QueryResult $res
+ * @param $outputMode
+ * @param string $classAffix
+ *
+ * @return SMWInfolink
+ */
+ protected function getLink( QueryResult $res, $outputMode, $classAffix = '' ) {
+ $link = $res->getQueryLink( $this->getSearchLabel( $outputMode ) );
+
+ if ( $classAffix !== '' ){
+ $link->setStyle( 'smw-' . $this->params['format'] . '-' . Sanitizer::escapeClass( $classAffix ) );
+ }
+
+ if ( isset( $this->params['format'] ) ) {
+ $link->setParameter( $this->params['format'], 'format' );
+ }
+
+ /**
+ * @var \IParam $param
+ */
+ foreach ( $this->fullParams as $param ) {
+ if ( !$param->wasSetToDefault() && !( $param->getName() == 'limit' && $param->getValue() === 0 ) ) {
+ $link->setParameter( $param->getOriginalValue(), $param->getName() );
+ }
+ }
+
+ return $link;
+ }
+
+ /**
+ * Gets a SMWInfolink object that allows linking to further results for the query.
+ *
+ * @since 1.8
+ *
+ * @param QueryResult $res
+ * @param $outputMode
+ *
+ * @return SMWInfolink
+ */
+ protected function getFurtherResultsLink( QueryResult $res, $outputMode ) {
+ $link = $this->getLink( $res, $outputMode, 'furtherresults' );
+ $link->setParameter( $this->params['offset'] + $res->getCount(), 'offset' );
+ return $link;
+ }
+
+ /**
+ * @see IResultPrinter::getQueryMode
+ *
+ * @param $context
+ *
+ * @return integer
+ */
+ public function getQueryMode( $context ) {
+ // TODO: Now that we are using RequestContext object maybe
+ // $context is misleading
+ return SMWQuery::MODE_INSTANCES;
+ }
+
+ /**
+ * @see IResultPrinter::getName
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->mFormat;
+ }
+
+ /**
+ * Provides a simple formatted string of all the error messages that occurred.
+ * Can be used if not specific error formatting is desired. Compatible with HTML
+ * and Wiki.
+ *
+ * @param QueryResult $res
+ *
+ * @return string
+ */
+ protected function getErrorString( QueryResult $res ) {
+ return $this->mShowErrors ? smwfEncodeMessages( array_merge( $this->mErrors, $res->getErrors() ) ) : '';
+ }
+
+ /**
+ * @see IResultPrinter::setShowErrors
+ *
+ * @param boolean $show
+ */
+ public function setShowErrors( $show ) {
+ $this->mShowErrors = $show;
+ }
+
+ /**
+ * Individual printer can override this method to allow for unified loading
+ * practice.
+ *
+ * Styles are loaded first to avoid a possible FOUC (Flash of unstyled content).
+ *
+ * @since 3.0
+ *
+ * @return []
+ */
+ protected function getResources() {
+ return [ 'modules' => [], 'styles' => [] ];
+ }
+
+ /**
+ * If $outputmode is SMW_OUTPUT_HTML, escape special characters occurring in the
+ * given text. Otherwise return text as is.
+ *
+ * @param string $text
+ * @param $outputmode
+ *
+ * @return string
+ */
+ protected function escapeText( $text, $outputmode ) {
+ return $outputmode == SMW_OUTPUT_HTML ? htmlspecialchars( $text ) : $text;
+ }
+
+ /**
+ * Get the string the user specified as a text for the "further results" link,
+ * properly escaped for the current output mode.
+ *
+ * @param $outputmode
+ *
+ * @return string
+ */
+ protected function getSearchLabel( $outputmode ) {
+ return $this->escapeText( $this->mSearchlabel, $outputmode );
+ }
+
+ /**
+ * Check whether a "further results" link would normally be generated for this
+ * result set with the given parameters. Individual result printers may decide to
+ * create or hide such a link independent of that, but this is the default.
+ *
+ * @param QueryResult $results
+ *
+ * @return boolean
+ */
+ protected function linkFurtherResults( QueryResult $results ) {
+ return $this->mInline && $results->hasFurtherResults() && $this->mSearchlabel !== '';
+ }
+
+ /**
+ * Adds an error message for a parameter handling error so a list
+ * of errors can be created later on.
+ *
+ * @since 1.6
+ *
+ * @param string $errorMessage
+ */
+ protected function addError( $errorMessage ) {
+ $this->mErrors[] = $errorMessage;
+ }
+
+ /**
+ * A function to describe the allowed parameters of a query using
+ * any specific format - most query printers should override this
+ * function.
+ *
+ * @deprecated since 1.8, use getParamDefinitions instead.
+ *
+ * @since 1.5
+ *
+ * @return array
+ */
+ public function getParameters() {
+ return [];
+ }
+
+ /**
+ * @see IResultPrinter::getParamDefinitions
+ *
+ * @since 1.8
+ *
+ * @param ParamDefinition[] $definitions
+ *
+ * @return array
+ */
+ public function getParamDefinitions( array $definitions ) {
+ return array_merge( $definitions, $this->getParameters() );
+ }
+
+ /**
+ * Returns the parameter definitions as an associative array where
+ * the keys hold the parameter names and point to their full definitions.
+ * array( name => array|IParamDefinition )
+ *
+ * @since 1.8
+ *
+ * @param array $definitions List of definitions to prepend to the result printers list before further processing.
+ *
+ * @return array
+ */
+ public final function getNamedParameters( array $definitions = [] ) {
+ $params = [];
+
+ foreach ( $this->getParamDefinitions( $definitions ) as $param ) {
+ $params[is_array( $param ) ? $param['name'] : $param->getName()] = $param;
+ }
+
+ return $params;
+ }
+
+ /**
+ * @see IResultPrinter::isExportFormat
+ *
+ * @since 1.8
+ *
+ * @return boolean
+ */
+ public function isExportFormat() {
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function isDeferrable() {
+ return false;
+ }
+
+ /**
+ * Returns if the result printer supports using a "full parse" instead of a
+ * '[[SMW::off]]' . $wgParser->replaceVariables( $result ) . '[[SMW::on]]'
+ *
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public function supportsRecursiveAnnotation() {
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getDefaultSort() {
+ return 'ASC';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/TableResultPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/TableResultPrinter.php
new file mode 100644
index 00000000..208e9102
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/TableResultPrinter.php
@@ -0,0 +1,380 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use Html;
+use SMW\DIWikiPage;
+use SMW\Message;
+use SMW\Query\PrintRequest;
+use SMW\Query\QueryStringifier;
+use SMW\Utils\HtmlTable;
+use SMWDataValue;
+use SMWDIBlob as DIBlob;
+use SMWQueryResult as QueryResult;
+use SMWResultArray as ResultArray;
+
+/**
+ * Print query results in tables
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author mwjames
+ */
+class TableResultPrinter extends ResultPrinter {
+
+ /**
+ * @var HtmlTable
+ */
+ private $htmlTable;
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return $this->msg( 'smw_printername_' . $this->mFormat )->text();
+ }
+
+ /**
+ * @see ResultPrinter::isDeferrable
+ *
+ * {@inheritDoc}
+ */
+ public function isDeferrable() {
+ return true;
+ }
+
+ /**
+ * @see ResultPrinter::getParamDefinitions
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+
+ $params = parent::getParamDefinitions( $definitions );
+
+ $params['class'] = [
+ 'name' => 'class',
+ 'message' => 'smw-paramdesc-table-class',
+ 'default' => 'sortable wikitable smwtable',
+ ];
+
+ $params['transpose'] = [
+ 'type' => 'boolean',
+ 'default' => false,
+ 'message' => 'smw-paramdesc-table-transpose',
+ ];
+
+ $params['sep'] = [
+ 'message' => 'smw-paramdesc-sep',
+ 'default' => '',
+ ];
+
+ return $params;
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * {@inheritDoc}
+ */
+ protected function getResultText( QueryResult $res, $outputMode ) {
+
+ $this->isHTML = ( $outputMode === SMW_OUTPUT_HTML );
+ $this->isDataTable = false;
+ $class = isset( $this->params['class'] ) ? $this->params['class'] : '';
+
+ if ( strpos( $class, 'datatable' ) !== false && $this->mShowHeaders !== SMW_HEADERS_HIDE ) {
+ $this->isDataTable = true;
+ }
+
+ $this->htmlTable = new HtmlTable();
+
+ $columnClasses = [];
+ $headerList = [];
+
+ // Default cell value separator
+ if ( !isset( $this->params['sep'] ) || $this->params['sep'] === '' ) {
+ $this->params['sep'] = '<br>';
+ }
+
+ // building headers
+ if ( $this->mShowHeaders != SMW_HEADERS_HIDE ) {
+ $isPlain = $this->mShowHeaders == SMW_HEADERS_PLAIN;
+ foreach ( $res->getPrintRequests() as /* SMWPrintRequest */ $pr ) {
+ $attributes = [];
+ $columnClass = str_replace( [ ' ', '_' ], '-', strip_tags( $pr->getText( SMW_OUTPUT_WIKI ) ) );
+ $attributes['class'] = $columnClass;
+ // Also add this to the array of classes, for
+ // use in displaying each row.
+ $columnClasses[] = $columnClass;
+
+ // #2702 Use a fixed output on a requested plain printout
+ $mode = $this->isHTML && $isPlain ? SMW_OUTPUT_WIKI : $outputMode;
+ $text = $pr->getText( $mode, ( $isPlain ? null : $this->mLinker ) );
+ $headerList[] = $pr->getCanonicalLabel();
+ $this->htmlTable->header( ( $text === '' ? '&nbsp;' : $text ), $attributes );
+ }
+ }
+
+ $rowNumber = 0;
+
+ while ( $subject = $res->getNext() ) {
+ $rowNumber++;
+ $this->getRowForSubject( $subject, $outputMode, $columnClasses );
+
+ $this->htmlTable->row(
+ [
+ 'data-row-number' => $rowNumber
+ ]
+ );
+ }
+
+ // print further results footer
+ if ( $this->linkFurtherResults( $res ) ) {
+ $link = $this->getFurtherResultsLink( $res, $outputMode );
+
+ $this->htmlTable->cell(
+ $link->getText( $outputMode, $this->mLinker ),
+ [ 'class' => 'sortbottom', 'colspan' => $res->getColumnCount() ]
+ );
+
+ $this->htmlTable->row( [ 'class' => 'smwfooter' ] );
+ }
+
+ $tableAttrs = [ 'class' => $class ];
+
+ if ( $this->mFormat == 'broadtable' ) {
+ $tableAttrs['width'] = '100%';
+ $tableAttrs['class'] .= ' broadtable';
+ }
+
+ if ( $this->isDataTable ) {
+ $this->addDataTableAttrs(
+ $res,
+ $headerList,
+ $tableAttrs
+ );
+ }
+
+ $transpose = $this->mShowHeaders !== SMW_HEADERS_HIDE && $this->params['transpose'];
+
+ $html = $this->htmlTable->table(
+ $tableAttrs,
+ $transpose,
+ $this->isHTML
+ );
+
+ if ( $this->isDataTable ) {
+
+ // Simple approximation to avoid a massive text reflow once the DT JS
+ // has finished processing the HTML table
+ $count = $this->params['transpose'] ? $res->getColumnCount() : $res->getCount();
+ $height = ( min( ( $count + ( $res->hasFurtherResults() ? 1 : 0 ) ), 10 ) * 50 ) + 40;
+
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-datatable smw-placeholder is-disabled smw-flex-center' . (
+ $this->params['class'] !== '' ? ' ' . $this->params['class'] : ''
+ ),
+ 'style' => "height:{$height}px;"
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-overlay-spinner medium flex'
+ ]
+ ) . $html
+ );
+ }
+
+ return $html;
+ }
+
+ /**
+ * Gets a single table row for a subject, ie page.
+ *
+ * @since 1.6.1
+ *
+ * @param SMWResultArray[] $subject
+ * @param int $outputMode
+ * @param string[] $columnClasses
+ *
+ * @return string
+ */
+ private function getRowForSubject( array $subject, $outputMode, array $columnClasses ) {
+ foreach ( $subject as $i => $field ) {
+ // $columnClasses will be empty if "headers=hide"
+ // was set.
+ if ( array_key_exists( $i, $columnClasses ) ) {
+ $columnClass = $columnClasses[$i];
+ } else {
+ $columnClass = null;
+ }
+
+ $this->getCellForPropVals( $field, $outputMode, $columnClass );
+ }
+ }
+
+ /**
+ * Gets a table cell for all values of a property of a subject.
+ *
+ * @since 1.6.1
+ *
+ * @param SMWResultArray $resultArray
+ * @param int $outputMode
+ * @param string $columnClass
+ *
+ * @return string
+ */
+ protected function getCellForPropVals( ResultArray $resultArray, $outputMode, $columnClass ) {
+ /** @var SMWDataValue[] $dataValues */
+ $dataValues = [];
+
+ while ( ( $dv = $resultArray->getNextDataValue() ) !== false ) {
+ $dataValues[] = $dv;
+ }
+
+ $printRequest = $resultArray->getPrintRequest();
+ $printRequestType = $printRequest->getTypeID();
+
+ $cellTypeClass = " smwtype$printRequestType";
+
+ // We would like the cell class to always be defined, even if the cell itself is empty
+ $attributes = [
+ 'class' => $columnClass . $cellTypeClass
+ ];
+
+ $content = null;
+
+ if ( count( $dataValues ) > 0 ) {
+ $sortKey = $dataValues[0]->getDataItem()->getSortKey();
+ $dataValueType = $dataValues[0]->getTypeID();
+
+ // The data value type might differ from the print request type - override in this case
+ if ( $dataValueType !== '' && $dataValueType !== $printRequestType ) {
+ $attributes['class'] = "$columnClass smwtype$dataValueType";
+ }
+
+ if ( is_numeric( $sortKey ) ) {
+ $attributes['data-sort-value'] = $sortKey;
+ }
+
+ if ( $this->isDataTable && $sortKey !== '' ) {
+ $attributes['data-order'] = $sortKey;
+ }
+
+ $alignment = trim( $printRequest->getParameter( 'align' ) );
+
+ if ( in_array( $alignment, [ 'right', 'left', 'center' ] ) ) {
+ $attributes['style'] = "text-align:$alignment;";
+ }
+
+ $width = htmlspecialchars(
+ trim( $printRequest->getParameter( 'width' ) ),
+ ENT_QUOTES
+ );
+
+ if ( $width ) {
+ $attributes['style'] = ( isset( $attributes['style'] ) ? $attributes['style'] . ' ' : '' ) . "width:$width;";
+ }
+
+ $content = $this->getCellContent(
+ $dataValues,
+ $outputMode,
+ $printRequest->getMode() == PrintRequest::PRINT_THIS
+ );
+ }
+
+ // Sort the cell HTML attributes, to make test behavior more deterministic
+ ksort( $attributes );
+
+ $this->htmlTable->cell( $content, $attributes );
+ }
+
+ /**
+ * Gets the contents for a table cell for all values of a property of a subject.
+ *
+ * @since 1.6.1
+ *
+ * @param SMWDataValue[] $dataValues
+ * @param $outputMode
+ * @param boolean $isSubject
+ *
+ * @return string
+ */
+ protected function getCellContent( array $dataValues, $outputMode, $isSubject ) {
+ $values = [];
+
+ foreach ( $dataValues as $dv ) {
+
+ // Restore output in Special:Ask on:
+ // - file/image parsing
+ // - text formatting on string elements including italic, bold etc.
+ if ( $outputMode === SMW_OUTPUT_HTML && $dv->getDataItem() instanceof DIWikiPage && $dv->getDataItem()->getNamespace() === NS_FILE ||
+ $outputMode === SMW_OUTPUT_HTML && $dv->getDataItem() instanceof DIBlob ) {
+ // Too lazy to handle the Parser object and besides the Message
+ // parse does the job and ensures no other hook is executed
+ $value = Message::get(
+ [ 'smw-parse', $dv->getShortText( SMW_OUTPUT_WIKI, $this->getLinker( $isSubject ) ) ],
+ Message::PARSE
+ );
+ } else {
+ $value = $dv->getShortText( $outputMode, $this->getLinker( $isSubject ) );
+ }
+
+
+ $values[] = $value === '' ? '&nbsp;' : $value;
+ }
+
+ return implode( $this->params['sep'], $values );
+ }
+
+ /**
+ * @see ResultPrinter::getResources
+ */
+ protected function getResources() {
+
+ $class = isset( $this->params['class'] ) ? $this->params['class'] : '';
+
+ if ( strpos( $class, 'datatable' ) === false ) {
+ return [];
+ }
+
+ return [
+ 'modules' => [
+ 'smw.tableprinter.datatable'
+ ],
+ 'styles' => [
+ 'onoi.dataTables.styles',
+ 'smw.tableprinter.datatable.styles'
+ ]
+ ];
+ }
+
+ private function addDataTableAttrs( $res, $headerList, &$tableAttrs ) {
+
+ $tableAttrs['width'] = '100%';
+ $tableAttrs['style'] = 'opacity:.0';
+
+ $tableAttrs['data-column-sort'] = json_encode(
+ [
+ 'list' => $headerList,
+ 'sort' => $this->params['sort'],
+ 'order' => $this->params['order']
+ ]
+ );
+
+ $tableAttrs['data-query'] = QueryStringifier::toJson(
+ $res->getQuery()
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/TemplateFileExportPrinter.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/TemplateFileExportPrinter.php
new file mode 100644
index 00000000..af739f35
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ResultPrinters/TemplateFileExportPrinter.php
@@ -0,0 +1,252 @@
+<?php
+
+namespace SMW\Query\ResultPrinters;
+
+use Sanitizer;
+use SMW\ApplicationFactory;
+use SMWQueryResult as QueryResult;
+
+/**
+ * Exports data as file in a format that is defined by its invoked templates.
+ * Custom specifications and requirements can be specified freely by relying on
+ * the available template system.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TemplateFileExportPrinter extends FileExportPrinter {
+
+ /**
+ * @var integer
+ */
+ private $numRows = 0;
+
+ /**
+ * @var TemplateRenderer
+ */
+ private $templateRenderer;
+
+ /**
+ * @see ResultPrinter::getName
+ *
+ * {@inheritDoc}
+ */
+ public function getName() {
+ return $this->msg( 'smw_printername_templatefile' )->text();
+ }
+
+ /**
+ * @see FileExportPrinter::getMimeType
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getMimeType( QueryResult $queryResult ) {
+
+ if ( $this->params['mimetype'] !== '' ) {
+ return $this->params['mimetype'];
+ }
+
+ return 'text/plain';
+ }
+
+ /**
+ * @see FileExportPrinter::getFileName
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getFileName( QueryResult $queryResult ) {
+ return $this->params['filename'];
+ }
+
+ /**
+ * @see ResultPrinter::getParamDefinitions
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getParamDefinitions( array $definitions ) {
+ $params = parent::getParamDefinitions( $definitions );
+
+ $params['searchlabel']->setDefault( 'templateFile' );
+
+ $params['template arguments'] = [
+ 'message' => 'smw-paramdesc-template-arguments',
+ 'default' => 'legacy',
+ 'values' => [ 'numbered', 'named', 'legacy' ],
+ ];
+
+ $params['template'] = [
+ 'type' => 'string',
+ 'default' => '',
+ 'message' => 'smw-paramdesc-template',
+ ];
+
+ $params['valuesep'] = [
+ 'message' => 'smw-paramdesc-sep',
+ 'default' => ',',
+ ];
+
+ $params['userparam'] = [
+ 'message' => 'smw-paramdesc-userparam',
+ 'default' => '',
+ ];
+
+ $params['introtemplate'] = [
+ 'message' => 'smw-paramdesc-introtemplate',
+ 'default' => '',
+ ];
+
+ $params['outrotemplate'] = [
+ 'message' => 'smw-paramdesc-outrotemplate',
+ 'default' => '',
+ ];
+
+ $params['filename'] = [
+ 'message' => 'smw-paramdesc-filename',
+ 'default' => 'file.txt',
+ ];
+
+ $params['mimetype'] = [
+ 'type' => 'string',
+ 'message' => 'smw-paramdesc-mimetype',
+ 'default' => 'text/plain',
+ ];
+
+ return $params;
+ }
+
+ /**
+ * @see ResultPrinter::getResultText
+ *
+ * {@inheritDoc}
+ */
+ protected function getResultText( QueryResult $queryResult, $outputMode ) {
+
+ // Always return a link for when the output mode is not a file request,
+ // a file request is normally only initiated when resolving the query
+ // via Special:Ask
+ if ( $outputMode !== SMW_OUTPUT_FILE ) {
+ return $this->getFileLink( $queryResult, $outputMode );
+ }
+
+ $text = $this->expandTemplates(
+ $this->getText( $queryResult )
+ );
+
+ return trim( $text, "\n" );
+ }
+
+ private function getFileLink( QueryResult $queryResult, $outputMode ) {
+
+ // Can be viewed as HTML if requested, no more parsing needed
+ $this->isHTML = $outputMode == SMW_OUTPUT_HTML;
+
+ $link = $this->getLink(
+ $queryResult,
+ $outputMode
+ );
+
+ return $link->getText( $outputMode, $this->mLinker );
+ }
+
+ private function getText( $queryResult ) {
+
+ $this->templateRenderer = ApplicationFactory::getInstance()->newMwCollaboratorFactory()->newWikitextTemplateRenderer();
+ $result = '';
+
+ $link = $this->getLink(
+ $queryResult,
+ SMW_OUTPUT_RAW
+ );
+
+ $link = $link->getText( SMW_OUTPUT_RAW, $this->mLinker );
+
+ // Extra fields include:
+ // - {{{userparam}}}
+ // - {{{querylink}}}
+
+ if ( $this->params['introtemplate'] !== '' ) {
+ $this->templateRenderer->addField( 'userparam', $this->params['userparam'] );
+ $this->templateRenderer->addField( 'querylink', $link );
+
+ $this->templateRenderer->packFieldsForTemplate(
+ $this->params['introtemplate']
+ );
+
+ $result .= $this->templateRenderer->render();
+ }
+
+ while ( $row = $queryResult->getNext() ) {
+ $result .= $this->row( $queryResult, $row );
+ }
+
+ // Extra fields include:
+ // - {{{userparam}}}
+ // - {{{querylink}}}
+
+ if ( $this->params['outrotemplate'] !== '' ) {
+ $this->templateRenderer->addField( 'userparam', $this->params['userparam'] );
+ $this->templateRenderer->addField( 'querylink', $link );
+
+ $this->templateRenderer->packFieldsForTemplate(
+ $this->params['outrotemplate']
+ );
+
+ $result .= $this->templateRenderer->render();
+ }
+
+ return $result;
+ }
+
+ private function row( QueryResult $queryResult, array $row ) {
+
+ $this->numRows + 1;
+ $this->addFields( $row );
+
+ $this->templateRenderer->packFieldsForTemplate(
+ $this->params['template']
+ );
+
+ return $this->templateRenderer->render();
+ }
+
+ private function addFields( $row ) {
+
+ foreach ( $row as $i => $field ) {
+
+ $value = '';
+ $fieldName = '';
+
+ // {{{?Foo}}}
+ if ( $this->params['template arguments'] === 'legacy' ) {
+ $fieldName = '?' . $field->getPrintRequest()->getLabel();
+ }
+
+ // {{{Foo}}}
+ if ( $this->params['template arguments'] === 'named' ) {
+ $fieldName = $field->getPrintRequest()->getLabel();
+ }
+
+ // {{{1}}}
+ if ( $fieldName === '' || $fieldName === '?' || $this->params['template arguments'] === 'numbered' ) {
+ $fieldName = intval( $i + 1 );
+ }
+
+ while ( ( $text = $field->getNextText( SMW_OUTPUT_WIKI, $this->getLinker( $i == 0 ) ) ) !== false ) {
+ $value .= $value === '' ? $text : $this->params['valuesep'] . ' ' . $text;
+ }
+
+ $this->templateRenderer->addField( $fieldName, $value );
+ }
+
+ $this->templateRenderer->addField( '#', $this->numRows );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Query/ScoreSet.php b/www/wiki/extensions/SemanticMediaWiki/src/Query/ScoreSet.php
new file mode 100644
index 00000000..5ac38724
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Query/ScoreSet.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace SMW\Query;
+
+use SMW\DIWikiPage;
+
+/**
+ * Record scores for query results retrieved from stores that support the computation
+ * of relevance scores.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ScoreSet {
+
+ /**
+ * @var []
+ */
+ private $scores = [];
+
+ /**
+ * @var integer|null
+ */
+ private $max_score = null;
+
+ /**
+ * @var integer|null
+ */
+ private $min_score = null;
+
+ /**
+ * @since 3.0
+ *
+ * @param string|integer $max_score
+ */
+ public function max_score( $max_score ) {
+ $this->max_score = $max_score;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|integer $min_score
+ */
+ public function min_score( $min_score ) {
+ $this->min_score = $min_score;
+ }
+
+ /**
+ * @note The hash is expected to match DIWikiPage::getHash to easily match
+ * result subjects available in an QueryResult instance.
+ *
+ * @since 3.0
+ *
+ * @param DIWikiPage|string $hash
+ * @param string|integer $score
+ */
+ public function addScore( $hash, $score, $pos = null ) {
+
+ if ( $hash instanceof DIWikiPage ) {
+ $hash = $hash->getHash();
+ }
+
+ if ( $pos === null ) {
+ $this->scores[] = [ $hash, $score ];
+ } else {
+ $this->scores[$pos] = [ $hash, $score ];
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage|string $hash
+ *
+ * @return string|integer|false
+ */
+ public function getScore( $hash ) {
+
+ if ( $hash instanceof DIWikiPage ) {
+ $hash = $hash->getHash();
+ }
+
+ foreach ( $this->scores as $map ) {
+ if ( $map[0] === $hash ) {
+ return $map[1];
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getScores() {
+ return $this->scores;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $usort
+ */
+ public function usort( $usort ) {
+
+ if ( !$usort|| $this->scores === [] ) {
+ return;
+ }
+
+ usort( $this->scores, function( $a, $b ) {
+
+ if ( $a[1] == $b[1] ) {
+ return 0;
+ }
+
+ return ( $a[1] > $b[1] ) ? -1 : 1;
+ } );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $class
+ *
+ * @return string
+ */
+ public function asTable( $class = '' ) {
+
+ if ( $this->scores === [] ) {
+ return '';
+ }
+
+ $table = "<table class='$class'><thead>";
+ $table .= "<th>Score</th><th>Subject</th><th><span title='Sorting position'>Pos</span></th>";
+ $table .= "</thead><tbody>";
+
+ ksort( $this->scores );
+
+ foreach ( $this->scores as $pos => $set ) {
+ $table .= '<tr><td>' . $set[1] . '</td><td>' . $set[0] . '</td><td>' . $pos . '</td></tr>';
+ }
+
+ $table .= '</tbody></table>';
+
+ return $table;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/QueryEngine.php b/www/wiki/extensions/SemanticMediaWiki/src/QueryEngine.php
new file mode 100644
index 00000000..b25541c2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/QueryEngine.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace SMW;
+
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+
+/**
+ * Interface for query answering that depend on concrete implementations to
+ * provide the filtering and matching process for specific conditions against a
+ * select back-end.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+interface QueryEngine {
+
+ /**
+ * Returns a QueryResult object that matches the condition described by a
+ * query.
+ *
+ * @note If the request was made for a debug (querymode MODE_DEBUG) query
+ * then a simple HTML-compatible string is returned.
+ *
+ * @since 2.5
+ *
+ * @param Query $query
+ *
+ * @return QueryResult|string
+ */
+ public function getQueryResult( Query $query );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/QueryFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/QueryFactory.php
new file mode 100644
index 00000000..ca0a36dd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/QueryFactory.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace SMW;
+
+use SMW\Query\DescriptionFactory;
+use SMW\Query\Language\Description;
+use SMW\Query\Parser as QueryParser;
+use SMW\Query\Parser\DescriptionProcessor;
+use SMW\Query\Parser\LegacyParser;
+use SMW\Query\Parser\Tokenizer;
+use SMW\Query\PrintRequestFactory;
+use SMW\Query\ProfileAnnotatorFactory;
+use SMW\Query\QueryCreator;
+use SMW\Query\QueryToken;
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class QueryFactory {
+
+ /**
+ * @since 2.5
+ *
+ * @return ProfileAnnotatorFactory
+ */
+ public function newProfileAnnotatorFactory() {
+ return new ProfileAnnotatorFactory();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Description $description
+ * @param integer|false $context
+ *
+ * @return Query
+ */
+ public function newQuery( Description $description, $context = false ) {
+ return new Query( $description, $context );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return DescriptionFactory
+ */
+ public function newDescriptionFactory() {
+ return new DescriptionFactory();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return PrintRequestFactory
+ */
+ public function newPrintRequestFactory() {
+ return new PrintRequestFactory();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return RequestOptions
+ */
+ public function newRequestOptions() {
+ return new RequestOptions();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $string
+ * @param integer $condition
+ * @param boolean $isDisjunctiveCondition
+ *
+ * @return StringCondition
+ */
+ public function newStringCondition( $string, $condition, $isDisjunctiveCondition = false ) {
+ return new StringCondition( $string, $condition, $isDisjunctiveCondition );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer|boolean $queryFeatures
+ *
+ * @return QueryParser
+ */
+ public function newQueryParser( $queryFeatures = false ) {
+ return $this->newLegacyQueryParser( $queryFeatures );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer|boolean $queryFeatures
+ *
+ * @return QueryParser
+ */
+ public function newLegacyQueryParser( $queryFeatures = false ) {
+
+ if ( $queryFeatures === false ) {
+ $queryFeatures = Applicationfactory::getInstance()->getSettings()->get( 'smwgQFeatures' );
+ }
+
+ return new LegacyParser(
+ new DescriptionProcessor( $queryFeatures ),
+ new Tokenizer(),
+ new QueryToken()
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param Query $query
+ * @param DIWikiPage[]|[] $results = array()
+ * @param boolean $continue
+ *
+ * @return QueryResult
+ */
+ public function newQueryResult( Store $store, Query $query, $results = [], $continue = false ) {
+
+ $queryResult = new QueryResult(
+ $query->getDescription()->getPrintrequests(),
+ $query,
+ $results,
+ $store,
+ $continue
+ );
+
+ return $queryResult;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/RequestOptions.php b/www/wiki/extensions/SemanticMediaWiki/src/RequestOptions.php
new file mode 100644
index 00000000..a668ac5a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/RequestOptions.php
@@ -0,0 +1,212 @@
+<?php
+
+namespace SMW;
+
+/**
+ * Container object for various options that can be used when retrieving
+ * data from the store. These options are mostly relevant for simple,
+ * direct requests -- inline queries may require more complex options due
+ * to their more complex structure.
+ * Options that should not be used or where default values should be used
+ * can be left as initialised.
+ *
+ * @license GNU GPL v2+
+ * @since 1.0
+ *
+ * @author Markus Krötzsch
+ */
+class RequestOptions {
+
+ const SEARCH_FIELD = 'search_field';
+
+ /**
+ * The maximum number of results that should be returned.
+ */
+ public $limit = -1;
+
+ /**
+ * A numerical offset. The first $offset results are skipped.
+ * Note that this does not imply a defined order of results
+ * (see SMWRequestOptions->$sort below).
+ */
+ public $offset = 0;
+
+ /**
+ * Should the result be ordered? The employed order is defined
+ * by the type of result that are requested: wiki pages and strings
+ * are ordered alphabetically, whereas other data is ordered
+ * numerically. Usually, the order should be fairly "natural".
+ */
+ public $sort = false;
+
+ /**
+ * If SMWRequestOptions->$sort is true, this parameter defines whether
+ * the results are ordered in ascending or descending order.
+ */
+ public $ascending = true;
+
+ /**
+ * Specifies a lower or upper bound for the values returned by the query.
+ * Whether it is lower or upper is specified by the parameter "ascending"
+ * (true->lower, false->upper).
+ */
+ public $boundary = null;
+
+ /**
+ * Specifies whether or not the requested boundary should be returned
+ * as a result.
+ */
+ public $include_boundary = true;
+
+ /**
+ * An array of string conditions that are applied if the result has a
+ * string label that can be subject to those patterns.
+ *
+ * @var StringCondition[]
+ */
+ private $stringConditions = [];
+
+ /**
+ * Contains extra conditions which a consumer is being allowed to interpret
+ * freely to modify a search condition.
+ *
+ * @var array
+ */
+ private $extraConditions = [];
+
+ /**
+ * @var array
+ */
+ private $options = [];
+
+ /**
+ * @since 1.0
+ *
+ * @param string $string to match
+ * @param integer $condition one of STRCOND_PRE, STRCOND_POST, STRCOND_MID
+ * @param boolean $isOr
+ * @param boolean $isNot
+ */
+ public function addStringCondition( $string, $condition, $isOr = false, $isNot = false ) {
+ $this->stringConditions[] = new StringCondition( $string, $condition, $isOr, $isNot );
+ }
+
+ /**
+ * Return the specified array of SMWStringCondition objects.
+ *
+ * @since 1.0
+ *
+ * @return array
+ */
+ public function getStringConditions() {
+ return $this->stringConditions;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param mixed $extraCondition
+ */
+ public function addExtraCondition( $extraCondition ) {
+ $this->extraConditions[] = $extraCondition;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array
+ */
+ public function getExtraConditions() {
+ return $this->extraConditions;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param string $value
+ */
+ public function setOption( $key, $value ) {
+ $this->options[$key] = $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getOption( $key, $default = null ) {
+
+ if ( isset( $this->options[$key] ) ) {
+ return $this->options[$key];
+ }
+
+ return $default;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $limit
+ */
+ public function setLimit( $limit ) {
+ $this->limit = (int)$limit;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return integer
+ */
+ public function getLimit() {
+ return (int)$this->limit;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $offset
+ */
+ public function setOffset( $offset ) {
+ $this->offset = (int)$offset;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return integer
+ */
+ public function getOffset() {
+ return (int)$this->offset;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ public function getHash() {
+
+ $stringConditions = '';
+
+ foreach ( $this->stringConditions as $stringCondition ) {
+ $stringConditions .= $stringCondition->getHash();
+ }
+
+ return json_encode( [
+ $this->limit,
+ $this->offset,
+ $this->sort,
+ $this->ascending,
+ $this->boundary,
+ $this->include_boundary,
+ $stringConditions,
+ $this->extraConditions,
+ $this->options,
+ ] );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Rule/Rule.php b/www/wiki/extensions/SemanticMediaWiki/src/Rule/Rule.php
new file mode 100644
index 00000000..3722bb28
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Rule/Rule.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace SMW\Rule;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Rule {
+
+ /**
+ * @var string
+ */
+ private $name = '';
+
+ /**
+ * @var []
+ */
+ private $if = [];
+
+ /**
+ * @var []
+ */
+ private $then = [];
+
+ /**
+ * @var []
+ */
+ private $dependencies = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param string $name
+ */
+ public function __construct( $name, array $if, array $then, array $dependencies = [] ) {
+ $this->name = $name;
+ $this->if = $if;
+ $this->then = $then;
+ $this->dependencies = $dependencies;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getDependencies() {
+ return $this->dependencies;
+ }
+
+ /**
+ * @note < 7.1 unexpected 'if' (T_IF), expecting identifier (T_STRING) ...
+ *
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function when( $key = null ) {
+
+ if ( $key === null ) {
+ return $this->if;
+ }
+
+ if ( isset( $this->if[$key] ) ) {
+ return $this->if[$key];
+ }
+
+ return [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function then( $key = null ) {
+
+ if ( $key === null ) {
+ return $this->then;
+ }
+
+ if ( isset( $this->then[$key] ) ) {
+ return $this->then[$key];
+ }
+
+ return [];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/BadHttpEndpointResponseException.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/BadHttpEndpointResponseException.php
new file mode 100644
index 00000000..5686fc03
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/BadHttpEndpointResponseException.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace SMW\SPARQLStore\Exception;
+
+/**
+ * Class to escalate SPARQL query errors to the interface. We only do this for
+ * malformed queries, permission issues, etc. Connection problems are usually
+ * ignored so as to keep the wiki running even if the SPARQL backend is down.
+ *
+ * @ingroup Sparql
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class BadHttpEndpointResponseException extends \Exception {
+
+ /// Error code: malformed query
+ const ERROR_MALFORMED = 1;
+ /// Error code: service refused to handle the request
+ const ERROR_REFUSED = 2;
+ /// Error code: the query required a graph that does not exist
+ const ERROR_GRAPH_NOEXISTS = 3;
+ /// Error code: some existing graph should not exist to run this query
+ const ERROR_GRAPH_EXISTS = 4;
+ /// Error code: unknown error
+ const ERROR_OTHER = 5;
+ /// Error code: required service not known
+ const ERROR_NOSERVICE = 6;
+
+ /**
+ * SPARQL query that caused the problem.
+ * @var string
+ */
+ public $queryText;
+
+ /**
+ * Error code
+ * @var integer
+ */
+ public $errorCode;
+
+ /**
+ * Constructor that creates an error message based on the given data.
+ *
+ * @param $errorCode integer error code as defined in this class
+ * @param $queryText string with the original SPARQL query/update
+ * @param $endpoint string URL of the endpoint
+ * @param $httpCode mixed integer HTTP error code or some string to print there
+ */
+ function __construct( $errorCode, $queryText, $endpoint, $httpCode = '<not given>' ) {
+
+ switch ( $errorCode ) {
+ case self::ERROR_MALFORMED:
+ $errorName = 'Malformed query';
+ break;
+ case self::ERROR_REFUSED:
+ $errorName = 'Query refused';
+ break;
+ case self::ERROR_GRAPH_NOEXISTS:
+ $errorName = 'Graph not existing';
+ break;
+ case self::ERROR_GRAPH_EXISTS:
+ $errorName = 'Graph already exists';
+ break;
+ case self::ERROR_NOSERVICE:
+ $errorName = 'Required service has not been defined';
+ break;
+ default:
+ $errorCode = self::ERROR_OTHER;
+ $errorName = 'Unkown error';
+ }
+
+ $message = "A SPARQL query error has occurred\n" .
+ "Query: $queryText\n" .
+ "Error: $errorName\n" .
+ "Endpoint: $endpoint\n" .
+ "HTTP response code: $httpCode\n";
+
+ parent::__construct( $message, $errorCode );
+ $this->errorCode = $errorCode;
+ $this->queryText = $queryText;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/HttpEndpointConnectionException.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/HttpEndpointConnectionException.php
new file mode 100644
index 00000000..75c347c4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/HttpEndpointConnectionException.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace SMW\SPARQLStore\Exception;
+
+/**
+ * @ingroup Sparql
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class HttpEndpointConnectionException extends \Exception {
+
+ /**
+ * @since 2.1
+ *
+ * @param string $endpoint
+ * @param integer $errorCode
+ * @param string $errorText
+ */
+ public function __construct( $endpoint, $errorCode, $errorText ) {
+ parent::__construct( "Failed to communicate to Endpoint: $endpoint\n" . "due to curl error: $errorCode ($errorText).\n" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/XmlParserException.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/XmlParserException.php
new file mode 100644
index 00000000..a81e7c73
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/Exception/XmlParserException.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace SMW\SPARQLStore\Exception;
+
+/**
+ * @ingroup Sparql
+ *
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class XmlParserException extends \Exception {
+
+ /**
+ * @since 2.1
+ *
+ * @param string $errorText
+ * @param integer $errorLine
+ * @param integer $errorColumn
+ */
+ public function __construct( $errorText, $errorLine, $errorColumn ) {
+ parent::__construct( "Failed with $errorText on line $errorLine and column $errorColumn .\n" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/HttpResponseErrorMapper.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/HttpResponseErrorMapper.php
new file mode 100644
index 00000000..080cef7a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/HttpResponseErrorMapper.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+use Exception;
+use Onoi\HttpRequest\HttpRequest;
+use SMW\SPARQLStore\Exception\BadHttpEndpointResponseException;
+use SMW\SPARQLStore\Exception\HttpEndpointConnectionException;
+
+/**
+ * Post-processing for a bad inbound responses
+ *
+ * @ingroup Sparql
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class HttpResponseErrorMapper {
+
+ private $httpRequest = null;
+
+ /**
+ * @since 2.0
+ *
+ * @param HttpRequest $httpRequest
+ */
+ public function __construct( HttpRequest $httpRequest ) {
+ $this->httpRequest = $httpRequest;
+ }
+
+ /**
+ * Either throw a suitable exception or fall through if the error should be
+ * handled gracefully. It is attempted to throw exceptions for all errors that
+ * can generally be prevented by proper coding or configuration (e.g. query
+ * syntax errors), and to be silent on all errors that might be caused by
+ * network issues or temporary overloading of the server. In this case, calling
+ * methods rather return something that helps to make the best out of the situation.
+ *
+ * @since 2.0
+ *
+ * @param $endpoint string URL of endpoint that was used
+ * @param $sparql string query that caused the problem
+ *
+ * @throws Exception
+ * @throws SparqlDatabaseException
+ */
+ public function mapErrorResponse( $endpoint, $sparql ) {
+ $error = $this->httpRequest->getLastErrorCode();
+
+ switch ( $error ) {
+ case 22: // equals CURLE_HTTP_RETURNED_ERROR but this constant is not defined in PHP
+ $this->createResponseToHttpError( $this->httpRequest->getLastTransferInfo( CURLINFO_HTTP_CODE ), $endpoint, $sparql );
+ break;
+ case 52:
+ case CURLE_GOT_NOTHING:
+ break; // happens when 4Store crashes, do not bother the wiki
+ case CURLE_COULDNT_CONNECT:
+ break; // fail gracefully if backend is down
+ default:
+ throw new HttpEndpointConnectionException(
+ $endpoint,
+ $error,
+ $this->httpRequest->getLastError()
+ );
+ }
+ }
+
+ private function createResponseToHttpError( $httpCode, $endpoint, $sparql ) {
+
+ /// TODO We are guessing the meaning of HTTP codes here -- the SPARQL 1.1 spec does not yet provide this information for updates (April 15 2011)
+
+ if ( $httpCode == 400 ) { // malformed query
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_MALFORMED, $sparql, $endpoint, $httpCode );
+ } elseif ( $httpCode == 500 ) { // query refused; maybe fail gracefully here (depending on how stores use this)
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_REFUSED, $sparql, $endpoint, $httpCode );
+ } elseif ( $httpCode == 404 ) {
+ return; // endpoint not found, maybe down; fail gracefully
+ }
+
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_OTHER, $sparql, $endpoint, $httpCode );
+ }
+
+}
+
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/HttpResponseParser.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/HttpResponseParser.php
new file mode 100644
index 00000000..7e94f737
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/HttpResponseParser.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+/**
+ * Provides an interface for which responses from a http client (repositor
+ * connection) are parsed into a unified format
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+interface HttpResponseParser {
+
+ /**
+ * @since 2.2
+ *
+ * @param string $response
+ *
+ * @return RepositoryResult
+ */
+ public function parse( $response );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/Condition.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/Condition.php
new file mode 100644
index 00000000..82fe9b22
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/Condition.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\Condition;
+
+/**
+ * Abstract class that represents a SPARQL (sub-)pattern and relevant pieces
+ * of associated information for using it in query building.
+ *
+ * @ingroup SMWStore
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+abstract class Condition {
+
+ /**
+ * If results could be ordered by the things that this condition
+ * matches, then this is the name of the variable to use in ORDER BY.
+ * Otherwise it is ''.
+ * @note SPARQL variable names do not include the initial "?" or "$".
+ * @var string
+ */
+ public $orderByVariable = '';
+
+ /**
+ * Array that relates sortkeys (given by the users, i.e. property
+ * names) to variable names in the generated SPARQL query.
+ * Format sortkey => variable name
+ * @var array
+ */
+ public $orderVariables = [];
+
+ /**
+ * Associative array of additional conditions that should not narrow
+ * down the set of results, but that introduce some relevant variable,
+ * typically for ordering. For instance, selecting the sortkey of a
+ * page needs only be done once per query. The array is indexed by the
+ * name of the (main) selected variable, e.g. "v42sortkey" to allow
+ * elimination of duplicate weak conditions that aim to introduce this
+ * variable.
+ * @var array of format "condition identifier" => "condition"
+ */
+ public $weakConditions = [];
+
+ /**
+ * Associative array of additional conditions that should can narrow
+ * down the set of results,
+ *
+ * @var array of format "condition identifier" => "condition"
+ */
+ public $cogentConditions = [];
+
+ /**
+ * Associative array of additional namespaces that this condition
+ * requires to be declared
+ * @var array of format "shortName" => "namespace URI"
+ */
+ public $namespaces = [];
+
+ /**
+ * Get the SPARQL condition string that this object represents. This
+ * does not include the weak conditions, or additional formulations to
+ * match singletons (see SMWSparqlSingletonCondition).
+ *
+ * @return string
+ */
+ abstract public function getCondition();
+
+ /**
+ * Tell whether the condition string returned by getCondition() is safe
+ * in the sense that it can be used alone in a SPARQL query. This
+ * requires that all filtered variables occur in some graph pattern,
+ * and that the condition is not empty.
+ *
+ * @return boolean
+ */
+ abstract public function isSafe();
+
+ public function addNamespaces( array $namespaces ) {
+ $this->namespaces = array_merge( $this->namespaces, $namespaces );
+ }
+
+ public function getWeakConditionString() {
+ return implode( '', $this->weakConditions );
+ }
+
+ public function getCogentConditionString() {
+ return implode( '', $this->cogentConditions );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/FalseCondition.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/FalseCondition.php
new file mode 100644
index 00000000..2446cd02
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/FalseCondition.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\Condition;
+
+/**
+ * Represents a condition that cannot match anything.
+ * Ordering is not relevant, as there is nothing to order.
+ *
+ * @ingroup SMWStore
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class FalseCondition extends Condition {
+
+ public function getCondition() {
+ return "<http://www.example.org> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#nothing> .\n";
+ }
+
+ public function isSafe() {
+ return true;
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/FilterCondition.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/FilterCondition.php
new file mode 100644
index 00000000..e03f90c5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/FilterCondition.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\Condition;
+
+/**
+ * A SPARQL condition that consists in a FILTER term only (possibly with some
+ * weak conditions to introduce the variables that the filter acts on).
+ *
+ * @ingroup SMWStore
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class FilterCondition extends Condition {
+
+ /**
+ * Additional filter condition, i.e. a string that could be placed in
+ * "FILTER( ... )".
+ * @var string
+ */
+ public $filter;
+
+ public function __construct( $filter, $namespaces = [] ) {
+ $this->filter = $filter;
+ $this->namespaces = $namespaces;
+ }
+
+ public function getCondition() {
+ return "FILTER( {$this->filter} )\n";
+ }
+
+ public function isSafe() {
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/SingletonCondition.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/SingletonCondition.php
new file mode 100644
index 00000000..a7ba530b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/SingletonCondition.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\Condition;
+
+use SMWExpElement;
+
+/**
+ * A SPARQL condition that can match only a single element, or nothing at all.
+ *
+ * @ingroup SMWStore
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class SingletonCondition extends Condition {
+
+ /**
+ * Pattern string. Anything that can be used as a WHERE condition
+ * when put between "{" and "}". Can be empty if the result
+ * unconditionally is the given element.
+ * @var string
+ */
+ public $condition;
+
+ /**
+ * The single element that this condition may possibly match.
+ * @var SMWExpElement
+ */
+ public $matchElement;
+
+ /**
+ * Whether this condition is safe.
+ * @see SMWSparqlCondition::isSafe().
+ * @var boolean
+ */
+ public $isSafe;
+
+ public function __construct( SMWExpElement $matchElement, $condition = '', $isSafe = false, $namespaces = [] ) {
+ $this->matchElement = $matchElement;
+ $this->condition = $condition;
+ $this->isSafe = $isSafe;
+ $this->namespaces = $namespaces;
+ }
+
+ public function getCondition() {
+ return $this->condition;
+ }
+
+ public function isSafe() {
+ return $this->isSafe;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/TrueCondition.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/TrueCondition.php
new file mode 100644
index 00000000..5ec622d5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/TrueCondition.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\Condition;
+
+/**
+ * Represents a condition that matches everything. Weak conditions (see
+ * SMWSparqlCondition::$weakConditions) might be still be included to
+ * enable ordering (selecting sufficient data to order by).
+ *
+ * @ingroup SMWStore
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class TrueCondition extends Condition {
+
+ public function getCondition() {
+ return '';
+ }
+
+ public function isSafe() {
+ return false;
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/WhereCondition.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/WhereCondition.php
new file mode 100644
index 00000000..002282c2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/Condition/WhereCondition.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\Condition;
+
+/**
+ * Container class that represents a SPARQL (sub-)pattern and relevant pieces
+ * of associated information for using it in query building.
+ *
+ * @ingroup SMWStore
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class WhereCondition extends Condition {
+
+ /**
+ * The pattern string. Anything that can be used as a WHERE condition
+ * when put between "{" and "}".
+ * @var string
+ */
+ public $condition;
+
+ /**
+ * Whether this condition is safe.
+ * @see SMWSparqlCondition::isSafe().
+ * @var boolean
+ */
+ public $isSafe;
+
+ public function __construct( $condition, $isSafe, $namespaces = [] ) {
+ $this->condition = $condition;
+ $this->isSafe = $isSafe;
+ $this->namespaces = $namespaces;
+ }
+
+ public function getCondition() {
+ return $this->condition;
+ }
+
+ public function isSafe() {
+ return $this->isSafe;
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php
new file mode 100644
index 00000000..0f8587e8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/ConditionBuilder.php
@@ -0,0 +1,629 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use RuntimeException;
+use SMW\DataTypeRegistry;
+use SMW\DataValues\PropertyChainValue;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\HierarchyLookup;
+use SMW\Message;
+use SMW\Query\DescriptionFactory;
+use SMW\Query\Language\Description;
+use SMW\SPARQLStore\HierarchyFinder;
+use SMW\SPARQLStore\QueryEngine\Condition\Condition;
+use SMW\SPARQLStore\QueryEngine\Condition\SingletonCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\TrueCondition;
+use SMW\Utils\CircularReferenceGuard;
+use SMWDataItem as DataItem;
+use SMWExpElement as ExpElement;
+use SMWExpNsResource as ExpNsResource;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * Build an internal representation for a SPARQL condition from individual query
+ * descriptions
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ConditionBuilder {
+
+ /**
+ * @var EngineOptions
+ */
+ private $engineOptions;
+
+ /**
+ * @var DispatchingDescriptionInterpreter
+ */
+ private $dispatchingDescriptionInterpreter;
+
+ /**
+ * @var CircularReferenceGuard
+ */
+ private $circularReferenceGuard;
+
+ /**
+ * @var HierarchyLookup
+ */
+ private $hierarchyLookup;
+
+ /**
+ * @var DescriptionFactory
+ */
+ private $descriptionFactory;
+
+ /**
+ * @var array
+ */
+ private $errors = [];
+
+ /**
+ * Counter used to generate globally fresh variables.
+ * @var integer
+ */
+ private $variableCounter = 0;
+
+ /**
+ * sortKeys that are being used while building the query conditions
+ * @var array
+ */
+ private $sortKeys = [];
+
+ /**
+ * The name of the SPARQL variable that represents the query result
+ * @var string
+ */
+ private $resultVariable = 'result';
+
+ /**
+ * @var string
+ */
+ private $joinVariable;
+
+ /**
+ * @var DIProperty|null
+ */
+ private $orderByProperty;
+
+ /**
+ * @var array
+ */
+ private $redirectByVariableReplacementMap = [];
+
+ /**
+ * @since 2.2
+ *
+ * @param DescriptionInterpreterFactory $descriptionInterpreterFactory
+ * @param EngineOptions|null $engineOptions
+ */
+ public function __construct( DescriptionInterpreterFactory $descriptionInterpreterFactory, EngineOptions $engineOptions = null ) {
+ $this->dispatchingDescriptionInterpreter = $descriptionInterpreterFactory->newDispatchingDescriptionInterpreter( $this );
+ $this->engineOptions = $engineOptions;
+
+ if ( $this->engineOptions === null ) {
+ $this->engineOptions = new EngineOptions();
+ }
+
+ $this->descriptionFactory = new DescriptionFactory();
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param string $resultVariable
+ */
+ public function setResultVariable( $resultVariable ) {
+ $this->resultVariable = $resultVariable;
+ return $this;
+ }
+
+ /**
+ * Get a fresh unused variable name for building SPARQL conditions.
+ *
+ * @return string
+ */
+ public function getNextVariable( $prefix = 'v' ) {
+ return $prefix . ( ++$this->variableCounter );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param array $sortKeys
+ */
+ public function setSortKeys( $sortKeys ) {
+ $this->sortKeys = $sortKeys;
+ return $this;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @return array
+ */
+ public function getSortKeys() {
+ return $this->sortKeys;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $error
+ */
+ public function addError( $error, $type = Message::TEXT ) {
+ $this->errors[Message::getHash( $error, $type )] = Message::encode( $error, $type );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param CircularReferenceGuard $circularReferenceGuard
+ */
+ public function setCircularReferenceGuard( CircularReferenceGuard $circularReferenceGuard ) {
+ $this->circularReferenceGuard = $circularReferenceGuard;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return CircularReferenceGuard
+ */
+ public function getCircularReferenceGuard() {
+ return $this->circularReferenceGuard;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param HierarchyLookup $hierarchyLookup
+ */
+ public function setHierarchyLookup( HierarchyLookup $hierarchyLookup ) {
+ $this->hierarchyLookup = $hierarchyLookup;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return HierarchyLookup
+ */
+ public function getHierarchyLookup() {
+ return $this->hierarchyLookup;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $joinVariable name of the variable that conditions
+ * will refer to
+ */
+ public function setJoinVariable( $joinVariable ) {
+ $this->joinVariable = $joinVariable;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getJoinVariable() {
+ return $this->joinVariable;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param DIProperty|null $orderByProperty if given then
+ * this is the property the values of which this condition will refer
+ * to, and the condition should also enable ordering by this value
+ */
+ public function setOrderByProperty( $orderByProperty ) {
+ $this->orderByProperty = $orderByProperty;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return DIProperty|null
+ */
+ public function getOrderByProperty() {
+ return $this->orderByProperty;
+ }
+
+ /**
+ * Get a Condition object for a Description.
+ *
+ * This conversion is implemented by a number of recursive functions,
+ * and this is the main entry point for this recursion. In particular,
+ * it resets global variables that are used for the construction.
+ *
+ * If property value variables should be recorded for ordering results
+ * later on, the keys of the respective properties need to be given in
+ * sortKeys earlier.
+ *
+ * @param Description $description
+ *
+ * @return Condition
+ */
+ public function getConditionFrom( Description $description ) {
+ $this->variableCounter = 0;
+
+ $this->setJoinVariable( $this->resultVariable );
+ $this->setOrderByProperty( null );
+
+ $condition = $this->mapDescriptionToCondition( $description );
+
+ $this->addMissingOrderByConditions(
+ $condition
+ );
+
+ $this->addPropertyPathToMatchRedirectTargets(
+ $condition
+ );
+
+ $this->addFilterToRemoveEntitiesThatContainRedirectPredicate(
+ $condition
+ );
+
+ return $condition;
+ }
+
+ /**
+ * Recursively create a Condition from a Description
+ *
+ * @param Description $description
+ *
+ * @return Condition
+ */
+ public function mapDescriptionToCondition( Description $description ) {
+ return $this->dispatchingDescriptionInterpreter->interpretDescription( $description );
+ }
+
+ /**
+ * Build the condition (WHERE) string for a given Condition.
+ * The function also expresses the single value of
+ * SingletonCondition objects in the condition, which may
+ * lead to additional namespaces for serializing its URI.
+ *
+ * @param Condition $condition
+ *
+ * @return string
+ */
+ public function convertConditionToString( Condition &$condition ) {
+
+ $conditionAsString = $condition->getWeakConditionString();
+
+ if ( ( $conditionAsString === '' ) && !$condition->isSafe() ) {
+ $swivtPageResource = Exporter::getInstance()->getSpecialNsResource( 'swivt', 'page' );
+ $conditionAsString = '?' . $this->resultVariable . ' ' . $swivtPageResource->getQName() . " ?url .\n";
+ }
+
+ $conditionAsString .= $condition->getCondition();
+ $conditionAsString .= $condition->getCogentConditionString();
+
+ if ( $condition instanceof SingletonCondition ) { // prepare for ASK, maybe rather use BIND?
+
+ $matchElement = $condition->matchElement;
+
+ if ( $matchElement instanceof ExpElement ) {
+ $matchElementName = TurtleSerializer::getTurtleNameForExpElement( $matchElement );
+ } else {
+ $matchElementName = $matchElement;
+ }
+
+ if ( $matchElement instanceof ExpNsResource ) {
+ $condition->namespaces[$matchElement->getNamespaceId()] = $matchElement->getNamespace();
+ }
+
+ $conditionAsString = str_replace( '?' . $this->resultVariable . ' ', "$matchElementName ", $conditionAsString );
+ }
+
+ return $conditionAsString;
+ }
+
+ /**
+ * Create an Condition from an empty (true) description.
+ * May still require helper conditions for ordering.
+ *
+ * @param $joinVariable string name, see mapDescriptionToCondition()
+ * @param $orderByProperty mixed DIProperty or null, see mapDescriptionToCondition()
+ *
+ * @return Condition
+ */
+ public function newTrueCondition( $joinVariable, $orderByProperty ) {
+ $result = new TrueCondition();
+ $this->addOrderByDataForProperty( $result, $joinVariable, $orderByProperty );
+ return $result;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param DataItem|null $dataItem
+ *
+ * @return string|null
+ */
+ public function tryToFindRedirectVariableForDataItem( DataItem $dataItem = null ) {
+
+ if ( !$dataItem instanceof DIWikiPage || !$this->isSetFlag( SMW_SPARQL_QF_REDI ) ) {
+ return null;
+ }
+
+ // Maybe there is a better way to verify the "isRedirect" state other
+ // than by using the Title object
+ if ( $dataItem->getTitle() === null || !$dataItem->getTitle()->isRedirect() ) {
+ return null;
+ }
+
+ $redirectExpElement = Exporter::getInstance()->getResourceElementForWikiPage( $dataItem );
+
+ // If the resource was matched to an imported vocab then no redirect is required
+ if ( $redirectExpElement->isImported() ) {
+ return null;
+ }
+
+ $valueName = TurtleSerializer::getTurtleNameForExpElement( $redirectExpElement );
+
+ // Add unknow redirect target/variable for value
+ if ( !isset( $this->redirectByVariableReplacementMap[$valueName] ) ) {
+
+ $namespaces[$redirectExpElement->getNamespaceId()] = $redirectExpElement->getNamespace();
+ $redirectByVariable = '?' . $this->getNextVariable( 'r' );
+
+ $this->redirectByVariableReplacementMap[$valueName] = [
+ $redirectByVariable,
+ $namespaces
+ ];
+ }
+
+ // Reuse an existing variable for the value to allow to be used more than
+ // once when referring to the same property/value redirect
+ list( $redirectByVariable, $namespaces ) = $this->redirectByVariableReplacementMap[$valueName];
+
+ return $redirectByVariable;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param integer $featureFlag
+ *
+ * @return boolean
+ */
+ public function isSetFlag( $featureFlag ) {
+
+ $canUse = true;
+
+ // Adhere additional condition
+ if ( $featureFlag === SMW_SPARQL_QF_SUBP ) {
+ $canUse = $this->engineOptions->get( 'smwgQSubpropertyDepth' ) > 0;
+ }
+
+ if ( $featureFlag === SMW_SPARQL_QF_SUBC ) {
+ $canUse = $this->engineOptions->get( 'smwgQSubcategoryDepth' ) > 0;
+ }
+
+ return $this->engineOptions->get( 'smwgSparqlQFeatures' ) === ( (int)$this->engineOptions->get( 'smwgSparqlQFeatures' ) | (int)$featureFlag ) && $canUse;
+ }
+
+ /**
+ * Extend the given SPARQL condition by a suitable order by variable,
+ * if an order by property is set.
+ *
+ * @param Condition $sparqlCondition condition to modify
+ * @param string $mainVariable the variable that represents the value to be ordered
+ * @param mixed $orderByProperty DIProperty or null
+ * @param integer $diType DataItem type id if known, or DataItem::TYPE_NOTYPE to determine it from the property
+ */
+ public function addOrderByDataForProperty( Condition &$sparqlCondition, $mainVariable, $orderByProperty, $diType = DataItem::TYPE_NOTYPE ) {
+ if ( is_null( $orderByProperty ) ) {
+ return;
+ }
+
+ if ( $diType == DataItem::TYPE_NOTYPE ) {
+ $diType = DataTypeRegistry::getInstance()->getDataItemId( $orderByProperty->findPropertyTypeID() );
+ }
+
+ $this->addOrderByData( $sparqlCondition, $mainVariable, $diType );
+ }
+
+ /**
+ * Extend the given SPARQL condition by a suitable order by variable,
+ * possibly adding conditions if required for the type of data.
+ *
+ * @param Condition $sparqlCondition condition to modify
+ * @param string $mainVariable the variable that represents the value to be ordered
+ * @param integer $diType DataItem type id
+ */
+ public function addOrderByData( Condition &$condition, $mainVariable, $diType ) {
+
+ if ( $diType !== DataItem::TYPE_WIKIPAGE ) {
+ return $condition->orderByVariable = $mainVariable;
+ }
+
+ $condition->orderByVariable = $mainVariable . 'sk';
+
+ if ( $this->isSetFlag( SMW_SPARQL_QF_COLLATION ) ) {
+ $skeyExpElement = Exporter::getInstance()->getSpecialNsResource( 'swivt', 'sort' );
+ } else {
+ $skeyExpElement = Exporter::getInstance()->getSpecialPropertyResource( '_SKEY' );
+ }
+
+ $weakConditions = [
+ $condition->orderByVariable =>"?$mainVariable " . $skeyExpElement->getQName() . " ?{$condition->orderByVariable} .\n"
+ ];
+
+ $condition->weakConditions += $weakConditions;
+ }
+
+ /**
+ * Extend the given Condition with additional conditions to
+ * ensure that it can be ordered by all requested properties. After
+ * this operation, every key in sortKeys is assigned to a query
+ * variable by $sparqlCondition->orderVariables.
+ *
+ * @param Condition $condition condition to modify
+ */
+ protected function addMissingOrderByConditions( Condition &$condition ) {
+ foreach ( $this->sortKeys as $propertyKey => $order ) {
+
+ if ( !is_string( $propertyKey ) ) {
+ throw new RuntimeException( "Expected a string value as sortkey" );
+ }
+
+ if ( strpos( $propertyKey, " " ) !== false ) {
+ throw new RuntimeException( "Expected the canonical form of {$propertyKey} (without any whitespace)" );
+ }
+
+ if ( !array_key_exists( $propertyKey, $condition->orderVariables ) ) { // Find missing property to sort by.
+ $this->addOrderForUnknownPropertyKey( $condition, $propertyKey, $order );
+ }
+ }
+ }
+
+ private function addOrderForUnknownPropertyKey( Condition &$condition, $propertyKey, $order ) {
+
+ if ( $propertyKey === '' || $propertyKey === '#' ) { // order by result page sortkey
+
+ $this->addOrderByData(
+ $condition,
+ $this->resultVariable,
+ DataItem::TYPE_WIKIPAGE
+ );
+
+ $condition->orderVariables[$propertyKey] = $condition->orderByVariable;
+ return;
+ } elseif ( PropertyChainValue::isChained( $propertyKey ) ) { // Try to extend query.
+ $propertyChainValue = new PropertyChainValue();
+ $propertyChainValue->setUserValue( $propertyKey );
+
+ if ( !$propertyChainValue->isValid() ) {
+ return $description;
+ }
+
+ $lastDataItem = $propertyChainValue->getLastPropertyChainValue()->getDataItem();
+
+ $description = $this->descriptionFactory->newSomeProperty(
+ $lastDataItem,
+ $this->descriptionFactory->newThingDescription()
+ );
+
+ foreach ( $propertyChainValue->getPropertyChainValues() as $val ) {
+ $description = $this->descriptionFactory->newSomeProperty(
+ $val->getDataItem(),
+ $description
+ );
+ }
+
+ // Add and replace Foo.Bar=asc with Bar=asc as we ultimately only
+ // order to the result of the last element
+ $this->sortKeys[$lastDataItem->getKey()] = $order;
+ unset( $this->sortKeys[$propertyKey] );
+ $propertyKey = $lastDataItem->getKey();
+
+ $auxDescription = $description;
+ } else {
+ $auxDescription = $this->descriptionFactory->newSomeProperty(
+ new DIProperty( $propertyKey ),
+ $this->descriptionFactory->newThingDescription()
+ );
+ }
+
+ $this->setJoinVariable( $this->resultVariable );
+ $this->setOrderByProperty( null );
+
+ $auxCondition = $this->mapDescriptionToCondition(
+ $auxDescription
+ );
+
+ // orderVariables MUST be set for $propertyKey -- or there is a bug; let it show!
+ $condition->orderVariables[$propertyKey] = $auxCondition->orderVariables[$propertyKey];
+ $condition->weakConditions[$condition->orderVariables[$propertyKey]] = $auxCondition->getWeakConditionString() . $auxCondition->getCondition();
+ $condition->namespaces = array_merge( $condition->namespaces, $auxCondition->namespaces );
+ }
+
+ /**
+ * @see http://www.w3.org/TR/sparql11-query/#propertypaths
+ *
+ * Query of:
+ *
+ * SELECT DISTINCT ?result WHERE {
+ * ?result swivt:wikiPageSortKey ?resultsk .
+ * {
+ * ?result property:FOO ?v1 .
+ * FILTER( ?v1sk >= "=BAR" )
+ * ?v1 swivt:wikiPageSortKey ?v1sk .
+ * } UNION {
+ * ?result property:FOO ?v2 .
+ * }
+ * }
+ *
+ * results in:
+ *
+ * SELECT DISTINCT ?result WHERE {
+ * ?result swivt:wikiPageSortKey ?resultsk .
+ * ?r2 ^swivt:redirectsTo property:FOO .
+ * {
+ * ?result ?r2 ?v1 .
+ * FILTER( ?v1sk >= "=BAR" )
+ * ?v1 swivt:wikiPageSortKey ?v1sk .
+ * } UNION {
+ * ?result ?r2 ?v3 .
+ * }
+ * }
+ */
+ private function addPropertyPathToMatchRedirectTargets( Condition &$condition ) {
+
+ if ( $this->redirectByVariableReplacementMap === [] ) {
+ return;
+ }
+
+ $weakConditions = [];
+ $namespaces = [];
+
+ $rediExpElement = Exporter::getInstance()->getSpecialPropertyResource( '_REDI' );
+ $namespaces[$rediExpElement->getNamespaceId()] = $rediExpElement->getNamespace();
+
+ foreach ( $this->redirectByVariableReplacementMap as $valueName => $content ) {
+ list( $redirectByVariable, $ns ) = $content;
+ $weakConditions[] = "$redirectByVariable " . "^" . $rediExpElement->getQName() . " $valueName .\n";
+ $namespaces = array_merge( $namespaces, $ns );
+ }
+
+ $condition->namespaces = array_merge( $condition->namespaces, $namespaces );
+ $condition->weakConditions += $weakConditions;
+ }
+
+ /**
+ * @see https://www.w3.org/TR/rdf-sparql-query/#func-bound
+ *
+ * Remove entities that contain a "swivt:redirectsTo" predicate
+ */
+ private function addFilterToRemoveEntitiesThatContainRedirectPredicate( Condition &$condition ) {
+
+ $rediExpElement = Exporter::getInstance()->getSpecialPropertyResource( '_REDI' );
+ $namespaces[$rediExpElement->getNamespaceId()] = $rediExpElement->getNamespace();
+
+ $boundVariable = '?' . $this->getNextVariable( 'o' );
+ $cogentCondition = " OPTIONAL { ?$this->resultVariable " . $rediExpElement->getQName() . " $boundVariable } .\n FILTER ( !bound( $boundVariable ) ) .\n";
+
+ $condition->addNamespaces( $namespaces );
+ $condition->cogentConditions[$boundVariable] = $cogentCondition;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreter.php
new file mode 100644
index 00000000..68c87df2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreter.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use SMW\Query\Language\Description;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+interface DescriptionInterpreter {
+
+ /**
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description );
+
+ /**
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return Condition
+ */
+ public function interpretDescription( Description $description );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreterFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreterFactory.php
new file mode 100644
index 00000000..1ebdfb23
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreterFactory.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\ClassDescriptionInterpreter;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\ConceptDescriptionInterpreter;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\ConjunctionInterpreter;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\DisjunctionInterpreter;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\DispatchingDescriptionInterpreter;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\NamespaceDescriptionInterpreter;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\SomePropertyInterpreter;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\ThingDescriptionInterpreter;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreters\ValueDescriptionInterpreter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DescriptionInterpreterFactory {
+
+ /**
+ * @since 2.5
+ *
+ * @param ConditionBuilder $conditionBuilder
+ *
+ * @return DispatchingDescriptionInterpreter
+ */
+ public function newDispatchingDescriptionInterpreter( ConditionBuilder $conditionBuilder ) {
+
+ $dispatchingDescriptionInterpreter = new DispatchingDescriptionInterpreter();
+
+ $dispatchingDescriptionInterpreter->addDefaultInterpreter(
+ new ThingDescriptionInterpreter( $conditionBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new SomePropertyInterpreter( $conditionBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new ConjunctionInterpreter( $conditionBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new DisjunctionInterpreter( $conditionBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new NamespaceDescriptionInterpreter( $conditionBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new ClassDescriptionInterpreter( $conditionBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new ValueDescriptionInterpreter( $conditionBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new ConceptDescriptionInterpreter( $conditionBuilder )
+ );
+
+ return $dispatchingDescriptionInterpreter;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php
new file mode 100644
index 00000000..8edc933e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\ClassDescription;
+use SMW\Query\Language\Description;
+use SMW\SPARQLStore\QueryEngine\Condition\FalseCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\WhereCondition;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+use SMWDataItem as DataItem;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ClassDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.1
+ *
+ * @param ConditionBuilder|null $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder = null ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->exporter = Exporter::getInstance();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof ClassDescription;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function interpretDescription( Description $description ) {
+
+ $joinVariable = $this->conditionBuilder->getJoinVariable();
+ $orderByProperty = $this->conditionBuilder->getOrderByProperty();
+
+ list( $condition, $namespaces ) = $this->mapCategoriesToConditionElements(
+ $description->getCategories(),
+ $description->getHierarchyDepth(),
+ $joinVariable
+ );
+
+ // empty disjunction: always false, no results to order
+ if ( $condition === '' ) {
+ return new FalseCondition();
+ }
+
+ $result = new WhereCondition( $condition, true, $namespaces );
+
+ $this->conditionBuilder->addOrderByDataForProperty(
+ $result,
+ $joinVariable,
+ $orderByProperty,
+ DataItem::TYPE_WIKIPAGE
+ );
+
+ return $result;
+ }
+
+ private function mapCategoriesToConditionElements( array $categories, $depth, $joinVariable ) {
+
+ $condition = '';
+ $namespaces = [];
+ $instExpElement = $this->exporter->getSpecialPropertyResource( '_INST' );
+
+ foreach( $categories as $category ) {
+
+ $categoryExpElement = $this->exporter->getResourceElementForWikiPage( $category );
+ $categoryExpName = TurtleSerializer::getTurtleNameForExpElement( $categoryExpElement );
+
+ $namespaces[$categoryExpElement->getNamespaceId()] = $categoryExpElement->getNamespace();
+
+ $classHierarchyPattern = $this->tryToAddClassHierarchyPattern(
+ $category,
+ $depth,
+ $categoryExpName
+ );
+
+ $newcondition = $classHierarchyPattern === '' ? "{ " : "{\n" . $classHierarchyPattern;
+ $newcondition .= "?$joinVariable " . $instExpElement->getQName() . " $categoryExpName . }\n";
+
+ if ( $condition === '' ) {
+ $condition = $newcondition;
+ } else {
+ $condition .= "UNION\n$newcondition";
+ }
+ }
+
+ return [ $condition, $namespaces ];
+ }
+
+ private function tryToAddClassHierarchyPattern( $category, $depth, &$categoryExpName ) {
+
+ if ( !$this->conditionBuilder->isSetFlag( SMW_SPARQL_QF_SUBC ) || ( $depth !== null && $depth < 1 ) ) {
+ return '';
+ }
+
+ if ( $this->conditionBuilder->getHierarchyLookup() === null || !$this->conditionBuilder->getHierarchyLookup()->hasSubcategory( $category ) ) {
+ return '';
+ }
+
+ $subClassExpElement = $this->exporter->getSpecialPropertyResource( '_SUBC' );
+
+ // @see notes in SomePropertyInterpreter
+ $pathOp = $depth > 1 || $depth === null ? '*' : '?';
+
+ $classHierarchyByVariable = "?" . $this->conditionBuilder->getNextVariable( 'sc' );
+ $condition = "$classHierarchyByVariable " . $subClassExpElement->getQName() . "$pathOp $categoryExpName .\n";
+ $categoryExpName = "$classHierarchyByVariable";
+
+ return $condition;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php
new file mode 100644
index 00000000..8c78c922
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Query\Language\ConceptDescription;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\SPARQLStore\QueryEngine\Condition\FalseCondition;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+use SMWExporter as Exporter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ConceptDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.1
+ *
+ * @param ConditionBuilder|null $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder = null ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->exporter = Exporter::getInstance();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof ConceptDescription;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function interpretDescription( Description $description ) {
+
+ $joinVariable = $this->conditionBuilder->getJoinVariable();
+ $orderByProperty = $this->conditionBuilder->getOrderByProperty();
+
+ $conceptDescription = $this->getConceptDescription(
+ $description->getConcept()
+ );
+
+ if ( $conceptDescription === '' ) {
+ return new FalseCondition();
+ }
+
+ $hash = 'concept-' . $conceptDescription->getQueryString();
+
+ $this->conditionBuilder->getCircularReferenceGuard()->mark( $hash );
+
+ if ( $this->conditionBuilder->getCircularReferenceGuard()->isCircular( $hash ) ) {
+
+ $this->conditionBuilder->addError(
+ [ 'smw-query-condition-circular', $description->getQueryString() ]
+ );
+
+ return new FalseCondition();
+ }
+
+ $this->conditionBuilder->setJoinVariable( $joinVariable );
+ $this->conditionBuilder->setOrderByProperty( $orderByProperty );
+
+ $condition = $this->conditionBuilder->mapDescriptionToCondition(
+ $conceptDescription
+ );
+
+ $this->conditionBuilder->getCircularReferenceGuard()->unmark( $hash );
+
+ return $condition;
+ }
+
+ private function getConceptDescription( DIWikiPage $concept ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $value = $applicationFactory->getStore()->getSemanticData( $concept )->getPropertyValues(
+ new DIProperty( '_CONC' )
+ );
+
+ if ( $value === null || $value === [] ) {
+ return '';
+ }
+
+ $value = end( $value );
+
+ $description = $applicationFactory->newQueryParser()->getQueryDescription(
+ $value->getConceptQuery()
+ );
+
+ $this->findCircularDescription( $concept, $description );
+
+ return $description;
+ }
+
+ private function findCircularDescription( $concept, $description ) {
+
+ if ( $description instanceof ConceptDescription ) {
+ if ( $description->getConcept()->equals( $concept ) ) {
+ $this->conditionBuilder->addError(
+ [ 'smw-query-condition-circular', $description->getQueryString() ]
+ );
+ return;
+ }
+ }
+
+ if ( $description instanceof Conjunction || $description instanceof Disjunction ) {
+ foreach ( $description->getDescriptions() as $desc ) {
+ $this->findCircularDescription( $concept, $desc );
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ConjunctionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ConjunctionInterpreter.php
new file mode 100644
index 00000000..0c8896ed
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ConjunctionInterpreter.php
@@ -0,0 +1,248 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\SPARQLStore\QueryEngine\Condition\FalseCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\FilterCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\SingletonCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\TrueCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\WhereCondition;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+use SMWExpElement as ExpElement;
+use SMWExpNsResource as ExpNsResource;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ConjunctionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.1
+ *
+ * @param ConditionBuilder|null $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder = null ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->exporter = Exporter::getInstance();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof Conjunction;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function interpretDescription( Description $description ) {
+
+ $joinVariable = $this->conditionBuilder->getJoinVariable();
+ $orderByProperty = $this->conditionBuilder->getOrderByProperty();
+
+ $subDescriptions = $description->getDescriptions();
+
+ $result = $this->doPreliminarySubDescriptionCheck(
+ $subDescriptions,
+ $joinVariable,
+ $orderByProperty
+ );
+
+ if ( $result !== null ) {
+ return $result;
+ }
+
+ $subConditionElements = $this->doResolveSubDescriptionsRecursively(
+ $subDescriptions,
+ $joinVariable
+ );
+
+ if ( $subConditionElements instanceof FalseCondition ) {
+ return $subConditionElements;
+ }
+
+ $result = $this->createConditionFromSubConditionElements( $subConditionElements );
+
+ $result->weakConditions = $subConditionElements->weakConditions;
+ $result->orderVariables = $subConditionElements->orderVariables;
+
+ $this->conditionBuilder->addOrderByDataForProperty(
+ $result,
+ $joinVariable,
+ $orderByProperty
+ );
+
+ return $result;
+ }
+
+ private function doPreliminarySubDescriptionCheck( $subDescriptions, $joinVariable, $orderByProperty ) {
+
+ $count = count( $subDescriptions );
+
+ // empty conjunction: true
+ if ( $count == 0 ) {
+ return $this->conditionBuilder->newTrueCondition(
+ $joinVariable,
+ $orderByProperty
+ );
+ }
+
+ // conjunction with one element
+ if ( $count == 1 ) {
+
+ $this->conditionBuilder->setJoinVariable( $joinVariable );
+ $this->conditionBuilder->setOrderByProperty( $orderByProperty );
+
+ return $this->conditionBuilder->mapDescriptionToCondition(
+ reset( $subDescriptions )
+ );
+ }
+
+ return null;
+ }
+
+ private function doResolveSubDescriptionsRecursively( $subDescriptions, $joinVariable ) {
+
+ // Using a stdClass as data container for simpler handling in follow-up tasks
+ // and as the class is not exposed publicly we don't need to create
+ // an extra "real" class to manage its elements
+ $subConditionElements = new \stdClass;
+
+ $subConditionElements->condition = '';
+ $subConditionElements->filter = '';
+ $subConditionElements->singletonMatchElement = null;
+
+ $namespaces = $weakConditions = $orderVariables = [];
+ $singletonMatchElementName = '';
+ $hasSafeSubconditions = false;
+
+ foreach ( $subDescriptions as $subDescription ) {
+
+ $this->conditionBuilder->setJoinVariable( $joinVariable );
+ $this->conditionBuilder->setOrderByProperty( null );
+
+ $subCondition = $this->conditionBuilder->mapDescriptionToCondition(
+ $subDescription
+ );
+
+ if ( $subCondition instanceof FalseCondition ) {
+ return new FalseCondition();
+ } elseif ( $subCondition instanceof TrueCondition ) {
+ // ignore true conditions in a conjunction
+ } elseif ( $subCondition instanceof WhereCondition ) {
+ $subConditionElements->condition .= $subCondition->condition;
+ } elseif ( $subCondition instanceof FilterCondition ) {
+ $subConditionElements->filter .= ( $subConditionElements->filter ? ' && ' : '' ) . $subCondition->filter;
+ } elseif ( $subCondition instanceof SingletonCondition ) {
+ $matchElement = $subCondition->matchElement;
+
+ if ( $matchElement instanceof ExpElement ) {
+ $matchElementName = TurtleSerializer::getTurtleNameForExpElement( $matchElement );
+ } else {
+ $matchElementName = $matchElement;
+ }
+
+ if ( $matchElement instanceof ExpNsResource ) {
+ $namespaces[$matchElement->getNamespaceId()] = $matchElement->getNamespace();
+ }
+
+ if ( ( $subConditionElements->singletonMatchElement !== null ) &&
+ ( $singletonMatchElementName !== $matchElementName ) ) {
+ return new FalseCondition();
+ }
+
+ $subConditionElements->condition .= $subCondition->condition;
+ $subConditionElements->singletonMatchElement = $subCondition->matchElement;
+ $singletonMatchElementName = $matchElementName;
+ }
+
+ $hasSafeSubconditions = $hasSafeSubconditions || $subCondition->isSafe();
+ $namespaces = array_merge( $namespaces, $subCondition->namespaces );
+ $weakConditions = array_merge( $weakConditions, $subCondition->weakConditions );
+ $orderVariables = array_merge( $orderVariables, $subCondition->orderVariables );
+ }
+
+ $subConditionElements->hasSafeSubconditions = $hasSafeSubconditions;
+ $subConditionElements->namespaces = $namespaces;
+ $subConditionElements->weakConditions = $weakConditions;
+ $subConditionElements->orderVariables = $orderVariables;
+
+ return $subConditionElements;
+ }
+
+ private function createConditionFromSubConditionElements( $subConditionElements ) {
+
+ if ( $subConditionElements->singletonMatchElement instanceof ExpElement ) {
+ return $this->createSingletonCondition( $subConditionElements );
+ }
+
+ if ( $subConditionElements->condition === '' ) {
+ return $this->createFilterCondition( $subConditionElements );
+ }
+
+ return $this->createWhereCondition( $subConditionElements );
+ }
+
+ private function createSingletonCondition( $subConditionElements ) {
+
+ if ( $subConditionElements->filter !== '' ) {
+ $subConditionElements->condition .= "FILTER( $subConditionElements->filter )";
+ }
+
+ $result = new SingletonCondition(
+ $subConditionElements->singletonMatchElement,
+ $subConditionElements->condition,
+ $subConditionElements->hasSafeSubconditions,
+ $subConditionElements->namespaces
+ );
+
+ return $result;
+ }
+
+ private function createFilterCondition( $subConditionElements ) {
+ return new FilterCondition(
+ $subConditionElements->filter,
+ $subConditionElements->namespaces
+ );
+ }
+
+ private function createWhereCondition( $subConditionElements ) {
+
+ if ( $subConditionElements->filter !== '' ) {
+ $subConditionElements->condition .= "FILTER( $subConditionElements->filter )";
+ }
+
+ $result = new WhereCondition(
+ $subConditionElements->condition,
+ $subConditionElements->hasSafeSubconditions,
+ $subConditionElements->namespaces
+ );
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/DisjunctionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/DisjunctionInterpreter.php
new file mode 100644
index 00000000..90af7fd1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/DisjunctionInterpreter.php
@@ -0,0 +1,256 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\SPARQLStore\QueryEngine\Condition\FalseCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\FilterCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\SingletonCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\TrueCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\WhereCondition;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+use SMWExpElement as ExpElement;
+use SMWExpNsResource as ExpNsResource;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class DisjunctionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.1
+ *
+ * @param ConditionBuilder|null $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder = null ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->exporter = Exporter::getInstance();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof Disjunction;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function interpretDescription( Description $description ) {
+
+ $joinVariable = $this->conditionBuilder->getJoinVariable();
+ $orderByProperty = $this->conditionBuilder->getOrderByProperty();
+
+ $subDescriptions = $description->getDescriptions();
+
+ $result = $this->doPreliminarySubDescriptionCheck(
+ $subDescriptions,
+ $joinVariable,
+ $orderByProperty
+ );
+
+ if ( $result !== null ) {
+ return $result;
+ }
+
+ $subConditionElements = $this->doResolveSubDescriptionsRecursively(
+ $subDescriptions,
+ $joinVariable,
+ $orderByProperty
+ );
+
+ if ( $subConditionElements instanceof TrueCondition ) {
+ return $subConditionElements;
+ }
+
+ if ( ( $subConditionElements->unionCondition === '' ) && ( $subConditionElements->filter === '' ) ) {
+ return new FalseCondition();
+ }
+
+ $result = $this->createConditionFromSubConditionElements(
+ $subConditionElements,
+ $joinVariable
+ );
+
+ $result->weakConditions = $subConditionElements->weakConditions;
+
+ $this->conditionBuilder->addOrderByDataForProperty(
+ $result,
+ $joinVariable,
+ $orderByProperty
+ );
+
+ return $result;
+ }
+
+ private function doPreliminarySubDescriptionCheck( $subDescriptions, $joinVariable, $orderByProperty ) {
+
+ $count = count( $subDescriptions );
+
+ // empty Disjunction: true
+ if ( $count == 0 ) {
+ return new FalseCondition();
+ }
+
+ // Disjunction with one element
+ // else: proper disjunction; note that orderVariables found in subconditions cannot be used for the whole disjunction
+ if ( $count == 1 ) {
+
+ $this->conditionBuilder->setJoinVariable( $joinVariable );
+ $this->conditionBuilder->setOrderByProperty( $orderByProperty );
+
+ return $this->conditionBuilder->mapDescriptionToCondition(
+ reset( $subDescriptions )
+ );
+ }
+
+ return null;
+ }
+
+ private function doResolveSubDescriptionsRecursively( $subDescriptions, $joinVariable, $orderByProperty ) {
+
+ // Using a stdClass as data container for simpler handling in follow-up tasks
+ // and as the class is not exposed publicly we don't need to create
+ // an extra "real" class to manage its elements
+ $subConditionElements = new \stdClass;
+
+ $subConditionElements->unionCondition = '';
+ $subConditionElements->filter = '';
+
+ $namespaces = $weakConditions = [];
+ $hasSafeSubconditions = false;
+
+ foreach ( $subDescriptions as $subDescription ) {
+
+ $this->conditionBuilder->setJoinVariable( $joinVariable );
+ $this->conditionBuilder->setOrderByProperty( null );
+
+ $subCondition = $this->conditionBuilder->mapDescriptionToCondition(
+ $subDescription
+ );
+
+ if ( $subCondition instanceof FalseCondition ) {
+ // empty parts in a disjunction can be ignored
+ } elseif ( $subCondition instanceof TrueCondition ) {
+ return $this->conditionBuilder->newTrueCondition(
+ $joinVariable,
+ $orderByProperty
+ );
+ } elseif ( $subCondition instanceof WhereCondition ) {
+ $hasSafeSubconditions = $hasSafeSubconditions || $subCondition->isSafe();
+ $subConditionElements->unionCondition .= ( $subConditionElements->unionCondition ? ' UNION ' : '' ) .
+ "{\n" . $subCondition->condition . "}";
+ } elseif ( $subCondition instanceof FilterCondition ) {
+ $subConditionElements->filter .= ( $subConditionElements->filter ? ' || ' : '' ) . $subCondition->filter;
+ } elseif ( $subCondition instanceof SingletonCondition ) {
+
+ $hasSafeSubconditions = $hasSafeSubconditions || $subCondition->isSafe();
+ $matchElement = $subCondition->matchElement;
+
+ if ( $matchElement instanceof ExpElement ) {
+ $matchElementName = TurtleSerializer::getTurtleNameForExpElement( $matchElement );
+ } else {
+ $matchElementName = $matchElement;
+ }
+
+ if ( $matchElement instanceof ExpNsResource ) {
+ $namespaces[$matchElement->getNamespaceId()] = $matchElement->getNamespace();
+ }
+
+ if ( $subCondition->condition === '' ) {
+ $subConditionElements->filter .= ( $subConditionElements->filter ? ' || ' : '' ) . "?$joinVariable = $matchElementName";
+ } else {
+ $subConditionElements->unionCondition .= ( $subConditionElements->unionCondition ? ' UNION ' : '' ) .
+ "{\n" . $subCondition->condition . " FILTER( ?$joinVariable = $matchElementName ) }";
+ }
+
+ // Relates to wikipage [[Foo::~*a*||~*A*]] in value regex disjunction
+ // where a singleton is required to search against the sortkey but
+ // replacing the filter with the condition temporary stored in
+ // weakconditions
+ if ( $subConditionElements->unionCondition && $subCondition->weakConditions !== [] ) {
+ $weakCondition = array_shift( $subCondition->weakConditions );
+ $subConditionElements->unionCondition = str_replace(
+ "FILTER( ?$joinVariable = $matchElementName )",
+ $weakCondition,
+ $subConditionElements->unionCondition
+ );
+ }
+ }
+
+ $namespaces = array_merge( $namespaces, $subCondition->namespaces );
+ $weakConditions = array_merge( $weakConditions, $subCondition->weakConditions );
+ }
+
+ $subConditionElements->namespaces = $namespaces;
+ $subConditionElements->weakConditions = $weakConditions;
+ $subConditionElements->hasSafeSubconditions = $hasSafeSubconditions;
+
+ return $subConditionElements;
+ }
+
+ private function createConditionFromSubConditionElements( $subConditionElements, $joinVariable ) {
+
+ if ( $subConditionElements->unionCondition === '' ) {
+ return $this->createFilterCondition( $subConditionElements );
+ }
+
+ if ( $subConditionElements->filter === '' ) {
+ return $this->createWhereCondition( $subConditionElements );
+ }
+
+ $subJoinVariable = $this->conditionBuilder->getNextVariable();
+
+ $subConditionElements->unionCondition = str_replace(
+ "?$joinVariable ",
+ "?$subJoinVariable ",
+ $subConditionElements->unionCondition
+ );
+
+ $subConditionElements->filter .= " || ?$joinVariable = ?$subJoinVariable";
+ $subConditionElements->hasSafeSubconditions = false;
+
+ $subConditionElements->unionCondition = "OPTIONAL { $subConditionElements->unionCondition }\n FILTER( $subConditionElements->filter )\n";
+
+ return $this->createWhereCondition( $subConditionElements );
+ }
+
+ private function createFilterCondition( $subConditionElements ) {
+ return new FilterCondition(
+ $subConditionElements->filter,
+ $subConditionElements->namespaces
+ );
+ }
+
+ private function createWhereCondition( $subConditionElements ) {
+ return new WhereCondition(
+ $subConditionElements->unionCondition,
+ $subConditionElements->hasSafeSubconditions,
+ $subConditionElements->namespaces
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/DispatchingDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/DispatchingDescriptionInterpreter.php
new file mode 100644
index 00000000..7c105477
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/DispatchingDescriptionInterpreter.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Description;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class DispatchingDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var DescriptionInterpreter[]
+ */
+ private $interpreters = [];
+
+ /**
+ * @var DescriptionInterpreter
+ */
+ private $defaultInterpreter = null;
+
+ /**
+ * @param Description $description
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+
+ foreach ( $this->interpreters as $interpreter ) {
+ if ( $interpreter->canInterpretDescription( $description ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param Description $description
+ *
+ * @return Condition
+ */
+ public function interpretDescription( Description $description ) {
+
+ foreach ( $this->interpreters as $interpreter ) {
+ if ( $interpreter->canInterpretDescription( $description ) ) {
+ return $interpreter->interpretDescription( $description );
+ }
+ }
+
+ // Instead of throwing an exception we return a ThingDescriptionInterpreter
+ // for all unregistered/unknown descriptions
+ return $this->defaultInterpreter->interpretDescription( $description );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param DescriptionInterpreter $interpreter
+ */
+ public function addInterpreter( DescriptionInterpreter $interpreter ) {
+ $this->interpreters[] = $interpreter;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param DescriptionInterpreter $defaultInterpreter
+ */
+ public function addDefaultInterpreter( DescriptionInterpreter $defaultInterpreter ) {
+ $this->defaultInterpreter = $defaultInterpreter;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php
new file mode 100644
index 00000000..a898a733
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Description;
+use SMW\Query\Language\NamespaceDescription;
+use SMW\SPARQLStore\QueryEngine\Condition\WhereCondition;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+use SMWDataItem as DataItem;
+use SMWExpLiteral as ExpLiteral;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class NamespaceDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.1
+ *
+ * @param ConditionBuilder|null $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder = null ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->exporter = Exporter::getInstance();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof NamespaceDescription;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function interpretDescription( Description $description ) {
+
+ $joinVariable = $this->conditionBuilder->getJoinVariable();
+ $orderByProperty = $this->conditionBuilder->getOrderByProperty();
+
+ $nspropExpElement = $this->exporter->getSpecialNsResource( 'swivt', 'wikiNamespace' );
+ $nsExpElement = new ExpLiteral( strval( $description->getNamespace() ), 'http://www.w3.org/2001/XMLSchema#integer' );
+
+ $nsName = TurtleSerializer::getTurtleNameForExpElement( $nsExpElement );
+ $condition = "{ ?$joinVariable " . $nspropExpElement->getQName() . " $nsName . }\n";
+
+ $result = new WhereCondition( $condition, true, [] );
+
+ $this->conditionBuilder->addOrderByDataForProperty(
+ $result,
+ $joinVariable,
+ $orderByProperty,
+ DataItem::TYPE_WIKIPAGE
+ );
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php
new file mode 100644
index 00000000..9350a7c1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\DIProperty;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\SomeProperty;
+use SMW\SPARQLStore\QueryEngine\Condition\FalseCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\FilterCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\SingletonCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\WhereCondition;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+use SMWDataItem as DataItem;
+use SMWExpElement as ExpElement;
+use SMWExpNsResource as ExpNsResource;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class SomePropertyInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.1
+ *
+ * @param ConditionBuilder|null $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder = null ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->exporter = Exporter::getInstance();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof SomeProperty;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function interpretDescription( Description $description ) {
+
+ $joinVariable = $this->conditionBuilder->getJoinVariable();
+ $orderByProperty = $this->conditionBuilder->getOrderByProperty();
+
+ $property = $description->getProperty();
+
+ list( $innerOrderByProperty, $innerCondition, $innerJoinVariable ) = $this->doResolveInnerConditionRecursively(
+ $property,
+ $description->getDescription()
+ );
+
+ if ( $innerCondition instanceof FalseCondition ) {
+ return new FalseCondition();
+ }
+
+ $namespaces = $innerCondition->namespaces;
+
+ $objectName = $this->findObjectNameFromInnerCondition(
+ $innerCondition,
+ $innerJoinVariable,
+ $namespaces
+ );
+
+ list ( $subjectName, $objectName, $nonInverseProperty ) = $this->doExchangeForWhenInversePropertyIsUsed(
+ $property,
+ $objectName,
+ $joinVariable
+ );
+
+ $propertyName = $this->findMostSuitablePropertyRepresentation(
+ $property,
+ $nonInverseProperty,
+ $namespaces
+ );
+
+ $this->tryToAddPropertyPathForSaturatedHierarchy(
+ $innerCondition,
+ $nonInverseProperty,
+ $propertyName,
+ $description->getHierarchyDepth()
+ );
+
+ $condition = $this->concatenateToConditionString(
+ $subjectName,
+ $propertyName,
+ $objectName,
+ $innerCondition
+ );
+
+ $result = new WhereCondition( $condition, true, $namespaces );
+
+ // Record inner ordering variable if found
+ $result->orderVariables = $innerCondition->orderVariables;
+
+ if ( $innerOrderByProperty !== null && $innerCondition->orderByVariable !== '' ) {
+ $result->orderVariables[$property->getKey()] = $innerCondition->orderByVariable;
+ }
+
+ $this->conditionBuilder->addOrderByDataForProperty(
+ $result,
+ $joinVariable,
+ $orderByProperty,
+ DataItem::TYPE_WIKIPAGE
+ );
+
+ return $result;
+ }
+
+ private function doResolveInnerConditionRecursively( DIProperty $property, Description $description ) {
+
+ $innerOrderByProperty = null;
+
+ // Find out if we should order by the values of this property
+ if ( array_key_exists( $property->getKey(), $this->conditionBuilder->getSortKeys() ) ) {
+ $innerOrderByProperty = $property;
+ }
+
+ // Prepare inner condition
+ $innerJoinVariable = $this->conditionBuilder->getNextVariable();
+
+ $this->conditionBuilder->setJoinVariable( $innerJoinVariable );
+ $this->conditionBuilder->setOrderByProperty( $innerOrderByProperty );
+
+ $innerCondition = $this->conditionBuilder->mapDescriptionToCondition(
+ $description
+ );
+
+ return [ $innerOrderByProperty, $innerCondition, $innerJoinVariable ];
+ }
+
+ private function findObjectNameFromInnerCondition( $innerCondition, $innerJoinVariable, &$namespaces ) {
+
+ if ( !$innerCondition instanceof SingletonCondition ) {
+ return '?' . $innerJoinVariable;
+ }
+
+ $matchElement = $innerCondition->matchElement;
+
+ if ( $matchElement instanceof ExpElement ) {
+ $objectName = TurtleSerializer::getTurtleNameForExpElement( $matchElement );
+ } else {
+ $objectName = $matchElement;
+ }
+
+ if ( $matchElement instanceof ExpNsResource ) {
+ $namespaces[$matchElement->getNamespaceId()] = $matchElement->getNamespace();
+ }
+
+ return $objectName;
+ }
+
+ private function findMostSuitablePropertyRepresentation( DIProperty $property, DIProperty $nonInverseProperty, &$namespaces ) {
+
+ $redirectByVariable = $this->conditionBuilder->tryToFindRedirectVariableForDataItem(
+ $nonInverseProperty->getDiWikiPage()
+ );
+
+ // If the property is represented by a redirect then use the variable instead
+ if ( $redirectByVariable !== null ) {
+ return $redirectByVariable;
+ }
+
+ // Use helper properties in encoding values, refer to this helper property:
+ if ( $this->exporter->hasHelperExpElement( $property ) ) {
+ $propertyExpElement = $this->exporter->getResourceElementForProperty( $nonInverseProperty, true );
+ } elseif( !$property->isUserDefined() ) {
+ $propertyExpElement = $this->exporter->getSpecialPropertyResource(
+ $nonInverseProperty->getKey(),
+ SMW_NS_PROPERTY
+ );
+ } else {
+ $propertyExpElement = $this->exporter->getResourceElementForProperty( $nonInverseProperty );
+ }
+
+ if ( $propertyExpElement instanceof ExpNsResource ) {
+ $namespaces[$propertyExpElement->getNamespaceId()] = $propertyExpElement->getNamespace();
+ }
+
+ return TurtleSerializer::getTurtleNameForExpElement( $propertyExpElement );
+ }
+
+ private function doExchangeForWhenInversePropertyIsUsed( DIProperty $property, $objectName, $joinVariable ) {
+
+ $subjectName = '?' . $joinVariable;
+ $nonInverseProperty = $property;
+
+ // Exchange arguments when property is inverse
+ // don't check if this really makes sense
+ if ( $property->isInverse() ) {
+ $subjectName = $objectName;
+ $objectName = '?' . $joinVariable;
+ $nonInverseProperty = new DIProperty( $property->getKey(), false );
+ }
+
+ return [ $subjectName, $objectName, $nonInverseProperty ];
+ }
+
+ private function concatenateToConditionString( $subjectName, $propertyName, $objectName, $innerCondition ) {
+
+ $condition = "$subjectName $propertyName $objectName .\n";
+
+ $innerConditionString = $innerCondition->getCondition() . $innerCondition->getWeakConditionString();
+
+ if ( $innerConditionString === '' ) {
+ return $condition;
+ }
+
+ if ( $innerCondition instanceof FilterCondition ) {
+ return $condition . $innerConditionString;
+ }
+
+ return $condition . "{ $innerConditionString}\n";
+ }
+
+ /**
+ * @note rdfs:subPropertyOf* where * means a property path of arbitrary length
+ * can be found using the "zero or more" will resolve the complete path
+ *
+ * @see http://www.w3.org/TR/sparql11-query/#propertypath-arbitrary-length
+ */
+ private function tryToAddPropertyPathForSaturatedHierarchy( &$condition, DIProperty $property, &$propertyName, $depth ) {
+
+ if ( !$this->conditionBuilder->isSetFlag( SMW_SPARQL_QF_SUBP ) || !$property->isUserDefined() || ( $depth !== null && $depth < 1 ) ) {
+ return null;
+ }
+
+ if ( $this->conditionBuilder->getHierarchyLookup() == null || !$this->conditionBuilder->getHierarchyLookup()->hasSubproperty( $property ) ) {
+ return null;
+ }
+
+ $subPropExpElement = $this->exporter->getSpecialPropertyResource( '_SUBP', SMW_NS_PROPERTY );
+
+ // A discret depth other than 0 or 1 is difficult to achieve
+ // @see https://stackoverflow.com/questions/18126949/limit-the-sparql-query-result-to-first-level-in-hierarchy
+ // Path operator is defined as:
+ // - elt* ZeroOrMorePath
+ // - elt? ZeroOrOnePath
+ $pathOp = $depth > 1 || $depth === null ? '*' : '?';
+
+ $propertyByVariable = '?' . $this->conditionBuilder->getNextVariable( 'sp' );
+ $condition->weakConditions[$propertyName] = "\n". "$propertyByVariable " . $subPropExpElement->getQName() . "$pathOp $propertyName .\n"."";
+ $propertyName = $propertyByVariable;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ThingDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ThingDescriptionInterpreter.php
new file mode 100644
index 00000000..4ca00f6a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ThingDescriptionInterpreter.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Description;
+use SMW\Query\Language\ThingDescription;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+use SMWExporter as Exporter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ThingDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.1
+ *
+ * @param ConditionBuilder|null $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder = null ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->exporter = Exporter::getInstance();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof ThingDescription;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function interpretDescription( Description $description ) {
+ return $this->conditionBuilder->newTrueCondition(
+ $this->conditionBuilder->getJoinVariable(),
+ $this->conditionBuilder->getOrderByProperty()
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php
new file mode 100644
index 00000000..0f85bf25
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php
@@ -0,0 +1,281 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\DIWikiPage;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\ValueDescription;
+use SMW\SPARQLStore\QueryEngine\Condition\FalseCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\FilterCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\SingletonCondition;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreter;
+use SMWDIBlob as DIBlob;
+use SMWDIUri as DIUri;
+use SMWExpElement as ExpElement;
+use SMWExpNsResource as ExpNsResource;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class ValueDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var Exporter
+ */
+ private $exporter;
+
+ /**
+ * @since 2.1
+ *
+ * @param ConditionBuilder|null $conditionBuilder
+ */
+ public function __construct( ConditionBuilder $conditionBuilder = null ) {
+ $this->conditionBuilder = $conditionBuilder;
+ $this->exporter = Exporter::getInstance();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof ValueDescription;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * {@inheritDoc}
+ */
+ public function interpretDescription( Description $description ) {
+
+ $joinVariable = $this->conditionBuilder->getJoinVariable();
+ $orderByProperty = $this->conditionBuilder->getOrderByProperty();
+ $asNoCase = $this->conditionBuilder->isSetFlag( SMW_SPARQL_QF_NOCASE );
+
+ $dataItem = $description->getDataItem();
+ $property = $description->getProperty();
+
+ switch ( $description->getComparator() ) {
+ case SMW_CMP_EQ: $comparator = '=';
+ break;
+ case SMW_CMP_LESS: $comparator = '<';
+ break;
+ case SMW_CMP_GRTR: $comparator = '>';
+ break;
+ case SMW_CMP_LEQ: $comparator = '<=';
+ break;
+ case SMW_CMP_GEQ: $comparator = '>=';
+ break;
+ case SMW_CMP_NEQ: $comparator = '!=';
+ break;
+ case SMW_CMP_PRIM_LIKE;
+ case SMW_CMP_LIKE: $comparator = 'regex';
+ break;
+ case SMW_CMP_PRIM_NLKE;
+ case SMW_CMP_NLKE: $comparator = '!regex';
+ break;
+ default: $comparator = ''; // unkown, unsupported
+ }
+
+ if ( $comparator === '' ) {
+ return $this->createConditionForEmptyComparator( $joinVariable, $orderByProperty );
+ } elseif ( $comparator == '=' && $asNoCase === false ) {
+ return $this->createConditionForEqualityComparator( $dataItem, $property, $joinVariable, $orderByProperty );
+ } elseif ( $comparator == 'regex' || $comparator == '!regex' ) {
+ return $this->createConditionForRegexComparator( $dataItem, $joinVariable, $orderByProperty, $comparator );
+ }
+
+ return $this->createFilterConditionForAnyOtherComparator(
+ $dataItem,
+ $joinVariable,
+ $orderByProperty,
+ $comparator
+ );
+ }
+
+ private function createConditionForEmptyComparator( $joinVariable, $orderByProperty ) {
+ return $this->conditionBuilder->newTrueCondition( $joinVariable, $orderByProperty );
+ }
+
+ private function createConditionForEqualityComparator( $dataItem, $property, $joinVariable, $orderByProperty ) {
+
+ $expElement = $this->exporter->getDataItemHelperExpElement( $dataItem );
+
+ if ( $expElement === null ) {
+ $expElement = $this->exporter->getDataItemExpElement( $dataItem );
+ }
+
+ if ( $expElement === null || !$expElement instanceof ExpElement ) {
+ return new FalseCondition();
+ }
+
+ $condition = new SingletonCondition( $expElement );
+
+ $redirectByVariable = $this->conditionBuilder->tryToFindRedirectVariableForDataItem(
+ $dataItem
+ );
+
+ // If it is a standalone value (e.g [[:Foo]] with no property) construct a
+ // filter condition otherwise just assign the variable and the succeeding
+ // process the ensure the replacement
+ if ( $redirectByVariable !== null && $property === null ) {
+
+ $condition = $this->createFilterConditionForAnyOtherComparator(
+ $dataItem,
+ $joinVariable,
+ $orderByProperty,
+ '='
+ );
+
+ $condition->filter = "?$joinVariable = $redirectByVariable";
+ } elseif ( $redirectByVariable !== null ) {
+ $condition->matchElement = $redirectByVariable;
+ }
+
+ $this->conditionBuilder->addOrderByDataForProperty(
+ $condition,
+ $joinVariable,
+ $orderByProperty,
+ $dataItem->getDIType()
+ );
+
+ return $condition;
+ }
+
+ private function createConditionForRegexComparator( $dataItem, $joinVariable, $orderByProperty, $comparator ) {
+
+ if ( !$dataItem instanceof DIBlob && !$dataItem instanceof DIWikiPage && !$dataItem instanceof DIUri ) {
+ return $this->conditionBuilder->newTrueCondition( $joinVariable, $orderByProperty );
+ }
+
+ if ( $dataItem instanceof DIBlob ) {
+ $search = $dataItem->getString();
+ } else {
+ $search = $dataItem->getSortKey();
+ }
+
+ // @codingStandardsIgnoreStart phpcs, ignore --sniffs=Generic.Files.LineLength
+ $pattern = '^' . str_replace(
+ [ 'https://', 'http://', '%2A', '.', '+', '{', '}', '(', ')', '|', '^', '$', '[', ']', '*', '?', "'", '\\\.', '\\', '"', '\\\\\\\"' ],
+ [ '*', '*', '*', '\.', '\+', '\{', '\}', '\(', '\)', '\|', '\^', '\$', '\[', '\]', '.*', '.' , "\'", '\\\\\.', '\\\\', '\\\\\"', '\\\\\\\\\\\"' ],
+ $search
+ ) . '$';
+ // @codingStandardsIgnoreEnd
+
+ $condition = $this->createFilterConditionToMatchRegexPattern(
+ $dataItem,
+ $joinVariable,
+ $comparator,
+ $pattern
+ );
+
+ $redirectByVariable = $this->conditionBuilder->tryToFindRedirectVariableForDataItem(
+ $dataItem
+ );
+
+ if ( $redirectByVariable !== null ) {
+ $condition->matchElement = $redirectByVariable;
+ }
+
+ $this->conditionBuilder->addOrderByDataForProperty(
+ $condition,
+ $joinVariable,
+ $orderByProperty,
+ $dataItem->getDIType()
+ );
+
+ return $condition;
+ }
+
+ private function createFilterConditionForAnyOtherComparator( $dataItem, $joinVariable, $orderByProperty, $comparator ) {
+
+ $result = new FilterCondition( '', [] );
+
+ $this->conditionBuilder->addOrderByData(
+ $result,
+ $joinVariable,
+ $dataItem->getDIType()
+ );
+
+ $orderByVariable = '?' . $result->orderByVariable;
+
+ if ( $dataItem instanceof DIWikiPage ) {
+ $expElement = $this->exporter->getDataItemExpElement( new DIBlob( $dataItem->getSortKey() ) );
+ } else {
+ $expElement = $this->exporter->getDataItemHelperExpElement( $dataItem );
+ if ( is_null( $expElement ) ) {
+ $expElement = $this->exporter->getDataItemExpElement( $dataItem );
+ }
+ }
+
+ $valueName = TurtleSerializer::getTurtleNameForExpElement( $expElement );
+
+ if ( $expElement instanceof ExpNsResource ) {
+ $result->namespaces[$expElement->getNamespaceId()] = $expElement->getNamespace();
+ $dataItem = $expElement->getDataItem();
+ }
+
+ $this->lcase( $dataItem, $orderByVariable, $valueName );
+
+ $result->filter = "$orderByVariable $comparator $valueName";
+
+ return $result;
+ }
+
+ private function createFilterConditionToMatchRegexPattern( $dataItem, &$joinVariable, $comparator, $pattern ) {
+
+ $flag = $this->conditionBuilder->isSetFlag( SMW_SPARQL_QF_NOCASE ) ? 'i' : 's';
+
+ if ( $dataItem instanceof DIBlob ) {
+ return new FilterCondition( "$comparator( ?$joinVariable, \"$pattern\", \"$flag\")", [] );
+ }
+
+ if ( $dataItem instanceof DIUri ) {
+ return new FilterCondition( "$comparator( str( ?$joinVariable ), \"$pattern\", \"i\")", [] );
+ }
+
+ // Pattern search for a wikipage object can only be done on the sortkey
+ // literal and not on it's resource
+ $skeyExpElement = Exporter::getInstance()->getSpecialPropertyResource( '_SKEY' );
+
+ $expElement = $this->exporter->getDataItemExpElement( $dataItem->getSortKeyDataItem() );
+ $condition = new SingletonCondition( $expElement );
+
+ $filterVariable = $this->conditionBuilder->getNextVariable();
+
+ $condition->condition = "?$joinVariable " . $skeyExpElement->getQName(). " ?$filterVariable .\n";
+ $condition->matchElement = "?$joinVariable";
+
+ $filterCondition = new FilterCondition( "$comparator( ?$filterVariable, \"$pattern\", \"$flag\")", [] );
+
+ $condition->weakConditions = [ $filterVariable => $filterCondition->getCondition() ];
+
+ return $condition;
+ }
+
+ private function lcase( $dataItem, &$orderByVariable, &$valueName ) {
+
+ $isValidDataItem = $dataItem instanceof DIBlob || $dataItem instanceof DIUri || $dataItem instanceof DIWikiPage;
+
+ // https://stackoverflow.com/questions/10660030/how-to-write-sparql-query-that-efficiently-matches-string-literals-while-ignorin
+ if ( $this->conditionBuilder->isSetFlag( SMW_SPARQL_QF_NOCASE ) && $isValidDataItem ) {
+ $orderByVariable = "lcase(str($orderByVariable) )";
+ $valueName = mb_strtolower( $valueName );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/EngineOptions.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/EngineOptions.php
new file mode 100644
index 00000000..8fe65bdd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/EngineOptions.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use SMW\Options;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class EngineOptions extends Options {
+
+ /**
+ * @since 2.2
+ */
+ public function __construct() {
+ parent::__construct( [
+ 'smwgIgnoreQueryErrors' => $GLOBALS['smwgIgnoreQueryErrors'],
+ 'smwgQSortFeatures' => $GLOBALS['smwgQSortFeatures'],
+ 'smwgQSubpropertyDepth' => $GLOBALS['smwgQSubpropertyDepth'],
+ 'smwgQSubcategoryDepth' => $GLOBALS['smwgQSubcategoryDepth'],
+ 'smwgSparqlQFeatures' => $GLOBALS['smwgSparqlQFeatures']
+ ] );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/QueryEngine.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/QueryEngine.php
new file mode 100644
index 00000000..046f4ce8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/QueryEngine.php
@@ -0,0 +1,272 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use RuntimeException;
+use SMW\Exporter\Element;
+use SMW\Query\DebugFormatter;
+use SMW\Query\Language\ThingDescription;
+use SMW\QueryEngine as QueryEngineInterface;
+use SMW\SPARQLStore\QueryEngine\Condition\Condition;
+use SMW\SPARQLStore\QueryEngine\Condition\FalseCondition;
+use SMW\SPARQLStore\QueryEngine\Condition\SingletonCondition;
+use SMW\SPARQLStore\RepositoryConnection;
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+
+/**
+ * Class mapping SMWQuery objects to SPARQL, and for controlling the execution
+ * of these queries to obtain suitable QueryResult objects.
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class QueryEngine implements QueryEngineInterface {
+
+ /**
+ * The name of the SPARQL variable that represents the query result.
+ */
+ const RESULT_VARIABLE = 'result';
+
+ /**
+ * @var RepositoryConnection
+ */
+ private $connection;
+
+ /**
+ * @var ConditionBuilder
+ */
+ private $conditionBuilder;
+
+ /**
+ * @var QueryResultFactory
+ */
+ private $queryResultFactory;
+
+ /**
+ * @var EngineOptions
+ */
+ private $engineOptions;
+
+ /**
+ * @var array
+ */
+ private $sortKeys = [];
+
+ /**
+ * @since 2.0
+ *
+ * @param RepositoryConnection $connection
+ * @param ConditionBuilder $conditionBuilder
+ * @param QueryResultFactory $queryResultFactory
+ * @param EngineOptions|null $EngineOptions
+ */
+ // @codingStandardsIgnoreStart phpcs, ignore --sniffs=Generic.Files.LineLength
+ public function __construct( RepositoryConnection $connection, ConditionBuilder $conditionBuilder, QueryResultFactory $queryResultFactory, EngineOptions $engineOptions = null ) {
+ // @codingStandardsIgnoreEnd
+ $this->connection = $connection;
+ $this->conditionBuilder = $conditionBuilder;
+ $this->queryResultFactory = $queryResultFactory;
+ $this->engineOptions = $engineOptions;
+
+ if ( $this->engineOptions === null ) {
+ $this->engineOptions = new EngineOptions();
+ }
+
+ $this->conditionBuilder->setResultVariable( self::RESULT_VARIABLE );
+ }
+
+ /**
+ * @since 2.0
+ * @param Query $query
+ *
+ * @return QueryResult|string
+ */
+ public function getQueryResult( Query $query ) {
+
+ if ( ( !$this->engineOptions->get( 'smwgIgnoreQueryErrors' ) || $query->getDescription() instanceof ThingDescription ) &&
+ $query->querymode != Query::MODE_DEBUG &&
+ count( $query->getErrors() ) > 0 ) {
+ return $this->queryResultFactory->newEmptyQueryResult( $query, false );
+ }
+
+ // don't query, but return something to the printer
+ if ( $query->querymode == Query::MODE_NONE || $query->getLimit() < 1 ) {
+ return $this->queryResultFactory->newEmptyQueryResult( $query, true );
+ }
+
+ $this->sortKeys = $query->sortkeys;
+ $this->conditionBuilder->setSortKeys( $this->sortKeys );
+
+ $compoundCondition = $this->conditionBuilder->getConditionFrom(
+ $query->getDescription()
+ );
+
+ $query->addErrors(
+ $this->conditionBuilder->getErrors()
+ );
+
+ if ( $query->querymode == Query::MODE_DEBUG ) {
+ return $this->getDebugQueryResult( $query, $compoundCondition );
+ } elseif ( $query->querymode == Query::MODE_COUNT ) {
+ return $this->getCountQueryResult( $query, $compoundCondition );
+ }
+
+ return $this->getInstanceQueryResult( $query, $compoundCondition );
+ }
+
+ private function getCountQueryResult( Query $query, Condition $compoundCondition ) {
+
+ if ( $this->isSingletonConditionWithElementMatch( $compoundCondition ) ) {
+ if ( $compoundCondition->condition === '' ) { // all URIs exist, no querying
+ return 1;
+ } else {
+ $condition = $this->conditionBuilder->convertConditionToString( $compoundCondition );
+ $namespaces = $compoundCondition->namespaces;
+ $askQueryResult = $this->connection->ask( $condition, $namespaces );
+
+ return $askQueryResult->isBooleanTrue() ? 1 : 0;
+ }
+ } elseif ( $compoundCondition instanceof FalseCondition ) {
+ return 0;
+ }
+
+ $condition = $this->conditionBuilder->convertConditionToString( $compoundCondition );
+ $namespaces = $compoundCondition->namespaces;
+ $this->sortKeys = $this->conditionBuilder->getSortKeys();
+
+ $options = $this->getOptions( $query, $compoundCondition );
+ $options['DISTINCT'] = true;
+
+ $repositoryResult = $this->connection->selectCount(
+ '?' . self::RESULT_VARIABLE,
+ $condition,
+ $options,
+ $namespaces
+ );
+
+ return $this->queryResultFactory->newQueryResult( $repositoryResult, $query );
+ }
+
+ private function getInstanceQueryResult( Query $query, Condition $compoundCondition ) {
+
+ if ( $this->isSingletonConditionWithElementMatch( $compoundCondition ) ) {
+ $matchElement = $compoundCondition->matchElement;
+
+ if ( $compoundCondition->condition === '' ) { // all URIs exist, no querying
+ $results = [ [ $matchElement ] ];
+ } else {
+ $condition = $this->conditionBuilder->convertConditionToString( $compoundCondition );
+ $namespaces = $compoundCondition->namespaces;
+ $askQueryResult = $this->connection->ask( $condition, $namespaces );
+ $results = $askQueryResult->isBooleanTrue() ? [ [ $matchElement ] ] : [];
+ }
+
+ $repositoryResult = new RepositoryResult( [ self::RESULT_VARIABLE => 0 ], $results );
+
+ } elseif ( $compoundCondition instanceof FalseCondition ) {
+ $repositoryResult = new RepositoryResult( [ self::RESULT_VARIABLE => 0 ], [] );
+ } else {
+ $condition = $this->conditionBuilder->convertConditionToString( $compoundCondition );
+ $namespaces = $compoundCondition->namespaces;
+ $this->sortKeys = $this->conditionBuilder->getSortKeys();
+
+ $options = $this->getOptions( $query, $compoundCondition );
+ $options['DISTINCT'] = true;
+
+ $repositoryResult = $this->connection->select(
+ '?' . self::RESULT_VARIABLE,
+ $condition,
+ $options,
+ $namespaces
+ );
+ }
+
+ return $this->queryResultFactory->newQueryResult( $repositoryResult, $query );
+ }
+
+ private function getDebugQueryResult( Query $query, Condition $compoundCondition ) {
+
+ $entries = [];
+
+ if ( $this->isSingletonConditionWithElementMatch( $compoundCondition ) ) {
+ if ( $compoundCondition->condition === '' ) { // all URIs exist, no querying
+ $sparql = 'None (no conditions).';
+ } else {
+ $condition = $this->conditionBuilder->convertConditionToString( $compoundCondition );
+ $namespaces = $compoundCondition->namespaces;
+ $sparql = $this->connection->getSparqlForAsk( $condition, $namespaces );
+ }
+ } elseif ( $compoundCondition instanceof FalseCondition ) {
+ $sparql = 'None (conditions can not be satisfied by anything).';
+ } else {
+ $condition = $this->conditionBuilder->convertConditionToString( $compoundCondition );
+ $namespaces = $compoundCondition->namespaces;
+ $this->sortKeys = $this->conditionBuilder->getSortKeys();
+
+ $options = $this->getOptions( $query, $compoundCondition );
+ $options['DISTINCT'] = true;
+
+ $sparql = $this->connection->getSparqlForSelect(
+ '?' . self::RESULT_VARIABLE,
+ $condition,
+ $options,
+ $namespaces
+ );
+ }
+
+ $entries['SPARQL Query'] = DebugFormatter::prettifySparql( $sparql );
+
+ return DebugFormatter::getStringFrom( 'SPARQLStore', $entries, $query );
+ }
+
+ private function isSingletonConditionWithElementMatch( $condition ) {
+ return $condition instanceof SingletonCondition && $condition->matchElement instanceof Element;
+ }
+
+ /**
+ * Get a SPARQL option array for the given query.
+ *
+ * @param Query $query
+ * @param Condition $compoundCondition (storing order by variable names)
+ *
+ * @return array
+ */
+ protected function getOptions( Query $query, Condition $compoundCondition ) {
+
+ $options = [
+ 'LIMIT' => $query->getLimit() + 1,
+ 'OFFSET' => $query->getOffset()
+ ];
+
+ // Build ORDER BY options using discovered sorting fields.
+ if ( !$this->engineOptions->isFlagSet( 'smwgQSortFeatures', SMW_QSORT ) || !is_array( $this->sortKeys ) ) {
+ return $options;
+ }
+
+ $orderByString = '';
+
+ foreach ( $this->sortKeys as $propkey => $order ) {
+
+ if ( !is_string( $propkey ) ) {
+ throw new RuntimeException( "Expected a string value as sortkey" );
+ }
+
+ if ( ( $order != 'RANDOM' ) && array_key_exists( $propkey, $compoundCondition->orderVariables ) ) {
+ $orderByString .= "$order(?" . $compoundCondition->orderVariables[$propkey] . ") ";
+ } elseif ( ( $order == 'RANDOM' ) && $this->engineOptions->isFlagSet( 'smwgQSortFeatures', SMW_QSORT_RANDOM ) ) {
+ // not supported in SPARQL; might be possible via function calls in some stores
+ }
+ }
+
+ if ( $orderByString !== '' ) {
+ $options['ORDER BY'] = $orderByString;
+ }
+
+ return $options;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/QueryResultFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/QueryResultFactory.php
new file mode 100644
index 00000000..1c6c944d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/QueryResultFactory.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use SMW\Exporter\Element\ExpElement;
+use SMW\Store;
+use SMWExporter as Exporter;
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class QueryResultFactory {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 2.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param Query $query QueryResults hold a reference to original query
+ * @param boolean $hasFurtherResults
+ *
+ * @return QueryResult
+ */
+ public function newEmptyQueryResult( Query $query, $hasFurtherResults = false ) {
+ return new QueryResult(
+ $query->getDescription()->getPrintrequests(),
+ $query,
+ [],
+ $this->store,
+ $hasFurtherResults
+ );
+ }
+
+ /**
+ * This function is used to generate instance query results, and the given
+ * result wrapper must have an according format (one result column that
+ * contains URIs of wiki pages).
+ *
+ * @param RepositoryResult|null $repositoryResult
+ * @param Query $query QueryResults hold a reference to original query
+ *
+ * @return QueryResult
+ */
+ public function newQueryResult( RepositoryResult $repositoryResult = null , Query $query ) {
+
+ if ( $repositoryResult === null ) {
+ return $this->newEmptyQueryResult( $query );
+ }
+
+ if ( $query->querymode === Query::MODE_COUNT ) {
+ return $this->makeQueryResultForCount( $repositoryResult, $query );
+ }
+
+ return $this->makeQueryResultForInstance( $repositoryResult, $query );
+ }
+
+ private function makeQueryResultForCount( RepositoryResult $repositoryResult, Query $query ) {
+
+ $queryResult = new QueryResult(
+ $query->getDescription()->getPrintrequests(),
+ $query,
+ [],
+ $this->store,
+ false
+ );
+
+ if ( $repositoryResult->getErrorCode() === RepositoryResult::ERROR_NOERROR ) {
+ $queryResult->setCountValue( $repositoryResult->getNumericValue() );
+ } else {
+ $queryResult->addErrors( [ wfMessage( 'smw_db_sparqlqueryproblem' )->inContentLanguage()->text() ] );
+ }
+
+ return $queryResult;
+ }
+
+ private function makeQueryResultForInstance( RepositoryResult $repositoryResult, Query $query ) {
+
+ $resultDataItems = [];
+
+ foreach ( $repositoryResult as $resultRow ) {
+
+ if ( count( $resultRow ) > 0 && $resultRow[0] instanceof ExpElement ) {
+ $dataItem = Exporter::getInstance()->findDataItemForExpElement( $resultRow[0] );
+
+ if ( !is_null( $dataItem ) ) {
+ $resultDataItems[] = $dataItem;
+ }
+ }
+ }
+
+ if ( $repositoryResult->numRows() > $query->getLimit() ) {
+ if ( count( $resultDataItems) > 1 ) {
+ array_pop( $resultDataItems );
+ }
+ $hasFurtherResults = true;
+ } else {
+ $hasFurtherResults = false;
+ }
+
+ $result = new QueryResult(
+ $query->getDescription()->getPrintrequests(),
+ $query,
+ $resultDataItems,
+ $this->store,
+ $hasFurtherResults
+ );
+
+ switch ( $repositoryResult->getErrorCode() ) {
+ case RepositoryResult::ERROR_NOERROR:
+ break;
+ case RepositoryResult::ERROR_INCOMPLETE:
+ $result->addErrors( [ wfMessage( 'smw_db_sparqlqueryincomplete' )->inContentLanguage()->text() ] );
+ break;
+ default:
+ $result->addErrors( [ wfMessage( 'smw_db_sparqlqueryproblem' )->inContentLanguage()->text() ] );
+ break;
+ }
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/RepositoryResult.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/RepositoryResult.php
new file mode 100644
index 00000000..db157cb4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/RepositoryResult.php
@@ -0,0 +1,204 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use Iterator;
+use SMWExpLiteral as ExpLiteral;
+
+/**
+ * Class for accessing SPARQL query results in a unified form. The data is
+ * structured in tabular form, with each cell containing some SMWExpElement.
+ * Rows should always have the same number of columns, but the datatype of the
+ * cells in each column may not be uniform throughout the result.
+ *
+ * @ingroup Sparql
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class RepositoryResult implements Iterator {
+
+ /// Error code: no errors occurred.
+ const ERROR_NOERROR = 0;
+ /// Error code: service unreachable; result will be empty
+ const ERROR_UNREACHABLE = 1;
+ /// Error code: results might be incomplete (e.g. due to some resource limit being reached)
+ const ERROR_INCOMPLETE = 2;
+
+ /**
+ * Associative array mapping SPARQL variable names to column indices.
+ * @var array of integer
+ */
+ protected $header;
+
+ /**
+ * List of result rows. Individual entries can be null if a cell in the
+ * SPARQL result table is empty (this is different from finding a blank
+ * node).
+ * @var array of array of (SMWExpElement or null)
+ */
+ protected $data;
+
+ /**
+ * List of comment strings found in the XML file (without surrounding
+ * markup, i.e. the actual string only).
+ * @var array of string
+ */
+ protected $comments;
+
+ /**
+ * Error code.
+ * @var integer
+ */
+ protected $errorCode;
+
+ /**
+ * Initialise a result set from a result string in SPARQL XML format.
+ *
+ * @param $header array mapping SPARQL variable names to column indices
+ * @param $data array of array of (SMWExpElement or null)
+ * @param $comments array of string comments if the result contained any
+ * @param $errorCode integer an error code
+ */
+ public function __construct( array $header = [], array $data = [], array $comments = [], $errorCode = self::ERROR_NOERROR ) {
+ $this->header = $header;
+ $this->data = $data;
+ $this->comments = $comments;
+ $this->errorCode = $errorCode;
+ reset( $this->data );
+ }
+
+ /**
+ * Get the number of rows in the result object.
+ *
+ * @return integer number of result rows
+ */
+ public function numRows() {
+ return count( $this->data );
+ }
+
+ /**
+ * Return error code. SMWSparqlResultWrapper::ERROR_NOERROR (0)
+ * indicates that no error occurred.
+ *
+ * @return integer error code
+ */
+ public function getErrorCode() {
+ return $this->errorCode;
+ }
+
+ /**
+ * Set the error code of this result set. This is used for allowing
+ * callers to add additional errors discovered only later on. It does
+ * not allow removing existing errors, since it will not accept
+ * SMWSparqlResultWrapper::ERROR_NOERROR as a parameter.
+ *
+ * @param $errorCode integer error code
+ */
+ public function setErrorCode( $errorCode ) {
+ if ( $errorCode != self::ERROR_NOERROR ) {
+ $this->errorCode = $errorCode;
+ }
+ }
+
+ /**
+ * Return a list of comment strings found in the SPARQL result. Comments
+ * are used by some RDF stores to provide additional information or
+ * warnings that can thus be accessed.
+ *
+ * @return array of string
+ */
+ public function getComments() {
+ return $this->comments;
+ }
+
+ /**
+ * Check if the result is what one would get for a SPARQL ASK query
+ * that returned true. Returns false in all other cases (including
+ * the case that the results do not look at all like the result of
+ * an ASK query).
+ *
+ * @return boolean
+ */
+ public function isBooleanTrue() {
+ if ( count( $this->data ) == 1 ) {
+ $row = reset( $this->data );
+ $expElement = reset( $row );
+ if ( ( count( $row ) == 1 ) && ( $expElement instanceof ExpLiteral ) &&
+ ( $expElement->getLexicalForm() == 'true' ) &&
+ ( $expElement->getDatatype() == 'http://www.w3.org/2001/XMLSchema#boolean' ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if the result is what one would get for a SPARQL SELECT COUNT
+ * query, and return the corresponding integer value. Returns 0 in all
+ * other cases (including the case that the results do not look at all
+ * like the result of a SELECT COUNT query).
+ *
+ * @return integer
+ */
+ public function getNumericValue() {
+ if ( count( $this->data ) == 1 ) {
+ $row = reset( $this->data );
+ $expElement = reset( $row );
+ if ( ( count( $row ) == 1 ) && ( $expElement instanceof ExpLiteral ) &&
+ ( $expElement->getDatatype() == 'http://www.w3.org/2001/XMLSchema#integer' ) ) {
+ return (int)$expElement->getLexicalForm();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Reset iterator to position 0. Standard method of Iterator.
+ */
+ public function rewind() {
+ reset( $this->data );
+ }
+
+ /**
+ * Return the current result row. Standard method of Iterator.
+ *
+ * @return array of (SMWExpElement or null), or false at end of data
+ */
+ public function current() {
+ return current( $this->data );
+ }
+
+ /**
+ * Return the next result row and advance the internal pointer.
+ * Standard method of Iterator.
+ *
+ * @return array of (SMWExpElement or null), or false at end of data
+ */
+ public function next() {
+ return next( $this->data );
+ }
+
+ /**
+ * Return the next result row and advance the internal pointer.
+ * Standard method of Iterator.
+ *
+ * @return array of (SMWExpElement or null), or false at end of data
+ */
+ public function key() {
+ return key( $this->data );
+ }
+
+ /**
+ * Return true if the internal pointer refers to a valid element.
+ * Standard method of Iterator.
+ *
+ * @return boolean
+ */
+ public function valid() {
+ return ( current( $this->data ) !== false );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/XmlResponseParser.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/XmlResponseParser.php
new file mode 100644
index 00000000..24182051
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/QueryEngine/XmlResponseParser.php
@@ -0,0 +1,234 @@
+<?php
+
+namespace SMW\SPARQLStore\QueryEngine;
+
+use SMW\SPARQLStore\Exception\XmlParserException;
+use SMW\SPARQLStore\HttpResponseParser;
+use SMWExpLiteral as ExpLiteral;
+use SMWExpResource as ExpResource;
+
+/**
+ * Class for parsing SPARQL results in XML format
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class XmlResponseParser implements HttpResponseParser {
+
+ /**
+ * @var resource
+ */
+ private $parser;
+
+ /**
+ * Associative array mapping SPARQL variable names to column indices.
+ * @var array of integer
+ */
+ private $header;
+
+ /**
+ * List of result rows. Individual entries can be null if a cell in the
+ * SPARQL result table is empty (this is different from finding a blank
+ * node).
+ * @var array of array of (SMWExpElement or null)
+ */
+ private $data;
+
+ /**
+ * List of comment strings found in the XML file (without surrounding
+ * markup, i.e. the actual string only).
+ * @var array of string
+ */
+ private $comments;
+
+ /**
+ * Stack of open XML tags during parsing.
+ * @var array of string
+ */
+ private $xmlOpenTags;
+
+ /**
+ * Integer index of the column that the current result binding fills.
+ * @var integer
+ */
+ private $xmlBindIndex;
+
+ /**
+ * Datatype URI for the current literal, or empty if none.
+ * @var string
+ */
+ private $currentDataType;
+
+ /**
+ * @since 2.0
+ */
+ public function __construct() {
+ $this->parser = xml_parser_create();
+
+ xml_parser_set_option( $this->parser, XML_OPTION_SKIP_WHITE, 0 );
+ xml_parser_set_option( $this->parser, XML_OPTION_TARGET_ENCODING, 'UTF-8' );
+ xml_parser_set_option( $this->parser, XML_OPTION_CASE_FOLDING, 0 );
+ xml_set_object( $this->parser, $this );
+ xml_set_element_handler( $this->parser, 'handleOpenElement', 'handleCloseElement' );
+ xml_set_character_data_handler( $this->parser, 'handleCharacterData' );
+ xml_set_default_handler( $this->parser, 'handleDefault' );
+ //xml_set_start_namespace_decl_handler($parser, 'handleNsDeclaration' );
+ }
+
+ /**
+ * @since 2.0
+ */
+ public function __destruct() {
+ xml_parser_free( $this->parser );
+ }
+
+ /**
+ * Parse the given XML result and return an RepositoryResult for
+ * the contained data.
+ *
+ * @param string $response
+ *
+ * @return RepositoryResult
+ * @throws XmlParserException
+ */
+ public function parse( $response ) {
+
+ $this->xmlOpenTags = [];
+ $this->header = [];
+ $this->data = [];
+ $this->comments = [];
+
+ // Sesame can return "" result
+ if ( $response === '' ) {
+ $this->data = [ [ new ExpLiteral( 'false', 'http://www.w3.org/2001/XMLSchema#boolean' ) ] ];
+ }
+
+ // #626 Virtuoso
+ if ( $response == 'true' ) {
+ $this->data = [ [ new ExpLiteral( 'true', 'http://www.w3.org/2001/XMLSchema#boolean' ) ] ];
+ }
+
+ // #474 Virtuoso allows `false` to be a valid raw result
+ if ( $response === '' || $response == 'false' || $response == 'true' || is_bool( $response ) || $this->parseXml( $response ) ) {
+ return new RepositoryResult(
+ $this->header,
+ $this->data,
+ $this->comments
+ );
+ }
+
+ throw new XmlParserException(
+ $this->getLastError(),
+ $this->getLastLineNumber(),
+ $this->getLastColumnNumber()
+ );
+ }
+
+ private function parseXml( $xmlResultData ) {
+ return xml_parse( $this->parser, $xmlResultData, true );
+ }
+
+ private function getLastError() {
+ return xml_error_string( xml_get_error_code( $this->parser ) );
+ }
+
+ private function getLastLineNumber() {
+ return xml_get_current_line_number( $this->parser );
+ }
+
+ private function getLastColumnNumber() {
+ return xml_get_current_column_number ( $this->parser );
+ }
+
+ private function handleDefault( $parser, $data ) {
+ if ( substr( $data, 0, 4 ) == '<!--' ) {
+ $comment = substr( $data, 4, strlen( $data ) - 7 );
+ $this->comments[] = trim( $comment );
+ }
+ }
+
+ /**
+ * @see xml_set_element_handler
+ */
+ private function handleOpenElement( $parser, $elementTag, $attributes ) {
+
+ $this->currentDataType = '';
+
+ $prevTag = end( $this->xmlOpenTags );
+ $this->xmlOpenTags[] = $elementTag;
+
+ switch ( $elementTag ) {
+ case 'binding' && ( $prevTag == 'result' ):
+ if ( ( array_key_exists( 'name', $attributes ) ) &&
+ ( array_key_exists( $attributes['name'], $this->header ) ) ) {
+ $this->xmlBindIndex = $this->header[$attributes['name']];
+ }
+ break;
+ case 'result' && ( $prevTag == 'results' ):
+ $this->data[] = array_fill( 0, count( $this->header ), null );
+ break;
+ case 'literal' && ( $prevTag == 'binding' ):
+ if ( array_key_exists( 'datatype', $attributes ) ) {
+ $this->currentDataType = $attributes['datatype'];
+ }
+ /// TODO handle xml:lang attributes here as well?
+ break;
+ case 'variable' && ( $prevTag == 'head' ):
+ if ( array_key_exists( 'name', $attributes ) ) {
+ $this->header[$attributes['name']] = count( $this->header );
+ }
+ break;
+ }
+ }
+
+ /**
+ * @see xml_set_element_handler
+ */
+ private function handleCloseElement( $parser, $elementTag ) {
+ array_pop( $this->xmlOpenTags );
+ }
+
+ /**
+ * @see xml_set_character_data_handler
+ */
+ private function handleCharacterData( $parser, $characterData ) {
+
+ $prevTag = end( $this->xmlOpenTags );
+ $rowcount = count( $this->data ) - 1;
+
+ // UTF-8 is being split therefore concatenate the string (use row as indicator
+ // to detect a sliced string)
+ if ( isset( $this->data[$rowcount] ) && ( $element = end( $this->data[$rowcount] ) ) !== null ) {
+ switch ( $prevTag ) {
+ case 'uri':
+ $characterData = $element->getUri() . $characterData;
+ break;
+ case 'literal':
+ $characterData = $element->getLexicalForm() . $characterData;
+ }
+ }
+
+ switch ( $prevTag ) {
+ case 'uri':
+ $this->data[$rowcount][$this->xmlBindIndex] = new ExpResource( $characterData );
+ break;
+ case 'literal':
+ $this->data[$rowcount][$this->xmlBindIndex] = new ExpLiteral( $characterData, $this->currentDataType );
+ break;
+ case 'bnode':
+ $this->data[$rowcount][$this->xmlBindIndex] = new ExpResource( '_' . $characterData );
+ break;
+ case 'boolean':
+ // no "results" in this case
+ $literal = new ExpLiteral( $characterData, 'http://www.w3.org/2001/XMLSchema#boolean' );
+
+ // ?? Really !!
+ $this->data = [ [ $literal ] ];
+ $this->header = [ '' => 0 ];
+ break;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/README.md b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/README.md
new file mode 100644
index 00000000..ec7da3d0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/README.md
@@ -0,0 +1,101 @@
+# SPARQLStore
+
+The `SPARQLStore` is the name for the component that can establish a connection between a [RDF triple store][tdb] and Semantic MediaWiki (a more general introduction can be found [here](https://www.semantic-mediawiki.org/wiki/Help:Using SPARQL and RDF stores)).
+
+The `SPARQLStore` is composed of a base store (by default using the existing `SQLStore`), a `QueryEngine`, and a connector to the RDF back-end. Currently, the base store takes the position of accumulating information about properties, value annotations, and statistics.
+
+## Overview
+
+```
+ SPARQLStore
+ |- SPARQLStoreFactory
+ |- ConnectionManager
+ | |- RepositoryConnectionProvider
+ | |- RepositoryClient
+ | |- RepositoryConnection
+ | |- FourstoreRepositoryConnector
+ | |- FusekiRepositoryConnector
+ | |- GenericRepositoryConnector
+ | |- VirtuosoRepositoryConnector
+ |- TurtleTriplesBuilder
+ |- RepositoryRedirectLookup
+ |- ReplicationDataTruncator
+ |- QueryEngine
+ |- HttpResponseParser
+ |- XmlResponseParser
+ |- ConditionBuilder
+ |- DescriptionInterpreter
+```
+
+## Repository connector
+
+A repository connector is responsible for establishing a communication between Semantic MediaWiki and an external [TDB][tdb] with the main objective to transfer/update triples from SMW to the back-end and to return result matches for a query request.
+
+The following client repositories have been tested:
+
+- [Jena Fuseki][fuseki]
+- [Virtuoso][virtuoso]
+- [Blazegraph][blazegraph]
+- [Sesame][sesame]
+- [4Store][4store]
+
+### Create a connection
+<pre>
+$connectionManager = new ConnectionManager();
+
+$connectionManager->registerConnectionProvider(
+ 'sparql',
+ new RepositoryConnectionProvider( 'fuseki' )
+);
+
+$connection = $connectionManager->getConnection( 'sparql' )
+</pre>
+
+## QueryEngine
+
+The `QueryEngine` is responsible for transforming an `#ask` description object into a qualified
+[`SPARQL` query][sparql-query] expression.
+
+- The `ConditionBuilder` builds a SPARQL condition from an `#ask` query artefact (aka [`Description`][ask query] object)
+- The condition is transformed into a qualified `SPARQL` statement for which the [repository connector][connector] is making a http request to the back-end while awaiting an expected list of subjects that matched the condition in form of a `XML` or `JSON` response
+- The raw results are being parsed by a `HttpResponseParser` to provide a unified `RepositoryResult` object
+- During the final step, the `QueryResultFactory` converts the `RepositoryResult` into a SMW specific `QueryResult` object which will fetch the remaining data (those selected as printrequests) from the base store and make them available to a [`QueryResultPrinter`][resultprinter]
+
+### Create a query request
+<pre>
+/**
+ * Equivalent to [[Foo::+]]
+ *
+ * SELECT DISTINCT ?result WHERE {
+ * ?result swivt:wikiPageSortKey ?resultsk .
+ * ?result property:Foo ?v1 .
+ * }
+ * ORDER BY ASC(?resultsk)
+ */
+$description = new SomeProperty(
+ new DIProperty( 'Foo' ),
+ new ThingDescription()
+);
+
+$query = new Query( $description );
+
+$sparqlStoreFactory = new SPARQLStoreFactory(
+ new SPARQLStore()
+);
+
+$queryEngine = $sparqlStoreFactory->newMasterQueryEngine();
+$queryResult = $queryEngine->getQueryResult( $query );
+</pre>
+
+[fuseki]: https://jena.apache.org/
+[fuseki-dataset]: https://jena.apache.org/documentation/tdb/dynamic_datasets.html
+[sparql-query]:http://www.w3.org/TR/sparql11-query/
+[sparql-dataset]: https://www.w3.org/TR/sparql11-query/#specifyingDataset
+[virtuoso]: https://github.com/openlink/virtuoso-opensource
+[4store]: https://github.com/garlik/4store
+[tdb]: http://en.wikipedia.org/wiki/Triplestore
+[sesame]: http://rdf4j.org/
+[blazegraph]: https://wiki.blazegraph.com/wiki/index.php/Main_Page
+[ask query]: https://www.semantic-mediawiki.org/wiki/Query_language
+[connector]: https://www.semantic-mediawiki.org/wiki/Help:SPARQLStore/RepositoryConnector
+[resultprinter]: https://www.semantic-mediawiki.org/wiki/Help:SPARQLStore/RepositoryConnector
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/ReplicationDataTruncator.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/ReplicationDataTruncator.php
new file mode 100644
index 00000000..a6d6d97c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/ReplicationDataTruncator.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+use SMW\DIProperty;
+use SMW\SemanticData;
+
+/**
+ * Truncate a SemanticData instance for the replication process
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ReplicationDataTruncator {
+
+ /**
+ * @var array
+ */
+ private $propertyExemptionList = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param array $propertyExemptionList
+ */
+ public function setPropertyExemptionList( array $propertyExemptionList ) {
+ $this->propertyExemptionList = str_replace( ' ', '_', $propertyExemptionList );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param SemanticData $semanticDat
+ *
+ * @return SemanticData
+ */
+ public function doTruncate( SemanticData $semanticData ) {
+
+ if ( $this->propertyExemptionList === [] ) {
+ return $semanticData;
+ }
+
+ foreach ( $this->propertyExemptionList as $property ) {
+ $semanticData->removeProperty( DIProperty::newFromUserLabel( $property ) );
+ }
+
+ return $semanticData;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryClient.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryClient.php
new file mode 100644
index 00000000..c3ab27ca
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryClient.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+/**
+ * Provides information about the client and how to communicate with
+ * its services
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class RepositoryClient {
+
+ /**
+ * The URI of the default graph that is used to store data.
+ * Can be the empty string to omit this information in all requests
+ * (not supported by all stores).
+ *
+ * @var string
+ */
+ private $defaultGraph = '';
+
+ /**
+ * The URL of the endpoint for executing read queries.
+ *
+ * @var string
+ */
+ private $queryEndpoint = '';
+
+ /**
+ * The URL of the endpoint for executing update queries, or empty if
+ * update is not allowed/supported.
+ *
+ * @var string
+ */
+ private $updateEndpoint = '';
+
+ /**
+ * The URL of the endpoint for using the SPARQL Graph Store HTTP
+ * Protocol with, or empty if this method is not allowed/supported.
+ *
+ * @var string
+ */
+ private $dataEndpoint = '';
+
+ /**
+ * @var string
+ */
+ private $name = '';
+
+ /**
+ * @since 2.2
+ *
+ * @param string $defaultGraph
+ * @param string $queryEndpoint
+ * @param string $updateEndpoint
+ * @param string $dataEndpoint
+ */
+ public function __construct( $defaultGraph, $queryEndpoint, $updateEndpoint = '', $dataEndpoint = '' ) {
+ $this->defaultGraph = $defaultGraph;
+ $this->queryEndpoint = $queryEndpoint;
+ $this->updateEndpoint = $updateEndpoint;
+ $this->dataEndpoint = $dataEndpoint;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $name
+ */
+ public function setName( $name ) {
+ $this->name = $name;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getDefaultGraph() {
+ return $this->defaultGraph;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string|false
+ */
+ public function getQueryEndpoint() {
+ return $this->queryEndpoint;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getUpdateEndpoint() {
+ return $this->updateEndpoint;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getDataEndpoint() {
+ return $this->dataEndpoint;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnection.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnection.php
new file mode 100644
index 00000000..84b1ae22
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnection.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+interface RepositoryConnection {
+
+ /**
+ * The function returns connection details required for establishing an active
+ * repository connection.
+ *
+ * @since 2.5
+ *
+ * @return RepositoryClient
+ */
+ public function getRepositoryClient();
+
+ /**
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $vars mixed array or string, field name(s) to be retrieved, can be '*'
+ * @param $where string WHERE part of the query, without surrounding { }
+ * @param $options array (associative) of options, e.g. array( 'LIMIT' => '10' )
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return RepositoryResult
+ */
+ public function select( $vars, $where, $options = [], $extraNamespaces = [] );
+
+ /**
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $where string WHERE part of the query, without surrounding { }
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return RepositoryResult
+ */
+ public function ask( $where, $extraNamespaces = [] );
+
+ /**
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $deletePattern string CONSTRUCT pattern of tripples to delete
+ * @param $where string condition for data to delete
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return boolean stating whether the operations succeeded
+ */
+ public function delete( $deletePattern, $where, $extraNamespaces = [] );
+
+ /**
+ * Execute a SPARQL query and return an RepositoryResult object
+ * that contains the results. The method throws exceptions based on
+ * GenericHttpDatabaseConnector::mapHttpRequestError(). If errors occur and this
+ * method does not throw anything, then an empty result with an error
+ * code is returned.
+ *
+ * @note This function sets the graph that is to be used as part of the
+ * request. Queries should not include additional graph information.
+ *
+ * @param string $sparql complete SPARQL query (SELECT or ASK)
+ *
+ * @return RepositoryResult
+ */
+ public function doQuery( $sparql );
+
+ /**
+ * Execute a SPARQL update and return a boolean to indicate if the
+ * operations was successful. The method throws exceptions based on
+ * GenericHttpDatabaseConnector::mapHttpRequestError(). If errors occur and this
+ * method does not throw anything, then false is returned.
+ *
+ * @note When this is written, it is not clear if the update protocol
+ * supports a default-graph-uri parameter. Hence the target graph for
+ * all updates is generally encoded in the query string and not fixed
+ * when sending the query. Direct callers to this function must include
+ * the graph information in the queries that they build.
+ *
+ * @param string $sparql complete SPARQL update query (INSERT or DELETE)
+ *
+ * @return boolean
+ */
+ public function doUpdate( $sparql );
+
+ /**
+ * Execute a HTTP-based SPARQL POST request according to
+ * http://www.w3.org/2009/sparql/docs/http-rdf-update/.
+ * The method throws exceptions based on
+ * GenericHttpDatabaseConnector::mapHttpRequestError(). If errors occur and this
+ * method does not throw anything, then an empty result with an error
+ * code is returned.
+ *
+ * @note This protocol is not part of the SPARQL standard and may not
+ * be supported by all stores. To avoid using it, simply do not provide
+ * a data endpoint URL when configuring the SPARQL database. If used,
+ * the protocol might lead to a better performance since there is less
+ * parsing required to fetch the data from the request.
+ * @note Some stores (e.g. 4Store) support another mode of posting data
+ * that may be implemented in a special database handler.
+ *
+ * @param string $payload Turtle serialization of data to send
+ *
+ * @return boolean
+ */
+ public function doHttpPost( $payload );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectionProvider.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectionProvider.php
new file mode 100644
index 00000000..c7d6984d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectionProvider.php
@@ -0,0 +1,215 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+use Onoi\HttpRequest\CurlRequest;
+use RuntimeException;
+use SMW\Connection\ConnectionProvider;
+use SMW\SPARQLStore\RepositoryConnectors\FourstoreRepositoryConnector;
+use SMW\SPARQLStore\RepositoryConnectors\FusekiRepositoryConnector;
+use SMW\SPARQLStore\RepositoryConnectors\GenericRepositoryConnector;
+use SMW\SPARQLStore\RepositoryConnectors\VirtuosoRepositoryConnector;
+
+/**
+ * @private
+ *
+ * Provides a RepositoryConnection on the available settings.
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class RepositoryConnectionProvider implements ConnectionProvider {
+
+ /**
+ * List of supported standard connectors
+ *
+ * @var array
+ */
+ private $repositoryConnectors = [
+ 'default' => GenericRepositoryConnector::class,
+ 'generic' => GenericRepositoryConnector::class,
+ 'sesame' => GenericRepositoryConnector::class,
+ 'fuseki' => FusekiRepositoryConnector::class,
+ 'virtuoso' => VirtuosoRepositoryConnector::class,
+ '4store' => FourstoreRepositoryConnector::class,
+ ];
+
+ /**
+ * @var RepositoryConnection
+ */
+ private $connection = null;
+
+ /**
+ * @var string|null
+ */
+ private $connectorId = null;
+
+ /**
+ * @var string|null
+ */
+ private $defaultGraph = null;
+
+ /**
+ * @var string|null
+ */
+ private $queryEndpoint = null;
+
+ /**
+ * @var string|null
+ */
+ private $updateEndpoint = null;
+
+ /**
+ * @var string|null
+ */
+ private $dataEndpoint = null;
+
+ /**
+ * @var HttpRequest
+ */
+ private $httpRequest;
+
+ /**
+ * @var boolean|integer
+ */
+ private $httpVersion = false;
+
+ /**
+ * @since 2.0
+ *
+ * @param string|null $connectorId
+ * @param string|null $defaultGraph
+ * @param string|null $queryEndpoint
+ * @param string|null $updateEndpoint
+ * @param string|null $dataEndpoint
+ */
+ public function __construct( $connectorId = null, $defaultGraph = null, $queryEndpoint = null, $updateEndpoint = null, $dataEndpoint = null ) {
+ $this->connectorId = $connectorId;
+ $this->defaultGraph = $defaultGraph;
+ $this->queryEndpoint = $queryEndpoint;
+ $this->updateEndpoint = $updateEndpoint;
+ $this->dataEndpoint = $dataEndpoint;
+
+ if ( $this->connectorId === null ) {
+ $this->connectorId = $GLOBALS['smwgSparqlRepositoryConnector'];
+ }
+
+ if ( $this->defaultGraph === null ) {
+ $this->defaultGraph = $GLOBALS['smwgSparqlDefaultGraph'];
+ }
+
+ if ( $this->queryEndpoint === null ) {
+ $this->queryEndpoint = $GLOBALS['smwgSparqlEndpoint']['query'];
+ }
+
+ if ( $this->updateEndpoint === null ) {
+ $this->updateEndpoint = $GLOBALS['smwgSparqlEndpoint']['update'];
+ }
+
+ if ( $this->dataEndpoint === null ) {
+ $this->dataEndpoint = $GLOBALS['smwgSparqlEndpoint']['data'];
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return HttpRequest $httpRequest
+ */
+ public function setHttpRequest( HttpRequest $httpRequest ) {
+ $this->httpRequest = $httpRequest;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return integer $httpVersion
+ */
+ public function setHttpVersionTo( $httpVersion ) {
+ $this->httpVersion = $httpVersion;
+ }
+
+ /**
+ * @see ConnectionProvider::getConnection
+ *
+ * @since 2.0
+ *
+ * @return SparqlDatabase
+ * @throws RuntimeException
+ */
+ public function getConnection() {
+
+ if ( $this->connection === null ) {
+ $this->connection = $this->connectTo( strtolower( $this->connectorId ) );
+ }
+
+ return $this->connection;
+ }
+
+ /**
+ * @see ConnectionProvider::releaseConnection
+ *
+ * @since 2.0
+ */
+ public function releaseConnection() {
+ $this->connection = null;
+ }
+
+ private function connectTo( $id ) {
+
+ if ( $this->httpRequest === null ) {
+ $this->httpRequest = new CurlRequest( curl_init() );
+ }
+
+ // https://github.com/SemanticMediaWiki/SemanticMediaWiki/issues/1306
+ if ( $this->httpVersion ) {
+ $this->httpRequest->setOption( CURLOPT_HTTP_VERSION, $this->httpVersion );
+ }
+
+ $repositoryClient = new RepositoryClient(
+ $this->defaultGraph,
+ $this->queryEndpoint,
+ $this->updateEndpoint,
+ $this->dataEndpoint
+ );
+
+ $repositoryClient->setName( $id );
+
+ $repositoryConnector = $this->createRepositoryConnector(
+ $id,
+ $repositoryClient
+ );
+
+ if ( $this->isRepositoryConnection( $repositoryConnector ) ) {
+ return $repositoryConnector;
+ }
+
+ throw new RuntimeException( 'Expected a RepositoryConnection instance' );
+ }
+
+ private function createRepositoryConnector( $id, $repositoryClient ) {
+
+ $repositoryConnector = $this->repositoryConnectors['default'];
+
+ if ( isset( $this->repositoryConnectors[$id] ) ) {
+ $repositoryConnector = $this->repositoryConnectors[$id];
+ }
+
+ if ( $id === 'custom' ) {
+ $repositoryConnector = $GLOBALS['smwgSparqlCustomConnector'];
+ }
+
+ if ( !class_exists( $repositoryConnector ) ) {
+ throw new RuntimeException( "{$repositoryConnector} is not available" );
+ }
+
+ return new $repositoryConnector( $repositoryClient, $this->httpRequest );
+ }
+
+ private function isRepositoryConnection( $connection ) {
+ return $connection instanceof RepositoryConnection;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/FourstoreRepositoryConnector.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/FourstoreRepositoryConnector.php
new file mode 100644
index 00000000..a5a5bfaf
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/FourstoreRepositoryConnector.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace SMW\SPARQLStore\RepositoryConnectors;
+
+use SMW\SPARQLStore\Exception\BadHttpEndpointResponseException;
+use SMW\SPARQLStore\QueryEngine\RepositoryResult;
+use SMW\SPARQLStore\QueryEngine\XmlResponseParser;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * Specific modifications of the SPARQL database implementation for 4Store.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class FourstoreRepositoryConnector extends GenericRepositoryConnector {
+
+ /**
+ * Execute a SPARQL query and return an RepositoryResult object
+ * that contains the results. Compared to GenericHttpDatabaseConnector::doQuery(),
+ * this also supports the parameter "restricted=1" which 4Store provides
+ * to enforce strict resource bounds on query answering. The method also
+ * checks if these bounds have been met, and records this in the query
+ * result.
+ *
+ * @note The restricted option in 4Store mainly enforces the given soft
+ * limit more strictly. To disable/configure it, simply change the soft
+ * limit settings of your 4Store server.
+ *
+ * @param $sparql string with the complete SPARQL query (SELECT or ASK)
+ * @return RepositoryResult
+ */
+ public function doQuery( $sparql ) {
+
+ if ( $this->repositoryClient->getQueryEndpoint() === '' ) {
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_NOSERVICE, $sparql, 'not specified' );
+ }
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getQueryEndpoint() );
+ $this->httpRequest->setOption( CURLOPT_HTTPHEADER, ['Accept: application/sparql-results+xml,application/xml;q=0.8' ]);
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $parameterString = "query=" . urlencode( $sparql ) . "&restricted=1" .
+ ( ( $defaultGraph !== '' )? '&default-graph-uri=' . urlencode( $defaultGraph ) : '' );
+
+ $this->httpRequest->setOption( CURLOPT_POSTFIELDS, $parameterString );
+
+ $httpResponse = $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ $xmlResponseParser = new XmlResponseParser();
+ $result = $xmlResponseParser->parse( $httpResponse );
+ } else {
+ $this->mapHttpRequestError( $this->repositoryClient->getQueryEndpoint(), $sparql );
+ $result = new RepositoryResult();
+ $result->setErrorCode( RepositoryResult::ERROR_UNREACHABLE );
+ }
+
+ foreach ( $result->getComments() as $comment ) {
+ if ( strpos( $comment, 'warning: hit complexity limit' ) === 0 ||
+ strpos( $comment, 'some results have been dropped' ) === 0 ) {
+ $result->setErrorCode( RepositoryResult::ERROR_INCOMPLETE );
+ } //else debug_zval_dump($comment);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Complex SPARQL Update delete operations are not supported in 4Store
+ * as of v1.1.3, hence this implementation uses a less efficient method
+ * for accomplishing this.
+ *
+ * @param $propertyName string Turtle name of marking property
+ * @param $objectName string Turtle name of marking object/value
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ * @return boolean stating whether the operations succeeded
+ */
+ public function deleteContentByValue( $propertyName, $objectName, $extraNamespaces = [] ) {
+ $affectedObjects = $this->select( '*', "?s $propertyName $objectName", [], $extraNamespaces );
+ $success = ( $affectedObjects->getErrorCode() == RepositoryResult::ERROR_NOERROR );
+
+ foreach ( $affectedObjects as $expElements ) {
+ if ( count( $expElements ) > 0 ) {
+ $turtleName = TurtleSerializer::getTurtleNameForExpElement( reset( $expElements ) );
+ $success = $this->delete( "$turtleName ?p ?o", "$turtleName ?p ?o", $extraNamespaces ) && $success;
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Execute a HTTP-based SPARQL POST request according to
+ * http://www.w3.org/2009/sparql/docs/http-rdf-update/.
+ * The method throws exceptions based on
+ * GenericHttpDatabaseConnector::mapHttpRequestError(). If errors occur and this
+ * method does not throw anything, then an empty result with an error
+ * code is returned.
+ *
+ * This method is specific to 4Store since it uses POST parameters that
+ * are not given in the specification.
+ *
+ * @param $payload string Turtle serialization of data to send
+ *
+ * @return boolean
+ */
+ public function doHttpPost( $payload ) {
+
+ if ( $this->repositoryClient->getDataEndpoint() === '' ) {
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_NOSERVICE, "SPARQL POST with data: $payload", 'not specified' );
+ }
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getDataEndpoint() );
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $parameterString = "data=" . urlencode( $payload ) . '&graph=' .
+ ( ( $defaultGraph !== '' )? urlencode( $defaultGraph ) : 'default' ) .
+ '&mime-type=application/x-turtle';
+
+ $this->httpRequest->setOption( CURLOPT_POSTFIELDS, $parameterString );
+ $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ return true;
+ }
+
+ $this->mapHttpRequestError( $this->repositoryClient->getDataEndpoint(), $payload );
+ return false;
+ }
+
+ /**
+ * @see GenericHttpDatabaseConnector::doUpdate
+ *
+ * @note 4store 1.1.4 breaks on update if charset is set in the Content-Type header
+ *
+ * @since 2.0
+ */
+ public function doUpdate( $sparql ) {
+
+ if ( $this->repositoryClient->getUpdateEndpoint() === '' ) {
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_NOSERVICE, $sparql, 'not specified' );
+ }
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getUpdateEndpoint() );
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+
+ $parameterString = "update=" . urlencode( $sparql );
+
+ $this->httpRequest->setOption( CURLOPT_POSTFIELDS, $parameterString );
+ $this->httpRequest->setOption( CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded' ] );
+
+ $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ return true;
+ }
+
+ $this->mapHttpRequestError( $this->repositoryClient->getUpdateEndpoint(), $sparql );
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/FusekiRepositoryConnector.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/FusekiRepositoryConnector.php
new file mode 100644
index 00000000..1ffdc94b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/FusekiRepositoryConnector.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace SMW\SPARQLStore\RepositoryConnectors;
+
+use SMW\SPARQLStore\Exception\BadHttpEndpointResponseException;
+use SMW\SPARQLStore\QueryEngine\RepositoryResult;
+use SMW\SPARQLStore\QueryEngine\XmlResponseParser;
+
+/**
+ * @see https://jena.apache.org/documentation/serving_data/index.html
+ *
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author mwjames
+ */
+class FusekiRepositoryConnector extends GenericRepositoryConnector {
+
+ /**
+ * @see GenericRepositoryConnector::doQuery
+ */
+ public function doQuery( $sparql ) {
+
+ if ( $this->repositoryClient->getQueryEndpoint() === '' ) {
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_NOSERVICE, $sparql, 'not specified' );
+ }
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getQueryEndpoint() );
+
+ $this->httpRequest->setOption( CURLOPT_HTTPHEADER, [
+ 'Accept: application/sparql-results+xml,application/xml;q=0.8',
+ 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8'
+ ] );
+
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $parameterString = "query=" . urlencode( $sparql ) .
+ ( ( $defaultGraph !== '' )? '&default-graph-uri=' . urlencode( $defaultGraph ) : '' ) . '&output=xml';
+
+ $this->httpRequest->setOption( CURLOPT_POSTFIELDS, $parameterString );
+
+ $httpResponse = $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ $xmlResponseParser = new XmlResponseParser();
+ return $xmlResponseParser->parse( $httpResponse );
+ }
+
+ $this->mapHttpRequestError( $this->repositoryClient->getQueryEndpoint(), $sparql );
+
+ $repositoryResult = new RepositoryResult();
+ $repositoryResult->setErrorCode( RepositoryResult::ERROR_UNREACHABLE );
+
+ return $repositoryResult;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/GenericRepositoryConnector.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/GenericRepositoryConnector.php
new file mode 100644
index 00000000..76361a4c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/GenericRepositoryConnector.php
@@ -0,0 +1,594 @@
+<?php
+
+namespace SMW\SPARQLStore\RepositoryConnectors;
+
+use Onoi\HttpRequest\HttpRequest;
+use SMW\SPARQLStore\Exception\BadHttpEndpointResponseException;
+use SMW\SPARQLStore\HttpResponseErrorMapper;
+use SMW\SPARQLStore\QueryEngine\RepositoryResult;
+use SMW\SPARQLStore\QueryEngine\XmlResponseParser;
+use SMW\SPARQLStore\RepositoryClient;
+use SMW\SPARQLStore\RepositoryConnection;
+use SMWExporter as Exporter;
+
+/**
+ * Basic database connector for exchanging data via SPARQL.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class GenericRepositoryConnector implements RepositoryConnection {
+
+ /**
+ * Flag denoting endpoints being capable of querying
+ */
+ const ENDP_QUERY = 1;
+
+ /**
+ * Flag denoting endpoints being capable of updating
+ */
+ const ENDP_UPDATE = 2;
+
+ /**
+ * Flag denoting endpoints being capable of SPARQL HTTP graph management
+ */
+ const ENDP_DATA = 4;
+
+ /**
+ * @var RepositoryClient
+ */
+ protected $repositoryClient;
+
+ /**
+ * @note Handles the curl handle and is reused throughout the instance to
+ * safe some initialization effort
+ *
+ * @var HttpRequest
+ */
+ protected $httpRequest;
+
+ /**
+ * @var HttpResponseErrorMapper
+ */
+ private $badHttpResponseMapper;
+
+ /**
+ * @note It is suggested to use the RepositoryConnectionProvider to create
+ * a valid instance
+ *
+ * @since 2.2
+ *
+ * @param RepositoryClient $repositoryClient
+ * @param HttpRequest $httpRequest
+ */
+ public function __construct( RepositoryClient $repositoryClient, HttpRequest $httpRequest ) {
+ $this->repositoryClient = $repositoryClient;
+ $this->httpRequest = $httpRequest;
+
+ $this->httpRequest->setOption( CURLOPT_FORBID_REUSE, false );
+ $this->httpRequest->setOption( CURLOPT_FRESH_CONNECT, false );
+ $this->httpRequest->setOption( CURLOPT_RETURNTRANSFER, true ); // put result into variable
+ $this->httpRequest->setOption( CURLOPT_FAILONERROR, true );
+
+ $this->setConnectionTimeout( 10 );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return RepositoryClient
+ */
+ public function getRepositoryClient() {
+ return $this->repositoryClient;
+ }
+
+ /**
+ * Get the URI of the default graph that this database connector is
+ * using, or the empty string if none is used (no graph related
+ * statements in queries/updates).
+ *
+ * @return string graph UIR or empty
+ */
+ public function getDefaultGraph() {
+ return $this->repositoryClient->getDefaultGraph();
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param integer $timeout in seconds
+ */
+ public function setConnectionTimeout( $timeout = 10 ) {
+ $this->httpRequest->setOption( CURLOPT_CONNECTTIMEOUT, $timeout );
+ }
+
+ /**
+ * Check if the database can be contacted.
+ *
+ * @todo SPARQL endpoints sometimes return errors if no (valid) query
+ * is posted. The current implementation tries to catch this, but this
+ * might not be entirely correct. Especially, the SPARQL 1.1 HTTP error
+ * codes for Update are not defined yet (April 15 2011).
+ *
+ * @param $pingQueryEndpoint boolean true if the query endpoint should be
+ * pinged, false if the update endpoint should be pinged
+ *
+ * @return boolean to indicate success
+ */
+ public function ping( $endpointType = self::ENDP_QUERY ) {
+ if ( $endpointType == self::ENDP_QUERY ) {
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getQueryEndpoint() );
+ $this->httpRequest->setOption( CURLOPT_NOBODY, true );
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+ } elseif ( $endpointType == self::ENDP_UPDATE ) {
+
+ if ( $this->repositoryClient->getUpdateEndpoint() === '' ) {
+ return false;
+ }
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getUpdateEndpoint() );
+
+ // 4Store gives 404 instead of 500 with CURLOPT_NOBODY
+ $this->httpRequest->setOption( CURLOPT_NOBODY, false );
+
+ } else { // ( $endpointType == self::ENDP_DATA )
+
+ if ( $this->repositoryClient->getDataEndpoint() === '' ) {
+ return false;
+ }
+
+ // try an empty POST
+ return $this->doHttpPost( '' );
+ }
+
+ $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ return true;
+ }
+
+ // Valid HTTP responses from a complaining SPARQL endpoint that is
+ // alive and kicking
+ $httpCode = $this->httpRequest->getLastTransferInfo( CURLINFO_HTTP_CODE );
+
+ return ( ( $httpCode == 500 ) || ( $httpCode == 400 ) );
+ }
+
+ /**
+ * SELECT wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $vars mixed array or string, field name(s) to be retrieved, can be '*'
+ * @param $where string WHERE part of the query, without surrounding { }
+ * @param $options array (associative) of options, e.g. array( 'LIMIT' => '10' )
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return RepositoryResult
+ */
+ public function select( $vars, $where, $options = [], $extraNamespaces = [] ) {
+ return $this->doQuery( $this->getSparqlForSelect( $vars, $where, $options, $extraNamespaces ) );
+ }
+
+ /**
+ * Build the SPARQL query that is used by GenericHttpDatabaseConnector::select().
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $where string WHERE part of the query, without surrounding { }
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return string SPARQL query
+ */
+ public function getSparqlForSelect( $vars, $where, $options = [], $extraNamespaces = [] ) {
+
+ $sparql = self::getPrefixString( $extraNamespaces ) . 'SELECT ';
+
+ if ( array_key_exists( 'DISTINCT', $options ) ) {
+ $sparql .= 'DISTINCT ';
+ }
+
+ if ( is_array( $vars ) ) {
+ $sparql .= implode( ',', $vars );
+ } else {
+ $sparql .= $vars;
+ }
+
+ $sparql .= " WHERE {\n" . $where . "\n}";
+
+ if ( array_key_exists( 'ORDER BY', $options ) ) {
+ $sparql .= "\nORDER BY " . $options['ORDER BY'];
+ }
+
+ if ( array_key_exists( 'OFFSET', $options ) ) {
+ $sparql .= "\nOFFSET " . $options['OFFSET'];
+ }
+
+ if ( array_key_exists( 'LIMIT', $options ) ) {
+ $sparql .= "\nLIMIT " . $options['LIMIT'];
+ }
+
+ return $sparql;
+ }
+
+ /**
+ * ASK wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $where string WHERE part of the query, without surrounding { }
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return RepositoryResult
+ */
+ public function ask( $where, $extraNamespaces = [] ) {
+ return $this->doQuery( $this->getSparqlForAsk( $where, $extraNamespaces ) );
+ }
+
+ /**
+ * Build the SPARQL query that is used by GenericHttpDatabaseConnector::ask().
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $where string WHERE part of the query, without surrounding { }
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return string SPARQL query
+ */
+ public function getSparqlForAsk( $where, $extraNamespaces = [] ) {
+ return self::getPrefixString( $extraNamespaces ) . "ASK {\n" . $where . "\n}";
+ }
+
+ /**
+ * SELECT wrapper for counting results.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $variable string variable name or '*'
+ * @param $where string WHERE part of the query, without surrounding { }
+ * @param $options array (associative) of options, e.g. array('LIMIT' => '10')
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return RepositoryResult
+ */
+ public function selectCount( $variable, $where, $options = [], $extraNamespaces = [] ) {
+
+ $sparql = self::getPrefixString( $extraNamespaces ) . 'SELECT (COUNT(';
+
+ if ( array_key_exists( 'DISTINCT', $options ) ) {
+ $sparql .= 'DISTINCT ';
+ }
+
+ $sparql .= $variable . ") AS ?count) WHERE {\n" . $where . "\n}";
+
+ if ( array_key_exists( 'OFFSET', $options ) ) {
+ $sparql .= "\nOFFSET " . $options['OFFSET'];
+ }
+
+ if ( array_key_exists( 'LIMIT', $options ) ) {
+ $sparql .= "\nLIMIT " . $options['LIMIT'];
+ }
+
+ return $this->doQuery( $sparql );
+ }
+
+ /**
+ * DELETE wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $deletePattern string CONSTRUCT pattern of tripples to delete
+ * @param $where string condition for data to delete
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return boolean stating whether the operations succeeded
+ */
+ public function delete( $deletePattern, $where, $extraNamespaces = [] ) {
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $sparql = self::getPrefixString( $extraNamespaces ) .
+ ( ( $defaultGraph !== '' )? "WITH <{$defaultGraph}> " : '' ) .
+ "DELETE { $deletePattern } WHERE { $where }";
+
+ return $this->doUpdate( $sparql );
+ }
+
+ /**
+ * Convenience method for deleting all triples that have a subject that
+ * occurs in a triple with the given property and object. This is used
+ * in SMW to delete subobjects with all their data. Some RDF stores fail
+ * on complex delete queries, hence a wrapper function is provided to
+ * allow more pedestrian implementations.
+ *
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $propertyName string Turtle name of marking property
+ * @param $objectName string Turtle name of marking object/value
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return boolean stating whether the operations succeeded
+ */
+ public function deleteContentByValue( $propertyName, $objectName, $extraNamespaces = [] ) {
+ return $this->delete( "?s ?p ?o", "?s $propertyName $objectName . ?s ?p ?o", $extraNamespaces );
+ }
+
+ /**
+ * Convenience method for deleting all triples of the entire store
+ *
+ * @return boolean
+ */
+ public function deleteAll() {
+ return $this->delete( "?s ?p ?o", "?s ?p ?o" );
+ }
+
+ /**
+ * INSERT DELETE wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $insertPattern string CONSTRUCT pattern of tripples to insert
+ * @param $deletePattern string CONSTRUCT pattern of tripples to delete
+ * @param $where string condition for data to delete
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return boolean stating whether the operations succeeded
+ */
+ public function insertDelete( $insertPattern, $deletePattern, $where, $extraNamespaces = [] ) {
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $sparql = self::getPrefixString( $extraNamespaces ) .
+ ( ( $defaultGraph !== '' )? "WITH <{$defaultGraph}> " : '' ) .
+ "DELETE { $deletePattern } INSERT { $insertPattern } WHERE { $where }";
+
+ return $this->doUpdate( $sparql );
+ }
+
+ /**
+ * INSERT DATA wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $triples string of triples to insert
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return boolean stating whether the operations succeeded
+ */
+ public function insertData( $triples, $extraNamespaces = [] ) {
+
+ if ( $this->repositoryClient->getDataEndpoint() !== '' ) {
+ $turtle = self::getPrefixString( $extraNamespaces, false ) . $triples;
+ return $this->doHttpPost( $turtle );
+ }
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $sparql = self::getPrefixString( $extraNamespaces, true ) .
+ "INSERT DATA " .
+ ( ( $defaultGraph !== '' )? " { GRAPH <{$defaultGraph}> " : '' ) .
+ "{ $triples } " .
+ ( ( $defaultGraph !== '' )? " } " : '' );
+
+ return $this->doUpdate( $sparql );
+ }
+
+ /**
+ * DELETE DATA wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $triples string of triples to delete
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ *
+ * @return boolean stating whether the operations succeeded
+ */
+ public function deleteData( $triples, $extraNamespaces = [] ) {
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $sparql = self::getPrefixString( $extraNamespaces ) .
+ "DELETE DATA { " .
+ ( ( $defaultGraph !== '' )? "GRAPH <{$defaultGraph}> " : '' ) .
+ "{ $triples } }";
+
+ return $this->doUpdate( $sparql );
+ }
+
+
+ /**
+ * Execute a SPARQL query and return an RepositoryResult object
+ * that contains the results. The method throws exceptions based on
+ * GenericHttpDatabaseConnector::mapHttpRequestError(). If errors occur and this
+ * method does not throw anything, then an empty result with an error
+ * code is returned.
+ *
+ * @note This function sets the graph that is to be used as part of the
+ * request. Queries should not include additional graph information.
+ *
+ * @param $sparql string with the complete SPARQL query (SELECT or ASK)
+ *
+ * @return RepositoryResult
+ */
+ public function doQuery( $sparql ) {
+
+ if ( $this->repositoryClient->getQueryEndpoint() === '' ) {
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_NOSERVICE, $sparql, 'not specified' );
+ }
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getQueryEndpoint() );
+
+ $this->httpRequest->setOption( CURLOPT_HTTPHEADER, [
+ 'Accept: application/sparql-results+xml,application/xml;q=0.8',
+ 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8'
+ ] );
+
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $parameterString = "query=" . urlencode( $sparql ) .
+ ( ( $defaultGraph !== '' )? '&default-graph-uri=' . urlencode( $defaultGraph ) : '' );
+
+ $this->httpRequest->setOption( CURLOPT_POSTFIELDS, $parameterString );
+
+ $httpResponse = $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ $xmlResponseParser = new XmlResponseParser();
+ return $xmlResponseParser->parse( $httpResponse );
+ }
+
+ $this->mapHttpRequestError( $this->repositoryClient->getQueryEndpoint(), $sparql );
+
+ $repositoryResult = new RepositoryResult();
+ $repositoryResult->setErrorCode( RepositoryResult::ERROR_UNREACHABLE );
+
+ return $repositoryResult;
+ }
+
+ /**
+ * Execute a SPARQL update and return a boolean to indicate if the
+ * operations was successful. The method throws exceptions based on
+ * GenericHttpDatabaseConnector::mapHttpRequestError(). If errors occur and this
+ * method does not throw anything, then false is returned.
+ *
+ * @note When this is written, it is not clear if the update protocol
+ * supports a default-graph-uri parameter. Hence the target graph for
+ * all updates is generally encoded in the query string and not fixed
+ * when sending the query. Direct callers to this function must include
+ * the graph information in the queries that they build.
+ *
+ * @param $sparql string with the complete SPARQL update query (INSERT or DELETE)
+ *
+ * @return boolean
+ */
+ public function doUpdate( $sparql ) {
+
+ if ( $this->repositoryClient->getUpdateEndpoint() === '' ) {
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_NOSERVICE, $sparql, 'not specified' );
+ }
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getUpdateEndpoint() );
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+
+ $parameterString = "update=" . urlencode( $sparql );
+
+ $this->httpRequest->setOption( CURLOPT_POSTFIELDS, $parameterString );
+ $this->httpRequest->setOption( CURLOPT_HTTPHEADER, [ 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' ] );
+
+ $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ return true;
+ }
+
+ $this->mapHttpRequestError( $this->repositoryClient->getUpdateEndpoint(), $sparql );
+ return false;
+ }
+
+ /**
+ * Execute a HTTP-based SPARQL POST request according to
+ * http://www.w3.org/2009/sparql/docs/http-rdf-update/.
+ * The method throws exceptions based on
+ * GenericHttpDatabaseConnector::mapHttpRequestError(). If errors occur and this
+ * method does not throw anything, then an empty result with an error
+ * code is returned.
+ *
+ * @note This protocol is not part of the SPARQL standard and may not
+ * be supported by all stores. To avoid using it, simply do not provide
+ * a data endpoint URL when configuring the SPARQL database. If used,
+ * the protocol might lead to a better performance since there is less
+ * parsing required to fetch the data from the request.
+ * @note Some stores (e.g. 4Store) support another mode of posting data
+ * that may be implemented in a special database handler.
+ *
+ * @param $payload string Turtle serialization of data to send
+ *
+ * @return boolean
+ */
+ public function doHttpPost( $payload ) {
+
+ if ( $this->repositoryClient->getDataEndpoint() === '' ) {
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_NOSERVICE, "SPARQL POST with data: $payload", 'not specified' );
+ }
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getDataEndpoint() .
+ ( ( $defaultGraph !== '' )? '?graph=' . urlencode( $defaultGraph ) : '?default' ) );
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+
+ // POST as file (fails in 4Store)
+ $payloadFile = tmpfile();
+ fwrite( $payloadFile, $payload );
+ fseek( $payloadFile, 0 );
+
+ $this->httpRequest->setOption( CURLOPT_INFILE, $payloadFile );
+ $this->httpRequest->setOption( CURLOPT_INFILESIZE, strlen( $payload ) );
+ $this->httpRequest->setOption( CURLOPT_HTTPHEADER, [ 'Content-Type: application/x-turtle' ] );
+
+ $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ return true;
+ }
+
+ // TODO The error reporting based on SPARQL (Update) is not adequate for the HTTP POST protocol
+ $this->mapHttpRequestError( $this->repositoryClient->getDataEndpoint(), $payload );
+ return false;
+ }
+
+ /**
+ * Create the standard PREFIX declarations for SPARQL or Turtle,
+ * possibly with additional namespaces involved.
+ *
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ * @param $forSparql boolean true to use SPARQL prefix syntax, false to use Turtle prefix syntax
+ *
+ * @return string
+ */
+ public static function getPrefixString( $extraNamespaces = [], $forSparql = true ) {
+ $prefixString = '';
+ $prefixIntro = $forSparql ? 'PREFIX ' : '@prefix ';
+ $prefixOutro = $forSparql ? "\n" : " .\n";
+
+ foreach ( [ 'wiki', 'rdf', 'rdfs', 'owl', 'swivt', 'property', 'xsd' ] as $shortname ) {
+ $prefixString .= "{$prefixIntro}{$shortname}: <" . Exporter::getInstance()->getNamespaceUri( $shortname ) . ">$prefixOutro";
+ unset( $extraNamespaces[$shortname] ); // avoid double declaration
+ }
+
+ foreach ( $extraNamespaces as $shortname => $uri ) {
+ $prefixString .= "{$prefixIntro}{$shortname}: <$uri>$prefixOutro";
+ }
+
+ return $prefixString;
+ }
+
+ /**
+ * @param $endpoint string URL of endpoint that was used
+ * @param $sparql string query that caused the problem
+ */
+ protected function mapHttpRequestError( $endpoint, $sparql ) {
+
+ if ( $this->badHttpResponseMapper === null ) {
+ $this->badHttpResponseMapper = new HttpResponseErrorMapper( $this->httpRequest );
+ }
+
+ $this->badHttpResponseMapper->mapErrorResponse( $endpoint, $sparql );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/VirtuosoRepositoryConnector.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/VirtuosoRepositoryConnector.php
new file mode 100644
index 00000000..e7a57e4d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryConnectors/VirtuosoRepositoryConnector.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace SMW\SPARQLStore\RepositoryConnectors;
+
+use SMW\SPARQLStore\Exception\BadHttpEndpointResponseException;
+
+/**
+ * Virtuoso specific adjustments for GenericRepositoryConnector
+ *
+ * Specific modifications of the SPARQL database implementation for Virtuoso.
+ * In particular, Virtuoso does not support SPARQL Update but only the non-standard
+ * SPARUL protocol that requires different syntax for update queries.
+ * If future versions of Virtuoso support SPARQL Update, the standard SPARQL
+ * database connector should work properly.
+ *
+ * Virtuoso uses the SPARQL query endpoint for updates as well. So both
+ * - $smwgSparqlEndpoint['update'] and
+ * - $smwgSparqlEndpoint['query'] should be something like 'http://localhost:8890/sparql/'.
+ * - $smwgSparqlEndpoint['data'] should be left empty.
+ *
+ * A graph is always needed, i.e., $smwgSparqlDefaultGraph must be set to some
+ * graph name (URI).
+ *
+ * Known limitations:
+ * (might be fixed in recent Virtuoso versions, please let us know)
+ *
+ * - Data endpoint not tested: $smwgSparqlEndpoint['data'] should be left empty
+ * - Numerical datatypes are not supported properly, and Virtuoso
+ * will miss query results when query conditions require number values.
+ * This also affects Type:Date properties since the use numerical values for
+ * querying.
+ * - Some edit (insert) queries fail for unknown reasons, probably related to
+ * unusual/complex input data (e.g., using special characters in strings);
+ * errors will occur when trying to store such values on a page.
+ * - Virtuoso stumbles over XSD dates with negative years, even if they have
+ * only four digits as per ISO. Trying to store such data will cause errors.
+ *
+ * @ingroup Sparql
+ *
+ * @license GNU GPL v2+
+ * @since 1.7.1
+ *
+ * @author Markus Krötzsch
+ */
+class VirtuosoRepositoryConnector extends GenericRepositoryConnector {
+
+ /**
+ * DELETE wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $deletePattern string CONSTRUCT pattern of tripples to delete
+ * @param $where string condition for data to delete
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ * @return boolean stating whether the operations succeeded
+ */
+ public function delete( $deletePattern, $where, $extraNamespaces = [] ) {
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $sparql = self::getPrefixString( $extraNamespaces ) . "DELETE" .
+ ( ( $defaultGraph !== '' )? " FROM <{$defaultGraph}> " : '' ) .
+ "{ $deletePattern } WHERE { $where }";
+
+ return $this->doUpdate( $sparql );
+ }
+
+ /**
+ * INSERT DELETE wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $insertPattern string CONSTRUCT pattern of tripples to insert
+ * @param $deletePattern string CONSTRUCT pattern of tripples to delete
+ * @param $where string condition for data to delete
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ * @return boolean stating whether the operations succeeded
+ */
+ public function insertDelete( $insertPattern, $deletePattern, $where, $extraNamespaces = [] ) {
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $sparql = self::getPrefixString( $extraNamespaces ) . "MODIFY" .
+ ( ( $defaultGraph !== '' )? " GRAPH <{$defaultGraph}> " : '' ) .
+ "DELETE { $deletePattern } INSERT { $insertPattern } WHERE { $where }";
+
+ return $this->doUpdate( $sparql );
+ }
+
+ /**
+ * INSERT DATA wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $triples string of triples to insert
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ * @return boolean stating whether the operations succeeded
+ */
+ public function insertData( $triples, $extraNamespaces = [] ) {
+
+ if ( $this->repositoryClient->getDataEndpoint() !== '' ) {
+ $turtle = self::getPrefixString( $extraNamespaces, false ) . $triples;
+ return $this->doHttpPost( $turtle );
+ }
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $sparql = self::getPrefixString( $extraNamespaces, true ) .
+ "INSERT DATA " .
+ ( ( $defaultGraph !== '' )? "INTO GRAPH <{$defaultGraph}> " : '' ) .
+ "{ $triples }";
+
+ return $this->doUpdate( $sparql );
+ }
+
+ /**
+ * DELETE DATA wrapper.
+ * The function declares the standard namespaces wiki, swivt, rdf, owl,
+ * rdfs, property, xsd, so these do not have to be included in
+ * $extraNamespaces.
+ *
+ * @param $triples string of triples to delete
+ * @param $extraNamespaces array (associative) of namespaceId => namespaceUri
+ * @return boolean stating whether the operations succeeded
+ */
+ public function deleteData( $triples, $extraNamespaces = [] ) {
+
+ $defaultGraph = $this->repositoryClient->getDefaultGraph();
+
+ $sparql = self::getPrefixString( $extraNamespaces ) .
+ "DELETE DATA " .
+ ( ( $defaultGraph !== '' )? "FROM GRAPH <{$defaultGraph}> " : '' ) .
+ "{ $triples }";
+
+ return $this->doUpdate( $sparql );
+ }
+
+ /**
+ * Execute a SPARQL update and return a boolean to indicate if the
+ * operations was successful.
+ *
+ * Virtuoso expects SPARQL updates to be posted using the "query"
+ * parameter (rather than "update").
+ *
+ * @param $sparql string with the complete SPARQL update query (INSERT or DELETE)
+ * @return boolean
+ */
+ public function doUpdate( $sparql ) {
+
+ if ( $this->repositoryClient->getUpdateEndpoint() === '' ) {
+ throw new BadHttpEndpointResponseException( BadHttpEndpointResponseException::ERROR_NOSERVICE, $sparql, 'not specified' );
+ }
+
+ $this->httpRequest->setOption( CURLOPT_URL, $this->repositoryClient->getUpdateEndpoint() );
+ $this->httpRequest->setOption( CURLOPT_POST, true );
+
+ $parameterString = "query=" . urlencode( $sparql );
+
+ $this->httpRequest->setOption( CURLOPT_POSTFIELDS, $parameterString );
+ $this->httpRequest->execute();
+
+ if ( $this->httpRequest->getLastErrorCode() == 0 ) {
+ return true;
+ }
+
+ $this->mapHttpRequestError( $this->repositoryClient->getUpdateEndpoint(), $sparql );
+
+ return false;
+ }
+
+}
+
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryRedirectLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryRedirectLookup.php
new file mode 100644
index 00000000..6dc83555
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/RepositoryRedirectLookup.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+use RuntimeException;
+use SMW\DIWikiPage;
+use SMW\InMemoryPoolCache;
+use SMWExpNsResource as ExpNsResource;
+use SMWExporter as Exporter;
+use SMWExpResource as ExpResource;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class RepositoryRedirectLookup {
+
+ /**
+ * ID used for the InMemoryPoolCache
+ */
+ const POOLCACHE_ID = 'sparql.repository.redirectLookup';
+
+ /**
+ * @var RepositoryConnection
+ */
+ private $repositoryConnection;
+
+ /**
+ * @since 2.0
+ *
+ * @param RepositoryConnection $repositoryConnection
+ */
+ public function __construct( RepositoryConnection $repositoryConnection ) {
+ $this->repositoryConnection = $repositoryConnection;
+ }
+
+ /**
+ * @since 2.1
+ */
+ public static function reset() {
+ InMemoryPoolCache::getInstance()->resetPoolCacheById( self::POOLCACHE_ID );
+ }
+
+ /**
+ * Find the redirect target of an ExpNsResource
+ *
+ * Returns an SMWExpNsResource object the input redirects to, the input
+ * itself if there is no redirect (or it cannot be used for making a resource
+ * with a prefix).
+ *
+ * @since 1.6
+ *
+ * @param ExpNsResource $expNsResource string URI to check
+ * @param boolean $existsthat is set to true if $expNsResource is in the
+ * store; always false for blank nodes; always true for subobjects
+ *
+ * @return ExpNsResource
+ * @throws RuntimeException
+ */
+ public function findRedirectTargetResource( ExpNsResource $expNsResource, &$exists ) {
+
+ $exists = true;
+
+ if ( $expNsResource->isBlankNode() || $this->isNonRedirectableResource( $expNsResource ) ) {
+ $exists = false;
+ return $expNsResource;
+ }
+
+ if ( ( $expNsResource->getDataItem() instanceof DIWikiPage ) &&
+ $expNsResource->getDataItem()->getSubobjectName() !== '' ) {
+ return $expNsResource;
+ }
+
+ $firstRow = $this->doLookupResourceUriTargetFor( $expNsResource );
+
+ if ( $firstRow === false ) {
+ $exists = false;
+ return $expNsResource;
+ }
+
+ if ( is_array( $firstRow ) && count( $firstRow ) > 1 && !is_null( $firstRow[1] ) ) {
+ return $this->getResourceForTargetElement( $expNsResource, $firstRow[1] );
+ }
+
+ return $expNsResource;
+ }
+
+ private function doLookupResourceUriTargetFor( ExpNsResource $expNsResource ) {
+
+ $poolCache = InMemoryPoolCache::getInstance()->getPoolCacheById( self::POOLCACHE_ID );
+
+ if ( !$poolCache->contains( $expNsResource->getUri() ) ) {
+ $poolCache->save(
+ $expNsResource->getUri(),
+ $this->lookupResourceUriTargetFromDatabase( $expNsResource )
+ );
+ }
+
+ return $poolCache->fetch( $expNsResource->getUri() );
+ }
+
+ private function isNonRedirectableResource( ExpNsResource $expNsResource ) {
+ return $expNsResource->getNamespaceId() === 'swivt' ||
+ $expNsResource->getNamespaceId() === 'rdf' ||
+ $expNsResource->getNamespaceId() === 'rdfs' ||
+ ( $expNsResource->getNamespaceId() === 'property' && strrpos( $expNsResource->getLocalName(), 'aux' ) ) ||
+ ( isset( $expNsResource->isUserDefined ) && !$expNsResource->isUserDefined );
+ }
+
+ private function lookupResourceUriTargetFromDatabase( ExpNsResource $expNsResource ) {
+
+ $resourceUri = TurtleSerializer::getTurtleNameForExpElement( $expNsResource );
+ $rediUri = TurtleSerializer::getTurtleNameForExpElement( Exporter::getInstance()->getSpecialPropertyResource( '_REDI' ) );
+ $skeyUri = TurtleSerializer::getTurtleNameForExpElement( Exporter::getInstance()->getSpecialPropertyResource( '_SKEY' ) );
+
+ $respositoryResult = $this->repositoryConnection->select(
+ '*',
+ "$resourceUri $skeyUri ?s OPTIONAL { $resourceUri $rediUri ?r }",
+ [ 'LIMIT' => 1 ],
+ [ $expNsResource->getNamespaceId() => $expNsResource->getNamespace() ]
+ );
+
+ return $respositoryResult->current();
+ }
+
+ private function getResourceForTargetElement( ExpNsResource $expNsResource, $rediTargetElement ) {
+
+ if ( !$rediTargetElement instanceof ExpResource ) {
+ throw new RuntimeException( 'Expected a ExpResource instance' );
+ }
+
+ $rediTargetUri = $rediTargetElement->getUri();
+ $wikiNamespace = Exporter::getInstance()->getNamespaceUri( 'wiki' );
+
+ if ( strpos( $rediTargetUri, $wikiNamespace ) === 0 ) {
+ return new ExpNsResource( substr( $rediTargetUri, strlen( $wikiNamespace ) ), $wikiNamespace, 'wiki' );
+ }
+
+ return $expNsResource;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/SPARQLStore.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/SPARQLStore.php
new file mode 100644
index 00000000..d83d32c2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/SPARQLStore.php
@@ -0,0 +1,486 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\SemanticData;
+use SMW\Store;
+use SMWDataItem as DataItem;
+use SMWExpNsResource as ExpNsResource;
+use SMWExporter as Exporter;
+use SMWQuery as Query;
+use SMWTurtleSerializer as TurtleSerializer;
+use Title;
+
+/**
+ * Storage and query access point for a SPARQL supported RepositoryConnector to
+ * enable SMW to communicate with a SPARQL endpoint.
+ *
+ * The store uses a base store to update certain aspects of the data that is not
+ * yet modelled and supported by a RepositoryConnector, which may become optional
+ * in future.
+ *
+ * @license GNU GPL v2+
+ * @since 1.6
+ *
+ * @author Markus Krötzsch
+ */
+class SPARQLStore extends Store {
+
+ /**
+ * @var SPARQLStoreFactory
+ */
+ private $factory;
+
+ /**
+ * Class to be used as an underlying base store. This can be changed in
+ * LocalSettings.php (after enableSemantics()) to use another base
+ * store.
+ *
+ * @since 1.8
+ * @var string
+ */
+ static public $baseStoreClass = 'SMWSQLStore3';
+
+ /**
+ * Underlying store to use for basic read operations.
+ *
+ * @since 1.8
+ * @var Store
+ */
+ private $baseStore;
+
+ /**
+ * @since 1.8
+ *
+ * @param Store $baseStore
+ */
+ public function __construct( Store $baseStore = null ) {
+ $this->factory = new SPARQLStoreFactory( $this );
+ $this->baseStore = $baseStore;
+
+ if ( $this->baseStore === null ) {
+ $this->baseStore = $this->factory->getBaseStore( self::$baseStoreClass );
+ }
+ }
+
+ /**
+ * @see Store::getSemanticData()
+ * @since 1.8
+ */
+ public function getSemanticData( DIWikiPage $subject, $filter = false ) {
+ return $this->baseStore->getSemanticData( $subject, $filter );
+ }
+
+ /**
+ * @see Store::getPropertyValues()
+ * @since 1.8
+ */
+ public function getPropertyValues( $subject, DIProperty $property, $requestoptions = null ) {
+ return $this->baseStore->getPropertyValues( $subject, $property, $requestoptions);
+ }
+
+ /**
+ * @see Store::getPropertySubjects()
+ * @since 1.8
+ */
+ public function getPropertySubjects( DIProperty $property, $value, $requestoptions = null ) {
+ return $this->baseStore->getPropertySubjects( $property, $value, $requestoptions );
+ }
+
+ /**
+ * @see Store::getAllPropertySubjects()
+ * @since 1.8
+ */
+ public function getAllPropertySubjects( DIProperty $property, $requestoptions = null ) {
+ return $this->baseStore->getAllPropertySubjects( $property, $requestoptions );
+ }
+
+ /**
+ * @see Store::getProperties()
+ * @since 1.8
+ */
+ public function getProperties( DIWikiPage $subject, $requestoptions = null ) {
+ return $this->baseStore->getProperties( $subject, $requestoptions );
+ }
+
+ /**
+ * @see Store::getInProperties()
+ * @since 1.8
+ */
+ public function getInProperties( DataItem $object, $requestoptions = null ) {
+ return $this->baseStore->getInProperties( $object, $requestoptions );
+ }
+
+ /**
+ * @see Store::deleteSubject()
+ * @since 1.6
+ */
+ public function deleteSubject( Title $subject ) {
+ $this->doSparqlDataDelete( DIWikiPage::newFromTitle( $subject ) );
+ $this->baseStore->deleteSubject( $subject );
+ }
+
+ /**
+ * @see Store::changeTitle()
+ * @since 1.6
+ */
+ public function changeTitle( Title $oldtitle, Title $newtitle, $pageid, $redirid = 0 ) {
+
+ $oldWikiPage = DIWikiPage::newFromTitle( $oldtitle );
+ $newWikiPage = DIWikiPage::newFromTitle( $newtitle );
+ $oldExpResource = Exporter::getInstance()->getDataItemExpElement( $oldWikiPage );
+ $newExpResource = Exporter::getInstance()->getDataItemExpElement( $newWikiPage );
+ $namespaces = [ $oldExpResource->getNamespaceId() => $oldExpResource->getNamespace() ];
+ $namespaces[$newExpResource->getNamespaceId()] = $newExpResource->getNamespace();
+ $oldUri = TurtleSerializer::getTurtleNameForExpElement( $oldExpResource );
+ $newUri = TurtleSerializer::getTurtleNameForExpElement( $newExpResource );
+
+ // do this only here, so Imported from is not moved too early
+ $this->baseStore->changeTitle(
+ $oldtitle,
+ $newtitle,
+ $pageid,
+ $redirid
+ );
+
+ $sparqlDatabase = $this->getConnection();
+ $sparqlDatabase->insertDelete( "?s ?p $newUri", "?s ?p $oldUri", "?s ?p $oldUri", $namespaces );
+
+ if ( $oldtitle->getNamespace() === SMW_NS_PROPERTY ) {
+ $sparqlDatabase->insertDelete( "?s $newUri ?o", "?s $oldUri ?o", "?s $oldUri ?o", $namespaces );
+ }
+
+ /**
+ * @since 2.3 Moved UpdateJob to the base-store to ensurethat both stores
+ * operate similar when dealing with redirects
+ *
+ * @note Note that we cannot change oldUri to newUri in triple subjects,
+ * since some triples change due to the move.
+ */
+
+ // #566 $redirid == 0 indicates a `move` not a redirect action
+ if ( $redirid == 0 ) {
+ $this->doSparqlDataDelete( $oldWikiPage );
+ }
+ }
+
+ /**
+ * Update the Sparql back-end.
+ *
+ * This method can be called independently to force an update of the Sparql
+ * database. In general it is suggested to use updateData to carry out a
+ * synchronized update of the base and Sparql store.
+ *
+ * @since 2.0
+ *
+ * @param SemanticData $semanticData
+ */
+ public function doSparqlDataUpdate( SemanticData $semanticData ) {
+
+ $replicationDataTruncator = $this->factory->newReplicationDataTruncator();
+ $semanticData = $replicationDataTruncator->doTruncate( $semanticData );
+
+ $turtleTriplesBuilder = $this->factory->newTurtleTriplesBuilder();
+
+ $this->doSparqlFlatDataUpdate( $semanticData, $turtleTriplesBuilder );
+
+ foreach( $semanticData->getSubSemanticData() as $subSemanticData ) {
+ $subSemanticData = $replicationDataTruncator->doTruncate( $subSemanticData );
+ $this->doSparqlFlatDataUpdate( $subSemanticData, $turtleTriplesBuilder );
+ }
+
+ //wfDebugLog( 'smw', ' InMemoryPoolCache: ' . json_encode( \SMW\InMemoryPoolCache::getInstance()->getStats() ) );
+
+ // Reset internal cache
+ $turtleTriplesBuilder->reset();
+ }
+
+ /**
+ * @param SemanticData $semanticData
+ * @param TurtleTriplesBuilder $turtleTriplesBuilder
+ */
+ private function doSparqlFlatDataUpdate( SemanticData $semanticData, TurtleTriplesBuilder $turtleTriplesBuilder ) {
+
+ $turtleTriplesBuilder->doBuildTriplesFrom( $semanticData );
+
+ if ( !$turtleTriplesBuilder->hasTriples() ) {
+ return;
+ }
+
+ if ( $semanticData->getSubject()->getSubobjectName() === '' ) {
+ $this->doSparqlDataDelete( $semanticData->getSubject() );
+ }
+
+ foreach( $turtleTriplesBuilder->getChunkedTriples() as $chunkedTriples ) {
+ $this->getConnection()->insertData(
+ $chunkedTriples,
+ $turtleTriplesBuilder->getPrefixes()
+ );
+ }
+ }
+
+ /**
+ * @see Store::doDataUpdate()
+ * @since 1.6
+ */
+ protected function doDataUpdate( SemanticData $semanticData ) {
+ $this->baseStore->doDataUpdate( $semanticData );
+ $this->doSparqlDataUpdate( $semanticData );
+ }
+
+ /**
+ * Delete a dataitem from the Sparql back-end together with all data that is
+ * associated resources
+ *
+ * @since 2.0
+ *
+ * @param DataItem $dataItem
+ *
+ * @return boolean
+ */
+ public function doSparqlDataDelete( DataItem $dataItem ) {
+
+ $extraNamespaces = [];
+
+ $expResource = Exporter::getInstance()->getDataItemExpElement( $dataItem );
+ $resourceUri = TurtleSerializer::getTurtleNameForExpElement( $expResource );
+
+ if ( $expResource instanceof ExpNsResource ) {
+ $extraNamespaces = [ $expResource->getNamespaceId() => $expResource->getNamespace() ];
+ }
+
+ $masterPageProperty = Exporter::getInstance()->getSpecialNsResource( 'swivt', 'masterPage' );
+ $masterPagePropertyUri = TurtleSerializer::getTurtleNameForExpElement( $masterPageProperty );
+
+ $success = $this->getConnection()->deleteContentByValue( $masterPagePropertyUri, $resourceUri, $extraNamespaces );
+
+ if ( $success ) {
+ return $this->getConnection()->delete( "$resourceUri ?p ?o", "$resourceUri ?p ?o", $extraNamespaces );
+ }
+
+ return false;
+ }
+
+ /**
+ * @note Move hooks to the base class in 3.*
+ *
+ * @see Store::getQueryResult
+ * @since 1.6
+ */
+ public function getQueryResult( Query $query ) {
+
+ // Use a fallback QueryEngine in case the QueryEndpoint is inaccessible
+ if ( !$this->hasQueryEndpoint() ) {
+ return $this->baseStore->getQueryResult( $query );
+ }
+
+ $result = null;
+ $start = microtime( true );
+
+ if ( \Hooks::run( 'SMW::Store::BeforeQueryResultLookupComplete', [ $this, $query, &$result, $this->factory->newMasterQueryEngine() ] ) ) {
+ $result = $this->fetchQueryResult( $query );
+ }
+
+ \Hooks::run( 'SMW::Store::AfterQueryResultLookupComplete', [ $this, &$result ] );
+
+ $query->setOption( Query::PROC_QUERY_TIME, microtime( true ) - $start );
+
+ return $result;
+ }
+
+ protected function fetchQueryResult( Query $query ) {
+ return $this->factory->newMasterQueryEngine()->getQueryResult( $query );
+ }
+
+ /**
+ * @see Store::getPropertiesSpecial()
+ * @since 1.8
+ */
+ public function getPropertiesSpecial( $requestoptions = null ) {
+ return $this->baseStore->getPropertiesSpecial( $requestoptions );
+ }
+
+ /**
+ * @see Store::getUnusedPropertiesSpecial()
+ * @since 1.8
+ */
+ public function getUnusedPropertiesSpecial( $requestoptions = null ) {
+ return $this->baseStore->getUnusedPropertiesSpecial( $requestoptions );
+ }
+
+ /**
+ * @see Store::getWantedPropertiesSpecial()
+ * @since 1.8
+ */
+ public function getWantedPropertiesSpecial( $requestoptions = null ) {
+ return $this->baseStore->getWantedPropertiesSpecial( $requestoptions );
+ }
+
+ /**
+ * @see Store::getStatistics()
+ * @since 1.8
+ */
+ public function getStatistics() {
+ return $this->baseStore->getStatistics();
+ }
+
+ /**
+ * @see Store::refreshConceptCache()
+ * @since 1.8
+ */
+ public function refreshConceptCache( Title $concept ) {
+ return $this->baseStore->refreshConceptCache( $concept );
+ }
+
+ /**
+ * @see Store::deleteConceptCache()
+ * @since 1.8
+ */
+ public function deleteConceptCache( $concept ) {
+ return $this->baseStore->deleteConceptCache( $concept );
+ }
+
+ /**
+ * @see Store::getConceptCacheStatus()
+ * @since 1.8
+ */
+ public function getConceptCacheStatus( $concept ) {
+ return $this->baseStore->getConceptCacheStatus( $concept );
+ }
+
+ /**
+ * @see Store::service
+ *
+ * {@inheritDoc}
+ */
+ public function service( $service, ...$args ) {
+ return $this->baseStore->service( $service, ...$args );
+ }
+
+ /**
+ * @see Store::setup()
+ * @since 1.8
+ */
+ public function setup( $verbose = true ) {
+
+ // Only copy required options to the base store
+ $options = $this->getOptions()->filter(
+ [
+ \SMW\SQLStore\Installer::OPT_TABLE_OPTIMIZE,
+ \SMW\SQLStore\Installer::OPT_IMPORT,
+ \SMW\SQLStore\Installer::OPT_SCHEMA_UPDATE,
+ \SMW\SQLStore\Installer::OPT_SUPPLEMENT_JOBS
+ ]
+ );
+
+ foreach ( $options as $key => $value ) {
+ $this->baseStore->setOption( $key, $value );
+ }
+
+ $this->baseStore->setMessageReporter( $this->messageReporter );
+ $this->baseStore->setup( $verbose );
+ }
+
+ /**
+ * @see Store::drop()
+ * @since 1.6
+ */
+ public function drop( $verbose = true ) {
+ $this->baseStore->drop( $verbose );
+ $this->getConnection()->deleteAll();
+ }
+
+ /**
+ * @see Store::refreshData()
+ * @since 1.8
+ */
+ public function refreshData( &$index, $count, $namespaces = false, $usejobs = true ) {
+ return $this->baseStore->refreshData( $index, $count, $namespaces, $usejobs );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return PropertyTableInfoFetcher
+ */
+ public function getPropertyTableInfoFetcher() {
+ return $this->baseStore->getPropertyTableInfoFetcher();
+ }
+
+ /**
+ * @since 2.0
+ */
+ public function getPropertyTables() {
+ return $this->baseStore->getPropertyTables();
+ }
+
+ /**
+ * @since 2.3
+ */
+ public function getObjectIds() {
+ return $this->baseStore->getObjectIds();
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function getPropertyTableIdReferenceFinder() {
+ return $this->baseStore->getPropertyTableIdReferenceFinder();
+ }
+
+ /**
+ * @since 1.9.2
+ */
+ public function clear() {
+ $this->baseStore->clear();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|null $type
+ *
+ * @return array
+ */
+ public function getInfo( $type = null ) {
+
+ $client = $this->getConnection( 'sparql' )->getRepositoryClient();
+
+ if ( $type === 'store' ) {
+ return [ 'SMWSPARQLStore', $client->getName() ];
+ }
+
+ $connection = $this->getConnection( 'mw.db' );
+
+ if ( $type === 'db' ) {
+ return $connection->getInfo();
+ }
+
+ return [
+ 'SMWSPARQLStore' => $connection->getInfo() + [ $client->getName() => 'n/a' ]
+ ];
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $type
+ *
+ * @return mixed
+ */
+ public function getConnection( $type = 'sparql' ) {
+
+ if ( $this->connectionManager === null ) {
+ $this->setConnectionManager( $this->factory->getConnectionManager() );
+ }
+
+ return parent::getConnection( $type );
+ }
+
+ private function hasQueryEndpoint() {
+ return $this->getConnection( 'sparql' )->getRepositoryClient()->getQueryEndpoint() !== false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/SPARQLStoreFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/SPARQLStoreFactory.php
new file mode 100644
index 00000000..0c9bd16b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/SPARQLStoreFactory.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+use SMW\ApplicationFactory;
+use SMW\Connection\ConnectionManager;
+use SMW\SPARQLStore\QueryEngine\ConditionBuilder;
+use SMW\SPARQLStore\QueryEngine\DescriptionInterpreterFactory;
+use SMW\SPARQLStore\QueryEngine\EngineOptions;
+use SMW\SPARQLStore\QueryEngine\QueryEngine;
+use SMW\SPARQLStore\QueryEngine\QueryResultFactory;
+use SMW\Store;
+use SMW\StoreFactory;
+use SMW\Utils\CircularReferenceGuard;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class SPARQLStoreFactory {
+
+ /**
+ * @var SPARQLStore
+ */
+ private $store;
+
+ /**
+ * @since 2.2
+ *
+ * @param SPARQLStore $store
+ */
+ public function __construct( SPARQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $storeClass
+ *
+ * @return Store
+ */
+ public function getBaseStore( $storeClass ) {
+ return StoreFactory::getStore( $storeClass );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return QueryEngine
+ */
+ public function newMasterQueryEngine() {
+
+ $engineOptions = new EngineOptions();
+
+ $circularReferenceGuard = new CircularReferenceGuard( 'sparql-queryengine' );
+ $circularReferenceGuard->setMaxRecursionDepth( 2 );
+
+ $conditionBuilder = new ConditionBuilder(
+ new DescriptionInterpreterFactory(),
+ $engineOptions
+ );
+
+ $conditionBuilder->setCircularReferenceGuard(
+ $circularReferenceGuard
+ );
+
+ $conditionBuilder->setHierarchyLookup(
+ ApplicationFactory::getInstance()->newHierarchyLookup()
+ );
+
+ $queryEngine = new QueryEngine(
+ $this->store->getConnection( 'sparql' ),
+ $conditionBuilder,
+ new QueryResultFactory( $this->store ),
+ $engineOptions
+ );
+
+ return $queryEngine;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return RepositoryRedirectLookup
+ */
+ public function newRepositoryRedirectLookup() {
+ return new RepositoryRedirectLookup( $this->store->getConnection( 'sparql' ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return TurtleTriplesBuilder
+ */
+ public function newTurtleTriplesBuilder() {
+
+ $turtleTriplesBuilder = new TurtleTriplesBuilder(
+ $this->newRepositoryRedirectLookup()
+ );
+
+ $turtleTriplesBuilder->setTriplesChunkSize( 80 );
+
+ return $turtleTriplesBuilder;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return ReplicationDataTruncator
+ */
+ public function newReplicationDataTruncator() {
+
+ $replicationDataTruncator = new ReplicationDataTruncator();
+
+ $replicationDataTruncator->setPropertyExemptionList(
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgSparqlReplicationPropertyExemptionList' )
+ );
+
+ return $replicationDataTruncator;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return ConnectionManager
+ */
+ public function getConnectionManager() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ $repositoryConnectionProvider = new RepositoryConnectionProvider(
+ $settings->get( 'smwgSparqlRepositoryConnector' ),
+ $settings->get( 'smwgSparqlDefaultGraph' ),
+ $settings->dotGet( 'smwgSparqlEndpoint.query' ),
+ $settings->dotGet( 'smwgSparqlEndpoint.update', '' ),
+ $settings->dotGet( 'smwgSparqlEndpoint.data', '' )
+ );
+
+ $repositoryConnectionProvider->setHttpVersionTo(
+ $settings->get( 'smwgSparqlRepositoryConnectorForcedHttpVersion' )
+ );
+
+ $repositoryConnectionProvider = new RepositoryConnectionProvider();
+
+ $connectionManager = $applicationFactory->getConnectionManager();
+ $connectionManager->registerConnectionProvider(
+ 'sparql',
+ $repositoryConnectionProvider
+ );
+
+ return $connectionManager;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/TurtleTriplesBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/TurtleTriplesBuilder.php
new file mode 100644
index 00000000..fa6e9659
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SPARQLStore/TurtleTriplesBuilder.php
@@ -0,0 +1,333 @@
+<?php
+
+namespace SMW\SPARQLStore;
+
+use Onoi\Cache\Cache;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\Exporter\Element;
+use SMW\Exporter\Element\ExpElement;
+use SMW\Exporter\Element\ExpNsResource;
+use SMW\Exporter\Element\ExpResource;
+use SMW\SemanticData;
+use SMWExpData as ExpData;
+use SMWExporter as Exporter;
+use SMWTurtleSerializer as TurtleSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.0
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class TurtleTriplesBuilder {
+
+ /**
+ * ID used for the InMemoryPoolCache
+ */
+ const POOLCACHE_ID = 'sparql.turtle.triplesbuilder';
+
+ /**
+ * @var SemanticData
+ */
+ private $semanticData = null;
+
+ /**
+ * @var RepositoryRedirectLookup
+ */
+ private $repositoryRedirectLookup = null;
+
+ /**
+ * @var null|string
+ */
+ private $triples = null;
+
+ /**
+ * @var array
+ */
+ private $prefixes = [];
+
+ /**
+ * @var boolean
+ */
+ private $hasTriplesForUpdate = false;
+
+ /**
+ * @var integer
+ */
+ private $triplesChunkSize = 80;
+
+ /**
+ * @var Cache
+ */
+ private $dataItemExportInMemoryCache;
+
+ /**
+ * @since 2.0
+ *
+ * @param RepositoryRedirectLookup $repositoryRedirectLookup
+ * @param Cache|null $cache
+ */
+ public function __construct( RepositoryRedirectLookup $repositoryRedirectLookup, Cache $cache = null ) {
+ $this->repositoryRedirectLookup = $repositoryRedirectLookup;
+ $this->dataItemExportInMemoryCache = ApplicationFactory::getInstance()->getInMemoryPoolCache()->getPoolCacheById( self::POOLCACHE_ID );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param integer $chunkSize
+ */
+ public function setTriplesChunkSize( $triplesChunkSize ) {
+ $this->triplesChunkSize = (int)$triplesChunkSize;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @param SemanticData $semanticData
+ */
+ public function doBuildTriplesFrom( SemanticData $semanticData ) {
+
+ $this->hasTriplesForUpdate = false;
+ $this->triples = '';
+ $this->prefixes = [];
+
+ $this->doSerialize( $semanticData );
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return boolean
+ */
+ public function hasTriples() {
+ return $this->hasTriplesForUpdate;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return string
+ */
+ public function getTriples() {
+ return $this->triples === null ? '' : $this->triples;
+ }
+
+ /**
+ * Split the triples into group of chunks as it can happen that some subjects
+ * contain SPARQL strings that exceed 1800 lines which may reach the capacity
+ * limit of a RespositoryConnector (#1110).
+ *
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getChunkedTriples() {
+
+ $chunkedTriples = [];
+
+ if ( $this->triples === null ) {
+ return $chunkedTriples;
+ }
+
+ if ( strpos( $this->triples, " ." ) === false ) {
+ return $chunkedTriples;
+ }
+
+ $triplesArrayChunks = array_chunk(
+ explode( " .", $this->triples ), $this->triplesChunkSize
+ );
+
+ foreach( $triplesArrayChunks as $triplesChunk ) {
+ $chunkedTriples[] = implode( " .", $triplesChunk ) . "\n";
+ }
+
+ return $chunkedTriples;
+ }
+
+ /**
+ * @since 2.0
+ *
+ * @return array
+ */
+ public function getPrefixes() {
+ return $this->prefixes;
+ }
+
+ /**
+ * @since 2.0
+ */
+ public static function reset() {
+ TurtleSerializer::reset();
+ }
+
+ private function doSerialize( SemanticData $semanticData ) {
+
+ $expDataArray = $this->prepareUpdateExpData( $semanticData );
+
+ if ( count( $expDataArray ) > 0 ) {
+
+ $this->hasTriplesForUpdate = true;
+
+ $turtleSerializer = new TurtleSerializer( true );
+ $turtleSerializer->startSerialization();
+
+ foreach ( $expDataArray as $expData ) {
+ $turtleSerializer->serializeExpData( $expData );
+ }
+
+ $turtleSerializer->finishSerialization();
+
+ $this->triples = $turtleSerializer->flushContent();
+ $this->prefixes = $turtleSerializer->flushSparqlPrefixes();
+ }
+ }
+
+ /**
+ * Prepare an array of SMWExpData elements that should be written to
+ * the SPARQL store. The result is empty if no updates should be done.
+ * Note that this is different from writing an SMWExpData element that
+ * has no content.
+ * Otherwise, the first SMWExpData object in the array is a translation
+ * of the given input data, but with redirects resolved. Further
+ * SMWExpData objects might be included in the resulting list to
+ * capture necessary stub declarations for objects that do not have
+ * any data in the RDF store yet.
+ *
+ * @since 1.6
+ *
+ * @param SemanticData $semanticData
+ *
+ * @return array of SMWExpData
+ */
+ private function prepareUpdateExpData( SemanticData $semanticData ) {
+
+ $result = [];
+
+ $expData = Exporter::getInstance()->makeExportData( $semanticData );
+ $newExpData = $this->expandUpdateExpData( $expData, $result, false );
+ array_unshift( $result, $newExpData );
+
+ return $result;
+ }
+
+ /**
+ * Find a normalized representation of the given SMWExpElement that can
+ * be used in an update of the stored data. Normalization uses
+ * redirects. The type of the ExpElement might change, especially into
+ * SMWExpData in order to store auxiliary properties.
+ * Moreover, the method records any auxiliary data that should be
+ * written to the store when including this SMWExpElement into updates.
+ * This auxiliary data is collected in a call-by-ref array.
+ *
+ * @since 1.6
+ *
+ * @param Element $expElement object containing the update data
+ * @param $auxiliaryExpData array of SMWExpData
+ *
+ * @return ExpElement
+ */
+ private function expandUpdateExpElement( Element $expElement, array &$auxiliaryExpData ) {
+
+ if ( $expElement instanceof ExpResource ) {
+ return $this->expandUpdateExpResource( $expElement, $auxiliaryExpData );
+ }
+
+ if ( $expElement instanceof ExpData ) {
+ return $this->expandUpdateExpData( $expElement, $auxiliaryExpData, true );
+ }
+
+ return $expElement;
+ }
+
+ /**
+ * Find a normalized representation of the given SMWExpResource that can
+ * be used in an update of the stored data. Normalization uses
+ * redirects. The type of the ExpElement might change, especially into
+ * SMWExpData in order to store auxiliary properties.
+ * Moreover, the method records any auxiliary data that should be
+ * written to the store when including this SMWExpElement into updates.
+ * This auxiliary data is collected in a call-by-ref array.
+ *
+ * @since 1.6
+ *
+ * @param ExpResource $expResource object containing the update data
+ * @param $auxiliaryExpData array of SMWExpData
+ *
+ * @return ExpElement
+ */
+ private function expandUpdateExpResource( ExpResource $expResource, array &$auxiliaryExpData ) {
+
+ $exists = true;
+
+ if ( $expResource instanceof ExpNsResource ) {
+ $elementTarget = $this->repositoryRedirectLookup->findRedirectTargetResource( $expResource, $exists );
+ } else {
+ $elementTarget = $expResource;
+ }
+
+ if ( !$exists && $elementTarget->getDataItem() instanceof DIWikiPage && $elementTarget->getDataItem()->getDBKey() !== '' ) {
+
+ $diWikiPage = $elementTarget->getDataItem();
+ $hash = $diWikiPage->getHash();
+
+ if ( !$this->dataItemExportInMemoryCache->contains( $hash ) ) {
+ $this->dataItemExportInMemoryCache->save( $hash, Exporter::getInstance()->makeExportDataForSubject( $diWikiPage, true ) );
+ }
+
+ $auxiliaryExpData[$hash] = $this->dataItemExportInMemoryCache->fetch( $hash );
+ }
+
+ return $elementTarget;
+ }
+
+ /**
+ * Find a normalized representation of the given SMWExpData that can
+ * be used in an update of the stored data. Normalization uses
+ * redirects.
+ * Moreover, the method records any auxiliary data that should be
+ * written to the store when including this SMWExpElement into updates.
+ * This auxiliary data is collected in a call-by-ref array.
+ *
+ * @since 1.6
+ * @param ExpData $expData object containing the update data
+ * @param $auxiliaryExpData array of SMWExpData
+ * @param $expandSubject boolean controls if redirects/auxiliary data should also be sought for subject
+ *
+ * @return ExpData
+ */
+ private function expandUpdateExpData( ExpData $expData, array &$auxiliaryExpData, $expandSubject ) {
+
+ $subjectExpResource = $expData->getSubject();
+
+ if ( $expandSubject ) {
+
+ $expandedExpElement = $this->expandUpdateExpElement( $subjectExpResource, $auxiliaryExpData );
+
+ if ( $expandedExpElement instanceof ExpData ) {
+ $newExpData = $expandedExpElement;
+ } else { // instanceof SMWExpResource
+ $newExpData = new ExpData( $subjectExpResource );
+ }
+ } else {
+ $newExpData = new ExpData( $subjectExpResource );
+ }
+
+ foreach ( $expData->getProperties() as $propertyResource ) {
+
+ $propertyTarget = $this->expandUpdateExpElement( $propertyResource, $auxiliaryExpData );
+
+ foreach ( $expData->getValues( $propertyResource ) as $element ) {
+ $newExpData->addPropertyObjectValue(
+ $propertyTarget,
+ $this->expandUpdateExpElement( $element, $auxiliaryExpData )
+ );
+ }
+ }
+
+ return $newExpData;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/ChangeDiff.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/ChangeDiff.php
new file mode 100644
index 00000000..7c6fe8d7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/ChangeDiff.php
@@ -0,0 +1,255 @@
+<?php
+
+namespace SMW\SQLStore\ChangeOp;
+
+use Onoi\Cache\Cache;
+use SMW\DIWikiPage;
+use SMW\Utils\HmacSerializer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class ChangeDiff {
+
+ /**
+ * Identifies the cache namespace
+ */
+ const CACHE_NAMESPACE = 'smw:store:diff';
+
+ /**
+ * Identifies the cache TTL (one week)
+ */
+ const CACHE_TTL = 604800;
+
+ /**
+ * @var string
+ */
+ private $time;
+
+ /**
+ * @var DIWikiPage
+ */
+ private $subject;
+
+ /**
+ * @var array
+ */
+ private $tableChangeOps = [];
+
+ /**
+ * @var array
+ */
+ private $dataOps = [];
+
+ /**
+ * @var array
+ */
+ private $propertyList = [];
+
+ /**
+ * @var array
+ */
+ private $textItems = [];
+
+ /**
+ * @var array
+ */
+ private $changeList = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ * @param array $tableChangeOps
+ * @param array $dataOps
+ * @param array $propertyList
+ * @param array $textItems
+ */
+ public function __construct( DIWikiPage $subject, array $tableChangeOps, array $dataOps, array $propertyList, array $textItems = [] ) {
+ $this->time = time();
+ $this->subject = $subject;
+ $this->tableChangeOps = $tableChangeOps;
+ $this->dataOps = $dataOps;
+ $this->propertyList = $propertyList;
+ $this->textItems = $textItems;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return DIWikiPage
+ */
+ public function getSubject() {
+ return $this->subject;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return TableChangeOps[]
+ */
+ public function getTableChangeOps() {
+ return $this->tableChangeOps;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return TableChangeOps[]
+ */
+ public function getDataOps() {
+ return $this->dataOps;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getTextItems() {
+ return $this->textItems;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $op
+ *
+ * @return []
+ */
+ public function getPropertyList( $op = false ) {
+
+ if ( $op === true || $op === 'flip' ) {
+ $list = [];
+
+ foreach ( $this->propertyList as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $list[$value['_id']] = $key;
+ } else {
+ $list[$value] = $key;
+ }
+ }
+
+ return $list;
+ }
+
+ if ( $op === 'id' ) {
+ $list = [];
+
+ foreach ( $this->propertyList as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $list[$value['_id']] = [ '_key' => $key, '_type'=> $value['_type'] ];
+ }
+ }
+
+ return $list;
+ }
+
+ return $this->propertyList;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ * @param array $changes
+ */
+ public function setChangeList( $type, array $changes ) {
+ $this->changeList[$type] = $changes;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return array
+ */
+ public function getChangeListByType( $type ) {
+ return isset( $this->changeList[$type] ) ? $this->changeList[$type] : [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function serialize() {
+ return HmacSerializer::compress( $this );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function toJson( $prettify = false ) {
+
+ $changes = [];
+
+ foreach ( $this->tableChangeOps as $tableChangeOp ) {
+ $changes[] = $tableChangeOp->toArray();
+ }
+
+ $data = [];
+
+ foreach ( $this->dataOps as $dataOp ) {
+ $data[] = $dataOp->toArray();
+ }
+
+ $flags = $prettify ? JSON_PRETTY_PRINT : 0;
+
+ return json_encode(
+ [
+ 'time' => $this->time,
+ 'subject' => $this->subject->getHash(),
+ 'changes' => $changes,
+ 'change_list' => $this->changeList,
+ 'data' => $data,
+ 'text_items' => $this->textItems,
+ 'property_list' => $this->propertyList
+ ],
+ $flags
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Cache $cache
+ */
+ public function save( Cache $cache ) {
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ $this->subject->getHash()
+ );
+
+ // Keep it a week
+ $cache->save( $key, HmacSerializer::compress( $this ), self::CACHE_TTL );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Cache $cache
+ * @param DIWikiPage $subject
+ */
+ public static function fetch( Cache $cache, DIWikiPage $subject ) {
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ $subject->getHash()
+ );
+
+ if ( ( $diff = $cache->fetch( $key ) ) !== false ) {
+ return HmacSerializer::uncompress( $diff );
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/ChangeOp.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/ChangeOp.php
new file mode 100644
index 00000000..30726851
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/ChangeOp.php
@@ -0,0 +1,371 @@
+<?php
+
+namespace SMW\SQLStore\ChangeOp;
+
+use ArrayIterator;
+use IteratorAggregate;
+use SMW\DIWikiPage;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class ChangeOp implements IteratorAggregate {
+
+ /**
+ * Type of change operations
+ */
+ const OP_INSERT = 'insert';
+ const OP_DELETE = 'delete';
+
+ /**
+ * @var array
+ */
+ private $diff = [];
+
+ /**
+ * @var array
+ */
+ private $data = [];
+
+ /**
+ * @var array
+ */
+ private $textItems = [];
+
+ /**
+ * @var array
+ */
+ private $orderedDiff = [];
+
+ /**
+ * @var DIWikiPage
+ */
+ private $subject;
+
+ /**
+ * @var array
+ */
+ private $fixedPropertyRecords = [];
+
+ /**
+ * @var array
+ */
+ private $propertyList = [];
+
+ /**
+ * @var boolean
+ */
+ private $textItemsFlag = false;
+
+ /**
+ * @since 2.3
+ *
+ * @param DIWikiPage|null $subject
+ * @param array $diff
+ */
+ public function __construct( DIWikiPage $subject = null, array $diff = [] ) {
+ $this->subject = $subject;
+ $this->diff = $diff;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $textItemsFlag
+ */
+ public function setTextItemsFlag( $textItemsFlag ) {
+ $this->textItemsFlag = (bool)$textItemsFlag;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DIWikiPage
+ */
+ public function getSubject() {
+ return $this->subject;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return ArrayIterator
+ */
+ public function getIterator() {
+ return new ArrayIterator( $this->diff );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getHash() {
+ return $this->subject->getHash();
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param array $fixedPropertyRecord
+ */
+ public function addFixedPropertyRecord( $tableName, array $fixedPropertyRecord ) {
+ $this->fixedPropertyRecords[$tableName] = $fixedPropertyRecord;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getFixedPropertyRecords() {
+ return $this->fixedPropertyRecords;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function addPropertyList( $propertyList ) {
+ $this->propertyList = array_merge( $this->propertyList, $propertyList );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getPropertyList() {
+ return $this->propertyList;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $hash
+ * @param array $data
+ */
+ public function addDataOp( $hash, array $data ) {
+ $this->data[$hash] = $data;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return TableChangeOp[]
+ */
+ public function getDataOps() {
+
+ $dataChangeOps = [];
+
+ foreach ( $this->data as $hash => $data ) {
+ foreach ( $data as $tableName => $d ) {
+
+ if ( isset( $this->fixedPropertyRecords[$tableName] ) ) {
+ $d['property'] = $this->fixedPropertyRecords[$tableName];
+ }
+
+ $dataChangeOps[] = new TableChangeOp( $tableName, $d );
+ }
+ }
+
+ return $dataChangeOps;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param array $data
+ */
+ public function addTextItems( $id, array $textItems ) {
+ if ( $this->textItemsFlag ) {
+ $this->textItems[$id] = $textItems;
+ }
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param array $insertOp
+ * @param array $deleteOp
+ */
+ public function addDiffOp( array $insertOp, array $deleteOp ) {
+
+ $diff = [
+ 'insert' => $insertOp,
+ 'delete' => $deleteOp
+ ];
+
+ $this->diff[] = $diff;
+ }
+
+ /**
+ * ChangeOp (TableChangeOp/FieldChangeOp) representation of the composite
+ * diff.
+ *
+ * @since 2.4
+ *
+ * @param string|null $table
+ *
+ * @return TableChangeOp[]|[]
+ */
+ public function getTableChangeOps( $table = null ) {
+
+ $tableChangeOps = [];
+
+ foreach ( $this->getOrderedDiffByTable( $table ) as $tableName => $diff ) {
+ $tableChangeOps[] = new TableChangeOp( $tableName, $diff );
+ }
+
+ return $tableChangeOps;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return ChangeDiff
+ */
+ public function newChangeDiff() {
+
+ $changeDiff = new ChangeDiff(
+ $this->subject,
+ $this->getTableChangeOps(),
+ $this->getDataOps(),
+ $this->getPropertyList(),
+ $this->textItems
+ );
+
+ $changeDiff->setChangeList(
+ self::OP_INSERT,
+ $this->getChangedEntityIdListByType( self::OP_INSERT )
+ );
+
+ $changeDiff->setChangeList(
+ self::OP_DELETE,
+ $this->getChangedEntityIdListByType( self::OP_DELETE )
+ );
+
+ return $changeDiff;
+ }
+
+ /**
+ * Simplified (ordered by table) diff array to allow for an easier
+ * post-processing
+ *
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getOrderedDiffByTable( $table = null ) {
+
+ if ( $table === null && $this->orderedDiff !== [] ) {
+ return $this->orderedDiff;
+ }
+
+ $ordered = [];
+
+ foreach ( $this as $diff ) {
+ foreach ( $diff as $key => $value ) {
+ foreach ( $value as $tableName => $val ) {
+
+ if ( $val === [] || ( $table !== null && $table !== $tableName ) ) {
+ continue;
+ }
+
+ if ( isset( $this->fixedPropertyRecords[$tableName] ) ) {
+ $ordered[$tableName]['property'] = $this->fixedPropertyRecords[$tableName];
+ }
+
+ if ( !isset( $ordered[$tableName] ) ) {
+ $ordered[$tableName] = [];
+ }
+
+ if ( !isset( $ordered[$tableName][$key] ) ) {
+ $ordered[$tableName][$key] = [];
+ }
+
+ foreach ( $val as $v ) {
+ $ordered[$tableName][$key][] = $v;
+ }
+ }
+ }
+ }
+
+ if ( $table === null ) {
+ $this->orderedDiff = $ordered;
+ }
+
+ return $ordered;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|null $type
+ *
+ * @return array
+ */
+ public function getChangedEntityIdListByType( $type = null ) {
+
+ $changedEntities = [];
+
+ foreach ( $this->getOrderedDiffByTable() as $diff ) {
+
+ if ( ( $type === 'insert' || $type === null ) && isset( $diff['insert'] ) ) {
+ $this->addToIdList( $changedEntities, $diff['insert'] );
+ }
+
+ if ( ( $type === 'delete' || $type === null ) && isset( $diff['delete'] ) ) {
+ $this->addToIdList( $changedEntities, $diff['delete'] );
+ }
+
+ if ( $type === null && isset( $diff['property'] ) ) {
+ $changedEntities[$diff['property']['p_id']] = true;
+ }
+ }
+
+ return $changedEntities;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getChangedEntityIdSummaryList() {
+ return array_keys( $this->getChangedEntityIdListByType() );
+ }
+
+ /**
+ * @deprecated since 3.0, use ChangeOp::getChangedEntityIdSummaryList
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getCombinedIdListOfChangedEntities() {
+ return $this->getChangedEntityIdSummaryList();
+ }
+
+ private function addToIdList( &$list, $value ) {
+ foreach ( $value as $element ) {
+
+ if ( isset( $element['p_id'] ) ) {
+ $list[$element['p_id']] = true;
+ }
+
+ if ( isset( $element['s_id'] ) ) {
+ $list[$element['s_id']] = true;
+ }
+
+ if ( isset( $element['o_id'] ) ) {
+ $list[$element['o_id']] = true;
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/FieldChangeOp.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/FieldChangeOp.php
new file mode 100644
index 00000000..293138d9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/FieldChangeOp.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace SMW\SQLStore\ChangeOp;
+
+use InvalidArgumentException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class FieldChangeOp {
+
+ /**
+ * @var array
+ */
+ private $changeOp = [];
+
+ /**
+ * @var string
+ */
+ private $type;
+
+ /**
+ * @since 2.4
+ *
+ * @param array $changeOp
+ * @param string|null $type
+ */
+ public function __construct( array $changeOp = [], $type = null ) {
+ $this->changeOp = $changeOp;
+ $this->type = $type;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function set( $key, $value ) {
+ $this->changeOp[$key] = $value;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function has( $key ) {
+ return isset( $this->changeOp[$key] ) || array_key_exists( $key, $this->changeOp );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $key
+ *
+ * @return string
+ * @throws InvalidArgumentException
+ */
+ public function get( $key ) {
+
+ if ( $this->has( $key ) ) {
+ return $this->changeOp[$key];
+ }
+
+ throw new InvalidArgumentException( "{$key} is an unregistered field" );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getChangeOp() {
+ return $this->changeOp;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function __toString() {
+ return json_encode( [ $this->type => $this->changeOp ] );
+ }
+
+}
+
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/TableChangeOp.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/TableChangeOp.php
new file mode 100644
index 00000000..3923b770
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangeOp/TableChangeOp.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace SMW\SQLStore\ChangeOp;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class TableChangeOp {
+
+ const OP_INSERT = 'insert';
+ const OP_DELETE = 'delete';
+
+ /**
+ * @var string
+ */
+ private $tableName;
+
+ /**
+ * @var array
+ */
+ private $changeOps;
+
+ /**
+ * @since 2.4
+ *
+ * @param string $tableName
+ * @param array $changeOps
+ */
+ public function __construct( $tableName, array $changeOps ) {
+ $this->tableName = $tableName;
+ $this->changeOps = $changeOps;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return $this->tableName;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return boolean
+ */
+ public function isFixedPropertyOp() {
+ return isset( $this->changeOps['property'] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $field
+ *
+ * @return null|string
+ */
+ public function getFixedPropertyValByField( $field ) {
+
+ if ( $this->isFixedPropertyOp() && isset( $this->changeOps['property'][$field] ) ) {
+ return $this->changeOps['property'][$field];
+ }
+
+ return null;
+ }
+
+ /**
+ * @deprecated since 3.0, use TableChangeOp::getFixedPropertyValByField
+ * @since 2.4
+ *
+ * @param string $field
+ *
+ * @return null|string
+ */
+ public function getFixedPropertyValueBy( $field ) {
+ return $this->getFixedPropertyValByField( $field );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string $opType
+ *
+ * @return boolean
+ */
+ public function hasChangeOp( $opType ) {
+ return isset( $this->changeOps[$opType] );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param string|null $opType
+ * @param array $filter
+ *
+ * @return FieldChangeOp[]|[]
+ */
+ public function getFieldChangeOps( $opType = null, $filter = [] ) {
+
+ if ( $opType !== null && !$this->hasChangeOp( $opType ) ) {
+ return [];
+ }
+
+ $fieldOps = [];
+ $changeOps = $this->changeOps;
+
+ if ( $opType !== null ) {
+ $changeOps = $this->changeOps[$opType];
+ } elseif ( !isset( $this->changeOps[self::OP_DELETE] ) && !isset( $this->changeOps[self::OP_INSERT] ) ) {
+ $changeOps = $this->changeOps;
+ } else {
+ return array_merge(
+ $this->getFieldChangeOps( self::OP_DELETE, $filter ),
+ $this->getFieldChangeOps( self::OP_INSERT, $filter )
+ );
+ }
+
+ unset( $changeOps['property'] );
+
+ foreach ( $changeOps as $changeOp ) {
+
+ // Filter defined as: [ 's_id' => [ 42 => true, 1001 => true ] ]
+ if ( isset( $filter['s_id' ] ) && isset( $changeOp['s_id'] ) && isset( $filter['s_id'][$changeOp['s_id']] ) ) {
+ continue;
+ }
+
+ if ( isset( $this->changeOps['property'] ) ) {
+ $changeOp['p_id'] = $this->changeOps['property']['p_id'];
+ }
+
+ $fieldOps[] = new FieldChangeOp( $changeOp, $opType );
+ }
+
+ return $fieldOps;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function __toString() {
+ return json_encode( $this->toArray() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function toArray() {
+ return [ $this->tableName => $this->changeOps ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangePropagationEntityFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangePropagationEntityFinder.php
new file mode 100644
index 00000000..c2e3994e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ChangePropagationEntityFinder.php
@@ -0,0 +1,189 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use RuntimeException;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\IteratorFactory;
+use SMW\Store;
+
+/**
+ * Find all entities related to a change propagation (only expected
+ * to be used by `ChangePropagationDispatchJob`).
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ChangePropagationEntityFinder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var IteratorFactory
+ */
+ private $iteratorFactory;
+
+ /**
+ * @var boolean
+ */
+ private $isTypePropagation = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param IteratorFactory $iteratorFactory
+ */
+ public function __construct( Store $store, IteratorFactory $iteratorFactory ) {
+ $this->store = $store;
+ $this->iteratorFactory = $iteratorFactory;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isTypePropagation
+ */
+ public function isTypePropagation( $isTypePropagation ) {
+ $this->isTypePropagation = (bool)$isTypePropagation;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty|DIWikiPage $entity
+ *
+ * @return Iterator
+ * @throws RuntimeException
+ */
+ public function findAll( $entity ) {
+
+ if ( $entity instanceof DIProperty ) {
+ return $this->findByProperty( $entity );
+ } elseif ( $entity instanceof DIWikiPage ) {
+ return $this->findByCategory( $entity );
+ }
+
+ throw new RuntimeException( 'Cannot match the entity type.' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ *
+ * @return Iterator
+ */
+ public function findByProperty( DIProperty $property ) {
+
+ $dataItems = [];
+ $appendIterator = $this->iteratorFactory->newAppendIterator();
+
+ $res = $this->store->getAllPropertySubjects(
+ $property
+ );
+
+ $appendIterator->add(
+ $res
+ );
+
+ // Select any remaining references that are hidden or have been left out
+ // during an update
+ $appendIterator->add(
+ $this->fetchOtherReferencesOnTypePropagation( $property )
+ );
+
+ $dataItems = $this->store->getPropertySubjects(
+ new DIProperty( DIProperty::TYPE_ERROR ),
+ $property->getCanonicalDiWikiPage()
+ );
+
+ $appendIterator->add(
+ $dataItems
+ );
+
+ return $appendIterator;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $category
+ *
+ * @return Iterator
+ */
+ public function findByCategory( DIWikiPage $category ) {
+
+ $appendIterator = $this->iteratorFactory->newAppendIterator();
+
+ $property = new DIProperty( '_INST' );
+
+ $appendIterator->add(
+ $this->store->getPropertySubjects( $property, $category )
+ );
+
+ // Only direct antecedents
+ $dataItems = $this->store->getPropertyValues(
+ $category,
+ new DIProperty( '_SUBC' )
+ );
+
+ foreach ( $dataItems as $dataItem ) {
+ $appendIterator->add(
+ $this->store->getPropertySubjects( $property, $dataItem )
+ );
+ }
+
+ return $appendIterator;
+ }
+
+ private function fetchOtherReferencesOnTypePropagation( $property ) {
+
+ // Find other references only on a type propagation (which causes a
+ // change of table/id assignments) for entity references
+ if ( $this->isTypePropagation === false ) {
+ return [];
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $pid = $this->store->getObjectIds()->getSMWPropertyID( $property );
+
+ $dataItemTables = $this->store->getPropertyTableInfoFetcher()->getDefaultDataItemTables();
+ $idList = [];
+
+ // Matches may temporary create duplicates in regrads to
+ // Store::getAllPropertySubjects but it will be dealt with by the
+ // deduplication in the ChangePropagationUpdateJob
+ foreach ( $dataItemTables as $tableName ) {
+
+ // Select any references that are hidden or remained active
+ $rows = $connection->select(
+ $connection->tableName( $tableName ),
+ [
+ 's_id'
+ ],
+ [
+ 'p_id' => $pid
+ ],
+ __METHOD__
+ );
+
+ foreach ( $rows as $row ) {
+ $idList[] = $row->s_id;
+ }
+ }
+
+ if ( $idList === [] ) {
+ return $idList;
+ }
+
+ return $this->store->getObjectIds()->getDataItemPoolHashListFor( $idList );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ConceptCache.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ConceptCache.php
new file mode 100644
index 00000000..f228ff88
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/ConceptCache.php
@@ -0,0 +1,270 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\DIConcept;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\ProcessingErrorMsgHandler;
+use SMW\SQLStore\QueryEngine\ConceptQuerySegmentBuilder;
+use SMWSQLStore3;
+use SMWWikiPageValue;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class ConceptCache {
+
+ /**
+ * @var SMWSQLStore3
+ */
+ private $store;
+
+ /**
+ * @var ConceptQuerySegmentBuilder
+ */
+ private $conceptQuerySegmentBuilder;
+
+ /**
+ * @var integer
+ */
+ private $upperLimit = 50;
+
+ /**
+ * @since 2.2
+ *
+ * @param SMWSQLStore3 $store
+ * @param ConceptQuerySegmentBuilder $conceptQueryResolver
+ */
+ public function __construct( SMWSQLStore3 $store, ConceptQuerySegmentBuilder $conceptQuerySegmentBuilder ) {
+ $this->store = $store;
+ $this->conceptQuerySegmentBuilder = $conceptQuerySegmentBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param integer $upperLimit
+ */
+ public function setUpperLimit( $upperLimit ) {
+ $this->upperLimit = (int)$upperLimit;
+ }
+
+ /**
+ * Refresh the concept cache for the given concept.
+ *
+ * @since 1.8
+ * @param $concept Title
+ * @return array of error strings (empty if no errors occurred)
+ */
+ public function refreshConceptCache( Title $concept ) {
+
+ $errors = array_merge(
+ $this->conceptQuerySegmentBuilder->getErrors(),
+ $this->refresh( $concept )
+ );
+
+ $this->conceptQuerySegmentBuilder->cleanUp();
+
+ return ProcessingErrorMsgHandler::normalizeAndDecodeMessages( $errors );
+ }
+
+ /**
+ * Delete the concept cache for the given concept.
+ *
+ * @param $concept Title
+ */
+ public function deleteConceptCache( $concept ) {
+ $this->delete( $concept );
+ }
+
+ /**
+ * @param Title $concept
+ *
+ * @return string[] array with error messages
+ */
+ public function refresh( Title $concept ) {
+ global $wgDBtype;
+
+ $db = $this->store->getConnection();
+
+ $cid = $this->store->smwIds->getSMWPageID( $concept->getDBkey(), SMW_NS_CONCEPT, '', '' );
+ $cid_c = $this->getIdOfConcept( $concept );
+
+ if ( $cid !== $cid_c ) {
+ return [ "Skipping redirect concept." ];
+ }
+
+ $conceptQueryText = $this->getConceptCacheText( $concept );
+
+ if ( $conceptQueryText === false ) {
+ $this->deleteConceptById( $cid );
+
+ return [ "No concept description found." ];
+ }
+
+ // Pre-process query:
+ $querySegment = $this->conceptQuerySegmentBuilder->getQuerySegmentFrom(
+ $conceptQueryText
+ );
+
+ if ( $querySegment === null || $querySegment->joinfield === '' || $querySegment->joinTable === '' ) {
+ return [];
+ }
+
+ // TODO: catch db exception
+ $db->delete(
+ SMWSQLStore3::CONCEPT_CACHE_TABLE,
+ [ 'o_id' => $cid ],
+ __METHOD__
+ );
+
+ $concCacheTableName = $db->tablename( SMWSQLStore3::CONCEPT_CACHE_TABLE );
+
+ // MySQL just uses INSERT IGNORE, no extra conditions
+ $where = $querySegment->where;
+
+ if ( $wgDBtype == 'postgres' ) {
+ // PostgresQL: no INSERT IGNORE, check for duplicates explicitly
+ // This code doesn't work and has created all sorts of issues therefore use LEFT JOIN instead
+ // http://people.planetpostgresql.org/dfetter/index.php?/archives/48-Adding-Only-New-Rows-INSERT-IGNORE,-Done-Right.html
+ // $where = $querySegment->where . ( $querySegment->where ? ' AND ' : '' ) .
+ // "NOT EXISTS (SELECT NULL FROM $concCacheTableName" .
+ // " WHERE {$concCacheTableName}.s_id = {$querySegment->alias}.s_id " .
+ // " AND {$concCacheTableName}.o_id = {$querySegment->alias}.o_id )";
+ $querySegment->from = str_replace( 'INNER JOIN', 'LEFT JOIN', $querySegment->from );
+ }
+
+ $db->query( "INSERT " . ( ( $wgDBtype == 'postgres' ) ? '' : 'IGNORE ' ) .
+ "INTO $concCacheTableName" .
+ " SELECT DISTINCT {$querySegment->joinfield} AS s_id, $cid AS o_id FROM " .
+ $db->tableName( $querySegment->joinTable ) . " AS {$querySegment->alias}" .
+ $querySegment->from .
+ ( $where ? ' WHERE ' : '' ) . $where . " LIMIT ". $this->upperLimit,
+ __METHOD__
+ );
+
+ $db->update(
+ 'smw_fpt_conc',
+ [ 'cache_date' => strtotime( "now" ), 'cache_count' => $db->affectedRows() ],
+ [ 's_id' => $cid ],
+ __METHOD__
+ );
+
+ return [];
+ }
+
+ /**
+ * @param Title $concept
+ *
+ * @return string
+ */
+ public function getConceptCacheText( Title $concept ) {
+ $values = $this->store->getPropertyValues(
+ DIWikiPage::newFromTitle( $concept ),
+ new DIProperty( '_CONC' )
+ );
+
+ /**
+ * @var bool|DIConcept $di
+ */
+ $di = end( $values );
+ $conceptQueryText = $di === false ?: $di->getConceptQuery();
+
+ return $conceptQueryText;
+ }
+
+ public function delete( Title $concept ) {
+ $this->deleteConceptById( $this->getIdOfConcept( $concept ) );
+ }
+
+ /**
+ * @param Title $concept
+ *
+ * @return int
+ */
+ private function getIdOfConcept( Title $concept ) {
+ return $this->store->smwIds->getSMWPageID(
+ $concept->getDBkey(),
+ SMW_NS_CONCEPT,
+ '',
+ '',
+ false
+ );
+ }
+
+ /**
+ * @param int $conceptId
+ */
+ private function deleteConceptById( $conceptId ) {
+ // TODO: exceptions should be caught
+
+ $db = $this->store->getConnection();
+
+ $db->delete(
+ SMWSQLStore3::CONCEPT_CACHE_TABLE,
+ [ 'o_id' => $conceptId ],
+ __METHOD__
+ );
+
+ $db->update(
+ 'smw_fpt_conc',
+ [ 'cache_date' => null, 'cache_count' => null ],
+ [ 's_id' => $conceptId ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @param Title|SMWWikiPageValue|DIWikiPage $concept
+ *
+ * @return DIConcept|null
+ */
+ public function getStatus( $concept ) {
+ $db = $this->store->getConnection();
+
+ $cid = $this->store->smwIds->getSMWPageID(
+ $concept->getDBkey(),
+ $concept->getNamespace(),
+ '',
+ '',
+ false
+ );
+
+ // TODO: catch db exception
+
+ $row = $db->selectRow(
+ 'smw_fpt_conc',
+ [ 'concept_txt', 'concept_features', 'concept_size', 'concept_depth', 'cache_date', 'cache_count' ],
+ [ 's_id' => $cid ],
+ __METHOD__
+ );
+
+ if ( $row === false ) {
+ return null;
+ }
+
+ $dataItem = new DIConcept(
+ $concept,
+ null,
+ $row->concept_features,
+ $row->concept_size,
+ $row->concept_depth
+ );
+
+ if ( $row->cache_date ) {
+ $dataItem->setCacheStatus( 'full' );
+ $dataItem->setCacheDate( $row->cache_date );
+ $dataItem->setCacheCount( $row->cache_count );
+ } else {
+ $dataItem->setCacheStatus( 'empty' );
+ }
+
+ return $dataItem;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityRebuildDispatcher.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityRebuildDispatcher.php
new file mode 100644
index 00000000..7ee18256
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityRebuildDispatcher.php
@@ -0,0 +1,512 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use Hooks;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\PropertyRegistry;
+use SMW\SemanticData;
+use SMW\Utils\Lru;
+use SMW\MediaWiki\TitleFactory;
+use Title;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author Nischay Nahata
+ * @author mwjames
+ */
+class EntityRebuildDispatcher {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var TitleFactory
+ */
+ private $titleFactory;
+
+ /**
+ * @var PropertyTableIdReferenceDisposer
+ */
+ private $propertyTableIdReferenceDisposer;
+
+ /**
+ * @var JobFactory
+ */
+ private $jobFactory;
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @var array
+ */
+ private $options;
+
+ /**
+ * @var array|false
+ */
+ private $namespaces = false;
+
+ /**
+ * @var integer
+ */
+ private $iterationLimit = 1;
+
+ /**
+ * @var integer
+ */
+ private $progress = 1;
+
+ /**
+ * @var array
+ */
+ private $dispatchedEntities = [];
+
+ /**
+ * @var array
+ */
+ private $updateJobs = [];
+
+ /**
+ * @var Lru
+ */
+ private $lru;
+
+ /**
+ * @since 2.3
+ *
+ * @param SQLStore $store
+ * @param TitleFactory $titleFactory
+ */
+ public function __construct( SQLStore $store, TitleFactory $titleFactory ) {
+ $this->store = $store;
+ $this->titleFactory = $titleFactory;
+ $this->propertyTableIdReferenceDisposer = new PropertyTableIdReferenceDisposer( $store );
+ $this->jobFactory = ApplicationFactory::getInstance()->newJobFactory();
+ $this->namespaceExaminer = ApplicationFactory::getInstance()->getNamespaceExaminer();
+ $this->lru = new Lru( 10000 );
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param array $options
+ */
+ public function setOptions( array $options ) {
+ $this->options = $options;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param array|false $namespaces
+ */
+ public function setRestrictionToNamespaces( $namespaces ) {
+ $this->namespaces = $namespaces;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param integer $iterationLimit
+ */
+ public function setDispatchRangeLimit( $iterationLimit ) {
+ $this->iterationLimit = (int)$iterationLimit;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return integer
+ */
+ public function getMaxId() {
+
+ $db = $this->store->getConnection( 'mw.db' );
+
+ $maxByPageId = (int)$db->selectField(
+ 'page',
+ 'MAX(page_id)',
+ '',
+ __METHOD__
+ );
+
+ $maxBySmwId = (int)$db->selectField(
+ \SMWSql3SmwIds::TABLE_NAME,
+ 'MAX(smw_id)',
+ '',
+ __METHOD__
+ );
+
+ return max( $maxByPageId, $maxBySmwId );
+ }
+
+ /**
+ * Decimal between 0 and 1 to indicate the overall progress of the rebuild
+ * process
+ *
+ * @since 2.3
+ *
+ * @return integer
+ */
+ public function getEstimatedProgress() {
+ return $this->progress;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getDispatchedEntities() {
+ return $this->dispatchedEntities;
+ }
+
+ /**
+ * Dispatching of a single or a chunk of ids in either online or batch mode
+ * using the JoblruScheduler
+ *
+ * @since 2.3
+ *
+ * @param integer &$id
+ */
+ public function rebuild( &$id ) {
+
+ $this->updateJobs = [];
+ $this->dispatchedEntities = [];
+
+ // was nothing done in this run?
+ $emptyRange = true;
+
+ $this->match_title( $id );
+
+ if ( $this->updateJobs !== [] ) {
+ $emptyRange = false;
+ }
+
+ $this->match_subject( $id, $emptyRange );
+
+ // Deprecated since 2.3, use 'SMW::SQLStore::BeforeDataRebuildJobInsert'
+ \Hooks::run('smwRefreshDataJobs', [ &$this->updateJobs ] );
+
+ Hooks::run( 'SMW::SQLStore::BeforeDataRebuildJobInsert', [ $this->store, &$this->updateJobs ] );
+
+ if ( isset( $this->options['use-job'] ) && $this->options['use-job'] ) {
+ $this->jobFactory->batchInsert( $this->updateJobs );
+ } else {
+ foreach ( $this->updateJobs as $job ) {
+ $job->run();
+ }
+ }
+
+ // -1 means that no next position is available
+ $this->next_position( $id, $emptyRange );
+
+ return $this->progress = $id > 0 ? $id / $this->getMaxId() : 1;
+ }
+
+ /**
+ * @param integer $id
+ * @param UpdateJob[] &$updateJobs
+ */
+ private function match_title( $id ) {
+
+ // Update by MediaWiki page id --> make sure we get all pages.
+ $tids = [];
+
+ // Array of ids
+ for ( $i = $id; $i < $id + $this->iterationLimit; $i++ ) {
+ $tids[] = $i;
+ }
+
+ $titles = $this->titleFactory->newFromIDs( $tids );
+
+ foreach ( $titles as $title ) {
+
+ if ( $this->lru->get( $title->getDBKey() . '#' . $title->getNamespace() ) !== null ) {
+ continue;
+ }
+
+ if ( ( $this->namespaces == false ) || ( in_array( $title->getNamespace(), $this->namespaces ) ) ) {
+ $this->add_update( $title );
+ }
+
+ $this->dispatchedEntities[] = [ 't' => $title->getPrefixedDBKey() ];
+ }
+ }
+
+ private function match_subject( $id, &$emptyRange ) {
+
+ // update by internal SMW id --> make sure we get all objects in SMW
+ $db = $this->store->getConnection( 'mw.db' );
+
+ // MW 1.29+ "Exception thrown with an uncommitted database transaction ...
+ // MWCallableUpdate::doUpdate: transaction round 'SMW\MediaWiki\Jobs\RefreshJob::run' already started"
+ $this->propertyTableIdReferenceDisposer->waitOnTransactionIdle();
+
+ $res = $db->select(
+ \SMWSql3SmwIds::TABLE_NAME,
+ [
+ 'smw_id',
+ 'smw_title',
+ 'smw_namespace',
+ 'smw_iw',
+ 'smw_subobject',
+ 'smw_sortkey',
+ 'smw_proptable_hash',
+ 'smw_rev'
+ ],
+ [
+ "smw_id >= $id ",
+ " smw_id < " . $db->addQuotes( $id + $this->iterationLimit )
+ ],
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ $emptyRange = false; // note this even if no jobs were created
+
+ if ( $this->namespaces && !in_array( $row->smw_namespace, $this->namespaces ) ) {
+ continue;
+ }
+
+ // If the reference is for some reason created as part of a not
+ // supported namespace, check and clean it!
+ //
+ // The check is required to ensure that annotations let's say
+ // [[Foo::SomeNS:Bar]] (where SomeNS is not enabled for SMW) are not
+ // removed and is kept as long as a reference to `SomeNS:Bar` exists
+ if ( !$this->namespaceExaminer->isSemanticEnabled( (int)$row->smw_namespace ) ) {
+ $this->propertyTableIdReferenceDisposer->removeOutdatedEntityReferencesById( $row->smw_id );
+ continue;
+ }
+
+ // Find page to refresh, even for special properties:
+ if ( $row->smw_title != '' && $row->smw_title{0} != '_' ) {
+ $titleKey = $row->smw_title;
+ } elseif ( $row->smw_namespace == SMW_NS_PROPERTY && $row->smw_iw == '' && $row->smw_subobject == '' ) {
+ $titleKey = str_replace( ' ', '_', PropertyRegistry::getInstance()->findCanonicalPropertyLabelById( $row->smw_title ) );
+ } else {
+ $titleKey = '';
+ }
+
+ $hash = $titleKey . '#' . $row->smw_namespace;
+
+ if ( $row->smw_subobject !== '' && $row->smw_iw !== SMW_SQL3_SMWDELETEIW ) {
+
+ $title = $this->titleFactory->makeTitleSafe( $row->smw_namespace, $titleKey );
+
+ // Remove tangling subobjects without a real page (created by a
+ // page preview etc.) otherwise leave subobjects alone; they ought
+ // to be changed with their pages
+ if ( $title !== null && !$title->exists() ) {
+ $this->propertyTableIdReferenceDisposer->cleanUpTableEntriesById( $row->smw_id );
+ } else {
+ $this->dispatchedEntities[] = [ 's' => $row->smw_title . '#' . $row->smw_namespace . '#' .$row->smw_subobject ];
+ }
+ } elseif ( $this->isPlainObjectValue( $row ) ) {
+ $this->propertyTableIdReferenceDisposer->removeOutdatedEntityReferencesById( $row->smw_id );
+ } elseif ( $row->smw_iw === '' && $titleKey != '' ) {
+
+ if ( $this->lru->get( $hash ) !== null ) {
+ continue;
+ }
+
+ // objects representing pages
+ $title = $this->titleFactory->makeTitleSafe( $row->smw_namespace, $titleKey );
+
+ if ( $title !== null ) {
+ $this->dispatchedEntities[] = [ 's' => $title->getPrefixedDBKey() ];
+ $this->add_update( $title, $row );
+ }
+ } elseif ( $row->smw_iw == SMW_SQL3_SMWREDIIW && $titleKey != '' ) {
+
+ if ( $this->lru->get( $hash ) !== null ) {
+ continue;
+ }
+
+ // TODO: special treatment of redirects needed, since the store will
+ // not act on redirects that did not change according to its records
+ $title = $this->titleFactory->makeTitleSafe( $row->smw_namespace, $titleKey );
+
+ if ( $title !== null && !$title->exists() ) {
+ $this->dispatchedEntities[] = [ 's' => $title->getPrefixedDBKey() ];
+ $this->add_update( $title, $row );
+ }
+
+ $this->propertyTableIdReferenceDisposer->cleanUpTableEntriesById( $row->smw_id );
+ } elseif ( $row->smw_iw == SMW_SQL3_SMWIW_OUTDATED || $row->smw_iw == SMW_SQL3_SMWDELETEIW ) { // remove outdated internal object references
+ $this->propertyTableIdReferenceDisposer->cleanUpTableEntriesById( $row->smw_id );
+ } elseif ( $titleKey != '' ) { // "normal" interwiki pages or outdated internal objects -- delete
+
+ if ( $this->lru->get( $hash ) !== null ) {
+ continue;
+ }
+
+ $subject = new DIWikiPage( $titleKey, $row->smw_namespace, $row->smw_iw );
+ $this->store->updateData( new SemanticData( $subject ) );
+ $this->dispatchedEntities[] = [ 's' => $subject ];
+ }
+
+ if ( $row->smw_namespace == SMW_NS_PROPERTY && $row->smw_iw == '' && $row->smw_subobject == '' ) {
+ $this->findDuplicateProperties( $row );
+ }
+ }
+
+ $db->freeResult( $res );
+ }
+
+ private function isPlainObjectValue( $row ) {
+
+ // A rogue title should never happen
+ if ( $row->smw_title === '' && $row->smw_proptable_hash === null ) {
+ return true;
+ }
+
+ return $row->smw_iw != SMW_SQL3_SMWDELETEIW &&
+ $row->smw_iw != SMW_SQL3_SMWREDIIW &&
+ $row->smw_iw != SMW_SQL3_SMWIW_OUTDATED &&
+ // Leave any pre-defined property (_...) untouched
+ $row->smw_title != '' &&
+ $row->smw_title{0} != '_' &&
+ // smw_proptable_hash === null means it is not a subject but an object value
+ $row->smw_proptable_hash === null;
+ }
+
+ private function findDuplicateProperties( $row ) {
+
+ $db = $this->store->getConnection( 'mw.db' );
+
+ // Use the sortkey (comparing the label and not the "_..." key) in order
+ // to match possible duplicate properties by label (not by key)
+ $duplicates = $db->select(
+ \SMWSql3SmwIds::TABLE_NAME,
+ [
+ 'smw_id',
+ 'smw_title' ],
+ [
+ "smw_id !=" . $db->addQuotes( $row->smw_id ),
+ "smw_sortkey =" . $db->addQuotes( $row->smw_sortkey ),
+ "smw_namespace =" . $row->smw_namespace,
+ "smw_subobject =" . $db->addQuotes( $row->smw_subobject )
+ ],
+ __METHOD__,
+ [
+ 'ORDER BY' => "smw_id ASC"
+ ]
+ );
+
+ if ( $duplicates === false ) {
+ return;
+ }
+
+ // Instead of copying ID's across DB tables have the re-parse to ensure
+ // that all property value ID's are reassigned together while the duplicate
+ // is marked for removal until the next run
+ foreach ( $duplicates as $duplicate ) {
+
+ // If titles don't match then continue because it could be that
+ // Property:Foo with displaytitle foobar -> sortkey ->foobar
+ // Property:Bar with displaytitle foobar -> sortkey ->foobar
+ if ( $row->smw_title !== $duplicate->smw_title ) {
+ continue;
+ }
+
+ $this->store->getObjectIds()->updateInterwikiField(
+ $duplicate->smw_id,
+ new DIWikiPage( $row->smw_title, $row->smw_namespace, SMW_SQL3_SMWDELETEIW )
+ );
+ }
+ }
+
+ private function next_position( &$id, $emptyRange ) {
+
+ $nextPosition = $id + $this->iterationLimit;
+ $db = $this->store->getConnection( 'mw.db' );
+
+ // nothing found, check if there will be more pages later on
+ if ( $emptyRange && $nextPosition > \SMWSql3SmwIds::FXD_PROP_BORDER_ID ) {
+
+ $nextByPageId = (int)$db->selectField(
+ 'page',
+ 'page_id',
+ "page_id >= $nextPosition",
+ __METHOD__,
+ [
+ 'ORDER BY' => "page_id ASC"
+ ]
+ );
+
+ $nextBySmwId = (int)$db->selectField(
+ \SMWSql3SmwIds::TABLE_NAME,
+ 'smw_id',
+ "smw_id >= $nextPosition",
+ __METHOD__,
+ [
+ 'ORDER BY' => "smw_id ASC"
+ ]
+ );
+
+ // Next position is determined by the pool with the maxId
+ $nextPosition = $nextBySmwId != 0 && $nextBySmwId > $nextByPageId ? $nextBySmwId : $nextByPageId;
+ }
+
+ $id = $nextPosition ? $nextPosition : -1;
+ }
+
+ private function add_update( $title, $row = false ) {
+
+ $hash = $title->getDBKey() . '#' . $title->getNamespace();
+ $this->lru->set( $hash, true );
+
+ if ( isset( $this->options['revision-mode'] ) && $this->options['revision-mode'] && !$this->options['force-update'] && $this->matchesLatestRevID( $title, $row ) ) {
+ return $this->dispatchedEntities[] = [ 'skipped' => $title->getPrefixedDBKey() ];
+ }
+
+ $params = [
+ 'origin' => 'EntityRebuildDispatcher'
+ ];
+
+ if ( isset( $this->options['shallow-update'] ) && $this->options['shallow-update'] ) {
+ $params += [ 'shallowUpdate' => true ];
+ } elseif ( isset( $this->options['force-update'] ) && $this->options['force-update'] ) {
+ $params += [ 'forcedUpdate' => true ];
+ }
+
+ $updateJob = $this->jobFactory->newUpdateJob(
+ $title,
+ $params
+ );
+
+ $this->updateJobs[] = $updateJob;
+ }
+
+ private function matchesLatestRevID( $title, $row = false ) {
+
+ $latestRevID = $title->getLatestRevID( Title::GAID_FOR_UPDATE );
+
+ if ( $row !== false ) {
+ return $latestRevID == $row->smw_rev;
+ };
+
+ $rev = $this->store->getObjectIds()->findAssociatedRev(
+ $title->getDBKey(),
+ $title->getNamespace(),
+ $title->getInterwiki()
+ );
+
+ return $latestRevID == $rev;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/CachingEntityLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/CachingEntityLookup.php
new file mode 100644
index 00000000..a94e28a8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/CachingEntityLookup.php
@@ -0,0 +1,437 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use Onoi\BlobStore\BlobStore;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\EntityLookup;
+use SMW\HashBuilder;
+use SMW\Localizer;
+use SMW\RequestOptions;
+use SMW\SemanticData;
+use SMW\SQLStore\Lookup\RedirectTargetLookup;
+use SMWDataItem as DataItem;
+
+/**
+ * Intermediary (fast) access to serialized blob values to avoid DB access on
+ * objects that are static until it is altered. An intermediary object will
+ * invalidate itself on any delete, update, change, or move operation.
+ *
+ * Each subject (including its subobjects) will be stored as individual blob with
+ * each operation belonging to that subject extending its blob to be able
+ * to discard the entire entity at once.
+ *
+ * Each operation request will either fill the cache or return the result from
+ * the cache until the subject is changed and the whole container is being
+ * flushed.
+ *
+ * The class could be decorator but due to the nature of the current Store design
+ * it is called from within each method. The class is not for public use and it
+ * is expected to be accessed only by a Store operation.
+ *
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class CachingEntityLookup implements EntityLookup {
+
+ /**
+ * Update this version number when the serialization format
+ * changes.
+ */
+ const VERSION = '1.1';
+
+ /**
+ * @var EntityLookup
+ */
+ private $entityLookup;
+
+ /**
+ * @var RedirectTargetLookup
+ */
+ private $redirectTargetLookup;
+
+ /**
+ * @var BlobStore
+ */
+ private $blobStore;
+
+ /**
+ * @var integer
+ */
+ private $lookupFeatures = 0;
+
+ /**
+ * @since 2.3
+ *
+ * @param EntityLookup $entityLookup
+ * @param RedirectTargetLookup $redirectTargetLookup
+ * @param BlobStore $blobStore
+ */
+ public function __construct( EntityLookup $entityLookup, RedirectTargetLookup $redirectTargetLookup, BlobStore $blobStore ) {
+ $this->entityLookup = $entityLookup;
+ $this->redirectTargetLookup = $redirectTargetLookup;
+ $this->blobStore = $blobStore;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param integer $lookupFeatures
+ */
+ public function setLookupFeatures( $lookupFeatures ) {
+ $this->lookupFeatures = $lookupFeatures;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param integer $lookupFeatures
+ *
+ * @return boolean
+ */
+ public function isEnabledFeature( $lookupFeatures ) {
+ return $this->lookupFeatures === ( $this->lookupFeatures | $lookupFeatures );
+ }
+
+ /**
+ * @see EntityLookup::getSemanticData
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getSemanticData( DIWikiPage $subject, $filter = false ) {
+
+ if ( !$this->blobStore->canUse() || !$this->isEnabledFeature( SMW_VL_SD ) ) {
+ return $this->entityLookup->getSemanticData( $subject, $filter );
+ }
+
+ // Use a separate container otherwise large serializations of subobjects
+ // will decrease performance when combined with other lists
+ $sid = $this->getHashFrom(
+ $subject,
+ $subject->getSubobjectName()
+ );
+
+ $container = $this->blobStore->read( $sid );
+
+ // Make sure that when switching user languages, user labels etc.
+ // are appropriately generated
+ $userLang = Localizer::getInstance()->getUserLanguage()->getCode();
+
+ $sdid = HashBuilder::createFromContent(
+ [
+ (array)$filter,
+ self::VERSION
+ ],
+ 'sd:'. $userLang . ':'
+ );
+
+ if ( $container->has( $sdid ) ) {
+ return $container->get( $sdid );
+ }
+
+ $semanticData = $this->entityLookup->getSemanticData(
+ $subject,
+ $filter
+ );
+
+ $semanticData->setOption(
+ SemanticData::OPT_LAST_MODIFIED,
+ wfTimestamp( TS_UNIX )
+ );
+
+ $container->set( $sdid, $semanticData );
+
+ $this->blobStore->save(
+ $container
+ );
+
+ $this->appendToList( $sid, $subject );
+
+ return $semanticData;
+ }
+
+ /**
+ * @see EntityLookup::getProperties
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getProperties( DIWikiPage $subject, RequestOptions $requestOptions = null ) {
+
+ if ( !$this->blobStore->canUse() || !$this->isEnabledFeature( SMW_VL_PL ) ) {
+ return $this->entityLookup->getProperties( $subject, $requestOptions );
+ }
+
+ $container = $this->blobStore->read(
+ $this->getHashFrom( $subject )
+ );
+
+ $plid = HashBuilder::createFromContent(
+ [
+ $subject->getSubobjectName(),
+ (array)$requestOptions,
+ self::VERSION
+ ],
+ 'pl:'
+ );
+
+ if ( $container->has( $plid ) ) {
+ return $this->resolveRedirectTargets( $container->get( $plid ) );
+ }
+
+ $result = $this->entityLookup->getProperties(
+ $subject,
+ $requestOptions
+ );
+
+ $container->set( $plid, $result );
+
+ $this->blobStore->save(
+ $container
+ );
+
+ return $result;
+ }
+
+ /**
+ * @see EntityLookup::getPropertyValues
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getPropertyValues( DIWikiPage $subject = null, DIProperty $property, RequestOptions $requestOptions = null ) {
+
+ // The cache is not used for $subject === null (means all values for
+ // the given property are returned)
+ if ( $subject === null || !$this->blobStore->canUse() || !$this->isEnabledFeature( SMW_VL_PV ) ) {
+ return $this->entityLookup->getPropertyValues( $subject, $property, $requestOptions );
+ }
+
+ // Too many subobjects in one list can kill the performance therefore split
+ // the container by subobject
+ $sid = $this->getHashFrom(
+ $subject,
+ $subject->getSubobjectName()
+ );
+
+ $container = $this->blobStore->read( $sid );
+
+ $pvid = HashBuilder::createFromContent(
+ [
+ $property->getKey(),
+ $property->isInverse(),
+ (array)$requestOptions,
+ self::VERSION
+ ],
+ 'pv:'
+ );
+
+ if ( $container->has( $pvid ) ) {
+ return $this->resolveRedirectTargets( $container->get( $pvid ) );
+ }
+
+ $result = $this->entityLookup->getPropertyValues(
+ $subject,
+ $property,
+ $requestOptions
+ );
+
+ // Returns a possible iterator, avoid "Serialization of 'Closure' is not allowed"
+ if ( $result instanceof \Iterator ) {
+ $result = iterator_to_array( $result );
+ }
+
+ $container->set( $pvid, $result );
+
+ $this->blobStore->save(
+ $container
+ );
+
+ $this->appendToList( $sid, $subject );
+
+ return $result;
+ }
+
+ /**
+ * @see EntityLookup::getPropertySubjects
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getPropertySubjects( DIProperty $property, DataItem $dataItem = null, RequestOptions $requestOptions = null ) {
+
+ // The cache is not used for $dataItem === null (means all values for
+ // the given property are returned)
+ if ( $dataItem === null || !$dataItem instanceof DIWikiPage || !$this->blobStore->canUse() || !$this->isEnabledFeature( SMW_VL_PS ) ) {
+ return $this->entityLookup->getPropertySubjects( $property, $dataItem, $requestOptions );
+ }
+
+ // Added as linked list as we keep the container ttl different from
+ // that of the main container
+ $sid = $this->getHashFrom(
+ $dataItem,
+ 'ps'
+ );
+
+ $container = $this->blobStore->read( $sid );
+
+ $psid = HashBuilder::createFromContent(
+ [
+ $property->getKey(),
+ $property->isInverse(),
+ (array)$requestOptions,
+ self::VERSION
+ ],
+ 'ps:'
+ );
+
+ if ( $container->has( $psid ) ) {
+ return $container->get( $psid );
+ }
+
+ $result = $this->entityLookup->getPropertySubjects(
+ $property,
+ $dataItem,
+ $requestOptions
+ );
+
+ // Returns an iterator, avoid "Serialization of 'Closure' is not allowed"
+ if ( $result instanceof \Iterator ) {
+ $result = iterator_to_array( $result );
+ }
+
+ $container->set( $psid, $result );
+
+ // We set a short lifetime (5 min) in order to cache repeated requests but
+ // avoiding a complex invalidation during a subject update otherwise all
+ // properties of a container would require scanning and removal
+ $container->setExpiryInSeconds( 60 * 5 );
+
+ $this->blobStore->save(
+ $container
+ );
+
+ $this->appendToList( $sid, $dataItem );
+
+ return $result;
+ }
+
+ /**
+ * @see EntityLookup::getAllPropertySubjects
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getAllPropertySubjects( DIProperty $property, RequestOptions $requestOptions = null ) {
+ return $this->entityLookup->getAllPropertySubjects( $property, $requestOptions );
+ }
+
+ /**
+ * @see EntityLookup::getInProperties
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getInProperties( DataItem $object, RequestOptions $requestOptions = null ) {
+ return $this->entityLookup->getInProperties( $object, $requestOptions );
+ }
+
+ /**
+ * Remove a cache item that appears during an alteration action (update,
+ * change, delete) to ensure that we always have the correct set of matches.
+ *
+ * @since 2.3
+ */
+ public function invalidateCache() {
+
+ $args = func_get_args();
+ $subject = array_shift( $args );
+
+ if ( !$this->blobStore->canUse() || !$subject instanceof DIWikiPage ) {
+ return null;
+ }
+
+ // Remove a redirect target subject directly
+ $redirects = $this->getSemanticData( $subject )->getPropertyValues(
+ new DIProperty( '_REDI' )
+ );
+
+ foreach ( $redirects as $redirectTarget ) {
+ $this->blobStore->delete( $this->getHashFrom(
+ $redirectTarget
+ ) );
+ }
+
+ $sid = $this->getHashFrom( $subject );
+
+ // Remove all linked objects
+ $container = $this->blobStore->read( $sid );
+
+ if ( $container->has( 'list' ) ) {
+ foreach ( array_keys( $container->get( 'list' ) ) as $id ) {
+ $this->blobStore->delete( $id );
+ }
+ }
+
+ $this->blobStore->delete( $sid );
+ }
+
+ /**
+ * Ensures that new redirects are resolved while a value is still kept
+ * in cache (internally it uses getPropertyValues hence we don't loose
+ * much as objects are reused during the lookup).
+ */
+ private function resolveRedirectTargets( array $results ) {
+
+ $dataItems = [];
+
+ foreach ( $results as $dataItem ) {
+ $dataItems[] = $this->redirectTargetLookup->findRedirectTarget( $dataItem );
+ }
+
+ return $dataItems;
+ }
+
+ /**
+ * The subobject is attached to a root subject therefore using the root as
+ * identifier to allow it to be invalidated at once with all other subobjects
+ * that relate to a subject
+ */
+ private function getHashFrom( DIWikiPage $subject, $suffix = '' ) {
+ return md5( HashBuilder::createHashIdFromSegments(
+ $subject->getDBkey(),
+ $subject->getNamespace(),
+ $subject->getInterwiki()
+ ) . $suffix );
+ }
+
+ private function appendToList( $id, $subject ) {
+
+ // Store the id with the main subject
+ $container = $this->blobStore->read(
+ $this->getHashFrom( $subject )
+ );
+
+ // Use the id as key to avoid unnecessary duplicate entries when
+ // employing append
+ $container->append(
+ 'list',
+ [ $id => true ]
+ );
+
+ $this->blobStore->save(
+ $container
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/CachingSemanticDataLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/CachingSemanticDataLookup.php
new file mode 100644
index 00000000..3826ed82
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/CachingSemanticDataLookup.php
@@ -0,0 +1,272 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use Onoi\Cache\Cache;
+use Onoi\Cache\NullCache;
+use Psr\Log\LoggerAwareTrait;
+use RuntimeException;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\RequestOptions;
+use SMW\SemanticData;
+use SMW\SQLStore\PropertyTableDefinition;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class CachingSemanticDataLookup {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var SemanticDataLookup
+ */
+ private $semanticDataLookup;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * Cache for SemanticData dataItems, indexed by SMW ID.
+ *
+ * @var array
+ */
+ private static $data = [];
+
+ /**
+ * Like SMWSQLStore3::data, but containing flags indicating
+ * completeness of the SemanticData objs.
+ *
+ * @var array
+ */
+ private static $state = [];
+
+ /**
+ * >0 while getSemanticData runs, used to prevent nested calls from clearing
+ * the cache while another call runs and is about to fill it with data
+ *
+ * @var int
+ */
+ private static $lookupCount = 0;
+
+ /**
+ * @since 3.0
+ *
+ * @param SemanticDataLookup $semanticDataLookup
+ * @param Cache|null $cache
+ */
+ public function __construct( SemanticDataLookup $semanticDataLookup, Cache $cache = null ) {
+ $this->semanticDataLookup = $semanticDataLookup;
+ $this->cache = $cache;
+
+ if ( $this->cache === null ) {
+ $this->cache = new NullCache();
+ }
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function lockCache() {
+ self::$lookupCount++;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function unlockCache() {
+ self::$lookupCount--;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ */
+ public function invalidateCache( $id ) {
+ unset( self::$data[$id] );
+ unset( self::$state[$id] );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public static function clear() {
+ self::$data = [];
+ self::$state = [];
+ self::$lookupCount = 0;
+ }
+
+ /**
+ * Helper method to make sure there is a cache entry for the data about
+ * the given subject with the given ID.
+ *
+ * @todo The management of this cache should be revisited.
+ *
+ * @since 3.0
+ *
+ * @param int $id
+ * @param DIWikiPage $subject
+ */
+ public function initLookupCache( $id, DIWikiPage $subject ) {
+
+ // *** Prepare the cache ***//
+ if ( !isset( self::$data[$id] ) ) {
+ self::$data[$id] = $this->semanticDataLookup->newStubSemanticData( $subject );
+ self::$state[$id] = [];
+ }
+
+ // Issue #622
+ // If a redirect was cached preceding this request and points to the same
+ // subject id ensure that in all cases the requested subject matches with
+ // the selected DB id
+ if ( self::$data[$id]->getSubject()->getHash() !== $subject->getHash() ) {
+ self::$data[$id] = $this->semanticDataLookup->newStubSemanticData( $subject );
+ self::$state[$id] = [];
+ }
+
+ // It is not so easy to find the sweet spot between cache size and
+ // performance gains (both memory and time), The value of 20 was chosen
+ // by profiling runtimes for large inline queries and heavily annotated
+ // pages. However, things might have changed in the meantime ...
+ if ( ( count( self::$data ) > 20 ) && ( self::$lookupCount == 1 ) ) {
+ self::$data = [ $id => self::$data[$id] ];
+ self::$state = [ $id => self::$state[$id] ];
+ }
+ }
+
+ /**
+ * Set the semantic data lookup cache to hold exactly the given value for the
+ * given ID.
+ *
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param SemanticData $semanticData
+ */
+ public function setLookupCache( $id, SemanticData $semanticData ) {
+
+ self::$data[$id] = $this->semanticDataLookup->newStubSemanticData(
+ $semanticData
+ );
+
+ self::$state[$id] = $this->semanticDataLookup->getTableUsageInfo(
+ $semanticData
+ );
+ }
+
+ /**
+ * Helper method to make sure there is a cache entry for the data about
+ * the given subject with the given ID.
+ *
+ * @since 3.0
+ *
+ * @param int $id
+ * @param DIWikiPage $subject
+ */
+ public function getSemanticDataById( $id ) {
+
+ if ( !isset( self::$data[$id] ) ) {
+ throw new RuntimeException( 'Data are not initialized.' );
+ }
+
+ return self::$data[$id];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param PropertyTableDefinition $propertyTableDef
+ * @param DIProperty $property
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return RequestOptions|null
+ */
+ public function newRequestOptions( PropertyTableDefinition $propertyTableDef, DIProperty $property, RequestOptions $requestOptions = null ) {
+ return $this->semanticDataLookup->newRequestOptions( $propertyTableDef, $property, $requestOptions );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param DataItem $dataItem
+ * @param PropertyTableDefinition $propertyTableDef
+ * @param RequestOptions $requestOptions
+ *
+ * @return RequestOptions|null
+ */
+ public function fetchSemanticData( $id, DataItem $dataItem = null, PropertyTableDefinition $propertyTableDef, RequestOptions $requestOptions = null ) {
+ return $this->semanticDataLookup->fetchSemanticData( $id, $dataItem, $propertyTableDef, $requestOptions );
+ }
+
+ /**
+ * Fetch and cache the data about one subject for one particular table
+ *
+ * @param integer $id
+ * @param DIWikiPage $subject
+ * @param PropertyTableDefinition $propertyTableDef
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return SemanticData
+ */
+ public function getSemanticDataFromTable( $id, DataItem $dataItem = null, PropertyTableDefinition $propertyTableDef, RequestOptions $requestOptions = null ) {
+
+ // Avoid the cache when a request is constrainted
+ if ( $requestOptions !== null || !$dataItem instanceof DIWikiPage ) {
+ return $this->semanticDataLookup->getSemanticData( $id, $dataItem, $propertyTableDef, $requestOptions );
+ }
+
+ return $this->fetchFromCache( $id, $dataItem, $propertyTableDef );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return StubSemanticData
+ */
+ public function newStubSemanticData( DIWikiPage $subject ) {
+ return $this->semanticDataLookup->newStubSemanticData( $subject );
+ }
+
+ private function fetchFromCache( $id, DataItem $dataItem = null, PropertyTableDefinition $propertyTableDef ) {
+
+ // Do not clear the cache when called recursively.
+ $this->lockCache();
+ $this->initLookupCache( $id, $dataItem );
+
+ // @see also setLookupCache
+ $name = $propertyTableDef->getName();
+
+ if ( isset( self::$state[$id][$name] ) ) {
+ $this->unlockCache();
+ return self::$data[$id];
+ }
+
+ $data = $this->semanticDataLookup->fetchSemanticData(
+ $id,
+ $dataItem,
+ $propertyTableDef
+ );
+
+ foreach ( $data as $d ) {
+ self::$data[$id]->addPropertyStubValue( reset( $d ), end( $d ) );
+ }
+
+ self::$state[$id][$name] = true;
+
+ $this->unlockCache();
+
+ return self::$data[$id];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIBlobHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIBlobHandler.php
new file mode 100644
index 00000000..f88ed878
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIBlobHandler.php
@@ -0,0 +1,252 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\DIHandlers;
+
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+use SMWDIBlob as DIBlob;
+
+/**
+ * This class implements Store access to blob (string) data items.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ */
+class DIBlobHandler extends DataItemHandler {
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableFields() {
+ return [
+ 'o_blob' => FieldType::TYPE_BLOB,
+ 'o_hash' => $this->getCharFieldType()
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFetchFields() {
+ return [
+ 'o_blob' => FieldType::TYPE_BLOB,
+ 'o_hash' => $this->getCharFieldType()
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableIndexes() {
+ return [
+
+ 's_id,o_hash',
+
+ // pvalue select
+ // SELECT p_id,o_hash FROM `smw_di_blob` WHERE p_id = '310174' AND ( o_hash LIKE '%test%' ) LIMIT 11
+ 'p_id,o_hash',
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexHint( $key ) {
+
+ // Store::getPropertySubjects has seen to choose the wrong index
+
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids`
+ // INNER JOIN `smw_di_blob` AS t1 FORCE INDEX(s_id) ON t1.s_id=smw_id
+ // WHERE t1.p_id='310174' AND smw_iw!=':smw'
+ // AND smw_iw!=':smw-delete' AND smw_iw!=':smw-redi'
+ // GROUP BY smw_sort, smw_id LIMIT 26
+ //
+ // 137.4161ms SMWSQLStore3Readers::getPropertySubjects
+ //
+ // vs.
+ //
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids`
+ // INNER JOIN `smw_di_blob` AS t1 ON t1.s_id=smw_id
+ // WHERE t1.p_id='310174' AND smw_iw!=':smw' AND smw_iw!=':smw-delete'
+ // AND smw_iw!=':smw-redi'
+ // GROUP BY smw_sort, smw_id LIMIT 26
+ //
+ // 23482.1451ms SMWSQLStore3Readers::getPropertySubjects
+ if ( 'property.subjects' && $this->isDbType( 'mysql' ) ) {
+ return 's_id';
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getWhereConds( DataItem $dataItem ) {
+
+ $isKeyword = $dataItem->getOption( 'is.keyword' );
+ $text = $dataItem->getString();
+
+ return [
+ 'o_hash' => $isKeyword ? $dataItem->normalize( $text ) : $this->makeHash( $text )
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getInsertValues( DataItem $dataItem ) {
+
+ $isKeyword = $dataItem->getOption( 'is.keyword' );
+
+ $text = htmlspecialchars_decode( trim( $dataItem->getString() ), ENT_QUOTES );
+ $hash = $isKeyword ? $dataItem->normalize( $text ) : $this->makeHash( $text );
+
+ if ( $this->isDbType( 'postgres' ) ) {
+ $text = pg_escape_bytea( $text );
+ }
+
+ if ( mb_strlen( $text ) <= $this->getMaxLength() && !$isKeyword ) {
+ $text = null;
+ }
+
+ return [
+ 'o_blob' => $text,
+ 'o_hash' => $hash,
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexField() {
+ return 'o_hash';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getLabelField() {
+ return 'o_hash';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function dataItemFromDBKeys( $dbkeys ) {
+
+ if ( !is_array( $dbkeys ) || count( $dbkeys ) != 2 ) {
+ throw new DataItemHandlerException( 'Failed to create data item from DB keys.' );
+ }
+
+ if ( $this->isDbType( 'postgres' ) ) {
+ $dbkeys[0] = pg_unescape_bytea( $dbkeys[0] );
+ }
+
+ // empty blob: use "hash" string
+ if ( $dbkeys[0] == '' ) {
+ return new DIBlob( $dbkeys[1] );
+ }
+
+ return new DIBlob( $dbkeys[0] );
+ }
+
+ /**
+ * Method to make a hashed representation for strings of length greater
+ * than DIBlobHandler::getMaxLength to be used for selecting and sorting.
+ *
+ * @since 1.8
+ * @param $string string
+ *
+ * @return string
+ */
+ private function makeHash( $string ) {
+
+ $length = $this->getMaxLength();
+
+ if( mb_strlen( $string ) <= $length ) {
+ return $string;
+ }
+
+ return mb_substr( $string, 0, $length - 32 ) . md5( $string );
+ }
+
+ /**
+ * Maximal number of bytes (chars) to be stored in the hash field of
+ * the table. Must not be bigger than 255 (the length of our VARCHAR
+ * field in the DB). Strings that are longer than this will be stored
+ * as a blob, and the hash will only start with the original string
+ * but the last 32 bytes are used for a hash. So the minimal portion
+ * of the string that is stored literally in the hash is 32 chars
+ * less.
+ *
+ * The value of 72 was chosen since it leads to a smaller index size
+ * at the cost of needing more blobs in cases where many strings are
+ * of length 73 to 255. But keeping the index small seems more
+ * important than saving disk space. Also, with 72 bytes there are at
+ * least 40 bytes of content available for sorting and prefix matching,
+ * which should be more than enough in most contexts.
+ *
+ * @since 1.8
+ *
+ * Using `SMW_FIELDT_CHAR_LONG` as option in `smwgFieldTypeFeatures`
+ * will extend the field size to 300 and expands the maximum matchable
+ * string length to 300-32 for LIKE/NLIKE queries.
+ *
+ * @since 3.0
+ */
+ private function getMaxLength() {
+
+ $length = 72;
+
+ if ( $this->isEnabledFeature( SMW_FIELDT_CHAR_LONG ) ) {
+ $length = FieldType::CHAR_LONG_LENGTH;
+ }
+
+ return $length;
+ }
+
+ private function getCharFieldType() {
+
+ $fieldType = FieldType::FIELD_TITLE;
+
+ if ( $this->isEnabledFeature( SMW_FIELDT_CHAR_NOCASE ) ) {
+ $fieldType = FieldType::TYPE_CHAR_NOCASE;
+ }
+
+ if ( $this->isEnabledFeature( SMW_FIELDT_CHAR_LONG ) ) {
+ $fieldType = FieldType::TYPE_CHAR_LONG;
+ }
+
+ if ( $this->isEnabledFeature( SMW_FIELDT_CHAR_LONG ) && $this->isEnabledFeature( SMW_FIELDT_CHAR_NOCASE ) ) {
+ $fieldType = FieldType::TYPE_CHAR_LONG_NOCASE;
+ }
+
+ return $fieldType;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIBooleanHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIBooleanHandler.php
new file mode 100644
index 00000000..5b6a2b12
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIBooleanHandler.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\DIHandlers;
+
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+use SMWDIBoolean as DIBoolean;
+
+/**
+ * This class implements Store access to Boolean data items.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ */
+class DIBooleanHandler extends DataItemHandler {
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableFields() {
+ return [
+ 'o_value' => FieldType::TYPE_BOOL
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFetchFields() {
+ return [
+ 'o_value' => FieldType::TYPE_BOOL
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getWhereConds( DataItem $dataItem ) {
+ return [
+ 'o_value' => $dataItem->getBoolean() ? 1 : 0,
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getInsertValues( DataItem $dataItem ) {
+ return [
+ 'o_value' => $dataItem->getBoolean() ? 1 : 0,
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexField() {
+ return 'o_value';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getLabelField() {
+ return 'o_value';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function dataItemFromDBKeys( $dbkeys ) {
+ global $wgDBtype;
+
+ //PgSQL returns as t and f and need special handling http://archives.postgresql.org/pgsql-php/2010-02/msg00005.php
+ if ( $wgDBtype == 'postgres' ) {
+ $value = ( $dbkeys == 't' );
+ } else {
+ $value = ( $dbkeys == '1' );
+ }
+
+ return new DIBoolean( $value );
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIConceptHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIConceptHandler.php
new file mode 100644
index 00000000..a9c07193
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIConceptHandler.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\DIHandlers;
+
+use SMW\DIConcept;
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+
+/**
+ * This class implements Store access to Concept data items.
+ *
+ * @note The table layout and behavior of this class is not coherent with the
+ * way that other DIs work. This is because of the unfortunate use of the
+ * concept table to store extra cache data, but also due to the design of
+ * concept DIs. This will be cleaned up at some point.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ */
+class DIConceptHandler extends DataItemHandler {
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableFields() {
+ return [
+ 'concept_txt' => FieldType::TYPE_BLOB,
+ 'concept_docu' => FieldType::TYPE_BLOB,
+ 'concept_features' => FieldType::FIELD_NAMESPACE,
+ 'concept_size' => FieldType::FIELD_NAMESPACE,
+ 'concept_depth' => FieldType::FIELD_NAMESPACE,
+ 'cache_date' => FieldType::TYPE_INT_UNSIGNED,
+ 'cache_count' => FieldType::TYPE_INT_UNSIGNED
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFetchFields() {
+ return [
+ 'concept_txt' => FieldType::TYPE_BLOB,
+ 'concept_docu' => FieldType::TYPE_BLOB,
+ 'concept_features' => FieldType::FIELD_NAMESPACE,
+ 'concept_size' => FieldType::FIELD_NAMESPACE,
+ 'concept_depth' => FieldType::FIELD_NAMESPACE,
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getWhereConds( DataItem $dataItem ) {
+ return [
+ 'concept_txt' => $dataItem->getConceptQuery(),
+ 'concept_docu' => $dataItem->getDocumentation(),
+ 'concept_features' => $dataItem->getQueryFeatures(),
+ 'concept_size' => $dataItem->getSize(),
+ 'concept_depth' => $dataItem->getDepth()
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getInsertValues( DataItem $dataItem ) {
+ return [
+ 'concept_txt' => $dataItem->getConceptQuery(),
+ 'concept_docu' => $dataItem->getDocumentation(),
+ 'concept_features' => $dataItem->getQueryFeatures(),
+ 'concept_size' => $dataItem->getSize(),
+ 'concept_depth' => $dataItem->getDepth()
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexField() {
+ return 'concept_txt';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getLabelField() {
+ return 'concept_txt';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function dataItemFromDBKeys( $dbkeys ) {
+
+ if ( is_array( $dbkeys) && count( $dbkeys ) == 5 ) {
+ return new DIConcept(
+ $dbkeys[0],
+ smwfXMLContentEncode( $dbkeys[1] ),
+ $dbkeys[2],
+ $dbkeys[3],
+ $dbkeys[4]
+ );
+ }
+
+ throw new DataItemHandlerException( 'Failed to create data item from DB keys.' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIGeoCoordinateHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIGeoCoordinateHandler.php
new file mode 100644
index 00000000..74dcfd07
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIGeoCoordinateHandler.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\DIHandlers;
+
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+use SMWDIGeoCoord as DIGeoCoord;
+
+/**
+ * This class implements store access to DIGeoCoord data items.
+ *
+ * @note The table layout and behavior of this class is not coherent with the
+ * way that other DIs work. This is because of the unfortunate use of the
+ * concept table to store extra cache data, but also due to the design of
+ * concept DIs. This will be cleaned up at some point.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ */
+class DIGeoCoordinateHandler extends DataItemHandler {
+
+ /**
+ * Coordinates have three fields: a string version to keep the
+ * serialized value (exact), and two floating point columns for
+ * latitude and longitude (inexact, useful for bounding box selects).
+ * Altitude is not stored in an extra column since no operation uses
+ * this for anything so far.
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableFields() {
+ return [
+ 'o_serialized' => FieldType::FIELD_TITLE,
+ 'o_lat' => FieldType::TYPE_DOUBLE,
+ 'o_lon' => FieldType::TYPE_DOUBLE
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFetchFields() {
+ return [
+ 'o_serialized' => FieldType::FIELD_TITLE
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableIndexes() {
+ return [
+ 'p_id,o_serialized',
+ 'o_lat,o_lon'
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getWhereConds( DataItem $dataItem ) {
+ return [
+ 'o_serialized' => $dataItem->getSerialization()
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getInsertValues( DataItem $dataItem ) {
+ return [
+ 'o_serialized' => $dataItem->getSerialization(),
+ 'o_lat' => (string)$dataItem->getLatitude(),
+ 'o_lon' => (string)$dataItem->getLongitude()
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexField() {
+ return 'o_serialized';
+ }
+
+ /**
+ * Coordinates do not have a general string version that
+ * could be used for string search, so this method returns
+ * no label column (empty string).
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getLabelField() {
+ return '';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function dataItemFromDBKeys( $dbkeys ) {
+
+ if ( is_string( $dbkeys ) ) {
+ return DIGeoCoord::doUnserialize( $dbkeys );
+ }
+
+ throw new DataItemHandlerException( 'Failed to create data item from DB keys.' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DINumberHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DINumberHandler.php
new file mode 100644
index 00000000..11a952aa
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DINumberHandler.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\DIHandlers;
+
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+use SMWDINumber as DINumber;
+
+/**
+ * This class implements Store access to Number data items.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ */
+class DINumberHandler extends DataItemHandler {
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableFields() {
+ return [
+ 'o_serialized' => FieldType::FIELD_TITLE,
+ 'o_sortkey' => FieldType::TYPE_DOUBLE
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFetchFields() {
+ return [
+ 'o_serialized' => FieldType::FIELD_TITLE
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableIndexes() {
+ return [
+
+ // API module pvalue lookup
+ 'p_id,o_serialized',
+ 'p_id,o_sortkey',
+
+ // QueryEngine::getInstanceQueryResult
+ 's_id,p_id,o_sortkey',
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexHint( $key ) {
+
+ // Store::getPropertySubjects has seen to choose the wrong index
+
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids` INNER JOIN `smw_di_number` AS t1 FORCE INDEX(s_id) ON t1.s_id=smw_id
+ // WHERE t1.p_id='310194' AND smw_iw!=':smw' AND smw_iw!=':smw-delete' AND smw_iw!=':smw-redi'
+ // GROUP BY smw_sort, smw_id
+ // LIMIT 26
+ //
+ // 584.9450ms SMWSQLStore3Readers::getPropertySubjects
+ //
+ // vs.
+ //
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids`
+ // INNER JOIN `smw_di_number` AS t1 ON t1.s_id=smw_id
+ // WHERE t1.p_id='310194' AND smw_iw!=':smw' AND smw_iw!=':smw-delete' AND smw_iw!=':smw-redi'
+ // GROUP BY smw_sort, smw_id
+ // LIMIT 26
+ //
+ // 21448.2622ms SMWSQLStore3Readers::getPropertySubjects
+ if ( 'property.subjects' && $this->isDbType( 'mysql' ) ) {
+ return 's_id';
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getWhereConds( DataItem $dataItem ) {
+ return [
+ 'o_sortkey' => floatval( $dataItem->getNumber() )
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getInsertValues( DataItem $dataItem ) {
+ return [
+ 'o_serialized' => $dataItem->getSerialization(),
+ 'o_sortkey' => floatval( $dataItem->getNumber() )
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexField() {
+ return 'o_sortkey';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getLabelField() {
+ return 'o_serialized';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function dataItemFromDBKeys( $dbkeys ) {
+
+ if ( is_string( $dbkeys ) ) {
+ return DINumber::doUnserialize( $dbkeys );
+ }
+
+ throw new DataItemHandlerException( 'Failed to create data item from DB keys.' );
+ }
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DITimeHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DITimeHandler.php
new file mode 100644
index 00000000..f9805c24
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DITimeHandler.php
@@ -0,0 +1,136 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\DIHandlers;
+
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+use SMWDITime as DITime;
+
+/**
+ * This class implements Store access to Time data items.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ */
+class DITimeHandler extends DataItemHandler {
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableFields() {
+ return [
+ 'o_serialized' => FieldType::FIELD_TITLE,
+ 'o_sortkey' => FieldType::TYPE_DOUBLE
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFetchFields() {
+ return [
+ 'o_serialized' => FieldType::FIELD_TITLE
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableIndexes() {
+ return [
+
+ // API module pvalue lookup
+ 'p_id,o_serialized',
+ 'p_id,o_sortkey',
+
+ // SMWSQLStore3Readers::fetchSemanticData
+ // SELECT p.smw_title as prop,o_serialized AS v0, o_sortkey AS v2
+ // FROM `smw_di_time` INNER JOIN `smw_object_ids` AS p ON
+ // p_id=p.smw_id WHERE s_id='104822' 7.9291ms
+ // ... FROM `smw_fpt_sobj` INNER JOIN `smw_object_ids` AS o0 ON
+ // o_id=o0.smw_id WHERE s_id='104322'
+ 's_id,p_id,o_sortkey,o_serialized',
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexHint( $key ) {
+
+ if ( 'property.subjects' && $this->isDbType( 'mysql' ) ) {
+ return 's_id';
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getWhereConds( DataItem $dataItem ) {
+ return [ 'o_sortkey' => $dataItem->getSortKey() ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getInsertValues( DataItem $dataItem ) {
+ return [
+ 'o_serialized' => $dataItem->getSerialization(),
+ 'o_sortkey' => $dataItem->getSortKey()
+ ];
+ }
+
+ /**
+ * This type is sorted by a numerical sortkey that maps time values to
+ * a time line.
+ *
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexField() {
+ return 'o_sortkey';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getLabelField() {
+ return 'o_serialized';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function dataItemFromDBKeys( $dbkeys ) {
+
+ if ( is_string( $dbkeys ) ) {
+ return DITime::doUnserialize( $dbkeys );
+ }
+
+ throw new DataItemHandlerException( 'Failed to create data item from DB keys.' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIUriHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIUriHandler.php
new file mode 100644
index 00000000..f9269b3b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIUriHandler.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\DIHandlers;
+
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+use SMWDIUri as DIUri;
+
+/**
+ * This class implements Store access to Uri data items.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ */
+class DIUriHandler extends DataItemHandler {
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableFields() {
+ return [
+ 'o_blob' => FieldType::TYPE_BLOB,
+ 'o_serialized' => $this->getCharFieldType()
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFetchFields() {
+ return [
+ 'o_blob' => FieldType::TYPE_BLOB,
+ 'o_serialized' => $this->getCharFieldType()
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableIndexes() {
+ return [
+ 'p_id,o_serialized',
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexHint( $key ) {
+
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids`
+ // INNER JOIN `smw_di_uri` AS t1
+ // FORCE INDEX(s_id) ON t1.s_id=smw_id
+ // WHERE t1.p_id='310165' AND smw_iw!=':smw' AND smw_iw!=':smw-delete' AND smw_iw!=':smw-redi'
+ // GROUP BY smw_sort, smw_id LIMIT 26
+ //
+ // 606.8370ms SMWSQLStore3Readers::getPropertySubjects
+ //
+ // vs.
+ //
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids`
+ // INNER JOIN `smw_di_uri` AS t1 ON t1.s_id=smw_id
+ // WHERE t1.p_id='310165' AND smw_iw!=':smw' AND smw_iw!=':smw-delete' AND smw_iw!=':smw-redi'
+ // GROUP BY smw_sort, smw_id LIMIT 26
+ //
+ // 8052.2099ms SMWSQLStore3Readers::getPropertySubjects
+ if ( 'property.subjects' && $this->isDbType( 'mysql' ) ) {
+ return 's_id';
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getWhereConds( DataItem $dataItem ) {
+ return [ 'o_serialized' => rawurldecode( $dataItem->getSerialization() ) ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getInsertValues( DataItem $dataItem ) {
+
+ $serialization = rawurldecode( $dataItem->getSerialization() );
+ $text = mb_strlen( $serialization ) <= $this->getMaxLength() ? null : $serialization;
+
+ // bytea type handling
+ if ( $text !== null && $this->isDbType( 'postgres' ) ) {
+ $text = pg_escape_bytea( $text );
+ }
+
+ return [
+ 'o_blob' => $text,
+ 'o_serialized' => $serialization,
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexField() {
+ return 'o_serialized';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getLabelField() {
+ return 'o_serialized';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function dataItemFromDBKeys( $dbkeys ) {
+
+ if ( !is_array( $dbkeys ) || count( $dbkeys ) != 2 ) {
+ throw new DataItemHandlerException( 'Failed to create data item from DB keys.' );
+ }
+
+ if ( $this->isDbType( 'postgres' ) ) {
+ $dbkeys[0] = pg_unescape_bytea( $dbkeys[0] );
+ }
+
+ return DIUri::doUnserialize( $dbkeys[0] == '' ? $dbkeys[1] : $dbkeys[0] );
+ }
+
+ private function getMaxLength() {
+
+ $length = 255;
+
+ if ( $this->isEnabledFeature( SMW_FIELDT_CHAR_LONG ) ) {
+ $length = FieldType::CHAR_LONG_LENGTH;
+ }
+
+ return $length;
+ }
+
+ private function getCharFieldType() {
+
+ $fieldType = FieldType::FIELD_TITLE;
+
+ if ( $this->isEnabledFeature( SMW_FIELDT_CHAR_NOCASE ) ) {
+ $fieldType = FieldType::TYPE_CHAR_NOCASE;
+ }
+
+ if ( $this->isEnabledFeature( SMW_FIELDT_CHAR_LONG ) ) {
+ $fieldType = FieldType::TYPE_CHAR_LONG;
+ }
+
+ if ( $this->isEnabledFeature( SMW_FIELDT_CHAR_LONG ) && $this->isEnabledFeature( SMW_FIELDT_CHAR_NOCASE ) ) {
+ $fieldType = FieldType::TYPE_CHAR_LONG_NOCASE;
+ }
+
+ return $fieldType;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIWikiPageHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIWikiPageHandler.php
new file mode 100644
index 00000000..dcad8b7f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DIHandlers/DIWikiPageHandler.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\DIHandlers;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\Exception\PredefinedPropertyLabelMismatchException;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+
+/**
+ * DataItemHandler for dataitems of type DIWikiPage.
+ *
+ * This handler is slightly different from other handlers since wikipages are
+ * stored in a separate table and referred to by numeric IDs. The handler thus
+ * returns IDs in most cases, but expects data from the SMW IDs table (with
+ * DBkey, namespace, interwiki, subobjectname) to be given for creating new
+ * dataitems. The store recognizes this special behavior from the field type
+ * 'p' that the handler reports for its only data field.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ * @author Markus Kroetzsch
+ */
+class DIWikiPageHandler extends DataItemHandler {
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableFields() {
+ return [ 'o_id' => FieldType::FIELD_ID ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getFetchFields() {
+ return [ 'o_id' => FieldType::FIELD_ID ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getTableIndexes() {
+ return [
+ 'o_id',
+
+ // SMWSQLStore3Readers::getPropertySubjects
+ 'p_id,s_id',
+
+ // SMWSQLStore3Readers::fetchSemanticData
+ // ... FROM `smw_fpt_sobj` INNER JOIN `smw_object_ids` AS o0 ON
+ // o_id=o0.smw_id WHERE s_id='104322'
+ 's_id,o_id',
+
+ // SMWSQLStore3Readers::fetchSemanticData
+ // ... FROM `smw_di_wikipage` INNER JOIN `smw_object_ids` AS p ON
+ // p_id=p.smw_id INNER JOIN `smw_object_ids` AS o0 ON o_id=o0.smw_id
+ // WHERE s_id='104815'
+ 's_id,p_id,o_id',
+
+ // QueryEngine::getInstanceQueryResult
+ // ... INNER JOIN `smw_fpt_inst` AS t3 ON t2.smw_id=t3.s_id WHERE
+ // (t2.smw_namespace='0' AND (t3.o_id='56')
+ 'o_id,s_id',
+
+ // QueryEngine::getInstanceQueryResult
+ //'p_id,o_id,s_sort',
+
+ // SMWSQLStore3Readers::getPropertySubjects
+ // SELECT DISTINCT s_id FROM `smw_fpt_sobj` ORDER BY s_sort
+ //'s_sort,s_id',
+
+ // SELECT DISTINCT s_id FROM `smw_fpt_subp` WHERE o_id='96' ORDER BY s_sort
+ //'o_id,s_sort,s_id',
+
+ // In-property lookup
+ 'o_id,p_id',
+ //'o_id,p_id,s_sort',
+
+ // SMWSQLStore3Readers::getPropertySubjects
+ // SELECT DISTINCT s_id FROM `smw_di_wikipage` WHERE (p_id='64' AND o_id='104') ORDER BY s_sort ASC
+ //'o_id,p_id,s_id,s_sort'
+ ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getWhereConds( DataItem $dataItem ) {
+
+ $oid = $this->store->getObjectIds()->getSMWPageID(
+ $dataItem->getDBkey(),
+ $dataItem->getNamespace(),
+ $dataItem->getInterwiki(),
+ $dataItem->getSubobjectName()
+ );
+
+ return [ 'o_id' => $oid ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getInsertValues( DataItem $dataItem ) {
+
+ $oid = $this->store->getObjectIds()->makeSMWPageID(
+ $dataItem->getDBkey(),
+ $dataItem->getNamespace(),
+ $dataItem->getInterwiki(),
+ $dataItem->getSubobjectName()
+ );
+
+ return [ 'o_id' => $oid ];
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getIndexField() {
+ return 'o_id';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function getLabelField() {
+ return 'o_id';
+ }
+
+ /**
+ * @since 1.8
+ *
+ * {@inheritDoc}
+ */
+ public function dataItemFromDBKeys( $dbkeys ) {
+
+ if ( !is_array( $dbkeys ) || count( $dbkeys ) != 5 ) {
+ throw new DataItemHandlerException( 'Failed to create data item from DB keys.' );
+ }
+
+ $namespace = intval( $dbkeys[1] );
+
+ // Correctly interpret internal property keys
+ if ( $namespace == SMW_NS_PROPERTY && $dbkeys[0] != '' &&
+ $dbkeys[0]{0} == '_' && $dbkeys[2] == '' ) {
+
+ try {
+ $property = new DIProperty( $dbkeys[0] );
+ } catch( PredefinedPropertyLabelMismatchException $e ) {
+ // Most likely an outdated, no longer existing predefined
+ // property, mark it as outdate
+ $dbkeys[2] = SMW_SQL3_SMWIW_OUTDATED;
+
+ return $this->newDiWikiPage( $dbkeys );
+ }
+
+ $wikipage = $property->getCanonicalDiWikiPage( $dbkeys[4] );
+
+ if ( !is_null( $wikipage ) ) {
+ return $wikipage;
+ }
+ }
+
+ return $this->newDiWikiPage( $dbkeys );
+ }
+
+ private function newDiWikiPage( $dbkeys ) {
+
+ $diWikiPage = new DIWikiPage(
+ $dbkeys[0],
+ intval( $dbkeys[1] ),
+ $dbkeys[2],
+ $dbkeys[4]
+ );
+
+ $diWikiPage->setSortKey( $dbkeys[3] );
+
+ return $diWikiPage;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DataItemHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DataItemHandler.php
new file mode 100644
index 00000000..6293807b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DataItemHandler.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use SMW\SQLStore\SQLStore;
+use SMWDataItem as DataItem;
+
+/**
+ * Classes extending this represent all store layout that is known about a certain dataitem
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ */
+abstract class DataItemHandler {
+
+ /**
+ * @var SQLStore
+ */
+ protected $store;
+
+ /**
+ * @var integer
+ */
+ protected $fieldTypeFeatures = false;
+
+ /**
+ * @var null|string
+ */
+ private $dbType;
+
+ /**
+ * @since 1.8
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $fieldTypeFeatures
+ */
+ public function setFieldTypeFeatures( $fieldTypeFeatures ) {
+ $this->fieldTypeFeatures = $fieldTypeFeatures;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $feature
+ *
+ * @return boolean
+ */
+ public function isEnabledFeature( $feature ) {
+ return ( (int)$this->fieldTypeFeatures & $feature ) != 0;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean
+ */
+ public function isDbType( $dbType ) {
+
+ if ( $this->dbType === null ) {
+ $this->dbType = $this->store->getConnection( 'mw.db' )->getType();
+ }
+
+ return $this->dbType === $dbType;
+ }
+
+ /**
+ * Return array of fields for a DI type.
+ *
+ * Tables declare value columns ("object fields") by specifying their
+ * name and type. Types are given using letters:
+ * - t for strings of the same maximal length as MediaWiki title names,
+ * - l for arbitrarily long strings; searching/sorting with such data
+ * may be limited for performance reasons,
+ * - w for strings as used in MediaWiki for encoding interwiki prefixes
+ * - n for namespace numbers (or other similar integers)
+ * - f for floating point numbers of double precision
+ * - p for a reference to an SMW ID as stored in the SMW IDs table;
+ * this corresponds to a data entry of ID "tnwt".
+ *
+ * @since 1.8
+ * @return array
+ */
+ abstract public function getTableFields();
+
+ /**
+ * Return an array with all the field names and types that need to be
+ * retrieved from the database in order to create a dataitem using
+ * dataItemFromDBKeys(). The result format is the same as for
+ * getTableFields(), but usually with fewer field names.
+ *
+ * @note In the future, we will most likely use a method that return
+ * only a single field name. Currently, we still need an array for
+ * concepts.
+ *
+ * @since 1.8
+ * @return array
+ */
+ abstract public function getFetchFields();
+
+ /**
+ * Return an array of additional indexes that should be provided for
+ * the table using this DI handler. By default, SMWSQLStore3 will
+ * already create indexes for all standard select operations, based
+ * on the indexfield provided by getIndexField(). Hence, most handlers
+ * do not need to define any indexes.
+ *
+ * @since 1.8
+ * @return array
+ */
+ public function getTableIndexes() {
+ return [];
+ }
+
+ /**
+ * Provides a possibility to return a specific index hint for a domain.
+ *
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return string
+ */
+ public function getIndexHint( $key ) {
+ return '';
+ }
+
+ /**
+ * Return an array of fields=>values to conditions (WHERE part) in SQL
+ * queries for the given DataItem. This method can return fewer
+ * fields than getInstertValues as long as they are enough to identify
+ * an item for search.
+ *
+ * @since 1.8
+ * @param DataItem $dataItem
+ * @return array
+ */
+ abstract public function getWhereConds( DataItem $dataItem );
+
+ /**
+ * Return an array of fields=>values that is to be inserted when
+ * writing the given DataItem to the database. Values should be set
+ * for all columns, even if NULL. This array is used to perform all
+ * insert operations into the DB.
+ *
+ * @since 1.8
+ * @param DataItem $dataItem
+ * @return array
+ */
+ abstract public function getInsertValues( DataItem $dataItem );
+
+ /**
+ * Return the field used to select this type of DataItem. In
+ * particular, this identifies the column that is used to sort values
+ * of this kind. Every type of data returns a non-empty string here.
+ *
+ * @since 1.8
+ * @return string
+ */
+ abstract public function getIndexField();
+
+ /**
+ * Return the label field for this type of DataItem. This should be
+ * a string column in the database table that can be used for selecting
+ * values using criteria such as "starts with". The return value can be
+ * empty if this is not supported. This is preferred for DataItem
+ * classes that do not have an obvious canonical string writing anyway.
+ *
+ * The return value can be a column name or the empty string (if the
+ * give type of DataItem does not have a label field).
+ *
+ * @since 1.8
+ * @return string
+ */
+ abstract public function getLabelField();
+
+ /**
+ * Returns the expected sort field.
+ *
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getSortField() {
+ return '';
+ }
+
+ /**
+ * Create a dataitem from an array of DB keys or a single DB key
+ * string. May throw an DataItemException if the given DB keys
+ * cannot be converted back into a dataitem. Each implementation
+ * of this method must otherwise run without errors for both array
+ * and string inputs.
+ *
+ * @since 1.8
+ * @param array|string $dbkeys
+ * @throws DataItemException
+ * @return DataItem
+ */
+ abstract public function dataItemFromDBKeys( $dbkeys );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DataItemHandlerDispatcher.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DataItemHandlerDispatcher.php
new file mode 100644
index 00000000..b17ce0cb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/DataItemHandlerDispatcher.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use SMW\SQLStore\EntityStore\DIHandlers\DIBlobHandler;
+use SMW\SQLStore\EntityStore\DIHandlers\DIBooleanHandler;
+use SMW\SQLStore\EntityStore\DIHandlers\DIConceptHandler;
+use SMW\SQLStore\EntityStore\DIHandlers\DIGeoCoordinateHandler;
+use SMW\SQLStore\EntityStore\DIHandlers\DINumberHandler;
+use SMW\SQLStore\EntityStore\DIHandlers\DITimeHandler;
+use SMW\SQLStore\EntityStore\DIHandlers\DIUriHandler;
+use SMW\SQLStore\EntityStore\DIHandlers\DIWikiPageHandler;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\SQLStore\SQLStore;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DataItemHandlerDispatcher {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var array
+ */
+ private $handlers = [];
+
+ /**
+ * @var integer
+ */
+ private $fieldTypeFeatures = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $fieldTypeFeatures
+ */
+ public function setFieldTypeFeatures( $fieldTypeFeatures ) {
+ $this->fieldTypeFeatures = $fieldTypeFeatures;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $type
+ *
+ * @return DIHandler
+ * @throws RuntimeException
+ */
+ public function getHandlerByType( $type ) {
+
+ if ( !isset( $this->handlers[$type] ) ) {
+ $this->handlers[$type] = $this->newHandlerByType( $type );
+ }
+
+ // $this->handlers[$type]->setFieldTypeFeatures(
+ // $this->fieldTypeFeatures
+ // );
+
+ return $this->handlers[$type];
+ }
+
+ private function newHandlerByType( $type ) {
+
+ switch ( $type ) {
+ case DataItem::TYPE_NUMBER:
+ $handler = new DINumberHandler( $this->store );
+ break;
+ case DataItem::TYPE_BLOB:
+ $handler = new DIBlobHandler( $this->store );
+ break;
+ case DataItem::TYPE_BOOLEAN:
+ $handler = new DIBooleanHandler( $this->store );
+ break;
+ case DataItem::TYPE_URI:
+ $handler = new DIUriHandler( $this->store );
+ break;
+ case DataItem::TYPE_TIME:
+ $handler = new DITimeHandler( $this->store );
+ break;
+ case DataItem::TYPE_GEO:
+ $handler = new DIGeoCoordinateHandler( $this->store );
+ break;
+ case DataItem::TYPE_WIKIPAGE:
+ $handler = new DIWikiPageHandler( $this->store );
+ break;
+ case DataItem::TYPE_CONCEPT:
+ $handler = new DIConceptHandler( $this->store );
+ break;
+ case DataItem::TYPE_PROPERTY:
+ throw new DataItemHandlerException( "There is no DI handler for DataItem::TYPE_PROPERTY." );
+ case DataItem::TYPE_CONTAINER:
+ throw new DataItemHandlerException( "There is no DI handler for DataItem::TYPE_CONTAINER." );
+ case DataItem::TYPE_ERROR:
+ throw new DataItemHandlerException( "There is no DI handler for DataItem::TYPE_ERROR." );
+ default:
+ throw new DataItemHandlerException( "The value \"$type\" is not a valid dataitem ID." );
+ }
+
+ $handler->setFieldTypeFeatures(
+ $this->fieldTypeFeatures
+ );
+
+ return $handler;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/Exception/DataItemHandlerException.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/Exception/DataItemHandlerException.php
new file mode 100644
index 00000000..514bef65
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/Exception/DataItemHandlerException.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DataItemHandlerException extends RuntimeException {
+
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdCacheManager.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdCacheManager.php
new file mode 100644
index 00000000..ca02bafb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdCacheManager.php
@@ -0,0 +1,226 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use RuntimeException;
+use SMW\DIWikiPage;
+use SMW\SQLStore\SQLStore;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class IdCacheManager {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param array $caches
+ */
+ public function __construct( array $caches ) {
+ $this->caches = $caches;
+
+ if ( !isset( $this->caches['entity.id'] ) ) {
+ throw new RuntimeException( "Missing 'entity.id' instance.");
+ }
+
+ if ( !isset( $this->caches['entity.sort'] ) ) {
+ throw new RuntimeException( "Missing 'entity.sort' instance.");
+ }
+
+ if ( !isset( $this->caches['entity.lookup'] ) ) {
+ throw new RuntimeException( "Missing 'entity.lookup' instance.");
+ }
+
+ if ( !isset( $this->caches['table.hash'] ) ) {
+ throw new RuntimeException( "Missing 'table.hash' instance.");
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|array $args
+ *
+ * @return string
+ */
+ public static function computeSha1( $args = '' ) {
+ return sha1( json_encode( $args ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return boolean
+ */
+ public function get( $key ) {
+
+ if ( !isset( $this->caches[$key] ) ) {
+ throw new RuntimeException( "$key is unknown");
+ }
+
+ return $this->caches[$key];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $hash
+ *
+ * @return boolean
+ */
+ public function hasCache( $hash ) {
+
+ if ( !is_string( $hash ) ) {
+ return false;
+ }
+
+ return $this->caches['entity.id']->contains( $hash );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @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 ) {
+
+ if ( strpos( $title, ' ' ) !== false ) {
+ throw new RuntimeException( "Somebody tried to use spaces in a cache title! ($title)");
+ }
+
+ $hash = $this->computeSha1(
+ [ $title, (int)$namespace, $interwiki, $subobject ]
+ );
+
+ $this->caches['entity.id']->save( $hash, $id );
+ $this->caches['entity.sort']->save( $hash, $sortkey );
+
+ $dataItem = new DIWikiPage( $title, $namespace, $interwiki, $subobject );
+ $dataItem->setId( $id );
+ $dataItem->setSortKey( $sortkey );
+
+ $this->caches['entity.lookup']->save( $id, $dataItem );
+
+ // Speed up detection of redirects when fetching IDs
+ if ( $interwiki == SMW_SQL3_SMWREDIIW ) {
+ $this->setCache( $title, $namespace, '', $subobject, 0, '' );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $title
+ * @param integer $namespace
+ * @param string $interwiki
+ * @param string $subobject
+ */
+ public function deleteCache( $title, $namespace, $interwiki, $subobject ) {
+
+ $hash = $this->computeSha1(
+ [ $title, (int)$namespace, $interwiki, $subobject ]
+ );
+
+ $this->caches['entity.id']->delete( $hash );
+ $this->caches['entity.sort']->delete( $hash );
+
+ if ( ( $id = $this->caches['entity.id']->fetch( $hash ) ) !== false ) {
+ $this->caches['entity.lookup']->delete( $id );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ */
+ public function deleteCacheById( $id ) {
+
+ $dataItem = $this->caches['entity.lookup']->fetch( $id );
+
+ if ( !$dataItem instanceof DIWikiPage ) {
+ return;
+ }
+
+ $hash = $this->computeSha1(
+ [
+ $dataItem->getDBKey(),
+ (int)$dataItem->getNamespace(),
+ $dataItem->getInterwiki(),
+ $dataItem->getSubobjectName()
+ ]
+ );
+
+ $this->caches['entity.id']->delete( $hash );
+ $this->caches['entity.sort']->delete( $hash );
+ $this->caches['entity.lookup']->delete( $id );
+ }
+
+ /**
+ * Get a cached SMW ID, or false if no cache entry is found.
+ *
+ * @since 3.0
+ *
+ * @param DIWikiPage|array $args
+ *
+ * @return integer|boolean
+ */
+ public function getId( $args ) {
+
+ if ( $args instanceof DIWikiPage ) {
+ $args = [
+ $args->getDBKey(),
+ (int)$args->getNamespace(),
+ $args->getInterwiki(),
+ $args->getSubobjectName()
+ ];
+ }
+
+ $hash = $this->computeSha1( $args );
+
+ if ( ( $id = $this->caches['entity.id']->fetch( $hash ) ) !== false ) {
+ return (int)$id;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a cached SMW sortkey, or false if no cache entry is found.
+ *
+ * @since 3.0
+ *
+ * @param string $title
+ * @param integer $namespace
+ * @param string $interwiki
+ * @param string $subobject
+ *
+ * @return string|boolean
+ */
+ public function getSort( $args ) {
+
+ $hash = $this->computeSha1( $args );
+
+ if ( ( $sort = $this->caches['entity.sort']->fetch( $hash ) ) !== false ) {
+ return $sort;
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdChanger.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdChanger.php
new file mode 100644
index 00000000..3823a906
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdChanger.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use RuntimeException;
+use SMW\SQLStore\SQLStore;
+use SMW\SQLStore\TableBuilder\FieldType;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class IdChanger {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * Change an SMW page id across all relevant tables. The redirect table
+ * is also updated (without much effect if the change happended due to
+ * some redirect, since the table should not contain the id of the
+ * redirected page). If namespaces are given, then they are used to
+ * delete any entries that are limited to one particular namespace (e.g.
+ * only properties can be used as properties) instead of moving them.
+ *
+ * The id in the SMW IDs table is not touched.
+ *
+ * @note This method only changes internal page IDs in SMW. It does not
+ * assume any change in (title-related) data, as e.g. in a page move.
+ * Internal objects (subobject) do not need to be updated since they
+ * refer to the title of their parent page, not to its ID.
+ *
+ * @since 1.8
+ *
+ * @param integer $old_id numeric ID that is to be changed
+ * @param integer $new_id numeric ID to which the records are to be changed
+ * @param integer $old_ns namespace of old id's page (-1 to ignore it)
+ * @param integer $new_ns namespace of new id's page (-1 to ignore it)
+ * @param boolean $s_data stating whether to update subject references
+ * @param boolean $po_data stating if to update property/object references
+ */
+ public function change( $old_id, $new_id, $old_ns = -1, $new_ns = -1, $s_data = true, $po_data = true ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ // Change all id entries in property tables:
+ foreach ( $this->store->getPropertyTables() as $proptable ) {
+
+ if ( $s_data && $proptable->usesIdSubject() ) {
+ $connection->update(
+ $proptable->getName(),
+ [ 's_id' => $new_id ],
+ [ 's_id' => $old_id ],
+ __METHOD__
+ );
+ }
+
+ if ( $po_data ) {
+ if ( ( ( $old_ns == -1 ) || ( $old_ns == SMW_NS_PROPERTY ) ) && ( !$proptable->isFixedPropertyTable() ) ) {
+ if ( ( $new_ns == -1 ) || ( $new_ns == SMW_NS_PROPERTY ) ) {
+ $connection->update(
+ $proptable->getName(),
+ [ 'p_id' => $new_id ],
+ [ 'p_id' => $old_id ],
+ __METHOD__
+ );
+ } else {
+ $connection->delete(
+ $proptable->getName(),
+ [ 'p_id' => $old_id ],
+ __METHOD__
+ );
+ }
+ }
+
+ foreach ( $proptable->getFields( $this->store ) as $fieldName => $fieldType ) {
+ if ( $fieldType === FieldType::FIELD_ID ) {
+ $connection->update(
+ $proptable->getName(),
+ [ $fieldName => $new_id ],
+ [ $fieldName => $old_id ],
+ __METHOD__
+ );
+ }
+ }
+ }
+ }
+
+ $this->update_concept( $old_id, $new_id, $old_ns, $new_ns, $s_data, $po_data );
+ }
+
+ private function update_concept( $old_id, $new_id, $old_ns, $new_ns, $s_data, $po_data ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ if ( $s_data && ( ( $old_ns == -1 ) || ( $old_ns == SMW_NS_CONCEPT ) ) ) {
+ if ( ( $new_ns == -1 ) || ( $new_ns == SMW_NS_CONCEPT ) ) {
+ $connection->update(
+ SQLStore::CONCEPT_TABLE,
+ [ 's_id' => $new_id ],
+ [ 's_id' => $old_id ],
+ __METHOD__
+ );
+
+ $connection->update(
+ SQLStore::CONCEPT_CACHE_TABLE,
+ [ 's_id' => $new_id ],
+ [ 's_id' => $old_id ],
+ __METHOD__
+ );
+ } else {
+ $connection->delete(
+ SQLStore::CONCEPT_TABLE,
+ [ 's_id' => $old_id ],
+ __METHOD__
+ );
+
+ $connection->delete(
+ SQLStore::CONCEPT_CACHE_TABLE,
+ [ 's_id' => $old_id ],
+ __METHOD__
+ );
+ }
+ }
+
+ if ( $po_data ) {
+ $connection->update(
+ SQLStore::CONCEPT_CACHE_TABLE,
+ [ 'o_id' => $new_id ],
+ [ 'o_id' => $old_id ],
+ __METHOD__
+ );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdEntityFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdEntityFinder.php
new file mode 100644
index 00000000..f081f27d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/IdEntityFinder.php
@@ -0,0 +1,202 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use Onoi\Cache\Cache;
+use SMW\DIWikiPage;
+use SMW\IteratorFactory;
+use SMW\RequestOptions;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class IdEntityFinder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var IteratorFactory
+ */
+ private $iteratorFactory;
+
+ /**
+ * @var IdCacheManager
+ */
+ private $idCacheManager;
+
+ /**
+ * @since 2.1
+ *
+ * @param Store $store
+ * @param IteratorFactory $iteratorFactory
+ * @param IdCacheManager $idCacheManager
+ */
+ public function __construct( Store $store, IteratorFactory $iteratorFactory, IdCacheManager $idCacheManager ) {
+ $this->store = $store;
+ $this->iteratorFactory = $iteratorFactory;
+ $this->idCacheManager = $idCacheManager;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param array $idList
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return DIWikiPage[]
+ */
+ public function getDataItemsFromList( array $idList, RequestOptions $requestOptions = null ) {
+
+ if ( $idList === [] ) {
+ return [];
+ }
+
+ $conditions = [
+ 'smw_id' => $idList,
+ ];
+
+ if ( $requestOptions !== null ) {
+ foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
+ $conditions[] = $extraCondition;
+ }
+ }
+
+ $rows = $this->fetchFromTable(
+ $conditions
+ );
+
+ if ( $rows === false ) {
+ return [];
+ }
+
+ return $this->iteratorFactory->newMappingIterator(
+ $this->iteratorFactory->newResultIterator( $rows ),
+ [ $this, 'newFromRow' ]
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param stdClass $row
+ *
+ * @return DIWikiPage
+ */
+ public function newFromRow( $row ) {
+
+ $dataItem = new DIWikiPage(
+ $row->smw_title,
+ $row->smw_namespace,
+ $row->smw_iw,
+ $row->smw_subobject
+ );
+
+ $dataItem->setId( $row->smw_id );
+
+ if ( isset( $row->smw_sortkey ) ) {
+ $dataItem->setSortKey( $row->smw_sortkey );
+ }
+
+ if ( isset( $row->smw_sort ) ) {
+ $dataItem->setOption( 'sort', $row->smw_sort );
+ }
+
+ if ( !$this->idCacheManager->hasCache( $row->smw_hash ) ) {
+ $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
+ );
+ }
+
+ return $dataItem;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param integer $id
+ *
+ * @return DIWikiPage|null
+ */
+ public function getDataItemById( $id ) {
+
+ if ( ( $dataItem = $this->get( (int)$id ) ) !== false ) {
+ return $dataItem;
+ }
+
+ return null;
+ }
+
+ private function get( $id ) {
+
+ $cache = $this->idCacheManager->get( 'entity.lookup' );
+
+ if ( ( $dataItem = $cache->fetch( $id ) ) !== false ) {
+ return $dataItem;
+ }
+
+ $rows = $this->fetchFromTable(
+ [ 'smw_id' => $id ],
+ [ 'LIMIT' => 1 ]
+ );
+
+ if ( $rows === false ) {
+ return false;
+ }
+
+ foreach ( $rows as $row ) {
+
+ if ( !isset( $row->smw_title ) ) {
+ continue;
+ }
+
+ if ( $row->smw_title !== '' && $row->smw_title{0} === '_' && (int)$row->smw_namespace === SMW_NS_PROPERTY ) {
+ // $row->smw_title = str_replace( ' ', '_', PropertyRegistry::getInstance()->findPropertyLabelById( $row->smw_title ) );
+ }
+
+ $row->smw_id = $id;
+ $dataItem = $this->newFromRow( $row );
+ }
+
+ $cache->save( $id, $dataItem );
+
+ return $dataItem;
+ }
+
+ private function fetchFromTable( $conditions, $options = [] ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ return $connection->select(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_id',
+ 'smw_title',
+ 'smw_namespace',
+ 'smw_iw',
+ 'smw_subobject',
+ 'smw_sortkey',
+ 'smw_sort',
+ 'smw_hash'
+ ],
+ $conditions,
+ __METHOD__,
+ $options
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/NativeEntityLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/NativeEntityLookup.php
new file mode 100644
index 00000000..36a335f6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/NativeEntityLookup.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\EntityLookup;
+use SMW\RequestOptions;
+use SMW\SQLStore\SQLStore;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class NativeEntityLookup implements EntityLookup {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @since 2.5
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @see Store::getSemanticData
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getSemanticData( DIWikiPage $subject, $filter = false ) {
+ return $this->store->getReader()->getSemanticData( $subject, $filter );
+ }
+
+ /**
+ * @see Store::getProperties
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getProperties( DIWikiPage $subject, RequestOptions $requestOptions = null ) {
+ return $this->store->getReader()->getProperties( $subject, $requestOptions );
+ }
+
+ /**
+ * @see Store::getPropertyValues
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getPropertyValues( DIWikiPage $subject = null, DIProperty $property, RequestOptions $requestOptions = null ) {
+ return $this->store->getReader()->getPropertyValues( $subject, $property, $requestOptions );
+ }
+
+ /**
+ * @see Store::getPropertySubjects
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getPropertySubjects( DIProperty $property, DataItem $dataItem = null, RequestOptions $requestOptions = null ) {
+ return $this->store->getReader()->getPropertySubjects( $property, $dataItem, $requestOptions );
+ }
+
+ /**
+ * @see Store::getProperties
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getAllPropertySubjects( DIProperty $property, RequestOptions $requestOptions = null ) {
+ return $this->store->getReader()->getAllPropertySubjects( $property, $requestOptions );
+ }
+
+ /**
+ * @see Store::getInProperties
+ *
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getInProperties( DataItem $object, RequestOptions $requestOptions = null ) {
+ return $this->store->getReader()->getInProperties( $object, $requestOptions );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function invalidateCache() {}
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/PropertiesLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/PropertiesLookup.php
new file mode 100644
index 00000000..87d0d748
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/PropertiesLookup.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use SMW\SQLStore\SQLStore;
+use SMW\SQLStore\PropertyTableDefinition as TableDefinition;
+use SMWDataItem as DataItem;
+use SMW\DIWikiPage;
+use SMW\RequestOptions;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PropertiesLookup {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return RequestOptions|null
+ */
+ public function newRequestOptions( RequestOptions $requestOptions = null ) {
+
+ if ( $requestOptions !== null ) {
+ $clone = clone $requestOptions;
+ $clone->limit = $requestOptions->limit + $requestOptions->offset;
+ $clone->offset = 0;
+ } else {
+ $clone = null;
+ }
+
+ return $clone;
+ }
+
+ /**
+ * @see Store::getProperties
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function fetchFromTable( DIWikiPage $subject, TableDefinition $propertyTable, RequestOptions $requestOptions = null ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $query = $connection->newQuery();
+
+ $query->type( 'SELECT' );
+ $query->table( $propertyTable->getName() );
+
+ if ( $propertyTable->usesIdSubject() ) {
+ $query->condition( $query->eq( 's_id', $subject->getId() ) );
+ } elseif ( $subject->getInterwiki() === '' ) {
+ $query->condition( $query->eq( 's_title', $subject->getDBkey() ) );
+ $query->condition( $query->eq( 's_namespace', $subject->getNamespace() ) );
+ } else {
+ // subjects with non-empty interwiki cannot have properties
+ return [];
+ }
+
+ if ( $propertyTable->isFixedPropertyTable() ) {
+ return $this->fetchFromFixedTable( $query, $propertyTable->getFixedProperty() );
+ }
+
+ $query->join(
+ 'INNER JOIN',
+ [ SQLStore::ID_TABLE => "ON smw_id=p_id" ]
+ );
+
+ $query->fields( [ 'smw_title', 'smw_sortkey' ] );
+
+ // (select sortkey since it might be used in ordering (needed by Postgres))
+ $query->condition( $this->store->getSQLConditions(
+ $requestOptions,
+ 'smw_sortkey',
+ 'smw_sortkey'
+ ) );
+
+ $opt = $this->store->getSQLOptions(
+ $requestOptions,
+ 'smw_sortkey'
+ );
+
+ $query->options( $opt + [ 'DISTINCT' => true ] );
+
+ return $query->execute( __METHOD__ );
+ }
+
+ private function fetchFromFixedTable( $query, $title ) {
+
+ // just check if subject occurs in table
+ $query->options(
+ [ 'LIMIT' => 1 ]
+ );
+
+ $query->field( '*' );
+ $res = $query->execute( __METHOD__ );
+
+ if ( $res->numRows() > 0 ) {
+ return [ $title ];
+ }
+
+ return [];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/PropertySubjectsLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/PropertySubjectsLookup.php
new file mode 100644
index 00000000..4866c18b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/PropertySubjectsLookup.php
@@ -0,0 +1,318 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use SMW\SQLStore\SQLStore;
+use SMW\SQLStore\PropertyTableDefinition as TableDefinition;
+use SMWDataItem as DataItem;
+use SMW\DIContainer;
+use SMW\RequestOptions;
+use SMW\Options;
+use SMW\MediaWiki\DatabaseHelper;
+use SMW\ApplicationFactory;
+use SMW\SQLStore\RequestOptionsProc;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PropertySubjectsLookup {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var IteratorFactory
+ */
+ private $iteratorFactory;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var DataItemHandler
+ */
+ private $dataItemHandler;
+
+ /**
+ * @since 3.0
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ $this->iteratorFactory = ApplicationFactory::getInstance()->getIteratorFactory();
+ }
+
+ /**
+ * @see Store::getPropertySubjects
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function fetchFromTable( $pid, TableDefinition $proptable, DataItem $dataItem = null, RequestOptions $requestOptions = null ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $group = false;
+
+ $dataItemHandler = $this->store->getDataItemHandlerForDIType(
+ $proptable->getDiType()
+ );
+
+ $sortField = $dataItemHandler->getSortField();
+ $query = $connection->newQuery();
+ $query->type( 'SELECT' );
+
+ if ( $requestOptions === null ) {
+ $requestOptions = new RequestOptions();
+ } else{
+ // Clone a `RequestOptions` instance so that it can be modified freely
+ // for the current request without a possible interference on an
+ // upcoming request (as in case where it is called from within a loop
+ // with the same initial RequestOptions instance)
+ $requestOptions = clone $requestOptions;
+ }
+
+ if ( $sortField === '' ) {
+ $sortField = 'smw_sort';
+ }
+
+ $index = '';
+
+ // For certain tables (blob) the query planner chooses a suboptimal plan
+ // and causes an unacceptable query time therefore force an index for
+ // those tables where the behaviour has been observed.
+ if ( $dataItemHandler->getIndexHint( 'property.subjects' ) !== '' && $dataItem === null ) {
+
+ // For tables with only a few entries, the index hint seems to create
+ // a disadvantage, yet when the amount reaches a certain level the
+ // index hint becomes necessary to retain an acceptable response
+ // time.
+ //
+ // Table with < 100 entries
+ //
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids` INNER JOIN `smw_di_number` AS t1 ON t1.s_id=smw_id
+ // WHERE (t1.p_id='196959') AND (smw_iw!=':smw') AND (smw_iw!=':smw-delete') AND (smw_iw!=':smw-redi')
+ // GROUP BY smw_sort, smw_id LIMIT 21 8.2510ms (without index hint)
+ //
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids` INNER JOIN `smw_di_number` AS t1 FORCE INDEX(s_id) ON t1.s_id=smw_id
+ // WHERE (t1.p_id='196959') AND (smw_iw!=':smw') AND (smw_iw!=':smw-delete') AND (smw_iw!=':smw-redi')
+ // GROUP BY smw_sort, smw_id LIMIT 21 7548.6171ms (with index hint)
+ //
+ // vs.
+ //
+ // Table with > 5000 entries
+ //
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids` INNER JOIN `smw_di_blob` AS t1 FORCE INDEX(s_id) ON t1.s_id=smw_id
+ // WHERE (t1.p_id='310170') AND (smw_iw!=':smw') AND (smw_iw!=':smw-delete') AND (smw_iw!=':smw-redi')
+ // GROUP BY smw_sort, smw_id LIMIT 21 62.6249ms (with index hint)
+ //
+ // SELECT smw_id, smw_title, smw_namespace, smw_iw, smw_subobject, smw_sortkey, smw_sort
+ // FROM `smw_object_ids` INNER JOIN `smw_di_blob` AS t1 ON t1.s_id=smw_id
+ // WHERE (t1.p_id='310170') AND (smw_iw!=':smw') AND (smw_iw!=':smw-delete') AND (smw_iw!=':smw-redi')
+ // GROUP BY smw_sort, smw_id LIMIT 21 8856.1242ms (without index hint)
+ //
+ $cq = $connection->newQuery();
+ $cq->type( 'SELECT' );
+ $cq->table( SQLStore::PROPERTY_STATISTICS_TABLE );
+ $cq->field( 'usage_count' );
+ $cq->condition( $cq->eq( 'p_id', $pid ) );
+ $res = $cq->execute( __METHOD__ );
+
+ foreach ( $res as $r ) {
+ // 5000? It just showed to be a sweet spot while doing some
+ // exploratory queries
+ if ( $r->usage_count > 5000 ) {
+ $index = 'FORCE INDEX(' . $dataItemHandler->getIndexHint( 'property.subjects' ) . ')';
+ }
+ }
+ }
+
+ $result = [];
+
+ if ( $proptable->usesIdSubject() ) {
+ $group = true;
+
+ $query->table( SQLStore::ID_TABLE );
+
+ $query->join(
+ 'INNER JOIN',
+ [ $proptable->getName() => "t1 $index ON t1.s_id=smw_id" ]
+ );
+
+ $query->fields(
+ [
+ 'smw_id',
+ 'smw_title',
+ 'smw_namespace',
+ 'smw_iw',
+ 'smw_subobject',
+ 'smw_sortkey',
+ 'smw_sort'
+ ]
+ );
+
+ } else { // no join needed, title+namespace as given in proptable
+ $query->table( $proptable->getName(), "t1" );
+
+ $query->fields(
+ [
+ 's_title AS smw_title',
+ 's_namespace AS smw_namespace',
+ '\'\' AS smw_iw',
+ '\'\' AS smw_subobject',
+ 's_title AS smw_sortkey',
+ 's_title AS smw_sort'
+ ]
+ );
+
+ $requestOptions->setOption( 'ORDER BY', false );
+ }
+
+ if ( !$proptable->isFixedPropertyTable() ) {
+ $query->condition( $query->eq( "t1.p_id", $pid ) );
+ }
+
+ $this->getWhereConds( $query, $dataItem );
+
+ if ( $requestOptions !== null ) {
+ foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
+ if ( isset( $extraCondition['o_id'] ) ) {
+ $query->condition( $query->eq( 't1.o_id', $extraCondition['o_id'] ) );
+ }
+
+ if ( is_callable( $extraCondition ) ) {
+ $extraCondition( $query );
+ }
+ }
+ }
+
+ if ( $proptable->usesIdSubject() ) {
+ foreach ( [ SMW_SQL3_SMWIW_OUTDATED, SMW_SQL3_SMWDELETEIW, SMW_SQL3_SMWREDIIW ] as $v ) {
+ $query->condition( $query->neq( "smw_iw", $v ) );
+ }
+ }
+
+ if ( $group && $connection->isType( 'postgres') ) {
+ // Avoid a "... 42803 ERROR: column "s....smw_title" must appear in
+ // the GROUP BY clause or be used in an aggregate function ..."
+ // https://stackoverflow.com/questions/1769361/postgresql-group-by-different-from-mysql
+ $requestOptions->setOption( 'DISTINCT', 'ON (smw_sort, smw_id)' );
+ $requestOptions->setOption( 'ORDER BY', false );
+ } elseif ( $group ) {
+ // Using GROUP BY will sort on the field and since we disinguish smw_sort
+ // and the ID at the end of the field, we ensure
+ // the filter duplicates while sorting the list without using DISTINCT which
+ // would cause a filesort
+ // http://www.mysqltutorial.org/mysql-distinct.aspx
+ $requestOptions->setOption( 'GROUP BY', $sortField . ', smw_id' );
+ $requestOptions->setOption( 'ORDER BY', false );
+ } else {
+ $requestOptions->setOption( 'DISTINCT', true );
+ }
+
+ $cond = $this->store->getSQLConditions(
+ $requestOptions,
+ 'smw_sortkey',
+ 'smw_sortkey',
+ false
+ );
+
+ $query->condition( $cond );
+
+ $opts = $this->store->getSQLOptions(
+ $requestOptions,
+ $sortField
+ );
+
+ $query->options( $opts );
+
+ $res = $connection->query(
+ $query,
+ __METHOD__
+ );
+
+ $this->dataItemHandler = $this->store->getDataItemHandlerForDIType(
+ DataItem::TYPE_WIKIPAGE
+ );
+
+ // Return an iterator and avoid resolving the resources directly as it
+ // may contain a large list of possible matches
+ $res = $this->iteratorFactory->newMappingIterator(
+ $this->iteratorFactory->newResultIterator( $res ),
+ [ $this, 'newFromRow' ]
+ );
+
+ return $res;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param stdClass $row
+ *
+ * @return DIWikiPage
+ */
+ public function newFromRow( $row ) {
+
+ try {
+ if ( $row->smw_iw === '' || $row->smw_iw{0} != ':' ) { // filter special objects
+
+ $keys = [
+ $row->smw_title,
+ $row->smw_namespace,
+ $row->smw_iw,
+ $row->smw_sort,
+ $row->smw_subobject
+
+ ];
+
+ $dataItem = $this->dataItemHandler->dataItemFromDBKeys( $keys );
+
+ if ( isset( $row->smw_id ) ) {
+ $dataItem->setId( $row->smw_id );
+ }
+
+ return $dataItem;
+ }
+ } catch ( DataItemHandlerException $e ) {
+ // silently drop data, should be extremely rare and will usually fix itself at next edit
+ }
+
+ $title = ( $row->smw_title !== '' ? $row->smw_title : 'Empty' ) . '/' . $row->smw_namespace;
+
+ // Avoid null return in Iterator
+ return $this->dataItemHandler->dataItemFromDBKeys( [ 'Blankpage/' . $title, NS_SPECIAL, '', '', '' ] );
+ }
+
+ private function getWhereConds( $query, $dataItem ) {
+
+ $conds = '';
+
+ if ( $dataItem instanceof \SMWDIContainer ) {
+ throw new RuntimeException( 'SMWDIContainer support is missing!');
+ }
+
+ if ( $dataItem !== null ) {
+ $dataItemHandler = $this->store->getDataItemHandlerForDIType(
+ $dataItem->getDIType()
+ );
+
+ foreach ( $dataItemHandler->getWhereConds( $dataItem ) as $fieldname => $value ) {
+ $query->condition( $query->eq( "t1.$fieldname", $value ) );
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/SemanticDataLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/SemanticDataLookup.php
new file mode 100644
index 00000000..ca1d95fa
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/SemanticDataLookup.php
@@ -0,0 +1,478 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use Psr\Log\LoggerAwareTrait;
+use RuntimeException;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\RequestOptions;
+use SMW\SemanticData;
+use SMW\SQLStore\PropertyTableDefinition;
+use SMW\SQLStore\SQLStore;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SemanticDataLookup {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param PropertyTableDefinition $propertyTableDef
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return RequestOptions|null
+ */
+ public function newRequestOptions( PropertyTableDefinition $propertyTableDef, DIProperty $property, RequestOptions $requestOptions = null ) {
+
+ if ( $requestOptions === null || !isset( $requestOptions->conditionConstraint ) ) {
+ return null;
+ }
+
+ $ropts = new RequestOptions();
+
+ $ropts->setLimit( $requestOptions->getLimit() );
+ $ropts->setOffset( $requestOptions->getOffset() );
+
+ if ( $propertyTableDef->isFixedPropertyTable() ) {
+ return $ropts;
+ }
+
+ $pid = $this->store->getObjectIds()->getSMWPropertyID(
+ $property
+ );
+
+ if ( $pid > 0 ) {
+ $ropts->addExtraCondition( [ 'p_id' => $pid ] );
+ }
+
+ return $ropts;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage|SemanticData $object
+ *
+ * @return StubSemanticData
+ * @throws RuntimeException
+ */
+ public function newStubSemanticData( $object ) {
+
+ if ( $object instanceof DIWikiPage ) {
+ return new StubSemanticData( $object, $this->store, false );
+ }
+
+ if ( $object instanceof SemanticData ) {
+ return StubSemanticData::newFromSemanticData( $object, $this->store );
+ }
+
+ throw new RuntimeException( 'Expectd either a DIWikiPage or SemanticData object!' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param SemanticData $semanticData
+ *
+ * @return array
+ */
+ public function getTableUsageInfo( SemanticData $semanticData ) {
+ $state = [];
+
+ foreach ( $semanticData->getProperties() as $property ) {
+ $state[$this->store->findPropertyTableID( $property )] = true;
+ }
+
+ return $state;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param DataItem $dataItem
+ * @param PropertyTableDefinition $propTable
+ * @param RequestOptions $requestOptions
+ *
+ * @return SemanticData
+ */
+ public function getSemanticData( $id, DataItem $dataItem = null, PropertyTableDefinition $propTable, RequestOptions $requestOptions = null ) {
+
+ if ( !$dataItem instanceof DIWikiPage ) {
+ throw new RuntimeException( 'Expected a DIWikiPage instance' );
+ }
+
+ $stubSemanticData = $this->newStubSemanticData( $dataItem );
+
+ $data = $this->fetchSemanticData(
+ $id,
+ $dataItem,
+ $propTable,
+ $requestOptions
+ );
+
+ foreach ( $data as $d ) {
+ $stubSemanticData->addPropertyStubValue( reset( $d ), end( $d ) );
+ }
+
+ return $stubSemanticData;
+ }
+
+ /**
+ * Helper function for reading all data for from a given property table
+ * (specified by an SMWSQLStore3Table dataItem), based on certain
+ * restrictions. The function can filter data based on the subject (1)
+ * or on the property it belongs to (2) -- but one of those must be
+ * done. The Boolean $issubject is true for (1) and false for (2).
+ *
+ * In case (1), the first two parameters are taken to refer to a
+ * subject; in case (2) they are taken to refer to a property. In any
+ * case, the retrieval is limited to the specified $proptable. The
+ * parameters are an internal $id (of a subject or property), and an
+ * $dataItem (being an DIWikiPage or SMWDIProperty). Moreover, when
+ * filtering by property, it is assumed that the given $proptable
+ * belongs to the property: if it is a table with fixed property, it
+ * will not be checked that this is the same property as the one that
+ * was given in $dataItem.
+ *
+ * In case (1), the result in general is an array of pairs (arrays of
+ * size 2) consisting of a property key (string), and DB keys (array if
+ * many, string if one) from which a datvalue dataItem for this value can
+ * be built. It is possible that some of the DB keys are based on
+ * internal dataItems; these will be represented by similar result arrays
+ * of (recursive calls of) fetchSemanticData().
+ *
+ * In case (2), the result is simply an array of DB keys (array)
+ * without the property keys. Container dataItems will be encoded with
+ * nested arrays like in case (1).
+ *
+ * @param integer $id
+ * @param DataItem $dataItem
+ * @param PropertyTableDefinition $propTable
+ * @param RequestOptions $requestOptions
+ *
+ * @return array
+ */
+ public function fetchSemanticData( $id, DataItem $dataItem = null, PropertyTableDefinition $propTable, RequestOptions $requestOptions = null ) {
+
+ $isSubject = $dataItem instanceof DIWikiPage || $dataItem === null;
+
+ // stop if there is not enough data:
+ // properties always need to be given as dataItem,
+ // subjects at least if !$proptable->idsubject
+ if ( ( $id == 0 ) ||
+ ( $dataItem === null && ( !$isSubject || !$propTable->usesIdSubject() ) ) ||
+ ( $propTable->getDIType() === null ) ) {
+ return [];
+ }
+
+ $result = [];
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ // Build something like:
+ //
+ // SELECT o_id AS id0,o0.smw_title AS v0,o0.smw_namespace AS v1,o0.smw_iw
+ // AS v2,o0.smw_sortkey AS v3,o0.smw_subobject AS v4
+ // FROM `smw_fpt_sobj`
+ // INNER JOIN `smw_object_ids` AS o0 ON o_id=o0.smw_id
+ // WHERE s_id='852'
+ // LIMIT 4
+ //
+ // or
+ //
+ // SELECT p.smw_title as prop,o_blob AS v0,o_hash AS v1 FROM `smw_di_blob`
+ // INNER JOIN `smw_object_ids` AS p ON p_id=p.smw_id
+ // WHERE s_id='80' AND p.smw_iw!=':smw' AND p.smw_iw!=':smw-delete'
+
+ $query = $this->newQuery(
+ $propTable,
+ $id,
+ $isSubject,
+ $dataItem
+ );
+
+ if ( $requestOptions !== null ) {
+ foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
+ if ( isset( $extraCondition['p_id'] ) ) {
+ $query->condition( $query->eq( 'p_id', $extraCondition['p_id'] ) );
+ }
+ }
+ } else {
+ $requestOptions = new RequestOptions();
+ }
+
+ $valueCount = 0;
+ $fieldname = '';
+
+ $diHandler = $this->store->getDataItemHandlerForDIType(
+ $propTable->getDiType()
+ );
+
+ $valueField = $diHandler->getIndexField();
+ $labelField = $diHandler->getLabelField();
+
+ $fields = $diHandler->getFetchFields();
+
+ $this->addFields(
+ $query,
+ $fields,
+ $valueField,
+ $labelField,
+ $valueCount,
+ $fieldname
+ );
+
+ // Don't use DISTINCT for subject related value match but make sure
+ // (#3531) it is used when requesting other values in order to retrieve
+ // all available unique values within the range of the limit
+ if ( !$isSubject ) {
+ $requestOptions->setOption( 'DISTINCT', true );
+
+ // Don't sort, this avoids a SQL `filesort`/`temporary table` usage
+ // in combination with DISTINCT, values will be listed as-is instead
+ // of a lexical representation but can be compensated by selecting a
+ // wider range in case this is used as retrieving "all" values
+ // for a property
+
+ // SELECT DISTINCT o_id AS id0, o0.smw_title AS v0, o0.smw_namespace
+ // AS v1, o0.smw_iw AS v2, o0.smw_sortkey AS v3, o0.smw_subobject AS
+ // v4 FROM `smw_di_wikipage` INNER JOIN `smw_object_ids` AS o0 ON
+ // o_id=o0.smw_id WHERE (p_id='x') LIMIT 51
+ //
+ // 8.6281ms
+ //
+ // vs.
+ //
+ // SELECT DISTINCT o_id AS id0, o0.smw_title AS v0, o0.smw_namespace
+ // AS v1, o0.smw_iw AS v2, o0.smw_sortkey AS v3, o0.smw_subobject AS
+ // v4 FROM `smw_di_wikipage` INNER JOIN `smw_object_ids` AS o0 ON
+ // o_id=o0.smw_id WHERE (p_id='x') ORDER BY o_id LIMIT 51
+ //
+ // 24189.0128ms
+ //
+ // PS: In case of a `TYPE_WIKIPAGE` entity, sorting by `o_id`
+ // wouldn't make much sense as it does not guarantee any lexical order
+ $requestOptions->setOption( 'ORDER BY', false );
+ }
+
+ // Apply sorting/string matching; only with given property
+ if ( !$isSubject ) {
+ $conds = $this->store->getSQLConditions(
+ $requestOptions,
+ $valueField,
+ $labelField,
+ $query->hasCondition()
+ );
+
+ $query->condition( $conds );
+ } else {
+ $valueField = '';
+ }
+
+ $query->options(
+ $this->store->getSQLOptions( $requestOptions, $valueField )
+ );
+
+ $res = $connection->query(
+ $query,
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ $propertykey = '';
+
+ // use joined or predefined property name
+ if ( $isSubject ) {
+ $propertykey = $propTable->isFixedPropertyTable() ? $propTable->getFixedProperty() : $row->prop;
+ }
+
+ $this->resultFromRow(
+ $result,
+ $row,
+ $fields,
+ $fieldname,
+ $valueCount,
+ $isSubject,
+ $propertykey
+ );
+ }
+
+ $connection->freeResult( $res );
+
+ // Sorting via PHP for an explicit disabled `ORDER BY` to ensure that
+ // the result set has at least a lexical order applied for the range of
+ // retrieved values
+ if ( $requestOptions->getOption( 'ORDER BY' ) === false ) {
+ sort( $result );
+ }
+
+ return $result;
+ }
+
+ private function newQuery( $propTable, $id, $isSubject, $dataItem ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $query = $connection->newQuery();
+
+ $query->type( 'select' );
+ $query->table( $propTable->getName() );
+
+ // Restrict property only
+ if ( !$isSubject && !$propTable->isFixedPropertyTable() ) {
+ $query->condition( $query->eq( 'p_id', $id ) );
+ }
+
+ // Restrict subject, select property
+ if ( $isSubject && $propTable->usesIdSubject() ) {
+ $query->condition( $query->eq( 's_id', $id ) );
+ } elseif ( $isSubject ) {
+ $query->condition( $query->eq( 's_title', $dataItem->getDBkey() ) );
+ $query->condition( $query->eq( 's_namespace', $dataItem->getNamespace() ) );
+ }
+
+ // Select property name
+ // In case of a fixed property, no select needed
+ if ( $isSubject && !$propTable->isFixedPropertyTable() ) {
+ $query->join(
+ 'INNER JOIN',
+ [ SQLStore::ID_TABLE => 'p ON p_id=p.smw_id' ]
+ );
+
+ $query->field( 'p.smw_title', 'prop' );
+
+ // Avoid displaying any property that has been marked deleted or outdated
+ $query->condition( $query->neq( "p.smw_iw", SMW_SQL3_SMWIW_OUTDATED ) );
+ $query->condition( $query->neq( "p.smw_iw", SMW_SQL3_SMWDELETEIW ) );
+ }
+
+ return $query;
+ }
+
+ private function addFields( &$query, $fields, $valueField, $labelField, &$valueCount, &$fieldname ) {
+
+ // Select dataItem column(s)
+ foreach ( $fields as $fieldname => $fieldType ) {
+
+ // Get data from ID table
+ if ( $fieldType === FieldType::FIELD_ID ) {
+ $query->join(
+ 'INNER JOIN',
+ [ SQLStore::ID_TABLE => "o$valueCount ON $fieldname=o$valueCount.smw_id" ]
+ );
+
+ $query->field( "$fieldname AS id$valueCount" );
+ $query->field( "o$valueCount.smw_title AS v$valueCount" );
+ $query->field( "o$valueCount.smw_namespace AS v" . ( $valueCount + 1 ) );
+ $query->field( "o$valueCount.smw_iw AS v" . ( $valueCount + 2 ) );
+ $query->field( "o$valueCount.smw_sortkey AS v" . ( $valueCount + 3 ) );
+ $query->field( "o$valueCount.smw_subobject AS v" . ( $valueCount + 4 ) );
+
+ if ( $valueField == $fieldname ) {
+ $valueField = "o$valueCount.smw_sortkey";
+ }
+ if ( $labelField == $fieldname ) {
+ $labelField = "o$valueCount.smw_sortkey";
+ }
+
+ $valueCount += 4;
+ } else {
+ $query->field( $fieldname, "v$valueCount" );
+ }
+
+ $valueCount += 1;
+ }
+
+ // Postgres
+ // Function: SMWSQLStore3Readers::fetchSemanticData
+ // Error: 42P10 ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
+ if ( !$query->hasField( $valueField ) ) {
+ $query->field( $valueField, "v" . ( $valueCount + 1 ) );
+ }
+ }
+
+ private function resultFromRow( &$result, $row, $fields, $fieldname, $valueCount, $isSubject, $propertykey ) {
+
+ $hash = '';
+
+ if ( $isSubject ) { // use joined or predefined property name
+ $hash = $propertykey;
+ }
+
+ // Use enclosing array only for results with many values:
+ if ( $valueCount > 1 ) {
+ $valueKeys = [];
+ for ( $i = 0; $i < $valueCount; $i += 1 ) { // read the value fields from the current row
+ $fieldname = "v$i";
+ $valueKeys[] = $row->$fieldname;
+ }
+ } else {
+ $valueKeys = $row->v0;
+ }
+
+ // #Issue 615
+ // If the iw field contains a redirect marker then remove it
+ if ( isset( $valueKeys[2] ) && ( $valueKeys[2] === SMW_SQL3_SMWREDIIW || $valueKeys[2] === SMW_SQL3_SMWDELETEIW ) ) {
+ $valueKeys[2] = '';
+ }
+
+ // The hash prevents from inserting duplicate entries of the same content
+ if ( $valueCount > 1 ) {
+ $hash = md5( $hash . implode( '#', $valueKeys ) );
+ } else {
+ $hash = md5( $hash . $valueKeys );
+ }
+
+ // Filter out any accidentally retrieved internal things (interwiki starts with ":"):
+ if ( $valueCount < 3 ||
+ implode( '', $fields ) !== FieldType::FIELD_ID ||
+ $valueKeys[2] === '' ||
+ $valueKeys[2]{0} != ':' ) {
+
+ if ( isset( $result[$hash] ) ) {
+ $this->reportDuplicate( $propertykey, $valueKeys );
+ }
+
+ if ( $isSubject ) {
+ $result[$hash] = [ $propertykey, $valueKeys ];
+ } else{
+ $result[$hash] = $valueKeys;
+ }
+ }
+ }
+
+ private function reportDuplicate( $propertykey, $valueKeys ) {
+ $this->logger->info(
+ "Found duplicate entry for {propertykey} with {valueKeys}",
+ [
+ 'method' => __METHOD__,
+ 'role' => 'user',
+ 'propertykey' => $propertykey,
+ 'valueKeys' => ( is_array( $valueKeys ) ? implode( ',', $valueKeys ) : $valueKeys )
+ ]
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/StubSemanticData.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/StubSemanticData.php
new file mode 100644
index 00000000..c049b0bd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/StubSemanticData.php
@@ -0,0 +1,384 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use SMW\DataTypeRegistry;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Exception\DataItemException;
+use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
+use SMW\SQLStore\SQLStore;
+use SMW\StoreFactory;
+use SMWDataItem as DataItem;
+use SMWSemanticData as SemanticData;
+
+/**
+ * This class provides a subclass of SemanticData that can store prefetched values
+ * from the SQL store, and unstub this data on demand when it is accessed.
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Markus Krötzs
+ * @author mwjames
+ */
+class StubSemanticData extends SemanticData {
+
+ /**
+ * @var SQLStore
+ */
+ protected $store;
+
+ /**
+ * Stub property data that is not part of $mPropVals and $mProperties
+ * yet. Entries use property keys as keys. The value is an array of
+ * DBkey-arrays that define individual datavalues. The stubs will be
+ * set up when first accessed.
+ *
+ * @since 1.8
+ *
+ * @var array
+ */
+ protected $mStubPropVals = [];
+
+ /**
+ * DIWikiPage object that is the subject of this container.
+ * Subjects that are null are used to represent "internal objects"
+ * only.
+ *
+ * @since 1.8
+ *
+ * @var DIWikiPage
+ */
+ protected $mSubject;
+
+ /**
+ * Whether SubSemanticData have been requested and added
+ *
+ * @var boolean
+ */
+ private $subSemanticDataInit = false;
+
+ /**
+ * @since 1.8
+ *
+ * @param DIWikiPage $subject to which this data refers
+ * @param SQLStore $store (the parent store)
+ * @param boolean $noDuplicates stating if duplicate data should be avoided
+ */
+ public function __construct( DIWikiPage $subject, SQLStore $store, $noDuplicates = true ) {
+ $this->store = $store;
+ parent::__construct( $subject, $noDuplicates );
+ }
+
+ /**
+ * Required to support php-serialization
+ *
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function __sleep() {
+ return [ 'mSubject', 'mPropVals', 'mProperties', 'subSemanticData', 'mStubPropVals', 'options', 'extensionData' ];
+ }
+
+ /**
+ * @since 2.3
+ */
+ public function __wakeup() {
+ $this->store = StoreFactory::getStore( 'SMW\SQLStore\SQLStore' );
+ }
+
+ /**
+ * Create a new StubSemanticData object that holds the data of a
+ * given SemanticData object. Array assignments create copies in PHP
+ * so the arrays are distinct in input and output object. The object
+ * references are copied as references in a shallow way. This is
+ * sufficient as the data items used there are immutable.
+ *
+ * @since 1.8
+ *
+ * @param $semanticData SemanticData
+ * @param SQLStore $store
+ *
+ * @return StubSemanticData
+ */
+ public static function newFromSemanticData( SemanticData $semanticData, SQLStore $store ) {
+ $result = new self( $semanticData->getSubject(), $store );
+ $result->mPropVals = $semanticData->mPropVals;
+ $result->mProperties = $semanticData->mProperties;
+ $result->mHasVisibleProps = $semanticData->mHasVisibleProps;
+ $result->mHasVisibleSpecs = $semanticData->mHasVisibleSpecs;
+ $result->stubObject = $semanticData->stubObject;
+ return $result;
+ }
+
+ /**
+ * Get the array of all properties that have stored values.
+ *
+ * @since 1.8
+ *
+ * @return array of SMWDIProperty objects
+ */
+ public function getProperties() {
+ $this->unstubProperties();
+ return parent::getProperties();
+ }
+
+ /**
+ * @see SemanticData::hasProperty
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return boolean
+ */
+ public function hasProperty( DIProperty $property ) {
+ $this->unstubProperties();
+ return parent::hasProperty( $property );
+ }
+
+ /**
+ * Get the array of all stored values for some property.
+ *
+ * @since 1.8
+ *
+ * @param DIProperty $property
+ *
+ * @return array of DataItem
+ */
+ public function getPropertyValues( DIProperty $property ) {
+ if ( $property->isInverse() ) { // we never have any data for inverses
+ return [];
+ }
+
+ if ( array_key_exists( $property->getKey(), $this->mStubPropVals ) ) {
+ // Not catching exception here; the
+ $this->unstubProperty( $property->getKey(), $property );
+ $propertyTypeId = $property->findPropertyTypeID();
+ $propertyDiId = DataTypeRegistry::getInstance()->getDataItemId( $propertyTypeId );
+
+ foreach ( $this->mStubPropVals[$property->getKey()] as $dbkeys ) {
+ try {
+ $diHandler = $this->store->getDataItemHandlerForDIType( $propertyDiId );
+ $di = $diHandler->dataItemFromDBKeys( $dbkeys );
+
+ if ( $this->mNoDuplicates ) {
+ $this->mPropVals[$property->getKey()][$di->getHash()] = $di;
+ } else {
+ $this->mPropVals[$property->getKey()][] = $di;
+ }
+ } catch ( DataItemHandlerException $e ) {
+ // ignore data
+ }
+ }
+
+ unset( $this->mStubPropVals[$property->getKey()] );
+ }
+
+ return parent::getPropertyValues( $property );
+ }
+
+ /**
+ * @see SemanticData::getSubSemanticData
+ *
+ * @note SubSemanticData are added only on request to avoid unnecessary DB
+ * transactions
+ *
+ * @since 2.0
+ */
+ public function getSubSemanticData() {
+
+ if ( $this->subSemanticDataInit ) {
+ return parent::getSubSemanticData();
+ }
+
+ $this->subSemanticDataInit = true;
+
+ foreach ( $this->getProperties() as $property ) {
+
+ // #619 Do not resolve subobjects for redirects
+ if ( !DataTypeRegistry::getInstance()->isSubDataType( $property->findPropertyTypeID() ) || $this->isRedirect() ) {
+ continue;
+ }
+
+ $this->initSubSemanticData( $property );
+ }
+
+ return parent::getSubSemanticData();
+ }
+
+ /**
+ * @see SemanticData::hasSubSemanticData
+ *
+ * @note This method will initialize SubSemanticData first if it wasn't done
+ * yet to ensure data consistency
+ *
+ * @since 2.0
+ */
+ public function hasSubSemanticData( $subobjectName = null ) {
+
+ if ( !$this->subSemanticDataInit ) {
+ $this->getSubSemanticData();
+ }
+
+ return parent::hasSubSemanticData( $subobjectName );
+ }
+
+ /**
+ * @see SemanticData::findSubSemanticData
+ *
+ * @since 2.5
+ */
+ public function findSubSemanticData( $subobjectName ) {
+
+ if ( !$this->subSemanticDataInit ) {
+ $this->getSubSemanticData();
+ }
+
+ return parent::findSubSemanticData( $subobjectName );
+ }
+
+ /**
+ * Remove a value for a property identified by its DataItem 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 SMWDIProperty
+ * @param $dataItem DataItem
+ *
+ * @since 1.8
+ */
+ public function removePropertyObjectValue( DIProperty $property, DataItem $dataItem ) {
+ $this->unstubProperties();
+ $this->getPropertyValues( $property );
+ parent::removePropertyObjectValue($property, $dataItem);
+ }
+
+ /**
+ * Return true if there are any visible properties.
+ *
+ * @since 1.8
+ *
+ * @return boolean
+ */
+ public function hasVisibleProperties() {
+ $this->unstubProperties();
+ return parent::hasVisibleProperties();
+ }
+
+ /**
+ * Return true if there are any special properties that can
+ * be displayed.
+ *
+ * @since 1.8
+ *
+ * @return boolean
+ */
+ public function hasVisibleSpecialProperties() {
+ $this->unstubProperties();
+ return parent::hasVisibleSpecialProperties();
+ }
+
+ /**
+ * Add data in abbreviated form so that it is only expanded if needed.
+ * The property key is the DB key (string) of a property value, whereas
+ * valuekeys is an array of DBkeys for the added value that will be
+ * used to initialize the value if needed at some point. If there is
+ * only one valuekey, a single string can be used.
+ *
+ * @since 1.8
+ * @param string $propertyKey
+ * @param array|string $valueKeys
+ */
+ public function addPropertyStubValue( $propertyKey, $valueKeys ) {
+ $this->mStubPropVals[$propertyKey][] = $valueKeys;
+ }
+
+ /**
+ * Delete all data other than the subject.
+ *
+ * @since 1.8
+ */
+ public function clear() {
+ $this->mStubPropVals = [];
+ parent::clear();
+ }
+
+ /**
+ * Process all mProperties that have been added as stubs.
+ * Associated data may remain in stub form.
+ *
+ * @since 1.8
+ */
+ protected function unstubProperties() {
+ foreach ( $this->mStubPropVals as $pkey => $values ) { // unstub property values only, the value lists are still kept as stubs
+ try {
+ $this->unstubProperty( $pkey );
+ } catch ( DataItemException $e ) {
+ // Likely cause: a property name from the DB is no longer valid.
+ // Do nothing; we could unset the data, but it will never be
+ // unstubbed anyway if there is no valid property DI for it.
+ }
+ }
+ }
+
+ /**
+ * Unstub a single property from the stub data array. If available, an
+ * existing object for that property might be provided, so we do not
+ * need to make a new one. It is not checked if the object matches the
+ * property name.
+ *
+ * @since 1.8
+ *
+ * @param string $propertyKey
+ * @param SMWDIProperty $diProperty if available
+ *
+ * @throws DataItemException if property key is not valid
+ * and $diProperty is null
+ */
+ protected function unstubProperty( $propertyKey, $diProperty = null ) {
+ if ( !array_key_exists( $propertyKey, $this->mProperties ) ) {
+ if ( is_null( $diProperty ) ) {
+ $diProperty = new DIProperty( $propertyKey, false );
+ }
+
+ $this->mProperties[$propertyKey] = $diProperty;
+
+ if ( !$diProperty->isUserDefined() ) {
+ if ( $diProperty->isShown() ) {
+ $this->mHasVisibleSpecs = true;
+ $this->mHasVisibleProps = true;
+ }
+ } else {
+ $this->mHasVisibleProps = true;
+ }
+ }
+ }
+
+ protected function isRedirect() {
+ return $this->store->getObjectIds()->isRedirect( $this->mSubject );
+ }
+
+ private function initSubSemanticData( DIProperty $property ) {
+ foreach ( $this->getPropertyValues( $property ) as $value ) {
+
+ if ( !$value instanceof DIWikiPage || $value->getSubobjectName() === '' ) {
+ continue;
+ }
+
+ if ( $this->hasSubSemanticData( $value->getSubobjectName() ) ) {
+ continue;
+ }
+
+ $this->addSubSemanticData( $this->store->getSemanticData( $value ) );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/SubobjectListFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/SubobjectListFinder.php
new file mode 100644
index 00000000..f26c3b20
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/SubobjectListFinder.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\IteratorFactory;
+use SMW\SQLStore\SQLStore;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SubobjectListFinder {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var IteratorFactory
+ */
+ private $iteratorFactory;
+
+ /**
+ * @var DIWikiPage
+ */
+ private $subject;
+
+ /**
+ * @var []
+ */
+ private $mappingIterator = [];
+
+ /**
+ * @var []
+ */
+ private $skipConditions = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param SQLStore $store
+ * @param IteratorFactory $iteratorFactory
+ */
+ public function __construct( SQLStore $store, IteratorFactory $iteratorFactory ) {
+ $this->store = $store;
+ $this->iteratorFactory = $iteratorFactory;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return MappingIterator
+ */
+ public function find( DIWikiPage $subject ) {
+
+ $key = $subject->getHash() . ':' . $subject->getId();
+
+ if ( !isset( $this->mappingIterator[$key] ) ) {
+ $this->mappingIterator[$key] = $this->newMappingIterator( $subject );
+ }
+
+ return $this->mappingIterator[$key];
+ }
+
+ /**
+ * Fetch all subobjects for a given subject using a lazy-mapping iterator
+ * in order to only resolve one subobject per iteration step.
+ *
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return MappingIterator
+ */
+ private function newMappingIterator( DIWikiPage $subject ) {
+
+ $callback = function( $row ) use ( $subject ) {
+
+ // #1955
+ if ( $subject->getNamespace() === SMW_NS_PROPERTY ) {
+ $property = new DIProperty( $subject->getDBkey() );
+ $subobject = $property->getCanonicalDiWikiPage( $row->smw_subobject );
+ } else {
+ $subobject = new DIWikiPage(
+ $subject->getDBkey(),
+ $subject->getNamespace(),
+ $subject->getInterwiki(),
+ $row->smw_subobject
+ );
+ }
+
+ $subobject->setSortKey( $row->smw_sortkey );
+ $subobject->setId( $row->smw_id );
+
+ return $subobject;
+ };
+
+ return $this->iteratorFactory->newMappingIterator(
+ $this->newResultIterator( $subject ),
+ $callback
+ );
+ }
+
+ private function newResultIterator( DIWikiPage $subject ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $key = $subject->getDBkey();
+
+ // #1955 Ensure to match a possible predefined property
+ // (Modification date -> _MDAT)
+ if ( $subject->getNamespace() === SMW_NS_PROPERTY ) {
+ $key = DIProperty::newFromUserLabel( $key )->getKey();
+ }
+
+ $conditions = [
+ 'smw_title=' . $connection->addQuotes( $key ),
+ 'smw_namespace=' . $connection->addQuotes( $subject->getNamespace() ),
+ 'smw_iw=' . $connection->addQuotes( $subject->getInterwiki() ),
+ 'smw_subobject!=' . $connection->addQuotes( '' )
+ ];
+
+ foreach ( $this->skipConditions as $skipOn ) {
+ $conditions[] = 'smw_subobject!=' . $connection->addQuotes( $skipOn );
+ }
+
+ $res = $connection->select(
+ $connection->tablename( SQLStore::ID_TABLE ),
+ [
+ 'smw_id',
+ 'smw_subobject',
+ 'smw_sortkey'
+ ],
+ implode( ' AND ' , $conditions ),
+ __METHOD__
+ );
+
+ return $this->iteratorFactory->newResultIterator( $res );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/TraversalPropertyLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/TraversalPropertyLookup.php
new file mode 100644
index 00000000..1ca99e48
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/TraversalPropertyLookup.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use RuntimeException;
+use SMW\DIContainer;
+use SMW\MediaWiki\Connection\OptionsBuilder;
+use SMW\Options;
+use SMW\RequestOptions;
+use SMW\SQLStore\PropertyTableDefinition as PropertyTableDef;
+use SMW\SQLStore\SQLStore;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TraversalPropertyLookup {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @see Store::getInProperties
+ *
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function fetchFromTable( PropertyTableDef $propertyTableDef, DataItem $dataItem, RequestOptions $requestOptions = null ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ if ( $dataItem instanceof DIContainer ) {
+ throw new RuntimeException( "DIContainer: " . $dataItem->getSerialization() );
+ }
+
+ // Potentially need to get more results, since options apply to union.
+ if ( $requestOptions !== null ) {
+ $subOptions = clone $requestOptions;
+ $subOptions->limit = $requestOptions->limit + $requestOptions->offset;
+ $subOptions->offset = 0;
+ } else {
+ $subOptions = null;
+ }
+
+ if ( !$propertyTableDef->isFixedPropertyTable() ) {
+
+ $cond = $this->getWhereConds( $dataItem );
+ $conditions = '';
+
+ // No sorting
+ $options = $this->store->getSQLOptions( $subOptions, '' );
+
+ // Avoid any limit or offset for the sub-query in order to find all
+ // incoming properties
+ unset( $options['LIMIT'] );
+ unset( $options['OFFSET'] );
+
+ // Ensure to group same IDs to reduce the amount of data transferred
+ // from the inner join
+ $options['GROUP BY'] = 'p_id';
+
+ $opt = OptionsBuilder::toString( $options );
+
+ $cond = ( $cond !== '' ? ' WHERE ' : '' ) . $cond;
+
+ // Use a subquery to match all possible IDs, no ORDER BY or DISTINCT to avoid
+ // a filesort
+ $from = $connection->tableName( SQLStore::ID_TABLE ) .
+ " INNER JOIN (" .
+ " SELECT p_id FROM " . $connection->tableName( $propertyTableDef->getName() ) .
+ " $cond $opt ) AS t1 ON t1.p_id=smw_id";
+
+ $conditions .= ( $conditions ? ' AND ' : ' ' ) .
+ " smw_iw!=" . $connection->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) .
+ " AND smw_iw!=" . $connection->addQuotes( SMW_SQL3_SMWDELETEIW );
+
+ $conditions .= $this->store->getSQLConditions( $subOptions, 'smw_sortkey', 'smw_sortkey', $conditions !== '' );
+
+ $options = $this->store->getSQLOptions( $subOptions, '' ) + [ 'DISTINCT' ];
+
+ $result = $connection->select(
+ $from,
+ ' smw_title,smw_sortkey,smw_iw',
+ $conditions,
+ __METHOD__,
+ $options
+ );
+
+ } else {
+ $from = $connection->tableName( $propertyTableDef->getName() ) . " AS t1";
+ $where = $this->getWhereConds( $dataItem );
+ $fields = $propertyTableDef->usesIdSubject() ? 's_id' : '*';
+
+ $result = $connection->select(
+ $from,
+ $fields,
+ $where,
+ __METHOD__,
+ [ 'LIMIT' => 1 ]
+ );
+
+ if ( $result->numRows() > 0 ) {
+ $res = new \stdClass;
+ $res->smw_title = $propertyTableDef->getFixedProperty();
+ $result = [ $res ];
+ }
+ }
+
+ return $result;
+ }
+
+ private function getWhereConds( $dataItem ) {
+
+ $where = '';
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ if ( $dataItem !== null ) {
+ $dataItemHandler = $this->store->getDataItemHandlerForDIType( $dataItem->getDIType() );
+ foreach ( $dataItemHandler->getWhereConds( $dataItem ) as $fieldname => $value ) {
+ $where .= ( $where ? ' AND ' : '' ) . "$fieldname=" . $connection->addQuotes( $value );
+ }
+ }
+
+ return $where;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/UniquenessLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/UniquenessLookup.php
new file mode 100644
index 00000000..00767015
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityStore/UniquenessLookup.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace SMW\SQLStore\EntityStore;
+
+use Onoi\Cache\Cache;
+use SMW\DIWikiPage;
+use SMW\IteratorFactory;
+use SMW\RequestOptions;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use InvalidArgumentException;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class UniquenessLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var IteratorFactory
+ */
+ private $iteratorFactory;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param IteratorFactory $iteratorFactory
+ */
+ public function __construct( Store $store, IteratorFactory $iteratorFactory ) {
+ $this->store = $store;
+ $this->iteratorFactory = $iteratorFactory;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DataItem $dataItem
+ *
+ * @return boolean
+ */
+ public function isUnique( DataItem $dataItem ) {
+
+ $type = $dataItem->getDIType();
+
+ if ( $type !== DataItem::TYPE_WIKIPAGE && $type !== DataItem::TYPE_PROPERTY ) {
+ throw new InvalidArgumentException( 'Expects a DIProperty or DIWikiPage object.' );
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $query = $connection->newQuery();
+
+ $query->type( 'SELECT' );
+ $query->options( [ 'LIMIT' => 2 ] );
+
+ $query->table( SQLStore::ID_TABLE );
+
+ // Only find entities
+ $query->fields( [ 'smw_id', 'smw_sortkey' ] );
+
+ if ( $type === DataItem::TYPE_WIKIPAGE ) {
+ $query->condition( $query->eq( 'smw_title', $dataItem->getDBKey() ) );
+ $query->condition( $query->eq( 'smw_namespace', $dataItem->getNamespace() ) );
+ $query->condition( $query->eq( 'smw_subobject', $dataItem->getSubobjectName() ) );
+ } else {
+ $query->condition( $query->eq( 'smw_sortkey', $dataItem->getCanonicalLabel() ) );
+ $query->condition( $query->eq( 'smw_namespace', SMW_NS_PROPERTY ) );
+ $query->condition( $query->eq( 'smw_subobject', '' ) );
+ }
+
+ $query->condition( $query->neq( 'smw_iw', SMW_SQL3_SMWIW_OUTDATED ) );
+ $query->condition( $query->neq( 'smw_iw', SMW_SQL3_SMWDELETEIW ) );
+ $query->condition( $query->neq( 'smw_iw', SMW_SQL3_SMWREDIIW ) );
+
+ $res = $connection->query(
+ $query,
+ __METHOD__
+ );
+
+ return $res->numRows() < 2;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Iterator|[]
+ */
+ public function findDuplicates() {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $query = $connection->newQuery();
+
+ $query->type( 'SELECT' );
+ $query->table( SQLStore::ID_TABLE );
+
+ $query->fields(
+ [
+ 'COUNT(*) as count',
+ 'smw_title',
+ 'smw_namespace',
+ 'smw_iw',
+ 'smw_subobject'
+ ]
+ );
+
+ $query->condition( $query->neq( 'smw_iw', SMW_SQL3_SMWIW_OUTDATED ) );
+ $query->condition( $query->neq( 'smw_iw', SMW_SQL3_SMWDELETEIW ) );
+
+ $query->options(
+ [
+ 'GROUP BY' => 'smw_title, smw_namespace, smw_iw, smw_subobject',
+
+ // @see https://stackoverflow.com/questions/8119489/postgresql-where-count-condition
+ // "HAVING count > 1"; doesn't work with postgres
+ 'HAVING' => 'count(*) > 1'
+ ]
+ );
+
+ $rows = $connection->query(
+ $query,
+ __METHOD__
+ );
+
+ if ( $rows === false ) {
+ return [];
+ }
+
+ $resultIterator = $this->iteratorFactory->newResultIterator(
+ $rows
+ );
+
+ $mappingIterator = $this->iteratorFactory->newMappingIterator( $resultIterator, function( $row ) {
+ return [
+ 'count'=> $row->count,
+ 'smw_title'=> $row->smw_title,
+ 'smw_namespace'=> $row->smw_namespace,
+ 'smw_iw'=> $row->smw_iw,
+ 'smw_subobject'=> $row->smw_subobject
+ ];
+ } );
+
+ return $mappingIterator;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityValueUniquenessConstraintChecker.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityValueUniquenessConstraintChecker.php
new file mode 100644
index 00000000..01a634e0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/EntityValueUniquenessConstraintChecker.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use SMWDataItem as DataItem;
+use SMW\RequestOptions;
+use SMW\IteratorFactory;
+use InvalidArgumentException;
+use RuntimeException;
+use SMWDIContainer as DIContainer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class EntityValueUniquenessConstraintChecker {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var IteratorFactory
+ */
+ private $iteratorFactory;
+
+ /**
+ * @since 3.0
+ *
+ * @param SQLStore $store
+ * @param IteratorFactory $iteratorFactory
+ */
+ public function __construct( Store $store, IteratorFactory $iteratorFactory ) {
+ $this->store = $store;
+ $this->iteratorFactory = $iteratorFactory;
+ }
+
+ /**
+ * Find references (all or limited by RequestOptions) for the combination of
+ * a property and a value. This can be used to identify uniqueness violations
+ * amongst entities where the same value (+property) is assigned to different
+ * subjects or it be used to count the cardinality for a specific value
+ * representation.
+ *
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ * @param DataItem $dataItem
+ * @param RequestOptions $requestOptions
+ *
+ * @return Iterator|[]
+ */
+ public function checkConstraint( DIProperty $property, DataItem $dataItem, RequestOptions $requestOptions ) {
+
+ $propTableId = $this->store->getPropertyTableInfoFetcher()->findTableIdForProperty(
+ $property
+ );
+
+ $proptables = $this->store->getPropertyTables();
+ $propertyTable = $proptables[$propTableId];
+
+ if ( !isset( $proptables[$propTableId] ) || !$propertyTable->usesIdSubject() ) {
+ return [];
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $query = $connection->newQuery();
+
+ $query->index = 1;
+ $query->alias = 't';
+ $i = $query->index;
+
+ $query->table( $propertyTable->getName(), "{$query->alias}{$i}" );
+
+ // Only find entities
+ $query->field( "{$query->alias}{$i}.s_id" );
+
+ $this->resolve_value_condition( $propertyTable, $property, $dataItem, $query );
+
+ foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
+ if ( is_callable( $extraCondition ) ) {
+ $query->condition( $extraCondition( $this->store, $query, "{$query->alias}{$i}" ) );
+ } else {
+ throw new RuntimeException( "Expected a callable at this point!" );
+ }
+ }
+
+ $query->type( 'SELECT' );
+ $query->options( [ 'LIMIT' => $requestOptions->getLimit() ] );
+
+ $res = $connection->query(
+ $query,
+ __METHOD__
+ );
+
+ $result = $this->iteratorFactory->newMappingIterator(
+ $this->iteratorFactory->newResultIterator( $res ),
+ function( $row ) {
+ return $this->store->getObjectIds()->getDataItemById( $row->s_id );
+ }
+ );
+
+ return $result;
+ }
+
+ private function resolve_value_condition( $propertyTable, $property, $dataItem, $query ) {
+
+ // Collect conditions to appear as
+ // `... (t1.p_id='121913' AND t1.o_sortkey='3520062') ...`
+ $conditions = [];
+
+ // Keep the index in case of a recursive iteration
+ $i = $query->index;
+
+ if ( !$propertyTable->isFixedPropertyTable() ) {
+
+ $pid = $this->store->getObjectIds()->getSMWPropertyID(
+ $property
+ );
+
+ $conditions[] = $query->eq( "{$query->alias}{$i}.p_id", $pid );
+ }
+
+ $diHandler = $this->store->getDataItemHandlerForDIType(
+ $propertyTable->getDiType()
+ );
+
+ if ( !$dataItem instanceof DIContainer ) {
+ foreach ( $diHandler->getWhereConds( $dataItem ) as $fieldName => $value ) {
+ $conditions[] = $query->eq( "{$query->alias}{$i}.$fieldName", $value );
+ }
+ } else {
+
+ /**
+ * For a container based property/value pair we expected something similar
+ * to:
+ *
+ * SELECT t1.s_id FROM `smw_di_wikipage` AS t1
+ * INNER JOIN `smw_di_wikipage` AS t2 ON t2.s_id=t1.o_id
+ * INNER JOIN `smw_di_wikipage` AS t3 ON t3.s_id=t1.o_id
+ * INNER JOIN `smw_di_number` AS t4 ON t4.s_id=t1.o_id
+ * WHERE
+ * (t2.p_id='333615' AND t2.o_id='302096') AND
+ * (t3.p_id='333611' AND t3.o_id='193213') AND
+ * (t4.p_id='121913' AND t4.o_sortkey='3520062') AND
+ * (t1.p_id='310161') AND (t1.s_id!='333608')
+ * LIMIT 2
+ */
+
+ // Handle containers recursively
+ $this->resolve_container_conditions( $propertyTable, $dataItem, $query );
+ }
+
+ $query->condition( $query->asAnd( $conditions ) );
+ }
+
+ private function resolve_container_conditions( $propertyTable, $dataItem, $query ) {
+
+ $proptables = $this->store->getPropertyTables();
+ $semanticData = $dataItem->getSemanticData();
+
+ $alias = $query->alias;
+ $i = $query->index;
+
+ // ought to be a type 'p' object
+ $keys = array_keys( $propertyTable->getFields( $this->store ) );
+ $joinfield = "{$alias}{$i}." . reset( $keys );
+
+ foreach ( $semanticData->getProperties() as $property ) {
+
+ $tableid = $this->store->findPropertyTableID( $property );
+ $subproptable = $proptables[$tableid];
+
+ foreach ( $semanticData->getPropertyValues( $property ) as $subvalue ) {
+ // Increase the index for each iteration to ensure that each
+ // condition has its own alias
+ $i++;
+
+ if ( $subproptable->usesIdSubject() ) {
+ // simply add property table to check values
+ $query->join(
+ 'INNER JOIN',
+ [
+ // e.g. `... INNER JOIN `smw_di_wikipage` AS t2 ON t2.s_id=t1.o_id ...`
+ $subproptable->getName() => "{$alias}{$i} ON {$alias}{$i}.s_id=$joinfield"
+ ]
+ );
+ } else {
+ // Rare case with a table that uses subject title+namespace
+ // in a container object (should never happen in SMW core!!)
+ $query->join(
+ 'INNER JOIN',
+ [
+ SQLStore::ID_TABLE => "ids{$i} ON ids{$i}.smw_id=$joinfield"
+ ]
+ );
+ $query->join(
+ 'INNER JOIN',
+ [
+ $subproptable->getName() => "{$alias}{$i} ON {$alias}{$i}.s_title=ids{$alias}{$i}.smw_title AND {$alias}{$i}.s_namespace=ids{$alias}{$i}.smw_namespace"
+ ]
+ );
+ }
+
+ $query->index = $i;
+ $this->resolve_value_condition( $subproptable, $property, $subvalue, $query );
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Exception/PropertyStatisticsInvalidArgumentException.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Exception/PropertyStatisticsInvalidArgumentException.php
new file mode 100644
index 00000000..af143789
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Exception/PropertyStatisticsInvalidArgumentException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace SMW\SQLStore\Exception;
+
+use InvalidArgumentException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyStatisticsInvalidArgumentException extends InvalidArgumentException {
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Exception/TableMissingIdFieldException.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Exception/TableMissingIdFieldException.php
new file mode 100644
index 00000000..9c7cf1aa
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Exception/TableMissingIdFieldException.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace SMW\SQLStore\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TableMissingIdFieldException extends RuntimeException {
+
+ /**
+ * @since 3.0
+ */
+ public function __construct( $name ) {
+ parent::__construct( "Operation is not supported for a table ({$name}) without subject IDs." );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Installer.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Installer.php
new file mode 100644
index 00000000..ec68ddf4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Installer.php
@@ -0,0 +1,463 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use Hooks;
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\MessageReporterAwareTrait;
+use Onoi\MessageReporter\MessageReporterFactory;
+use SMW\CompatibilityMode;
+use SMW\MediaWiki\Jobs\EntityIdDisposerJob;
+use SMW\MediaWiki\Jobs\PropertyStatisticsRebuildJob;
+use SMW\Options;
+use SMW\Site;
+use SMW\Utils\File;
+use SMW\Exception\FileNotWritableException;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class Installer implements MessageReporter {
+
+ use MessageReporterAwareTrait;
+
+ /**
+ * Optimize option
+ */
+ const OPT_TABLE_OPTIMIZE = 'installer.table.optimize';
+
+ /**
+ * Job option
+ */
+ const OPT_SUPPLEMENT_JOBS = 'installer.supplement.jobs';
+
+ /**
+ * Import option
+ */
+ const OPT_IMPORT = 'installer.import';
+
+ /**
+ * Related to ExtensionSchemaUpdates
+ */
+ const OPT_SCHEMA_UPDATE = 'installer.schema.update';
+
+ /**
+ * `smw_hash` field population
+ */
+ const POPULATE_HASH_FIELD_COMPLETE = 'populate.smw_hash_field_complete';
+
+ /**
+ * @var TableSchemaManager
+ */
+ private $tableSchemaManager;
+
+ /**
+ * @var TableBuilder
+ */
+ private $tableBuilder;
+
+ /**
+ * @var TableIntegrityExaminer
+ */
+ private $tableIntegrityExaminer;
+
+ /**
+ * @var Options
+ */
+ private $options;
+
+ /**
+ * @var File
+ */
+ private $file;
+
+ /**
+ * @since 2.5
+ *
+ * @param TableSchemaManager $tableSchemaManager
+ * @param TableBuilder $tableBuilder
+ * @param TableIntegrityExaminer $tableIntegrityExaminer
+ */
+ public function __construct( TableSchemaManager $tableSchemaManager, TableBuilder $tableBuilder, TableIntegrityExaminer $tableIntegrityExaminer ) {
+ $this->tableSchemaManager = $tableSchemaManager;
+ $this->tableBuilder = $tableBuilder;
+ $this->tableIntegrityExaminer = $tableIntegrityExaminer;
+ $this->options = new Options();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Options|array $options
+ */
+ public function setOptions( $options ) {
+
+ if ( !$options instanceof Options ) {
+ $options = new Options( $options );
+ }
+
+ $this->options = $options;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param File $file
+ */
+ public function setFile( File $file ) {
+ $this->file = $file;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $verbose
+ */
+ public function install( $verbose = true ) {
+
+ // If for some reason the enableSemantics was not yet enabled
+ // still allow to run the tables create in order for the
+ // setup to be completed
+ if ( CompatibilityMode::extensionNotEnabled() ) {
+ CompatibilityMode::enableTemporaryCliUpdateMode();
+ }
+
+ $messageReporter = $this->newMessageReporter( $verbose );
+
+ $messageReporter->reportMessage( "\nSelected storage engine: \"SMWSQLStore3\" (or an extension thereof)\n" );
+ $messageReporter->reportMessage( "\nSetting up standard database configuration for SMW ...\n\n" );
+
+ $this->tableBuilder->setMessageReporter(
+ $messageReporter
+ );
+
+ $this->tableIntegrityExaminer->setMessageReporter(
+ $messageReporter
+ );
+
+ foreach ( $this->tableSchemaManager->getTables() as $table ) {
+ $this->tableBuilder->create( $table );
+ }
+
+ $this->tableIntegrityExaminer->checkOnPostCreation( $this->tableBuilder );
+
+ $messageReporter->reportMessage( "\nDatabase initialized completed.\n" );
+
+ $this->table_optimization( $messageReporter );
+ $this->supplement_jobs( $messageReporter );
+
+ $file = $this->file !== null ? $this->file : new File();
+
+ self::setUpgradeKey( $GLOBALS, $messageReporter, $file );
+
+ Hooks::run(
+ 'SMW::SQLStore::Installer::AfterCreateTablesComplete',
+ [
+ $this->tableBuilder,
+ $messageReporter,
+ $this->options
+ ]
+ );
+
+ if ( $this->options->has( self::OPT_SCHEMA_UPDATE ) ) {
+ $messageReporter->reportMessage( "\n" );
+ }
+
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $verbose
+ */
+ public function uninstall( $verbose = true ) {
+
+ $messageReporter = $this->newMessageReporter( $verbose );
+
+ $messageReporter->reportMessage( "\nSelected storage engine: \"SMWSQLStore3\" (or an extension thereof)\n" );
+ $messageReporter->reportMessage( "\nDeleting all database content and tables generated by SMW ...\n\n" );
+
+ $this->tableBuilder->setMessageReporter(
+ $messageReporter
+ );
+
+ foreach ( $this->tableSchemaManager->getTables() as $table ) {
+ $this->tableBuilder->drop( $table );
+ }
+
+ $this->tableIntegrityExaminer->checkOnPostDestruction( $this->tableBuilder );
+
+ Hooks::run(
+ 'SMW::SQLStore::Installer::AfterDropTablesComplete',
+ [
+ $this->tableBuilder,
+ $messageReporter,
+ $this->options
+ ]
+ );
+
+ $messageReporter->reportMessage( "\nStandard and auxiliary tables with all corresponding data\n" );
+ $messageReporter->reportMessage( "have been removed successfully.\n" );
+
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $vars
+ */
+ public static function loadSchema( &$vars ) {
+
+ // @see #3506
+ $file = File::dir( $vars['smwgConfigFileDir'] . '/.smw.json' );
+
+ // Doesn't exist? The `Setup::init` will take care of it by trying to create
+ // a new file and if it fails or unable to do so wail raise an exception
+ // as we expect to have access to it.
+ if ( is_readable( $file ) ) {
+ $vars['smw.json'] = json_decode( file_get_contents( $file ), true );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isCli
+ *
+ * @return boolean
+ */
+ public static function isGoodSchema( $isCli = false ) {
+
+ if ( $isCli && defined( 'MW_PHPUNIT_TEST' ) ) {
+ return true;
+ }
+
+ if ( $isCli === false && ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) ) {
+ return true;
+ }
+
+ // #3563, Use the specific wiki-id as identifier for the instance in use
+ $id = Site::id();
+
+ if ( !isset( $GLOBALS['smw.json'][$id]['upgrade_key'] ) ) {
+ return false;
+ }
+
+ return self::makeUpgradeKey( $GLOBALS ) === $GLOBALS['smw.json'][$id]['upgrade_key'];
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param array $vars
+ *
+ * @return []
+ */
+ public static function incompleteTasks( $vars ) {
+
+ $id = Site::id();
+ $tasks = [];
+
+ // Key field => [ value that constitutes the `INCOMPLETE` state, error msg ]
+ $checks = [
+ self::POPULATE_HASH_FIELD_COMPLETE => [ false, 'smw-install-incomplete-populate-hash-field' ]
+ ];
+
+ foreach ( $checks as $key => $value ) {
+ if ( isset( $vars['smw.json'][$id][$key] ) && $vars['smw.json'][$id][$key] === $value[0] ) {
+ $tasks[] = $value[1];
+ }
+ }
+
+ return $tasks;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $vars
+ *
+ * @return string
+ */
+ public static function makeUpgradeKey( $vars ) {
+
+ // The following settings influence the "shape" of the tables required
+ // therefore use the content to compute a key that reflects any
+ // changes to them
+
+ // Only recognize those properties that require a fixed table
+ $pageSpecialProperties = array_intersect(
+ $vars['smwgPageSpecialProperties'],
+ PropertyTableInfoFetcher::getFixedSpecialPropertyList()
+ );
+
+ // Sort to ensure the key contains the same order
+ sort( $vars['smwgFixedProperties'] );
+ sort( $pageSpecialProperties );
+
+ return sha1(
+ json_encode(
+ [
+ $vars['smwgUpgradeKey'],
+ $vars['smwgFixedProperties'],
+ $pageSpecialProperties
+ ]
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $vars
+ * @param MessageReporter $messageReporter|null
+ * @param File $file|null
+ */
+ public static function setUpgradeKey( $vars, MessageReporter $messageReporter = null, File $file = null ) {
+
+ // #3563, Use the specific wiki-id as identifier for the instance in use
+ $key = self::makeUpgradeKey( $vars );
+ $id = Site::id();
+
+ if (
+ isset( $vars['smw.json'][$id]['upgrade_key'] ) &&
+ $key === $vars['smw.json'][$id]['upgrade_key'] ) {
+ return false;
+ }
+
+ if ( $messageReporter !== null ) {
+ $messageReporter->reportMessage( "\nSetting $id upgrade key ..." );
+ }
+
+ self::setUpgradeFile( $vars, [ 'upgrade_key' => $key ], $file );
+
+ if ( $messageReporter !== null ) {
+ $messageReporter->reportMessage( "\n ... done.\n" );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param File $file
+ * @param array $vars
+ */
+ public static function setUpgradeFile( $vars, $args = [], File $file = null ) {
+
+ $configFile = $vars['smwgConfigFileDir'] . '/.smw.json';
+
+ if ( $file === null ) {
+ $file = new File();
+ }
+
+ $id = Site::id();
+
+ if ( !isset( $vars['smw.json'] ) ) {
+ $vars['smw.json'] = [];
+ }
+
+ foreach ( $args as $key => $value ) {
+ $vars['smw.json'][$id][$key] = $value;
+ }
+
+ try {
+ $file->write(
+ $configFile,
+ json_encode( $vars['smw.json'], JSON_PRETTY_PRINT )
+ );
+ } catch( FileNotWritableException $e ) {
+ // Users may not have `wgShowExceptionDetails` enabled and would
+ // therefore not see the exception error message hence we fail hard
+ // and die
+ die(
+ "\n\nERROR: " . $e->getMessage() . "\n" .
+ "\n The \"smwgConfigFileDir\" setting should point to a" .
+ "\n directory that is persistent and writable!\n"
+ );
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $message
+ */
+ public function reportMessage( $message ) {
+ ob_start();
+ print $message;
+ ob_flush();
+ flush();
+ ob_end_clean();
+ }
+
+ private function newMessageReporter( $verbose = true ) {
+
+ if ( $this->messageReporter !== null && !$this->options->safeGet( self::OPT_SCHEMA_UPDATE, false ) ) {
+ return $this->messageReporter;
+ }
+
+ $messageReporterFactory = MessageReporterFactory::getInstance();
+
+ if ( !$verbose ) {
+ $messageReporter = $messageReporterFactory->newNullMessageReporter();
+ } else {
+ $messageReporter = $messageReporterFactory->newObservableMessageReporter();
+ $messageReporter->registerReporterCallback( [ $this, 'reportMessage' ] );
+ }
+
+ return $messageReporter;
+ }
+
+ private function table_optimization( $messageReporter ) {
+
+ if ( !$this->options->safeGet( self::OPT_TABLE_OPTIMIZE, false ) ) {
+ return $messageReporter->reportMessage( "\nSkipping the table optimization.\n" );
+ }
+
+ $messageReporter->reportMessage( "\nRunning table optimization (this may take a moment) ...\n\n" );
+
+ foreach ( $this->tableSchemaManager->getTables() as $table ) {
+ $this->tableBuilder->optimize( $table );
+ }
+
+ $messageReporter->reportMessage( "\nOptimization completed.\n" );
+ }
+
+ private function supplement_jobs( $messageReporter ) {
+
+ if ( !$this->options->safeGet( self::OPT_SUPPLEMENT_JOBS, false ) ) {
+ return $messageReporter->reportMessage( "\nSkipping supplement job creation.\n" );
+ }
+
+ $messageReporter->reportMessage( "\nAdding property statistics rebuild job ...\n" );
+
+ $title = \Title::newFromText( 'SMW\SQLStore\Installer' );
+
+ $job = new PropertyStatisticsRebuildJob(
+ $title,
+ PropertyStatisticsRebuildJob::newRootJobParams( 'smw.propertyStatisticsRebuild', $title ) + [ 'waitOnCommandLine' => true ]
+ );
+
+ $job->insert();
+
+ $messageReporter->reportMessage( " ... done.\n" );
+ $messageReporter->reportMessage( "\nAdding entity disposer job ...\n" );
+
+ $job = new EntityIdDisposerJob(
+ $title,
+ EntityIdDisposerJob::newRootJobParams( 'smw.entityIdDisposer', $title ) + [ 'waitOnCommandLine' => true ]
+ );
+
+ $job->insert();
+
+ $messageReporter->reportMessage( " ... done.\n" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/CachedListLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/CachedListLookup.php
new file mode 100644
index 00000000..d15bca3a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/CachedListLookup.php
@@ -0,0 +1,200 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+use Onoi\Cache\Cache;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class CachedListLookup implements ListLookup {
+
+ const VERSION = '0.2';
+
+ /**
+ * @var ListLookup
+ */
+ private $listLookup;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var stdClass
+ */
+ private $cacheOptions;
+
+ /**
+ * @var boolean
+ */
+ private $isFromCache = false;
+
+ /**
+ * @var integer
+ */
+ private $timestamp;
+
+ /**
+ * @var string
+ */
+ private $cachePrefix = 'smw:store:lookup:';
+
+ /**
+ * @since 2.2
+ *
+ * @param ListLookup $listLookup
+ * @param Cache $cache
+ * @param stdClass $cacheOptions
+ */
+ public function __construct( ListLookup $listLookup, Cache $cache, \stdClass $cacheOptions ) {
+ $this->listLookup = $listLookup;
+ $this->cache = $cache;
+ $this->cacheOptions = $cacheOptions;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $cachePrefix
+ */
+ public function setCachePrefix( $cachePrefix ) {
+ $this->cachePrefix = $cachePrefix . ':' . $this->cachePrefix;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function fetchList() {
+
+ list( $key, $optionsKey ) = $this->getCacheKey( $this->listLookup->getHash() );
+
+ if ( $this->cacheOptions->useCache && ( ( $result = $this->tryFetchFromCache( $key, $optionsKey ) ) !== null ) ) {
+ return $result;
+ }
+
+ $list = $this->listLookup->fetchList();
+
+ $this->saveToCache(
+ $key,
+ $optionsKey,
+ $list,
+ $this->listLookup->getTimestamp(),
+ $this->cacheOptions->ttl
+ );
+
+ return $list;
+ }
+
+ /**
+ * FIXME NEEDS TO BE REMOVED QUICK
+ * https://github.com/wikimedia/mediawiki-extensions-SemanticForms/blob/master/specials/SF_CreateTemplate.php#L36
+ */
+ public function runCollector() {
+ return $this->fetchList();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function isFromCache() {
+ return $this->isFromCache;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return integer
+ */
+ public function getTimestamp() {
+ return $this->timestamp;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getHash() {
+ return $this->listLookup->getHash();
+ }
+
+ /**
+ * @since 2.3
+ */
+ public function deleteCache() {
+
+ list( $id, $optionsKey ) = $this->getCacheKey(
+ $this->listLookup->getHash()
+ );
+
+ $data = unserialize( $this->cache->fetch( $id ) );
+
+ if ( $data && $data !== [] ) {
+ foreach ( $data as $key => $value ) {
+ $this->cache->delete( $key );
+ }
+ }
+
+ $this->cache->delete( $id );
+ }
+
+ private function tryFetchFromCache( $key, $optionsKey ) {
+
+ if ( !$this->cache->contains( $key ) ) {
+ return null;
+ }
+
+ $data = unserialize( $this->cache->fetch( $optionsKey ) );
+
+ if ( $data === [] ) {
+ return null;
+ }
+
+ $this->isFromCache = true;
+ $this->timestamp = $data['time'];
+
+ return $data['list'];
+ }
+
+ private function saveToCache( $key, $optionsKey, $list, $time, $ttl ) {
+
+ $this->timestamp = $time;
+ $this->isFromCache = false;
+
+ // Collect the options keys
+ $data = unserialize( $this->cache->fetch( $key ) );
+ $data[$optionsKey] = true;
+ $this->cache->save( $key, serialize( $data ), $ttl );
+
+ $data = [
+ 'time' => $this->timestamp,
+ 'list' => $list
+ ];
+
+ $this->cache->save( $optionsKey, serialize( $data ), $ttl );
+ }
+
+ private function getCacheKey( $id ) {
+
+ $optionsKey = '';
+
+ if ( strpos( $id, '#' ) !== false ) {
+ list( $id, $optionsKey ) = explode( '#', $id, 2 );
+ }
+
+ return [
+ $this->cachePrefix . md5( $id . self::VERSION ),
+ $this->cachePrefix . md5( $id . $optionsKey . self::VERSION )
+ ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/ListLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/ListLookup.php
new file mode 100644
index 00000000..5772c841
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/ListLookup.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+/**
+ * A simple interface for fetching a list from either a DB or being used as
+ * decorator to cache results
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+interface ListLookup {
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function fetchList();
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function isFromCache();
+
+ /**
+ * A unique identifier that can describe a specific lookup instance to
+ * distinguish it from other lookup's of the same list
+ *
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getHash();
+
+ /**
+ * @since 2.2
+ *
+ * @return integer
+ */
+ public function getTimestamp();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/PropertyLabelSimilarityLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/PropertyLabelSimilarityLookup.php
new file mode 100644
index 00000000..16bef594
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/PropertyLabelSimilarityLookup.php
@@ -0,0 +1,318 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+use Exception;
+use SMW\ApplicationFactory;
+use SMW\DataValueFactory;
+use SMW\DIProperty;
+use SMW\PropertySpecificationLookup;
+use SMW\RequestOptions;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyLabelSimilarityLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertySpecificationLookup
+ */
+ private $propertySpecificationLookup;
+
+ /**
+ * @var integer/float
+ */
+ private $threshold = 50;
+
+ /**
+ * @var DIProperty|null
+ */
+ private $exemptionProperty;
+
+ /**
+ * @var integer
+ */
+ private $lookupCount = 0;
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param PropertySpecificationLookup|null $propertySpecificationLookup
+ */
+ public function __construct( Store $store, PropertySpecificationLookup $propertySpecificationLookup = null ) {
+ $this->store = $store;
+ $this->propertySpecificationLookup = $propertySpecificationLookup;
+
+ if ( $this->propertySpecificationLookup === null ) {
+ $this->propertySpecificationLookup = ApplicationFactory::getInstance()->getPropertySpecificationLookup();
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $threshold
+ *
+ * @return boolean
+ */
+ public function setThreshold( $threshold ) {
+ $this->threshold = $threshold;
+ }
+
+ /**
+ * @note A property that when annotated as part of a property specification
+ * will be used as exemption marker during the similarity comparison.
+ *
+ * @since 2.5
+ *
+ * @param string $exemptionProperty
+ */
+ public function setExemptionProperty( $exemptionProperty ) {
+
+ if ( $exemptionProperty === '' ) {
+ return;
+ }
+
+ $this->exemptionProperty = DataValueFactory::getInstance()->newPropertyValueByLabel( $exemptionProperty )->getDataItem();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DIProperty|null
+ */
+ public function getExemptionProperty() {
+ return $this->exemptionProperty;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return integer
+ */
+ public function getLookupCount() {
+ return $this->lookupCount;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return integer
+ */
+ public function getPropertyMaxCount() {
+ $statistics = $this->store->getStatistics();
+
+ if ( isset( $statistics['TOTALPROPS'] ) ) {
+ return $statistics['TOTALPROPS'];
+ }
+
+ return 0;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return array
+ */
+ public function compareAndFindLabels( RequestOptions $requestOptions = null ) {
+
+ $withType = false;
+ $propertyList = $this->getPropertyList( $requestOptions );
+
+ if ( $requestOptions !== null ) {
+ foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
+ if ( isset( $extraCondition['type'] ) ) {
+ $withType = $extraCondition['type'];
+ }
+ }
+ }
+
+ $this->lookupCount = count( $propertyList );
+ $similarities = $this->matchLabels( $propertyList, $withType );
+
+ usort( $similarities, function ( $a, $b ) {
+ return $a['similarity'] < $b['similarity'];
+ } );
+
+ return $similarities;
+ }
+
+ private function matchLabels( $propertyList, $withType ) {
+
+ $similarities = [];
+ $lookupComplete = [];
+
+ foreach ( $propertyList as $first ) {
+
+ if ( !$first->isUserDefined() ) {
+ continue;
+ }
+
+ foreach ( $propertyList as $second ) {
+
+ // Was already completed when used as first element
+ if ( isset( $lookupComplete[$second->getKey()] ) ) {
+ continue;
+ }
+
+ if ( $first->getKey() === $second->getKey() || !$second->isUserDefined() ) {
+ continue;
+ }
+
+ $hash = $this->getHash( $first, $second );
+
+ if ( $this->isExempted( $first, $second ) || isset( $similarities[$hash] ) ) {
+ continue;
+ }
+
+ $percent = '';
+
+ similar_text( $first->getLabel(), $second->getLabel(), $percent );
+
+ if ( $percent >= $this->threshold ) {
+ $similarities[$hash] = $this->getSummary( $first, $second, $percent, $withType );
+ }
+ }
+
+ $lookupComplete[$first->getKey()] = true;
+ }
+
+ return $similarities;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $first
+ * @param DIProperty $second
+ *
+ * @return boolean
+ */
+ private function isExempted( DIProperty $first, DIProperty $second ) {
+
+ if ( $this->exemptionProperty === null ) {
+ return false;
+ }
+
+ $definedBy = $this->propertySpecificationLookup->getSpecification(
+ $first,
+ $this->exemptionProperty
+ );
+
+ foreach ( $definedBy as $dataItem ) {
+ if ( $dataItem->equals( $second->getCanonicalDiWikiPage() ) ) {
+ return true;
+ }
+ }
+
+ $definedBy = $this->propertySpecificationLookup->getSpecification(
+ $second,
+ $this->exemptionProperty
+ );
+
+ foreach ( $definedBy as $dataItem ) {
+ if ( $dataItem->equals( $first->getCanonicalDiWikiPage() ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function getHash( DIProperty $first, DIProperty $second ) {
+
+ $hashing = [];
+ $hashing[] = $first->getKey();
+ $hashing[] = $second->getKey();
+
+ sort( $hashing );
+
+ return md5( implode( '', $hashing ) );
+ }
+
+ private function getSummary( DIProperty $first, DIProperty $second, $percent, $withType ) {
+
+ $summary = [];
+
+ if ( $withType ) {
+ $summary[] = [
+ 'label' => $first->getLabel(),
+ 'type' => $first->findPropertyTypeID()
+ ];
+ } else {
+ $summary[] = $first->getLabel();
+ }
+
+ if ( $withType ) {
+ $summary[] = [
+ 'label' => $second->getLabel(),
+ 'type' => $second->findPropertyTypeID()
+ ];
+ } else {
+ $summary[] = $second->getLabel();
+ }
+
+ return [
+ 'property' => $summary,
+ 'similarity' => round( $percent, 2 )
+ ];
+ }
+
+ private function getPropertyList( RequestOptions $requestOptions = null ) {
+
+ $propertyList = [];
+
+ // the query needs to do the filtering of internal properties, else LIMIT is wrong
+ $options = [ 'ORDER BY' => 'smw_sort' ];
+
+ $conditions = [
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_iw' => '',
+ 'smw_subobject' => ''
+ ];
+
+ if ( $requestOptions !== null && $requestOptions->getLimit() > 0 ) {
+ $options['LIMIT'] = $requestOptions->getLimit();
+ $options['OFFSET'] = max( $requestOptions->getOffset(), 0 );
+ }
+
+ if ( $requestOptions !== null && $requestOptions->getStringConditions() ) {
+ $conditions[] = $this->store->getSQLConditions( $requestOptions, '', 'smw_sortkey', false );
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $res = $connection->select(
+ SQLStore::ID_TABLE,
+ [ 'smw_id', 'smw_title' ],
+ $conditions,
+ __METHOD__,
+ $options
+ );
+
+ foreach ( $res as $row ) {
+
+ try {
+ $propertyList[] = new DIProperty( str_replace( ' ', '_', $row->smw_title ) );
+ } catch ( Exception $e ) {
+ // Do nothing ...
+ }
+ }
+
+ return $propertyList;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/PropertyUsageListLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/PropertyUsageListLookup.php
new file mode 100644
index 00000000..819d92e6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/PropertyUsageListLookup.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+use RuntimeException;
+use SMW\DIProperty;
+use SMW\Exception\PropertyLabelNotResolvedException;
+use SMW\SQLStore\PropertyStatisticsStore;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use SMWDIError as DIError;
+use SMWRequestOptions as RequestOptions;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class PropertyUsageListLookup implements ListLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertyStatisticsStore
+ */
+ private $propertyStatisticsStore;
+
+ /**
+ * @var RequestOptions
+ */
+ private $requestOptions;
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param PropertyStatisticsStore $propertyStatisticsStore
+ * @param RequestOptions $requestOptions|null
+ */
+ public function __construct( Store $store, PropertyStatisticsStore $propertyStatisticsStore, RequestOptions $requestOptions = null ) {
+ $this->store = $store;
+ $this->propertyStatisticsStore = $propertyStatisticsStore;
+ $this->requestOptions = $requestOptions;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return DIProperty[]
+ * @throws RuntimeException
+ */
+ public function fetchList() {
+
+ if ( $this->requestOptions === null ) {
+ throw new RuntimeException( "Missing requestOptions" );
+ }
+
+ return $this->getPropertyList( $this->doQueryPropertyTable() );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function isFromCache() {
+ return false;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return integer
+ */
+ public function getTimestamp() {
+ return wfTimestamp( TS_UNIX );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getHash() {
+ return __METHOD__ . '#' . ( $this->requestOptions !== null ? $this->requestOptions->getHash() : '' );
+ }
+
+ private function doQueryPropertyTable() {
+
+ // the query needs to do the filtering of internal properties, else LIMIT is wrong
+ $options = [ 'ORDER BY' => 'smw_sort' ];
+ $search_field = 'smw_sortkey';
+
+ $conditions = [
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_iw' => '',
+ 'smw_subobject' => ''
+ ];
+
+ if ( $this->requestOptions->limit > 0 ) {
+ $options['LIMIT'] = $this->requestOptions->limit;
+ $options['OFFSET'] = max( $this->requestOptions->offset, 0 );
+ }
+
+ if ( $this->requestOptions->getOption( RequestOptions::SEARCH_FIELD ) ) {
+ $search_field = $this->requestOptions->getOption( RequestOptions::SEARCH_FIELD );
+ }
+
+ if ( $this->requestOptions->getStringConditions() ) {
+ $conditions[] = $this->store->getSQLConditions( $this->requestOptions, '', $search_field, false );
+ }
+
+ $db = $this->store->getConnection( 'mw.db' );
+
+ $res = $db->select(
+ [ $db->tableName( SQLStore::ID_TABLE ), $db->tableName( SQLStore::PROPERTY_STATISTICS_TABLE ) ],
+ [ 'smw_id', 'smw_title', 'usage_count' ],
+ $conditions,
+ __METHOD__,
+ $options,
+ [ $db->tableName( SQLStore::ID_TABLE ) => [ 'INNER JOIN', [ 'smw_id=p_id' ] ] ]
+ );
+
+ return $res;
+ }
+
+ private function getPropertyList( $res ) {
+
+ $result = [];
+
+ foreach ( $res as $row ) {
+
+ try {
+ $property = new DIProperty( str_replace( ' ', '_', $row->smw_title ) );
+ } catch ( PropertyLabelNotResolvedException $e ) {
+ $property = new DIError( new \Message( 'smw_noproperty', [ $row->smw_title ] ) );
+ }
+
+ $property->id = isset( $row->smw_id ) ? $row->smw_id : -1;
+ $result[] = [ $property, (int)$row->usage_count ];
+ }
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/ProximityPropertyValueLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/ProximityPropertyValueLookup.php
new file mode 100644
index 00000000..04610184
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/ProximityPropertyValueLookup.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Store;
+use SMW\DataTypeRegistry;
+use SMW\DataValueFactory;
+use SMW\RequestOptions;
+use SMW\SQLStore\SQLStore;
+use SMWDataItem as DataItem;
+use SMWDITime as DITime;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ProximityPropertyValueLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ * @param $search,
+ * @param RequestOptions $opts
+ *
+ * @return array
+ */
+ public function lookup( DIProperty $property, $search, RequestOptions $opts ) {
+ return $this->fetchFromTable( $property, $search, $opts );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIProperty $property
+ * @param $search,
+ * @param RequestOptions $opts
+ *
+ * @return array
+ */
+ public function fetchFromTable( DIProperty $property, $search, RequestOptions $opts ) {
+
+ $options = [];
+ $list = [];
+
+ $table = $this->store->findPropertyTableID(
+ $property
+ );
+
+ $pid = $this->store->getObjectIds()->getSMWPropertyID( $property );
+ $continueOffset = 0;
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $query = $connection->newQuery();
+
+ $query->type( 'SELECT' );
+ $query->table( $table );
+
+ list( $field, $diType ) = $this->getField( $property );
+
+ // look ahead +1
+ $limit = $opts->getLimit() + 1;
+ $offset = $opts->getOffset();
+ $sort = $opts->sort;
+
+ $options = [
+ 'LIMIT' => $limit,
+ 'OFFSET' => $offset
+ ];
+
+ if ( $diType === DataItem::TYPE_WIKIPAGE ) {
+ return $this->fetchFromIDTable( $query, $pid, $table, $field, $options, $search, $sort, $limit, $offset );
+ }
+
+ $query->field( $field );
+
+ if ( trim( $search ) !== '' ) {
+ if ( $diType === DataItem::TYPE_BLOB || $diType === DataItem::TYPE_URI ) {
+ $this->build_like( $query, $field, $search );
+ } else {
+ $query->condition( $query->like( $field, '%' . $search . '%' ) );
+ }
+ } else {
+ $query->condition( $query->neq( $field, 'NULL' ) );
+ }
+
+ if ( $this->isFixedPropertyTable( $table ) === false ) {
+ $query->condition( $query->asAnd( $query->eq( 'p_id', $pid ) ) );
+
+ // To make the MySQL query planner happy to pick the right index!
+ $query->field( 'p_id' );
+ }
+
+ if ( $sort ) {
+ $options['ORDER BY'] = "$field $sort";
+ }
+
+ $options['DISTINCT'] = true;
+
+ $query->options( $options );
+
+ $res = $connection->query(
+ $query,
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+
+ $value = $row->{$field};
+
+ // The internal serialization doesn't mean much to a user so
+ // transformed it!
+ if ( $diType === DataItem::TYPE_TIME ) {
+ $value = DataValueFactory::getInstance()->newDataValueByItem(
+ DITime::doUnserialize( $value ), $property )->getWikiValue();
+ }
+
+ $list[] = $value;
+ }
+
+ return $list;
+ }
+
+ private function fetchFromIDTable( $query, $pid, $table, $field, $options, $search, $sort, $limit, $offset ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $continueOffset = 0;
+ $res = [];
+
+ if ( trim( $search ) !== '' ) {
+ $this->build_like( $query, 'smw_sortkey', $search );
+ }
+
+ if ( $sort ) {
+ $options['ORDER BY'] = "smw_title $sort";
+ }
+
+ $options['DISTINCT'] = true;
+
+ $query->options( $options );
+ $query->fields( [ 'smw_id', 'smw_title', 'smw_sortkey' ] );
+
+ // Benchmarks showed that different select schema yield better results
+ // for the following use cases
+ if ( $this->isFixedPropertyTable( $table ) === false && $search !== '' ) {
+
+ /**
+ * SELECT DISTINCT smw_id,smw_title,smw_sortkey
+ * FROM `smw_object_ids`
+ * INNER JOIN (
+ * SELECT o_id FROM `smw_di_wikipage` WHERE p_id='310167' GROUP BY o_id
+ * ) AS t1 ON t1.o_id=smw_id
+ * WHERE ( smw_sortkey LIKE '%foo%' OR smw_sortkey LIKE '%Foo%' OR smw_sortkey LIKE '%FOO%')
+ * LIMIT 11
+ */
+
+ $query->table( SQLStore::ID_TABLE );
+
+ $query->join(
+ 'INNER JOIN',
+ '( SELECT o_id FROM ' . $connection->tableName( $table ) .
+ ' WHERE p_id=' . $connection->addQuotes( $pid ) .
+ ' GROUP BY o_id )' .
+ ' AS t1 ON t1.o_id=smw_id'
+ );
+
+ } elseif ( $this->isFixedPropertyTable( $table ) === false ) {
+
+ $query->condition( $query->asAnd( $query->eq( 'p_id', $pid ) ) );
+
+ // To make the MySQL query planner happy to pick the right index!
+ $query->field( 'p_id' );
+
+ $query->join(
+ 'INNER JOIN',
+ [ SQLStore::ID_TABLE => 'ON (smw_id=o_id)' ]
+ );
+
+ } else {
+
+ /**
+ * SELECT DISTINCT smw_id,smw_title,smw_sortkey
+ * FROM `smw_fpt_sobj`
+ * INNER JOIN `smw_object_ids` ON ((smw_id=o_id))
+ * WHERE ( smw_sortkey LIKE '%foo%' OR smw_sortkey LIKE '%Foo%' OR smw_sortkey LIKE '%FOO%' )
+ * LIMIT 11
+ */
+ $query->join(
+ 'INNER JOIN',
+ [ SQLStore::ID_TABLE => 'ON (smw_id=o_id)' ]
+ );
+ }
+
+ $res = $connection->query(
+ $query,
+ __METHOD__
+ );
+
+ $list = [];
+
+ foreach ( $res as $row ) {
+ $list[] = str_replace( '_', ' ', $row->smw_title );
+ }
+
+ return $list;
+ }
+
+ private function isFixedPropertyTable( $table ) {
+
+ $propertyTables = $this->store->getPropertyTables();
+
+ foreach ( $propertyTables as $propertyTable ) {
+ if ( $propertyTable->getName() === $table ) {
+ return $propertyTable->isFixedPropertyTable();
+ }
+ }
+
+ return false;
+ }
+
+ private function getField( $property ) {
+
+ $typeId = $property->findPropertyTypeID();
+ $diType = DataTypeRegistry::getInstance()->getDataItemId( $typeId );
+
+ $diHandler = $this->store->getDataItemHandlerForDIType(
+ $diType
+ );
+
+ return [ $diHandler->getLabelField(), $diType ];
+ }
+
+ private function build_like( $query, $field, $search ) {
+
+ $conds = [
+ '%' . $search . '%',
+ '%' . ucfirst( $search ) . '%',
+ '%' . strtoupper( $search ) . '%'
+ ] + ( $search !== strtolower( $search ) ? [ '%' . strtolower( $search ) . '%' ] : [] );
+
+ $cond = [];
+
+ foreach ( $conds as $c ) {
+ $query->condition( $query->asOr( $query->like( $field, $c ) ) );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/RedirectTargetLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/RedirectTargetLookup.php
new file mode 100644
index 00000000..b3aff967
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/RedirectTargetLookup.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Store;
+use SMW\Utils\CircularReferenceGuard;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class RedirectTargetLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var CircularReferenceGuard
+ */
+ private $circularReferenceGuard;
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ * @param CircularReferenceGuard $circularReferenceGuard
+ */
+ public function __construct( Store $store, CircularReferenceGuard $circularReferenceGuard ) {
+ $this->store = $store;
+ $this->circularReferenceGuard = $circularReferenceGuard;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param $dataItem
+ *
+ * @return DataItem
+ */
+ public function findRedirectTarget( $dataItem ) {
+
+ if ( !$dataItem instanceof DIWikiPage && !$dataItem instanceof DIProperty ) {
+ return $dataItem;
+ }
+
+ $hash = $dataItem->getSerialization();
+
+ // Guard against a dataItem that points to itself
+ $this->circularReferenceGuard->mark( $hash );
+
+ if ( !$this->circularReferenceGuard->isCircular( $hash ) ) {
+ $dataItem = $this->store->getRedirectTarget( $dataItem );
+ }
+
+ $this->circularReferenceGuard->unmark( $hash );
+
+ return $dataItem;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UndeclaredPropertyListLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UndeclaredPropertyListLookup.php
new file mode 100644
index 00000000..d5c8435c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UndeclaredPropertyListLookup.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+use RuntimeException;
+use SMW\DIProperty;
+use SMW\Exception\PropertyLabelNotResolvedException;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use SMWDIError as DIError;
+use SMWRequestOptions as RequestOptions;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ * @author Nischay Nahata
+ */
+class UndeclaredPropertyListLookup implements ListLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var string
+ */
+ private $defaultPropertyType;
+
+ /**
+ * @var RequestOptions
+ */
+ private $requestOptions;
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param string $defaultPropertyType
+ * @param RequestOptions $requestOptions|null
+ */
+ public function __construct( Store $store, $defaultPropertyType, RequestOptions $requestOptions = null ) {
+ $this->store = $store;
+ $this->defaultPropertyType = $defaultPropertyType;
+ $this->requestOptions = $requestOptions;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return DIProperty[]
+ * @throws RuntimeException
+ */
+ public function fetchList() {
+
+ if ( $this->requestOptions === null ) {
+ throw new RuntimeException( "Missing requestOptions" );
+ }
+
+ // Wanted Properties must have the default type
+ $propertyTable = $this->getPropertyTableForType( $this->defaultPropertyType );
+
+ if ( $propertyTable->isFixedPropertyTable() ) {
+ return [];
+ }
+
+ return $this->buildPropertyList( $this->selectPropertiesFromTable( $propertyTable ) );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function isFromCache() {
+ return false;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return integer
+ */
+ public function getTimestamp() {
+ return wfTimestamp( TS_UNIX );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getHash() {
+ return __METHOD__ . '#' . ( $this->requestOptions !== null ? $this->requestOptions->getHash() : '' );
+ }
+
+ private function selectPropertiesFromTable( $propertyTable ) {
+
+ $options = $this->store->getSQLOptions( $this->requestOptions, 'title' );
+ $idTable = SQLStore::ID_TABLE;
+
+ $options['ORDER BY'] = 'count DESC';
+
+ // Postgres Error: 42803 ERROR: ...smw_title must appear in the GROUP BY
+ // clause or be used in an aggregate function
+ $options['GROUP BY'] = 'smw_id, smw_title';
+
+ $conditions = [
+ 'smw_id > ' . SQLStore::FIXED_PROPERTY_ID_UPPERBOUND,
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_proptable_hash IS NULL',
+ 'smw_iw' => '',
+ 'smw_subobject' => ''
+ ];
+
+ $joinCond = 'p_id';
+
+ foreach ( $this->requestOptions->getExtraConditions() as $extaCondition ) {
+ if ( isset( $extaCondition['filter.unapprove'] ) ) {
+ $joinCond = 'o_id';
+ }
+ }
+
+ $res = $this->store->getConnection( 'mw.db' )->select(
+ [ $idTable, $propertyTable->getName() ],
+ [ 'smw_id', 'smw_title', 'COUNT(*) as count' ],
+ $conditions,
+ __METHOD__,
+ $options,
+ [
+ $idTable => [
+ 'INNER JOIN', "$joinCond=smw_id"
+ ]
+ ]
+ );
+
+ return $res;
+ }
+
+ private function buildPropertyList( $res ) {
+
+ $result = [];
+
+ foreach ( $res as $row ) {
+ $result[] = [ $this->addPropertyFor( $row->smw_title ), $row->count ];
+ }
+
+ return $result;
+ }
+
+ private function addPropertyFor( $title ) {
+
+ try {
+ $property = new DIProperty( $title );
+ } catch ( PropertyLabelNotResolvedException $e ) {
+ $property = new DIError( new \Message( 'smw_noproperty', [ $title ] ) );
+ }
+
+ return $property;
+ }
+
+ private function getPropertyTableForType( $type ) {
+
+ $propertyTables = $this->store->getPropertyTables();
+ $tableIdForType = $this->store->findTypeTableId( $type );
+
+ if ( isset( $propertyTables[$tableIdForType] ) ) {
+ return $propertyTables[$tableIdForType];
+ }
+
+ throw new RuntimeException( "Tried to access a table that doesn't exist for {$type}." );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UnusedPropertyListLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UnusedPropertyListLookup.php
new file mode 100644
index 00000000..ca3b8285
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UnusedPropertyListLookup.php
@@ -0,0 +1,154 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+use RuntimeException;
+use SMW\DIProperty;
+use SMW\Exception\PropertyLabelNotResolvedException;
+use SMW\SQLStore\PropertyStatisticsStore;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use SMWDIError as DIError;
+use SMWRequestOptions as RequestOptions;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ * @author Nischay Nahata
+ */
+class UnusedPropertyListLookup implements ListLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertyStatisticsStore
+ */
+ private $propertyStatisticsStore;
+
+ /**
+ * @var RequestOptions
+ */
+ private $requestOptions;
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param PropertyStatisticsStore $propertyStatisticsStore
+ * @param RequestOptions $requestOptions|null
+ */
+ public function __construct( Store $store, PropertyStatisticsStore $propertyStatisticsStore, RequestOptions $requestOptions = null ) {
+ $this->store = $store;
+ $this->propertyStatisticsStore = $propertyStatisticsStore;
+ $this->requestOptions = $requestOptions;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return DIProperty[]
+ * @throws RuntimeException
+ */
+ public function fetchList() {
+
+ if ( $this->requestOptions === null ) {
+ throw new RuntimeException( "Missing requestOptions" );
+ }
+
+ return $this->buildPropertyList( $this->selectPropertiesFromTable() );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function isFromCache() {
+ return false;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return integer
+ */
+ public function getTimestamp() {
+ return wfTimestamp( TS_UNIX );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getHash() {
+ return __METHOD__ . '#' . ( $this->requestOptions !== null ? $this->requestOptions->getHash() : '' );
+ }
+
+ private function selectPropertiesFromTable() {
+
+ // the query needs to do the filtering of internal properties, else LIMIT is wrong
+ $options = [ 'ORDER BY' => 'smw_sort' ];
+
+ if ( $this->requestOptions->limit > 0 ) {
+ $options['LIMIT'] = $this->requestOptions->limit;
+ $options['OFFSET'] = max( $this->requestOptions->offset, 0 );
+ }
+
+ $conditions = [
+ "smw_title NOT LIKE '\_%'", // #2182, exclude predefined properties
+ 'smw_id > ' . SQLStore::FIXED_PROPERTY_ID_UPPERBOUND,
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_iw' => '',
+ 'smw_subobject' => '',
+ 'smw_proptable_hash IS NOT NULL'
+ ];
+
+ $conditions['usage_count'] = 0;
+
+ if ( $this->requestOptions->getStringConditions() ) {
+ $conditions[] = $this->store->getSQLConditions( $this->requestOptions, '', 'smw_sortkey', false );
+ }
+
+ $idTable = $this->store->getObjectIds()->getIdTable();
+
+ $res = $this->store->getConnection( 'mw.db' )->select(
+ [ $idTable ,$this->propertyStatisticsStore->getStatisticsTable() ],
+ [ 'smw_title', 'usage_count' ],
+ $conditions,
+ __METHOD__,
+ $options,
+ [ $idTable => [ 'INNER JOIN', [ 'smw_id=p_id' ] ] ]
+ );
+
+ return $res;
+ }
+
+ private function buildPropertyList( $res ) {
+
+ $result = [];
+
+ foreach ( $res as $row ) {
+ $result[] = $this->addPropertyFor( $row->smw_title );
+ }
+
+ return $result;
+ }
+
+ private function addPropertyFor( $title ) {
+
+ try {
+ $property = new DIProperty( $title );
+ } catch ( PropertyLabelNotResolvedException $e ) {
+ $property = new DIError( new \Message( 'smw_noproperty', [ $title ] ) );
+ }
+
+ return $property;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UsageStatisticsListLookup.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UsageStatisticsListLookup.php
new file mode 100644
index 00000000..34529efb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/Lookup/UsageStatisticsListLookup.php
@@ -0,0 +1,352 @@
+<?php
+
+namespace SMW\SQLStore\Lookup;
+
+use RuntimeException;
+use SMW\DIProperty;
+use SMW\SQLStore\PropertyStatisticsStore;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class UsageStatisticsListLookup implements ListLookup {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertyStatisticsStore
+ */
+ private $propertyStatisticsStore;
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param PropertyStatisticsStore $propertyStatisticsStore
+ */
+ public function __construct( Store $store, PropertyStatisticsStore $propertyStatisticsStore ) {
+ $this->store = $store;
+ $this->propertyStatisticsStore = $propertyStatisticsStore;
+ }
+
+ /**
+ * Returns a list with statistical information where keys are matched to:
+ *
+ * - 'PROPUSES': Number of property instances (value assignments) in the connection
+ * - 'USEDPROPS': Number of properties that are used with at least one value
+ * - 'DECLPROPS': Number of properties that have been declared (i.e. assigned a type)
+ * - 'OWNPAGE': Number of properties with their own page
+ * - 'QUERY': Number of inline queries
+ * - 'QUERYSIZE': Represents collective query size
+ * - 'CONCEPTS': Number of declared concepts
+ * - 'SUBOBJECTS': Number of declared subobjects
+ * - 'QUERYFORMATS': Array of used formats and its usage count
+ * - 'TOTALPROPS': Total number of registered properties
+ * - 'ERRORUSES': Number of annotations with an error
+ * - 'DELETECOUNT': Number of "marked for deletion"
+ *
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function fetchList() {
+ return [
+ 'OWNPAGE' => $this->getPropertyPageCount(),
+ 'QUERY' => $this->getQueryCount(),
+ 'QUERYSIZE' => $this->getQuerySize(),
+ 'QUERYFORMATS' => $this->getQueryFormatsCount(),
+ 'CONCEPTS' => $this->getConceptCount(),
+ 'SUBOBJECTS' => $this->getSubobjectCount(),
+ 'DECLPROPS' => $this->getDeclaredPropertiesCount(),
+ 'PROPUSES' => $this->getPropertyUsageCount(),
+ 'USEDPROPS' => $this->getUsedPropertiesCount(),
+ 'TOTALPROPS' => $this->getTotalPropertiesCount(),
+ 'ERRORUSES' => $this->getImproperValueForCount(),
+ 'DELETECOUNT' => $this->getDeleteCount()
+ ];
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function isFromCache() {
+ return false;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return integer
+ */
+ public function getTimestamp() {
+ return wfTimestamp( TS_UNIX );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getHash() {
+ return 'statistics-lookup';
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return number
+ */
+ public function getImproperValueForCount() {
+ return $this->propertyStatisticsStore->getUsageCount(
+ $this->store->getObjectIds()->getSMWPropertyID( new DIProperty( '_ERRP' ) )
+ );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return number
+ */
+ public function getQueryCount() {
+ return $this->count( '_ASK' );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return number
+ */
+ public function getQuerySize() {
+ return $this->count( '_ASKSI' );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return number
+ */
+ public function getConceptCount() {
+ return $this->count( '_CONC' );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return number
+ */
+ public function getSubobjectCount() {
+ return $this->count( DIProperty::TYPE_SUBOBJECT );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return number
+ */
+ public function getDeclaredPropertiesCount() {
+ return $this->count( DIProperty::TYPE_HAS_TYPE );
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return int[]
+ */
+ public function getQueryFormatsCount() {
+ $count = [];
+
+ $res = $this->store->getConnection()->select(
+ $this->findPropertyTableByType( '_ASKFO' )->getName(),
+ 'o_hash, COUNT(s_id) AS count',
+ [],
+ __METHOD__,
+ [
+ 'ORDER BY' => 'count DESC',
+ 'GROUP BY' => 'o_hash'
+ ]
+ );
+
+ foreach ( $res as $row ) {
+ $count[$row->o_hash] = (int)$row->count;
+ }
+
+ return $count;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return number
+ */
+ public function getPropertyPageCount() {
+
+ $options = [];
+
+ // Only match entities that have a NOT null smw_proptable_hash entry
+ // which indicates that it is not a object but a subject value (has
+ // annotations such as `has type` == page was created with ... etc.)
+ $conditions = [
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_iw' => '',
+ 'smw_subobject' => '',
+ 'smw_proptable_hash IS NOT NULL'
+ ];
+
+ $db = $this->store->getConnection( 'mw.db' );
+
+ // Select object ID's against known property ID's that match the conditions
+ $res = $db->select(
+ [ $db->tableName( SQLStore::ID_TABLE ), $db->tableName( SQLStore::PROPERTY_STATISTICS_TABLE ) ],
+ 'smw_id',
+ $conditions,
+ __METHOD__,
+ $options,
+ [ $db->tableName( SQLStore::ID_TABLE ) => [ 'INNER JOIN', [ 'smw_id=p_id' ] ] ]
+ );
+
+ return $res->numRows();
+ }
+
+ /**
+ * Count property uses by summing up property statistics table
+ *
+ * @note subproperties that are part of container values are counted
+ * individually and it does not seem to be important to filter them by
+ * adding more conditions.
+ *
+ * @since 1.9
+ *
+ * @return number
+ */
+ public function getPropertyUsageCount() {
+ $count = 0;
+
+ $row = $this->store->getConnection()->selectRow(
+ [ $this->store->getStatisticsTable() ],
+ 'SUM( usage_count ) AS count',
+ [],
+ __METHOD__
+ );
+
+ $count = $row ? $row->count : $count;
+
+ return (int)$count;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return number
+ */
+ public function getTotalPropertiesCount() {
+
+ $count = 0;
+
+ $conditions = [
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_iw' => '',
+ 'smw_subobject' => ''
+ ];
+
+ $row = $this->store->getConnection()->selectRow(
+ SQLStore::ID_TABLE,
+ 'Count( * ) AS count',
+ $conditions,
+ __METHOD__
+ );
+
+ $count = $row ? $row->count : $count;
+
+ return (int)$count;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @return number
+ */
+ public function getUsedPropertiesCount() {
+
+ $options = [];
+
+ $conditions = [
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_iw' => '',
+ 'smw_subobject' => '',
+ 'usage_count > 0'
+ ];
+
+ $db = $this->store->getConnection( 'mw.db' );
+
+ // Select object ID's against known property ID's that match the conditions
+ $res = $db->select(
+ [ $db->tableName( SQLStore::ID_TABLE ), $db->tableName( SQLStore::PROPERTY_STATISTICS_TABLE ) ],
+ 'smw_id',
+ $conditions,
+ __METHOD__,
+ $options,
+ [
+ $db->tableName( SQLStore::ID_TABLE ) => [ 'INNER JOIN', [ 'smw_id=p_id' ] ]
+ ]
+ );
+
+ return $res->numRows();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return number
+ */
+ public function getDeleteCount() {
+ $count = 0;
+
+ $row = $this->store->getConnection()->selectRow(
+ SQLStore::ID_TABLE,
+ 'Count( * ) AS count',
+ [ 'smw_iw' => ':smw-delete' ],
+ __METHOD__
+ );
+
+ $count = $row ? $row->count : $count;
+
+ return (int)$count;
+ }
+
+ private function count( $type ) {
+
+ $res = $this->store->getConnection()->select(
+ $this->findPropertyTableByType( $type )->getName(),
+ 'COUNT(s_id) AS count',
+ [],
+ __METHOD__
+ );
+
+ $row = $this->store->getConnection()->fetchObject( $res );
+
+ return isset( $row->count ) ? (int)$row->count : 0;
+ }
+
+ private function findPropertyTableByType( $type ) {
+ $propertyTables = $this->store->getPropertyTables();
+
+ $tableIdForType = $this->store->findPropertyTableID( new DIProperty( $type ) );
+
+ if ( isset( $propertyTables[$tableIdForType] ) ) {
+ return $propertyTables[$tableIdForType];
+ }
+
+ throw new RuntimeException( "Tried to access a table that doesn't exist for {$tableIdForType}." );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyStatisticsStore.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyStatisticsStore.php
new file mode 100644
index 00000000..f54bd283
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyStatisticsStore.php
@@ -0,0 +1,369 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use MWException;
+use Psr\Log\LoggerAwareTrait;
+use SMW\MediaWiki\Database;
+use SMW\SQLStore\Exception\PropertyStatisticsInvalidArgumentException;
+
+/**
+ * Simple implementation of PropertyStatisticsTable using MediaWikis
+ * database abstraction layer and a single table.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author Nischay Nahata
+ */
+class PropertyStatisticsStore {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var boolean
+ */
+ private $isCommandLineMode = false;
+
+ /**
+ * @var boolean
+ */
+ private $onTransactionIdle = false;
+
+ /**
+ * @since 1.9
+ *
+ * @param Database $connection
+ */
+ public function __construct( Database $connection ) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:$wgCommandLineMode
+ * Indicates whether MW is running in command-line mode or not.
+ *
+ * @since 2.5
+ *
+ * @param boolean $isCommandLineMode
+ */
+ public function isCommandLineMode( $isCommandLineMode ) {
+ $this->isCommandLineMode = $isCommandLineMode;
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function waitOnTransactionIdle() {
+ $this->onTransactionIdle = !$this->isCommandLineMode;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string
+ */
+ public function getStatisticsTable() {
+ return SQLStore::PROPERTY_STATISTICS_TABLE;
+ }
+
+ /**
+ * Change the usage count for the property of the given ID by the given
+ * value. The method does nothing if the count is 0.
+ *
+ * @since 1.9
+ *
+ * @param integer $propertyId
+ * @param integer $value
+ *
+ * @return boolean Success indicator
+ */
+ public function addToUsageCount( $pid, $value ) {
+
+ $usageVal = 0;
+ $nullVal = 0;
+
+ if ( is_array( $value ) ) {
+ $usageVal = $value[0];
+ $nullVal = $value[1];
+ } else {
+ $usageVal = $value;
+ }
+
+ if ( !is_int( $usageVal ) || !is_int( $nullVal ) ) {
+ throw new PropertyStatisticsInvalidArgumentException( 'The value to add must be an integer' );
+ }
+
+ if ( !is_int( $pid ) || $pid <= 0 ) {
+ throw new PropertyStatisticsInvalidArgumentException( 'The property id to add must be a positive integer' );
+ }
+
+ if ( $usageVal == 0 && $nullVal == 0 ) {
+ return true;
+ }
+
+ try {
+ $this->connection->update(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ [
+ 'usage_count = usage_count ' . ( $usageVal > 0 ? '+ ' : '- ' ) . $this->connection->addQuotes( abs( $usageVal ) ),
+ 'null_count = null_count ' . ( $nullVal > 0 ? '+ ' : '- ' ) . $this->connection->addQuotes( abs( $nullVal ) ),
+ ],
+ [
+ 'p_id' => $pid
+ ],
+ __METHOD__
+ );
+ } catch ( \DBQueryError $e ) {
+ // #2345 Do nothing as it most likely an "Error: 1264 Out of range
+ // value for column" in strict mode
+ // As an unsigned int, we expected it to be 0
+ $this->setUsageCount( $pid, [ 0, 0 ] );
+ }
+
+ return true;
+ }
+
+ /**
+ * Increase the usage counts of multiple properties.
+ *
+ * The $additions parameter should be an array with integer
+ * keys that are property ids, and associated integer values
+ * that are the amount the usage count should be increased.
+ *
+ * @since 1.9
+ *
+ * @param array $additions
+ *
+ * @return boolean Success indicator
+ */
+ public function addToUsageCounts( array $additions ) {
+
+ $success = true;
+
+ if ( $additions === [] ) {
+ return $success;
+ }
+
+ $method = __METHOD__;
+
+ if ( $this->onTransactionIdle ) {
+ $this->connection->onTransactionIdle( function () use( $method, $additions ) {
+ $this->log( $method . ' (onTransactionIdle)' );
+ $this->onTransactionIdle = false;
+ $this->addToUsageCounts( $additions );
+ } );
+
+ return $success;
+ }
+
+ foreach ( $additions as $pid => $addition ) {
+
+ if ( is_array( $addition ) ) {
+ // We don't check this, have it fail in case this isn't set correctly
+ $addition = [ $addition['usage'], $addition['null'] ];
+ }
+
+ $success = $this->addToUsageCount( $pid, $addition ) && $success;
+ }
+
+ return $success;
+ }
+
+ /**
+ * Updates an existing usage count.
+ *
+ * @since 1.9
+ *
+ * @param integer $propertyId
+ * @param integer $value
+ *
+ * @return boolean Success indicator
+ * @throws PropertyStatisticsInvalidArgumentException
+ */
+ public function setUsageCount( $propertyId, $value ) {
+
+ $usageCount = 0;
+ $nullCount = 0;
+
+ if ( is_array( $value ) ) {
+ $usageCount = $value[0];
+ $nullCount = $value[1];
+ } else {
+ $usageCount = $value;
+ }
+
+ if ( !is_int( $usageCount ) || $usageCount < 0 || !is_int( $nullCount ) || $nullCount < 0 ) {
+ throw new PropertyStatisticsInvalidArgumentException( 'The value to add must be a positive integer' );
+ }
+
+ if ( !is_int( $propertyId ) || $propertyId <= 0 ) {
+ throw new PropertyStatisticsInvalidArgumentException( 'The property id to add must be a positive integer' );
+ }
+
+ return $this->connection->update(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ [
+ 'usage_count' => $usageCount,
+ 'null_count' => $nullCount,
+ ],
+ [
+ 'p_id' => $propertyId
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * Adds a new usage count.
+ *
+ * @since 1.9
+ *
+ * @param integer $propertyId
+ * @param integer $value
+ *
+ * @return boolean Success indicator
+ * @throws PropertyStatisticsInvalidArgumentException
+ */
+ public function insertUsageCount( $propertyId, $value ) {
+
+ $usageCount = 0;
+ $nullCount = 0;
+
+ if ( is_array( $value ) ) {
+ $usageCount = $value[0];
+ $nullCount = $value[1];
+ } else {
+ $usageCount = $value;
+ }
+
+ if ( !is_int( $usageCount ) || $usageCount < 0 || !is_int( $nullCount ) || $nullCount < 0 ) {
+ throw new PropertyStatisticsInvalidArgumentException( 'The value to add must be a positive integer' );
+ }
+
+ if ( !is_int( $propertyId ) || $propertyId <= 0 ) {
+ throw new PropertyStatisticsInvalidArgumentException( 'The property id to add must be a positive integer' );
+ }
+
+ try {
+ $this->connection->insert(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ [
+ 'usage_count' => $usageCount,
+ 'null_count' => $nullCount,
+ 'p_id' => $propertyId,
+ ],
+ __METHOD__
+ );
+ } catch ( \DBQueryError $e ) {
+ // Most likely hit "Error: 1062 Duplicate entry ..."
+ $this->setUsageCount( $propertyId, $value );
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the usage count for a provided property id.
+ *
+ * @since 2.2
+ *
+ * @param integer $propertyId
+ *
+ * @return integer
+ */
+ public function getUsageCount( $propertyId ) {
+
+ if ( !is_int( $propertyId ) ) {
+ return 0;
+ }
+
+ $row = $this->connection->selectRow(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ [
+ 'usage_count'
+ ],
+ [
+ 'p_id' => $propertyId,
+ ],
+ __METHOD__
+ );
+
+ return $row !== false ? (int)$row->usage_count : 0;
+ }
+
+ /**
+ * Returns the usage counts of the provided properties.
+ *
+ * The returned array contains integer keys which are property ids,
+ * with the associated values being their usage count (also integers).
+ *
+ * Properties for which no usage count is found will not have
+ * an entry in the result array.
+ *
+ * @since 1.9
+ *
+ * @param array $propertyIds
+ *
+ * @return array
+ */
+ public function getUsageCounts( array $propertyIds ) {
+ if ( $propertyIds === [] ) {
+ return [];
+ }
+
+ $propertyStatistics = $this->connection->select(
+ $this->connection->tablename( SQLStore::PROPERTY_STATISTICS_TABLE ),
+ [
+ 'usage_count',
+ 'p_id',
+ ],
+ [
+ 'p_id' => $propertyIds,
+ ],
+ __METHOD__
+ );
+
+ $usageCounts = [];
+
+ foreach ( $propertyStatistics as $propertyStatistic ) {
+ assert( ctype_digit( $propertyStatistic->p_id ) );
+ assert( ctype_digit( $propertyStatistic->usage_count ) );
+
+ $usageCounts[(int)$propertyStatistic->p_id] = (int)$propertyStatistic->usage_count;
+ }
+
+ return $usageCounts;
+ }
+
+ /**
+ * Deletes all rows in the table.
+ *
+ * @since 1.9
+ *
+ * @return boolean Success indicator
+ */
+ public function deleteAll() {
+ return $this->connection->delete(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ '*',
+ __METHOD__
+ );
+ }
+
+ private function log( $message, $context = [] ) {
+
+ if ( $this->logger === null ) {
+ return;
+ }
+
+ $this->logger->info( $message, $context );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableDefinition.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableDefinition.php
new file mode 100644
index 00000000..9cb77c84
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableDefinition.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use OutOfBoundsException;
+
+/**
+ * Simple data container for storing information about property tables. A
+ * property table is a DB table that is used to store subject-property-value
+ * records about data in SMW. Tables mostly differ in the composition of the
+ * value, but also in whether the property is explicitly named (or fixed),
+ * and in the way subject pages are referred to.
+ *
+ *
+ * @license GNU GPL v2+
+ * @since 1.8
+ *
+ * @author Nischay Nahata
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class PropertyTableDefinition {
+
+ /**
+ * Name of the table in the DB.
+ *
+ * @since 1.8
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * DIType of this table.
+ *
+ * @since 1.8
+ * @var integer
+ */
+ protected $diType;
+
+ /**
+ * If the table is only for one property, this field holds its key.
+ * Empty otherwise. Tables without a fixed property have a column "p_id"
+ * for storing the SMW page id of the property.
+ *
+ * @note It is important that this is the DB key form or special
+ * property key, not the label. This is not checked eagerly in SMW but
+ * can lead to spurious errors when properties are compared to each
+ * other or to the contents of the store.
+ *
+ * @since 1.8
+ * @var string|boolean false
+ */
+ protected $fixedProperty;
+
+ /**
+ * Boolean that states how subjects are stored. If true, a column "s_id"
+ * with an SMW page id is used. If false, two columns "s_title" and
+ * "s_namespace" are used. The latter de-normalized form cannot store
+ * sortkeys and interwiki prefixes, and is used only for the redirect
+ * table. New tables should really keep the default "true" here.
+ *
+ * @since 1.8
+ * @var boolean
+ */
+ protected $idSubject = true;
+
+ /**
+ * Factory method to create an instance for a given
+ * DI type and the given table name.
+ *
+ * @since 1.8
+ *
+ * @param integer $DIType constant
+ * @param string $tableName logocal table name (not the DB version)
+ * @param string|false $fixedProperty property key if any
+ */
+ public function __construct( $DIType, $tableName, $fixedProperty = false ) {
+ $this->name = $tableName;
+ $this->fixedProperty = $fixedProperty;
+ $this->diType = $DIType;
+ }
+
+ /**
+ * Method to return the fields for this table
+ *
+ * @since 1.8
+ *
+ * @param SQLStore $store
+ *
+ * @return array
+ */
+ public function getFields( SQLStore $store ) {
+ $diHandler = $store->getDataItemHandlerForDIType( $this->diType );
+ return $diHandler->getTableFields();
+ }
+
+ /**
+ * @see $idSubject
+ *
+ * @since 1.8
+ *
+ * @return boolean
+ */
+ public function usesIdSubject() {
+ return $this->idSubject;
+ }
+
+ /**
+ * @see $idSubject
+ *
+ * @param $usesIdSubject
+ *
+ * @since 1.8
+ */
+ public function setUsesIdSubject( $usesIdSubject ) {
+ $this->idSubject = $usesIdSubject;
+ }
+
+ /**
+ * Returns the name of the fixed property which this table is for.
+ * Throws an exception when called on a table not for any fixed
+ * property, so call @see isFixedPropertyTable first when appropriate.
+ *
+ * @see $fixedProperty
+ *
+ * @since 1.8
+ *
+ * @return string
+ * @throws OutOfBoundsException
+ */
+ public function getFixedProperty() {
+
+ if ( $this->fixedProperty === false ) {
+ throw new OutOfBoundsException( 'Attempt to get the fixed property from a table that does not hold one' );
+ }
+
+ return $this->fixedProperty;
+ }
+
+ /**
+ * Returns if the table holds a fixed property or is a general table.
+ *
+ * @see $fixedProperty
+ *
+ * @since 1.8
+ *
+ * @return boolean
+ */
+ public function isFixedPropertyTable() {
+ return $this->fixedProperty !== false;
+ }
+
+ /**
+ * Returns the name of the table in the database.
+ *
+ * @since 1.8
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Returns @see $diType
+ *
+ * @since 1.8
+ *
+ * @return integer
+ */
+ public function getDiType() {
+ return $this->diType;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableDefinitionBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableDefinitionBuilder.php
new file mode 100644
index 00000000..49812cc4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableDefinitionBuilder.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use Hooks;
+use SMW\DataTypeRegistry;
+use SMW\DIProperty;
+use SMW\PropertyRegistry;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class PropertyTableDefinitionBuilder {
+
+ /**
+ * Fixed property table prefix
+ */
+ const PROPERTY_TABLE_PREFIX = 'smw_fpt';
+
+ /**
+ * @var PropertyTypeFinder
+ */
+ private $propertyTypeFinder;
+
+ /**
+ * @var TableDefinition[]
+ */
+ protected $propertyTables = [];
+
+ /**
+ * @var array
+ */
+ protected $fixedPropertyTableIds = [];
+
+ /**
+ * @since 1.9
+ *
+ * @param PropertyTypeFinder $propertyTypeFinder
+ */
+ public function __construct( PropertyTypeFinder $propertyTypeFinder ) {
+ $this->propertyTypeFinder = $propertyTypeFinder;
+ }
+
+ /**
+ * @since 1.9
+ *
+ * @param array $diType
+ * @param array $specialProperties
+ * @param array $userDefinedFixedProperties
+ */
+ public function doBuild( $diTypes, $specialProperties, $userDefinedFixedProperties ) {
+
+ $this->addTableDefinitionForDiTypes( $diTypes );
+
+ $this->addTableDefinitionForFixedProperties(
+ $specialProperties
+ );
+
+ $customFixedProperties = [];
+ $fixedPropertyTablePrefix = [];
+
+ // Allow to alter the prefix by an extension
+ Hooks::run( 'SMW::SQLStore::AddCustomFixedPropertyTables', [ &$customFixedProperties, &$fixedPropertyTablePrefix ] );
+
+ $this->addTableDefinitionForFixedProperties(
+ $customFixedProperties,
+ $fixedPropertyTablePrefix
+ );
+
+ $this->addRedirectTableDefinition();
+
+ $this->addTableDefinitionForUserDefinedFixedProperties(
+ $userDefinedFixedProperties
+ );
+
+ Hooks::run( 'SMW::SQLStore::updatePropertyTableDefinitions', [ &$this->propertyTables ] );
+
+ $this->createFixedPropertyTableIdIndex();
+ }
+
+ /**
+ * Returns table prefix
+ *
+ * @since 1.9
+ *
+ * @return string
+ */
+ public function getTablePrefix() {
+ return self::PROPERTY_TABLE_PREFIX;
+ }
+
+ /**
+ * Returns fixed properties table Ids
+ *
+ * @since 1.9
+ *
+ * @return array|null
+ */
+ public function getFixedPropertyTableIds() {
+ return $this->fixedPropertyTableIds;
+ }
+
+ /**
+ * Returns property table definitions
+ *
+ * @since 1.9
+ *
+ * @return TableDefinition[]
+ */
+ public function getTableDefinitions() {
+ return $this->propertyTables;
+ }
+
+ /**
+ * Returns new table definition
+ *
+ * @since 1.9
+ *
+ * @param $diType
+ * @param $tableName
+ * @param $fixedProperty
+ *
+ * @return TableDefinition
+ */
+ public function newTableDefinition( $diType, $tableName, $fixedProperty = false ) {
+ return new TableDefinition( $diType, $tableName, $fixedProperty );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $tableName
+ *
+ * @return string
+ */
+ public function createTableNameFrom( $tableName ) {
+ return self::PROPERTY_TABLE_PREFIX . strtolower( $tableName );
+ }
+
+ /**
+ * @see http://stackoverflow.com/questions/3763728/shorter-php-cipher-than-md5
+ * @since 2.5
+ *
+ * @param string $tableName
+ *
+ * @return string
+ */
+ public function createHashedTableNameFrom( $tableName ) {
+ return self::PROPERTY_TABLE_PREFIX . '_' . substr( base_convert( md5( $tableName ), 16, 32 ), 0, 12 );
+ }
+
+ /**
+ * Add property table definition
+ *
+ * @since 1.9
+ *
+ * @param $diType
+ * @param $tableName
+ * @param $fixedProperty
+ */
+ protected function addPropertyTable( $diType, $tableName, $fixedProperty = false ) {
+ $this->propertyTables[$tableName] = $this->newTableDefinition( $diType, $tableName, $fixedProperty );
+ }
+
+ /**
+ * @param array $diTypes
+ */
+ private function addTableDefinitionForDiTypes( array $diTypes ) {
+ foreach( $diTypes as $tableDIType => $tableName ) {
+ $this->addPropertyTable( $tableDIType, $tableName );
+ }
+ }
+
+ private function addTableDefinitionForFixedProperties( array $properties, array $fixedPropertyTablePrefix = [] ) {
+ foreach( $properties as $propertyKey => $propertyTableSuffix ) {
+
+ $tablePrefix = isset( $fixedPropertyTablePrefix[$propertyKey] ) ? $fixedPropertyTablePrefix[$propertyKey] : self::PROPERTY_TABLE_PREFIX;
+
+ // Either as plain index array containing the property key or as associated
+ // array with property key => tableSuffix
+ $propertyKey = is_int( $propertyKey ) ? $propertyTableSuffix : $propertyKey;
+
+ $this->addPropertyTable(
+ DataTypeRegistry::getInstance()->getDataItemByType( PropertyRegistry::getInstance()->getPropertyValueTypeById( $propertyKey ) ),
+ $tablePrefix . strtolower( $propertyTableSuffix ),
+ $propertyKey
+ );
+ }
+ }
+
+ private function addRedirectTableDefinition() {
+ // Redirect table uses another subject scheme for historic reasons
+ // TODO This should be changed if possible
+ $redirectTableName = $this->createTableNameFrom( '_REDI' );
+
+ if ( isset( $this->propertyTables[$redirectTableName]) ) {
+ $this->propertyTables[$redirectTableName]->setUsesIdSubject( false );
+ }
+ }
+
+ /**
+ * Get all the tables for the properties that are declared as fixed
+ * (overly used and thus having separate tables)
+ *
+ * @param array $fixedProperties
+ */
+ private function addTableDefinitionForUserDefinedFixedProperties( array $fixedProperties ) {
+
+ $this->propertyTypeFinder->setTypeTableName(
+ $this->createTableNameFrom( '_TYPE' )
+ );
+
+ foreach( $fixedProperties as $propertyKey ) {
+
+ // Normalize the key to be independent from a possible MW setting
+ // (has area == Has_area <> Has_Area)
+ $propertyKey = str_replace( ' ', '_', ucfirst( $propertyKey ) );
+ $property = new DIProperty( $propertyKey );
+
+ $this->addPropertyTable(
+ DataTypeRegistry::getInstance()->getDataItemByType( $this->propertyTypeFinder->findTypeID( $property ) ),
+ $this->createHashedTableNameFrom( $propertyKey ),
+ $propertyKey
+ );
+ }
+ }
+
+ private function createFixedPropertyTableIdIndex() {
+
+ foreach ( $this->propertyTables as $tid => $propTable ) {
+ if ( $propTable->isFixedPropertyTable() ) {
+ $this->fixedPropertyTableIds[$propTable->getFixedProperty()] = $tid;
+ }
+ }
+
+ // Specifically set properties that must not be stored in any
+ // property table to null here. Any function that hits this
+ // null unprepared is doing something wrong anyway.
+ $this->fixedPropertyTableIds['_SKEY'] = null;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableIdReferenceDisposer.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableIdReferenceDisposer.php
new file mode 100644
index 00000000..ab7336c5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableIdReferenceDisposer.php
@@ -0,0 +1,284 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\EventHandler;
+use SMW\Iterators\ResultIterator;
+
+/**
+ * @private
+ *
+ * Class responsible for the clean-up (aka disposal) of any outdated table entries
+ * that are contained in either the ID_TABLE or related property tables with
+ * reference to a matchable ID.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class PropertyTableIdReferenceDisposer {
+
+ /**
+ * @var SQLStore
+ */
+ private $store = null;
+
+ /**
+ * @var Database
+ */
+ private $connection = null;
+
+ /**
+ * @var boolean
+ */
+ private $onTransactionIdle = false;
+
+ /**
+ * @var boolean
+ */
+ private $redirectRemoval = false;
+
+ /**
+ * @since 2.4
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ $this->connection = $this->store->getConnection( 'mw.db' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $redirectRemoval
+ */
+ public function setRedirectRemoval( $redirectRemoval ) {
+ $this->redirectRemoval = $redirectRemoval;
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function waitOnTransactionIdle() {
+ $this->onTransactionIdle = true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ *
+ * @return boolean
+ */
+ public function isDisposable( $id ) {
+ return $this->store->getPropertyTableIdReferenceFinder()->hasResidualReferenceForId( $id ) === false;
+ }
+
+ /**
+ * Use case: After a property changed its type (_wpg -> _txt), object values in the
+ * ID table are not removed at the time of the conversion process.
+ *
+ * Before an attempt to remove the ID from entity tables, it is secured that no
+ * references exists for the ID.
+ *
+ * @note This method does not check for an ID being object or subject value
+ * and has to be done prior calling this routine.
+ *
+ * @since 2.4
+ *
+ * @param integer $id
+ */
+ public function removeOutdatedEntityReferencesById( $id ) {
+
+ if ( $this->store->getPropertyTableIdReferenceFinder()->hasResidualReferenceForId( $id ) ) {
+ return null;
+ }
+
+ $this->cleanUpSecondaryReferencesById( $id, false );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return ResultIterator
+ */
+ public function newOutdatedEntitiesResultIterator() {
+
+ $res = $this->connection->select(
+ SQLStore::ID_TABLE,
+ [ 'smw_id' ],
+ [ 'smw_iw' => SMW_SQL3_SMWDELETEIW ],
+ __METHOD__
+ );
+
+ return new ResultIterator( $res );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param stdClass $row
+ */
+ public function cleanUpTableEntriesByRow( $row ) {
+
+ if ( !isset( $row->smw_id ) ) {
+ return;
+ }
+
+ $this->cleanUpTableEntriesById( $row->smw_id );
+ }
+
+ /**
+ * @note This method does not make any assumption about the ID state and therefore
+ * has to be validated before this method is called.
+ *
+ * @since 2.4
+ *
+ * @param integer $id
+ */
+ public function cleanUpTableEntriesById( $id ) {
+
+ if ( $this->onTransactionIdle ) {
+ return $this->connection->onTransactionIdle( function() use ( $id ) {
+ $this->cleanUpReferencesById( $id );
+ } );
+ } else {
+ $this->cleanUpReferencesById( $id );
+ }
+ }
+
+ private function cleanUpReferencesById( $id ) {
+
+ $subject = $this->store->getObjectIds()->getDataItemById( $id );
+ $isRedirect = false;
+
+ if ( $subject instanceof DIWikiPage ) {
+ $isRedirect = $subject->getInterwiki() === SMW_SQL3_SMWREDIIW;
+
+ // Use the subject without an internal 'smw-delete' iw marker
+ $subject = new DIWikiPage(
+ $subject->getDBKey(),
+ $subject->getNamespace(),
+ '',
+ $subject->getSubobjectName()
+ );
+ }
+
+ $this->triggerCleanUpEvents( $subject );
+
+ $this->connection->beginAtomicTransaction( __METHOD__ );
+
+ foreach ( $this->store->getPropertyTables() as $proptable ) {
+ if ( $proptable->usesIdSubject() ) {
+ $this->connection->delete(
+ $proptable->getName(),
+ [ 's_id' => $id ],
+ __METHOD__
+ );
+ }
+
+ if ( !$proptable->isFixedPropertyTable() ) {
+ $this->connection->delete(
+ $proptable->getName(),
+ [ 'p_id' => $id ],
+ __METHOD__
+ );
+ }
+
+ $fields = $proptable->getFields( $this->store );
+
+ // Match tables (including ftp_redi) that contain an object reference
+ if ( isset( $fields['o_id'] ) ) {
+ $this->connection->delete(
+ $proptable->getName(),
+ [ 'o_id' => $id ],
+ __METHOD__
+ );
+ }
+ }
+
+ $this->cleanUpSecondaryReferencesById( $id, $isRedirect );
+ $this->connection->endAtomicTransaction( __METHOD__ );
+
+ \Hooks::run(
+ 'SMW::SQLStore::EntityReferenceCleanUpComplete',
+ [ $this->store, $id, $subject, $isRedirect ]
+ );
+ }
+
+ private function cleanUpSecondaryReferencesById( $id, $isRedirect ) {
+
+ // When marked as redirect, don't remove the reference
+ if ( $isRedirect === false || ( $isRedirect && $this->redirectRemoval ) ) {
+ $this->connection->delete(
+ SQLStore::ID_TABLE,
+ [ 'smw_id' => $id ],
+ __METHOD__
+ );
+ }
+
+ $this->connection->delete(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ [ 'p_id' => $id ],
+ __METHOD__
+ );
+
+ $this->connection->delete(
+ SQLStore::QUERY_LINKS_TABLE,
+ [ 's_id' => $id ],
+ __METHOD__
+ );
+
+ $this->connection->delete(
+ SQLStore::QUERY_LINKS_TABLE,
+ [ 'o_id' => $id ],
+ __METHOD__
+ );
+
+ // Avoid Query: DELETE FROM `smw_ft_search` WHERE s_id = '92575'
+ // Error: 126 Incorrect key file for table '.\mw@002d25@002d01\smw_ft_search.MYI'; ...
+ try {
+ $this->connection->delete(
+ SQLStore::FT_SEARCH_TABLE,
+ [ 's_id' => $id ],
+ __METHOD__
+ );
+ } catch ( \DBError $e ) {
+ ApplicationFactory::getInstance()->getMediaWikiLogger()->info( __METHOD__ . ' reported: ' . $e->getMessage() );
+ }
+ }
+
+ private function triggerCleanUpEvents( $subject ) {
+
+ if ( !$subject instanceof DIWikiPage ) {
+ return;
+ }
+
+ // Skip any reset for subobjects where it is expected that the base
+ // subject is cleaning up all related cache entries
+ if ( $subject->getSubobjectName() !== '' ) {
+ return;
+ }
+
+ $eventHandler = EventHandler::getInstance();
+
+ $dispatchContext = $eventHandler->newDispatchContext();
+ $dispatchContext->set( 'subject', $subject );
+ $dispatchContext->set( 'context', 'PropertyTableIdReferenceDisposal' );
+
+ $eventHandler->getEventDispatcher()->dispatch(
+ 'cached.prefetcher.reset',
+ $dispatchContext
+ );
+
+ $eventHandler->getEventDispatcher()->dispatch(
+ 'factbox.cache.delete',
+ $dispatchContext
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableIdReferenceFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableIdReferenceFinder.php
new file mode 100644
index 00000000..bd36d587
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableIdReferenceFinder.php
@@ -0,0 +1,267 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class PropertyTableIdReferenceFinder {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @var boolean
+ */
+ private $isCapitalLinks = true;
+
+ /**
+ * @since 2.4
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ $this->connection = $this->store->getConnection( 'mw.db' );
+ $this->namespaceExaminer = ApplicationFactory::getInstance()->getNamespaceExaminer();
+ }
+
+ /**
+ * @note If $wgCapitalLinks is set false then it will avoid forcing the first
+ * letter of page titles (including included pages, images and categories)
+ * to capitals
+ *
+ * @since 2.4
+ *
+ * @param booelan $isCapitalLinks
+ */
+ public function isCapitalLinks( $isCapitalLinks ) {
+ $this->isCapitalLinks = $isCapitalLinks;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIProperty $property
+ *
+ * @return DataItem|false
+ */
+ public function tryToFindAtLeastOneReferenceForProperty( DIProperty $property ) {
+
+ $dataItem = $property->getDiWikiPage();
+
+ $sid = $this->store->getObjectIds()->getSMWPageID(
+ $dataItem->getDBkey(),
+ $dataItem->getNamespace(),
+ $dataItem->getInterwiki(),
+ ''
+ );
+
+ // Lets see if we have some lower/upper case matching for
+ // when wgCapitalLinks setting was involved
+ if ( !$this->isCapitalLinks && $sid == 0 ) {
+ $sid = $this->store->getObjectIds()->getSMWPageID(
+ lcfirst( $dataItem->getDBkey() ),
+ $dataItem->getNamespace(),
+ $dataItem->getInterwiki(),
+ ''
+ );
+ }
+
+ return $this->findAtLeastOneActiveReferenceById( $sid );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ *
+ * @return boolean
+ */
+ public function hasResidualPropertyTableReference( $id ) {
+
+ if ( $id == SQLStore::FIXED_PROPERTY_ID_UPPERBOUND ) {
+ return true;
+ }
+
+ return (bool)$this->findAtLeastOneActiveReferenceById( $id, false );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $id
+ *
+ * @return boolean
+ */
+ public function hasResidualReferenceForId( $id ) {
+
+ if ( $id == SQLStore::FIXED_PROPERTY_ID_UPPERBOUND ) {
+ return true;
+ }
+
+ return (bool)$this->findAtLeastOneActiveReferenceById( $id );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $id
+ *
+ * @return array
+ */
+ public function searchAllTablesToFindAtLeastOneReferenceById( $id ) {
+
+ $references = [];
+
+ foreach ( $this->store->getPropertyTables() as $proptable ) {
+ $reference = false;
+
+ if ( ( $reference = $this->findReferenceByPropertyTable( $proptable, $id ) ) !== false ) {
+ $references[$proptable->getName()] = $reference;
+ }
+ }
+
+ if ( ( $reference = $this->findQueryLinksTableReferenceById( $id ) ) !== false ) {
+ $references[SQLStore::QUERY_LINKS_TABLE] = $reference;
+ }
+
+ return $references;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $id
+ * @param boolean $secondary_ref
+ *
+ * @return DataItem|false
+ */
+ public function findAtLeastOneActiveReferenceById( $id, $secondary_ref = true ) {
+
+ $reference = false;
+
+ foreach ( $this->store->getPropertyTables() as $proptable ) {
+
+ if ( ( $reference = $this->findReferenceByPropertyTable( $proptable, $id ) ) !== false ) {
+
+ // If null is returned it means that a reference was found but no DI could
+ // be matched therefore is categorized as false positive
+ if ( isset( $reference->s_id ) ) {
+ $reference = $this->store->getObjectIds()->getDataItemById( $reference->s_id );
+
+ // If the reference is for some reason not part of a supported namespace,
+ // it is assumed to be invalid
+ if ( $reference !== null && !$this->namespaceExaminer->isSemanticEnabled( $reference->getNamespace() ) ) {
+ $reference = false;
+ }
+ }
+ }
+
+ if ( $reference instanceof DataItem ) {
+ return $reference;
+ }
+ }
+
+ if ( $secondary_ref && !isset( $reference->s_id ) ) {
+ $reference = $this->findQueryLinksTableReferenceById( $id );
+ }
+
+ if ( isset( $reference->s_id ) ) {
+ $reference = $this->store->getObjectIds()->getDataItemById( $reference->s_id );
+ }
+
+ if ( $reference === false || $reference === null ) {
+ return false;
+ }
+
+ return $reference;
+ }
+
+ private function findReferenceByPropertyTable( $proptable, $id ) {
+
+ $row = false;
+
+ if ( $proptable->usesIdSubject() ) {
+ $row = $this->connection->selectRow(
+ $proptable->getName(),
+ [ 's_id' ],
+ [ 's_id' => $id ],
+ __METHOD__
+ );
+ }
+
+ if ( $row !== false ) {
+ return $row;
+ }
+
+ $fields = $proptable->getFields( $this->store );
+
+ // Check whether an object reference exists or not
+ if ( isset( $fields['o_id'] ) ) {
+
+ // This next time someone ... I'm going to Alaska
+ $field = strpos( $proptable->getName(), 'redi' ) ? [ 's_title', 's_namespace' ] : [ 's_id' ];
+
+ $row = $this->connection->selectRow(
+ $proptable->getName(),
+ $field,
+ [ 'o_id' => $id ],
+ __METHOD__
+ );
+
+ if ( $row !== false && strpos( $proptable->getName(), 'redi' ) ) {
+ $row->s_id = $this->store->getObjectIds()->findRedirect( $row->s_title, $row->s_namespace );
+ }
+ }
+
+ // If the property table is not a fixed table (== assigns a whole
+ // table to a specific property with the p_id column being suppressed)
+ // then check for the p_id field
+ if ( $row === false && !$proptable->isFixedPropertyTable() ) {
+ $row = $this->connection->selectRow(
+ $proptable->getName(),
+ [ 's_id' ],
+ [ 'p_id' => $id ],
+ __METHOD__
+ );
+ }
+
+ return $row;
+ }
+
+ private function findQueryLinksTableReferenceById( $id ) {
+
+ // If the query table contains a reference then we keep the object (could
+ // be a subject, property, or printrequest) where in case the query is
+ // removed the object will also loose its reference
+ $row = $this->connection->selectRow(
+ SQLStore::QUERY_LINKS_TABLE,
+ [ 's_id' ],
+ [ 'o_id' => $id ],
+ __METHOD__
+ );
+
+ return $row;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableInfoFetcher.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableInfoFetcher.php
new file mode 100644
index 00000000..41ee2d12
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableInfoFetcher.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\DataTypeRegistry;
+use SMW\DIProperty;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class PropertyTableInfoFetcher {
+
+ /**
+ * @var PropertyTypeFinder
+ */
+ private $propertyTypeFinder;
+
+ /**
+ * Array for keeping property table table data, indexed by table id.
+ * Access this only by calling getPropertyTables().
+ *
+ * @var TableDefinition[]|null
+ */
+ private $propertyTableDefinitions = null;
+
+ /**
+ * Array to cache "propkey => table id" associations for fixed property
+ * tables. Initialized by getPropertyTables(), which must be called
+ * before accessing this.
+ *
+ * @var array|null
+ */
+ private $fixedPropertyTableIds = null;
+
+ /**
+ * Keys of special properties that should have their own
+ * fixed property table.
+ *
+ * @var array
+ */
+ private static $customizableSpecialProperties = [
+ '_MDAT', '_CDAT', '_NEWP', '_LEDT', '_MIME', '_MEDIA',
+ ];
+
+ /**
+ * @var array
+ */
+ private $customSpecialPropertyList = [];
+
+ /**
+ * @var array
+ */
+ private $fixedSpecialProperties = [
+ // property declarations
+ '_TYPE', '_UNIT', '_CONV', '_PVAL', '_LIST', '_SERV', '_PREC', '_PPLB',
+ // query statistics (very frequently used)
+ '_ASK', '_ASKDE', '_ASKSI', '_ASKFO', '_ASKST', '_ASKDU', '_ASKPA',
+ // subproperties, classes, and instances
+ '_SUBP', '_SUBC', '_INST',
+ // redirects
+ '_REDI',
+ // has sub object
+ '_SOBJ',
+ // vocabulary import and URI assignments
+ '_IMPO', '_URI',
+ // Concepts
+ '_CONC',
+ // Monolingual text
+ '_LCODE', '_TEXT',
+ // Display title of
+ '_DTITLE'
+ ];
+
+ /**
+ * @var array
+ */
+ private $customFixedPropertyList = [];
+
+ /**
+ * Default tables to use for storing data of certain types.
+ *
+ * @var array
+ */
+ private $defaultDiTypeTableIdMap = [
+ DataItem::TYPE_NUMBER => 'smw_di_number',
+ DataItem::TYPE_BLOB => 'smw_di_blob',
+ DataItem::TYPE_BOOLEAN => 'smw_di_bool',
+ DataItem::TYPE_URI => 'smw_di_uri',
+ DataItem::TYPE_TIME => 'smw_di_time',
+ DataItem::TYPE_GEO => 'smw_di_coords', // currently created only if Semantic Maps are installed
+ DataItem::TYPE_WIKIPAGE => 'smw_di_wikipage',
+ //DataItem::TYPE_CONCEPT => '', // _CONC is the only property of this type
+ ];
+
+ /**
+ * @since 2.5
+ *
+ * @param PropertyTypeFinder $propertyTypeFinder
+ */
+ public function __construct( PropertyTypeFinder $propertyTypeFinder ) {
+ $this->propertyTypeFinder = $propertyTypeFinder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public static function getFixedSpecialPropertyList() {
+ return self::$customizableSpecialProperties;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param array $customFixedProperties
+ */
+ public function setCustomFixedPropertyList( array $customFixedProperties ) {
+ $this->customFixedPropertyList = $customFixedProperties;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param array $customSpecialProperties
+ */
+ public function setCustomSpecialPropertyList( array $customSpecialProperties ) {
+ $this->customSpecialPropertyList = $customSpecialProperties;
+ }
+
+ /**
+ * Find the id of a property table that is suitable for storing values of
+ * the given type. The type is specified by an SMW type id such as '_wpg'.
+ * An empty string is returned if no matching table could be found.
+ *
+ * @since 2.2
+ *
+ * @param string $dataTypeTypeId
+ *
+ * @return string
+ */
+ public function findTableIdForDataTypeTypeId( $dataTypeTypeId ) {
+ return $this->findTableIdForDataItemTypeId(
+ DataTypeRegistry::getInstance()->getDataItemId( $dataTypeTypeId )
+ );
+ }
+
+ /**
+ * Find the id of a property table that is normally used to store
+ * data items of the given type. The empty string is returned if
+ * no such table exists.
+ *
+ * @since 2.2
+ *
+ * @param integer $dataItemId
+ *
+ * @return string
+ */
+ public function findTableIdForDataItemTypeId( $dataItemId ) {
+
+ if ( array_key_exists( $dataItemId, $this->defaultDiTypeTableIdMap ) ) {
+ return $this->defaultDiTypeTableIdMap[$dataItemId];
+ }
+
+ return '';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public function getDefaultDataItemTables() {
+ return array_values( $this->defaultDiTypeTableIdMap );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return boolean
+ */
+ public function isFixedTableProperty( DIProperty $property ) {
+
+ if ( $this->fixedPropertyTableIds === null ) {
+ $this->buildDefinitionsForPropertyTables();
+ }
+
+ return array_key_exists( $property->getKey(), $this->fixedPropertyTableIds );
+ }
+
+ /**
+ * Retrieve the id of the property table that is to be used for storing
+ * values for the given property object.
+ *
+ * @since 2.2
+ *
+ * @param DIProperty $property
+ *
+ * @return string
+ */
+ public function findTableIdForProperty( DIProperty $property ) {
+
+ if ( $this->fixedPropertyTableIds === null ) {
+ $this->buildDefinitionsForPropertyTables();
+ }
+
+ $propertyKey = $property->getKey();
+
+ if ( array_key_exists( $propertyKey, $this->fixedPropertyTableIds ) ) {
+ return $this->fixedPropertyTableIds[$propertyKey];
+ }
+
+ return $this->findTableIdForDataTypeTypeId( $property->findPropertyTypeID() );
+ }
+
+ /**
+ * Return the array of predefined property table declarations, initialising
+ * it if necessary. The result is an array of SMWSQLStore3Table objects
+ * indexed by table ids.
+ *
+ * It is ensured that the keys of the returned array agree with the name of
+ * the table that they refer to.
+ *
+ * @since 2.2
+ *
+ * @return TableDefinition[]
+ */
+ public function getPropertyTableDefinitions() {
+
+ if ( $this->propertyTableDefinitions === null ) {
+ $this->buildDefinitionsForPropertyTables();
+ }
+
+ return $this->propertyTableDefinitions;
+ }
+
+ /**
+ * @since 2.2
+ */
+ public function clearCache() {
+ $this->propertyTableDefinitions = null;
+ $this->fixedPropertyTableIds = null;
+ }
+
+ private function buildDefinitionsForPropertyTables() {
+
+ $enabledSpecialProperties = $this->fixedSpecialProperties;
+ $customizableSpecialProperties = array_flip( self::$customizableSpecialProperties );
+
+ foreach ( $this->customSpecialPropertyList as $property ) {
+ if ( isset( $customizableSpecialProperties[$property] ) ) {
+ $enabledSpecialProperties[] = $property;
+ }
+ }
+
+ $definitionBuilder = new PropertyTableDefinitionBuilder(
+ $this->propertyTypeFinder
+ );
+
+ $definitionBuilder->doBuild(
+ $this->defaultDiTypeTableIdMap,
+ $enabledSpecialProperties,
+ $this->customFixedPropertyList
+ );
+
+ $this->propertyTableDefinitions = $definitionBuilder->getTableDefinitions();
+ $this->fixedPropertyTableIds = $definitionBuilder->getFixedPropertyTableIds();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableRowDiffer.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableRowDiffer.php
new file mode 100644
index 00000000..599f5fd7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableRowDiffer.php
@@ -0,0 +1,313 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use InvalidArgumentException;
+use SMW\DIProperty;
+use SMW\Exception\DataItemException;
+use SMW\SemanticData;
+use SMW\SQLStore\ChangeOp\ChangeOp;
+use SMW\Store;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author Markus Krötzsch
+ * @author Nischay Nahata
+ * @author mwjames
+ */
+class PropertyTableRowDiffer {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertyTableRowMapper
+ */
+ private $propertyTableRowMapper;
+
+ /**
+ * @var ChangeOp
+ */
+ private $changeOp;
+
+ /**
+ * @since 2.3
+ *
+ * @param Store $store
+ * @param PropertyTableRowMapper $propertyTableRowMapper
+ */
+ public function __construct( Store $store, PropertyTableRowMapper $propertyTableRowMapper ) {
+ $this->store = $store;
+ $this->propertyTableRowMapper = $propertyTableRowMapper;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ChangeOp|null $changeOp
+ */
+ public function setChangeOp( ChangeOp $changeOp = null ) {
+ $this->changeOp = $changeOp;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return ChangeOp
+ */
+ public function getChangeOp() {
+ return $this->changeOp;
+ }
+
+ /**
+ * Compute necessary insertions, deletions, and new table hashes for
+ * updating the database to contain $newData for the subject with ID
+ * $sid. Insertions and deletions are returned in as an array mapping
+ * table names to arrays of table rows. Each row is an array mapping
+ * column names to values as usual. The table hashes are returned as
+ * an array mapping table names to hash values.
+ *
+ * It is ensured that table names (keys) in the returned insert
+ * data are exaclty the same as the table names (keys) in the delete
+ * data, even if one of them maps to an empty array (no changes). If
+ * a table needs neither insertions nor deletions, then it will not
+ * be mentioned as a key anywhere.
+ *
+ * The given database is only needed for reading the data that is
+ * related assigned to sid.
+ *
+ * @since 2.3
+ *
+ * @param integer $sid
+ * @param SemanticData $semanticData
+ *
+ * @return array
+ */
+ public function computeTableRowDiff( $sid, SemanticData $semanticData ) {
+
+ $tablesDeleteRows = [];
+ $tablesInsertRows = [];
+
+ $propertyList = [];
+ $textItems = [];
+
+ $newHashes = [];
+
+ if ( $this->changeOp === null ) {
+ $this->setChangeOp( new ChangeOp( $semanticData->getSubject() ) );
+ }
+
+ list( $newData, $textItems, $propertyList, $fixedPropertyList ) = $this->propertyTableRowMapper->mapToRows(
+ $sid,
+ $semanticData
+ );
+
+ $this->changeOp->addPropertyList( $propertyList );
+
+ $oldHashes = $this->fetchPropertyTableHashesById(
+ $sid
+ );
+
+ $propertyTables = $this->store->getPropertyTables();
+
+ foreach ( $propertyTables as $propertyTable ) {
+
+ if ( !$propertyTable->usesIdSubject() ) { // ignore; only affects redirects anyway
+ continue;
+ }
+
+ $tableName = $propertyTable->getName();
+ $fixedProperty = false;
+
+ // Fixed property tables have no p_id declared, the auxiliary
+ // information is provided to easily map fixed tables and
+ // its assigned property/id
+ if ( $propertyTable->isFixedPropertyTable() ) {
+ $fixedProperty['key'] = $propertyTable->getFixedProperty();
+
+ // Isn't registered therefore leave it alone (property was removed etc.)
+ try {
+ $property = new DIProperty( $fixedProperty['key'] );
+ $fixedProperty['p_id'] = $this->store->getObjectIds()->getSMWPropertyID(
+ $property
+ );
+ } catch ( DataItemException $e ) {
+ $fixedProperty = false;
+ }
+ }
+
+ if ( $fixedProperty ) {
+ $this->changeOp->addFixedPropertyRecord( $tableName, $fixedProperty );
+ }
+
+ if ( array_key_exists( $tableName, $newData ) ) {
+ // Note: the order within arrays should remain the same while page is not updated.
+ // Hence we do not sort before serializing. It is hoped that this assumption is valid.
+ $newHashes[$tableName] = $this->createHash(
+ $tableName,
+ $newData,
+ $semanticData->getOption( SemanticData::OPT_LAST_MODIFIED )
+ );
+
+ if ( array_key_exists( $tableName, $oldHashes ) && $newHashes[$tableName] == $oldHashes[$tableName] ) {
+ // Table contains data and should contain the same data after update
+ continue;
+ } else { // Table contains no data or contains data that is different from the new
+ list( $tablesInsertRows[$tableName], $tablesDeleteRows[$tableName] ) = $this->arrayDeleteMatchingValues(
+ $this->fetchCurrentContentsForPropertyTable( $sid, $propertyTable ),
+ $newData[$tableName],
+ $propertyTable
+ );
+ }
+ } elseif ( array_key_exists( $tableName, $oldHashes ) ) {
+ // Table contains data but should not contain any after update
+ $tablesInsertRows[$tableName] = [];
+ $tablesDeleteRows[$tableName] = $this->fetchCurrentContentsForPropertyTable(
+ $sid,
+ $propertyTable
+ );
+ }
+ }
+
+ $this->changeOp->addTextItems(
+ $sid,
+ $textItems
+ );
+
+ $this->changeOp->addDataOp(
+ $semanticData->getSubject()->getHash(),
+ $newData
+ );
+
+ $this->changeOp->addDiffOp(
+ $tablesInsertRows,
+ $tablesDeleteRows
+ );
+
+ return [ $tablesInsertRows, $tablesDeleteRows, $newHashes ];
+ }
+
+ private function fetchPropertyTableHashesById( $sid ) {
+ return $this->store->getObjectIds()->getPropertyTableHashes( $sid );
+ }
+
+ /**
+ * @note The hashMutator can be used to force a modification in order to detect
+ * content edits where text has been changed but the md5 table hash remains
+ * unchanged and therefore would not re-compute the diff and misses out
+ * critical updates on property tables.
+ *
+ * The phenomenon has been observed in connection with a page turned from
+ * a redirect to a normal page or for undeleted pages.
+ */
+ private function createHash( $tableName, $newData, $hashMutator = '' ) {
+ return md5( serialize( array_values( $newData[$tableName] ) ) . $hashMutator );
+ }
+
+ /**
+ * Get the current data stored for the given ID in the given database
+ * table. The result is an array of updates, formatted like the one of
+ * the table insertion arrays created by preparePropertyTableInserts().
+ *
+ * @note Tables without IDs as subject are not supported. They will
+ * hopefully vanish soon anyway.
+ *
+ * @since 1.8
+ * @param integer $sid
+ * @param TableDefinition $tableDeclaration
+ * @return array
+ */
+ private function fetchCurrentContentsForPropertyTable( $sid, TableDefinition $propertyTable ) {
+
+ if ( !$propertyTable->usesIdSubject() ) { // does not occur, but let's be strict
+ throw new InvalidArgumentException('Operation not supported for tables without subject IDs.');
+ }
+
+ $contents = [];
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $result = $connection->select(
+ $connection->tablename( $propertyTable->getName() ),
+ '*',
+ [ 's_id' => $sid ],
+ __METHOD__
+ );
+
+ foreach( $result as $row ) {
+ if ( is_object( $row ) ) {
+
+ $resultRow = (array)$row;
+
+ // Always make sure to use int values for ids so
+ // that the compare/hash will be of the same type
+ if ( isset( $resultRow['s_id'] ) ) {
+ $resultRow['s_id'] = (int)$resultRow['s_id'];
+ }
+
+ if ( isset( $resultRow['p_id'] ) ) {
+ $resultRow['p_id'] = (int)$resultRow['p_id'];
+ }
+
+ if ( isset( $resultRow['o_id'] ) ) {
+ $resultRow['o_id'] = (int)$resultRow['o_id'];
+ }
+
+ $hash = $this->propertyTableRowMapper->makeHash( $resultRow );
+ $contents[$hash] = $resultRow;
+ }
+ }
+
+ return $contents;
+ }
+
+ /**
+ * Delete all matching values from old and new arrays and return the
+ * remaining new values as insert values and the remaining old values as
+ * delete values.
+ *
+ * @param array $oldValues
+ * @param array $newValues
+ * @param PropertyTableDefinition $propertyTable
+ *
+ * @return array
+ */
+ private function arrayDeleteMatchingValues( $oldValues, $newValues, $propertyTable ) {
+
+ $isString = $propertyTable->getDIType() === DataItem::TYPE_BLOB;
+
+ // Cycle through old values
+ foreach ( $oldValues as $oldKey => $oldValue ) {
+
+ // Cycle through new values
+ foreach ( $newValues as $newKey => $newValue ) {
+
+ // #2061
+ // Loose comparison on a string will fail for cases like 011 == 0011
+ // therefore use the strict comparison and have the values
+ // remain if they don't match
+ if ( $isString && $newValue !== $oldValue ) {
+ continue;
+ }
+
+ // Delete matching values
+ // use of == is intentional to account for oldValues only
+ // containing strings while new values might also contain other
+ // types
+ if ( $newValue == $oldValue ) {
+ unset( $newValues[$newKey] );
+ unset( $oldValues[$oldKey] );
+ }
+ }
+ };
+
+ // Arrays have to be renumbered because database functions expect an
+ // element with index 0 to be present in the array
+ return [ array_values( $newValues ), array_values( $oldValues ) ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableRowMapper.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableRowMapper.php
new file mode 100644
index 00000000..b12554ca
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableRowMapper.php
@@ -0,0 +1,310 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use RuntimeException;
+use SMW\Exception\PredefinedPropertyLabelMismatchException;
+use SMW\SemanticData;
+use SMW\SQLStore\ChangeOp\ChangeOp;
+use SMW\Store;
+use SMWDataItem as DataItem;
+use SMWDIError as DIError;
+
+/**
+ * Builds a table row representation for a SemanticData object.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PropertyTableRowMapper {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 2.3
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param SemanticData $semanticData
+ *
+ * @return ChangeOp
+ */
+ public function newChangeOp( $id, SemanticData $semanticData ) {
+
+ list( $dataArray, $textItems, $propertyList, $fixedPropertyList ) = $this->mapToRows(
+ $id,
+ $semanticData
+ );
+
+ $subject = $semanticData->getSubject();
+ $changeOp = new ChangeOp( $subject );
+
+ foreach ( $fixedPropertyList as $key => $record ) {
+ $changeOp->addFixedPropertyRecord( $key, $record );
+ }
+
+ $changeOp->addPropertyList( $propertyList );
+
+ $changeOp->addDataOp(
+ $subject->getHash(),
+ $dataArray
+ );
+
+ return $changeOp;
+ }
+
+ /**
+ * Create an array of rows to insert into property tables in order to
+ * store the given SemanticData. The given $sid (subject page id) is
+ * used directly and must belong to the subject of the data container.
+ * Sortkeys are ignored since they are not stored in a property table
+ * but in the ID table.
+ *
+ * The returned array uses property table names as keys and arrays of
+ * table rows as values. Each table row is an array mapping column
+ * names to values.
+ *
+ * @note Property tables that do not use ids as subjects are ignored.
+ * This just excludes redirects that are handled differently anyway;
+ * it would not make a difference to include them here.
+ *
+ * @since 3.0
+ *
+ * @param integer $sid
+ * @param SemanticData $semanticData
+ *
+ * @return array
+ */
+ public function mapToRows( $sid, SemanticData $semanticData ) {
+
+ list( $rows, $textItems, $propertyList, $fixedPropertyList ) = $this->mapData(
+ $sid,
+ $semanticData
+ );
+
+ return [ $rows, $textItems, $propertyList, $fixedPropertyList ];
+ }
+
+ /**
+ * Create a string key for hashing an array of values that represents a
+ * row in the database. Used to eliminate duplicates and to support
+ * diff computation. This is not stored in the database, so it can be
+ * changed without causing any problems with legacy data.
+ *
+ * @since 3.0
+ *
+ * @param array $fieldArray
+ *
+ * @return string
+ */
+ public function makeHash( array $array ) {
+ return md5( implode( '#', $array ) );;
+ }
+
+ /**
+ * Create an array of rows to insert into property tables in order to
+ * store the given SMWSemanticData. The given $sid (subject page id) is
+ * used directly and must belong to the subject of the data container.
+ * Sortkeys are ignored since they are not stored in a property table
+ * but in the ID table.
+ *
+ * The returned array uses property table names as keys and arrays of
+ * table rows as values. Each table row is an array mapping column
+ * names to values.
+ *
+ * @note Property tables that do not use ids as subjects are ignored.
+ * This just excludes redirects that are handled differently anyway;
+ * it would not make a difference to include them here.
+ *
+ * @since 1.8
+ *
+ * @param integer $sid
+ * @param SemanticData $semanticData
+ *
+ * @return array
+ */
+ private function mapData( $sid, SemanticData $semanticData ) {
+
+ $subject = $semanticData->getSubject();
+ $propertyTables = $this->store->getPropertyTables();
+
+ $rows = [];
+
+ // Keep the list for the Diff to avoid having to lookup any property ID
+ // reference during a post processing
+ $propertyList = [];
+ $fixedPropertyList = [];
+ $textItems = [];
+
+ foreach ( $semanticData->getProperties() as $property ) {
+
+ $tableId = $this->store->findPropertyTableID( $property );
+
+ // not stored in a property table, e.g., sortkeys
+ if ( $tableId === null ) {
+ continue;
+ }
+
+ // "Notice: Undefined index"
+ if ( !isset( $propertyTables[$tableId] ) ) {
+ throw new RuntimeException( "Unable to find a property table for " . $property->getKey() );
+ }
+
+ $propertyTable = $propertyTables[$tableId];
+
+ // not using subject ids, e.g., redirects
+ if ( !$propertyTable->usesIdSubject() ) {
+ continue;
+ }
+
+ $insertValues = [ 's_id' => $sid ];
+ $p_type = $property->findPropertyValueType();
+
+ if ( !$propertyTable->isFixedPropertyTable() ) {
+ $insertValues['p_id'] = $this->store->getObjectIds()->makeSMWPropertyID(
+ $property
+ );
+
+ $propertyList[$property->getKey()] = [ '_id' => $insertValues['p_id'], '_type' => $p_type ];
+ } else {
+ $pid = $this->store->getObjectIds()->makeSMWPropertyID(
+ $property
+ );
+
+ $fixedPropertyList[$tableId] = [
+ 'key' => $property->getKey(),
+ 'p_id' => $pid,
+ ];
+
+ $propertyList[$property->getKey()] = [ '_id' => $pid, '_type' => $p_type ];
+ }
+
+ $pid = $propertyList[$property->getKey()]['_id'];
+
+ if ( !isset( $textItems[$pid] ) ) {
+ $textItems[$pid] = [];
+ }
+
+ // Avoid issues when an expected predefined property is no longer
+ // available (i.e. an extension that defined that property was disabled)
+ try {
+ $propertyValues = $semanticData->getPropertyValues( $property );
+ } catch( PredefinedPropertyLabelMismatchException $e ) {
+ continue;
+ }
+
+ foreach ( $propertyValues as $dataItem ) {
+
+ if ( $dataItem instanceof DIError ) { // ignore error values
+ continue;
+ }
+
+ $tableName = $propertyTable->getName();
+
+ if ( !array_key_exists( $tableName, $rows ) ) {
+ $rows[$tableName] = [];
+ }
+
+ if ( $dataItem->getDIType() === DataItem::TYPE_BLOB ) {
+ $textItems[$pid][] = $dataItem->getString();
+ } elseif ( $dataItem->getDIType() === DataItem::TYPE_URI ) {
+ $textItems[$pid][] = $dataItem->getSortKey();
+ } elseif ( $dataItem->getDIType() === DataItem::TYPE_WIKIPAGE ) {
+ $textItems[$pid][] = $dataItem->getSortKey();
+ }
+
+ $dataItemValues = $this->store->getDataItemHandlerForDIType( $dataItem->getDIType() )->getInsertValues( $dataItem );
+
+ // Ensure that the sortkey is a string
+ if ( isset( $dataItemValues['o_sortkey'] ) ) {
+ $dataItemValues['o_sortkey'] = (string)$dataItemValues['o_sortkey'];
+ }
+
+ $insertValues = array_merge( $insertValues, $dataItemValues );
+
+ // Make sure to build a unique set without duplicates which could happen
+ // if an annotation is made to a property that has a redirect pointing
+ // to the same p_id
+ $hash = $this->makeHash(
+ $insertValues
+ );
+
+ $rows[$tableName][$hash] = $insertValues;
+ }
+
+ // Unused
+ if ( $textItems[$pid] === [] ) {
+ unset( $textItems[$pid] );
+ }
+ }
+
+ // Special handling of Concepts
+ if ( $subject->getNamespace() === SMW_NS_CONCEPT && $subject->getSubobjectName() == '' ) {
+ $this->mapConceptTable( $sid, $rows );
+ }
+
+ return [ $rows, $textItems, $propertyList, $fixedPropertyList ];
+ }
+
+ /**
+ * Add cache information to concept data and make sure that there is
+ * exactly one value for the concept table.
+ *
+ * @note This code will vanish when concepts have a more standard
+ * handling. So not point in optimizing this much now.
+ *
+ * @since 1.8
+ * @param integer $sid
+ * @param &array $insertData
+ */
+ private function mapConceptTable( $sid, &$insertData ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ // Make sure that there is exactly one row to be written:
+ if ( array_key_exists( 'smw_fpt_conc', $insertData ) && !empty( $insertData['smw_fpt_conc'] ) ) {
+ $insertValues = end( $insertData['smw_fpt_conc'] );
+ } else {
+ $insertValues = [
+ 's_id' => $sid,
+ 'concept_txt' => '',
+ 'concept_docu' => '',
+ 'concept_features' => 0,
+ 'concept_size' => -1,
+ 'concept_depth' => -1
+ ];
+ }
+
+ // Add existing cache status data to this row:
+ $row = $connection->selectRow(
+ 'smw_fpt_conc',
+ [ 'cache_date', 'cache_count' ],
+ [ 's_id' => $sid ],
+ __METHOD__
+ );
+
+ if ( $row === false ) {
+ $insertValues['cache_date'] = null;
+ $insertValues['cache_count'] = null;
+ } else {
+ $insertValues['cache_date'] = $row->cache_date;
+ $insertValues['cache_count'] = $row->cache_count;
+ }
+
+ $insertData['smw_fpt_conc'] = [ $insertValues ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableUpdater.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableUpdater.php
new file mode 100644
index 00000000..3b9d33ed
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTableUpdater.php
@@ -0,0 +1,231 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\Store;
+use SMW\ChangePropListener;
+use SMW\Parameters;
+use SMW\DIProperty;
+use SMW\SQLStore\Exception\TableMissingIdFieldException;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class PropertyTableUpdater {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var PropertyStatisticsStore
+ */
+ private $propertyStatisticsStore;
+
+ /**
+ * @var array
+ */
+ private $stats = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param Store $store
+ * @param PropertyStatisticsStore $propertyStatisticsStore
+ */
+ public function __construct( Store $store, PropertyStatisticsStore $propertyStatisticsStore ) {
+ $this->store = $store;
+ $this->propertyStatisticsStore = $propertyStatisticsStore;
+ }
+
+ /**
+ * Update all property tables and any dependent data (hashes,
+ * statistics, etc.) by inserting/deleting the given values. The ID of
+ * the page that is updated, and the hashes of the properties must be
+ * given explicitly (the hashes could not be computed from the insert
+ * and delete data alone anyway).
+ *
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param Parameters $parameters
+ */
+ public function update( $id, Parameters $parameters ) {
+
+ $this->stats = [];
+
+ $insert_rows = $parameters->get( 'insert_rows' );
+ $delete_rows = $parameters->get( 'delete_rows' );
+
+ $this->doUpdate( $insert_rows, $delete_rows );
+ $new_hashes = $parameters->get( 'new_hashes' );
+
+ // If only rows are marked for deletion then modify hashs to ensure that
+ // any inbalance can be corrected by the next insert operation for which
+ // the new_hashes are computed (seen in connection with redirects)
+ if ( $insert_rows === [] && $delete_rows !== [] ) {
+ foreach ( $new_hashes as $key => $hash ) {
+ $new_hashes[$key] = $hash . '.d';
+ }
+ }
+
+ if ( $insert_rows !== [] || $delete_rows !== [] ) {
+ $this->store->getObjectIds()->setPropertyTableHashes( $id, $new_hashes );
+ }
+
+ $this->propertyStatisticsStore->addToUsageCounts(
+ $this->stats
+ );
+ }
+
+ /**
+ * It is assumed and required that the tables mentioned in
+ * $tablesInsertRows and $tablesDeleteRows are the same, and that all
+ * $rows in these datasets refer to the same subject ID.
+ *
+ * @param array $insert_rows
+ * @param array $delete_rows
+ */
+ private function doUpdate( array $insert_rows, array $delete_rows ) {
+
+ $propertyTables = $this->store->getPropertyTables();
+
+ // Note: by construction, the inserts and deletes have the same table keys.
+ // Note: by construction, the inserts and deletes are currently disjoint;
+ // yet we delete first to make the method more robust/versatile.
+ foreach ( $insert_rows as $tableName => $insertRows ) {
+
+ $propertyTable = $propertyTables[$tableName];
+
+ // Should not occur, but let's be strict
+ if ( !$propertyTable->usesIdSubject() ) {
+ throw new TableMissingIdFieldException( $propertyTable->getName() );
+ }
+
+ // Delete
+ $this->update_rows( $propertyTable, $delete_rows[$tableName], false );
+
+ // Insert
+ $this->update_rows( $propertyTable, $insertRows, true );
+ }
+ }
+
+ /**
+ * Update one property table by inserting or deleting rows, and compute
+ * the changes that this entails for the property usage counts. The
+ * given rows are inserted into the table if $insert is true; otherwise
+ * they are deleted. The property usage counts are recorded in the
+ * call-by-ref parameter $propertyUseIncrements.
+ *
+ * The method assumes that all of the given rows are about the same
+ * subject. This is ensured by callers.
+ *
+ * @param PropertyTableDefinition $propertyTable
+ * @param array $rows array of rows to insert/delete
+ * @param boolean $insert
+ */
+ private function update_rows( PropertyTableDefinition $propertyTable, array $rows, $insert ) {
+
+ if ( empty( $rows ) ) {
+ return;
+ }
+
+ if ( $insert ) {
+ $this->insert( $propertyTable, $rows );
+ } else {
+ $this->delete( $propertyTable, $rows );
+ }
+
+ if ( $propertyTable->isFixedPropertyTable() ) {
+
+ $property = new DIProperty(
+ $propertyTable->getFixedProperty()
+ );
+
+ $pid = $this->store->getObjectIds()->makeSMWPropertyID( $property );
+ }
+
+ foreach ( $rows as $row ) {
+
+ if ( !$propertyTable->isFixedPropertyTable() ) {
+ $pid = $row['p_id'];
+ }
+
+ ChangePropListener::record(
+ $pid,
+ [
+ 'row' => $row,
+ 'is_insert' => $insert
+ ]
+ );
+
+ if ( !array_key_exists( $pid, $this->stats ) ) {
+ $this->stats[$pid] = 0;
+ }
+
+ $this->stats[$pid] += ( $insert ? 1 : -1 );
+ }
+ }
+
+ private function insert( PropertyTableDefinition $propertyTable, array $rows ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $tableName = $propertyTable->getName();
+
+ $connection->insert(
+ $tableName,
+ $rows,
+ __METHOD__ . "-$tableName"
+ );
+ }
+
+ private function delete( PropertyTableDefinition $propertyTable, array $rows ) {
+
+ $condition = '';
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ // We build a condition that mentions s_id only once,
+ // since it must be the same for all rows. This should
+ // help the DBMS in selecting the rows (it would not be
+ // easy for to detect that all tuples share one s_id).
+ $sid = false;
+ $tableName = $propertyTable->getName();
+
+ foreach ( $rows as $row ) {
+ if ( $sid === false ) {
+ if ( !array_key_exists( 's_id', (array)$row ) ) {
+ // FIXME: The assumption that s_id is present does not hold.
+ // This return is there to prevent fatal errors, but does
+ // not fix the issue of this code being broken
+ return;
+ }
+
+ // 's_id' exists for all tables with $propertyTable->usesIdSubject()
+ $sid = $row['s_id'];
+ }
+
+ unset( $row['s_id'] );
+
+ if ( $condition != '' ) {
+ $condition .= ' OR ';
+ }
+
+ $condition .= '(' . $connection->makeList( $row, LIST_AND ) . ')';
+ }
+
+ $condition = "s_id=" . $connection->addQuotes( $sid ) . " AND ($condition)";
+
+ $connection->delete(
+ $tableName,
+ [ $condition ],
+ __METHOD__ . "-$tableName"
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTypeFinder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTypeFinder.php
new file mode 100644
index 00000000..f65c8e7b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/PropertyTypeFinder.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use RuntimeException;
+use SMW\DIProperty;
+use SMW\MediaWiki\Database;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class PropertyTypeFinder {
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var string
+ */
+ private $typeTableName = '';
+
+ /**
+ * @since 2.5
+ *
+ * @param Database $connection
+ */
+ public function __construct( Database $connection ) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $typeTableName
+ */
+ public function setTypeTableName( $typeTableName ) {
+ $this->typeTableName = $typeTableName;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return string
+ * @throws RuntimeException
+ */
+ public function findTypeID( DIProperty $property ) {
+
+ try {
+ $row = $this->connection->selectRow(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_id'
+ ],
+ [
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_title' => $property->getKey(),
+ 'smw_iw' => '',
+ 'smw_subobject' => ''
+ ],
+ __METHOD__
+ );
+ } catch ( \Exception $e ) {
+ $row = false;
+ }
+
+ if ( !isset( $row->smw_id ) ) {
+ return $GLOBALS['smwgPDefaultType'];
+ }
+
+ if ( $this->typeTableName === '' ) {
+ throw new RuntimeException( "Missing a table name" );
+ }
+
+ // The Finder is executed before tables are initialized with a corresponding
+ // and matchable DIHandler therefore using Store::getPropertyValue cannot
+ // be used at this point as it would create a circular reference during
+ // the table initialization.
+ //
+ // We expect it to be a URI table with `o_serialized` containing the
+ // type string
+ $row = $this->connection->selectRow(
+ $this->typeTableName,
+ [
+ 'o_serialized'
+ ],
+ [
+ 's_id' => $row->smw_id
+ ],
+ __METHOD__
+ );
+
+ if ( $row === false ) {
+ return $GLOBALS['smwgPDefaultType'];
+ }
+
+ // e.g. http://semantic-mediawiki.org/swivt/1.0#_num
+ list( $url, $fragment ) = explode( "#", $row->o_serialized );
+
+ return $fragment;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/DependencyLinksTableUpdater.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/DependencyLinksTableUpdater.php
new file mode 100644
index 00000000..7a623771
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/DependencyLinksTableUpdater.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace SMW\SQLStore\QueryDependency;
+
+use Psr\Log\LoggerAwareTrait;
+use SMW\DIWikiPage;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class DependencyLinksTableUpdater {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var array
+ */
+ private static $updateList = [];
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ */
+ public function __construct( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return Store
+ */
+ public function getStore() {
+ return $this->store;
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function clear() {
+ self::$updateList = [];
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $sid
+ * @param array|null $dependencyList
+ */
+ public function addToUpdateList( $sid, array $dependencyList = null ) {
+
+ if ( $sid == 0 || $dependencyList === null || $dependencyList === [] ) {
+ return null;
+ }
+
+ if ( !isset( self::$updateList[$sid] ) ) {
+ return self::$updateList[$sid] = $dependencyList;
+ }
+
+ self::$updateList[$sid] = array_merge( self::$updateList[$sid], $dependencyList );
+ }
+
+ /**
+ * @since 2.4
+ */
+ public function doUpdate() {
+ foreach ( self::$updateList as $sid => $dependencyList ) {
+
+ if ( $dependencyList === [] ) {
+ continue;
+ }
+
+ $this->updateDependencyList( $sid, $dependencyList );
+ self::$updateList[$sid] = [];
+ }
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array $dependencyList
+ */
+ public function deleteDependenciesFromList( array $deleteIdList ) {
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'list' => implode( ' ,', $deleteIdList )
+ ];
+
+ $this->logger->info( '[QueryDependency] Delete dependencies: {list}', $context );
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $connection->beginAtomicTransaction( __METHOD__ );
+
+ $connection->delete(
+ SQLStore::QUERY_LINKS_TABLE,
+ [
+ 's_id' => $deleteIdList
+ ],
+ __METHOD__
+ );
+
+ $connection->endAtomicTransaction( __METHOD__ );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param integer $sid
+ * @param array $dependencyList
+ */
+ private function updateDependencyList( $sid, array $dependencyList ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $connection->beginAtomicTransaction( __METHOD__ );
+
+ // Before an insert, delete all entries that for the criteria which is
+ // cheaper then doing an individual upsert or selectRow, this also ensures
+ // that entries are self-corrected for dependencies matched
+ $connection->delete(
+ SQLStore::QUERY_LINKS_TABLE,
+ [
+ 's_id' => $sid
+ ],
+ __METHOD__
+ );
+
+ if ( $sid == 0 ) {
+ return $connection->endAtomicTransaction( __METHOD__ );
+ }
+
+ $inserts = [];
+
+ foreach ( $dependencyList as $dependency ) {
+
+ if ( !$dependency instanceof DIWikiPage ) {
+ continue;
+ }
+
+ $oid = $this->getId( $dependency );
+
+ // If the ID_TABLE didn't contained an valid ID then we create one ourselves
+ // to ensure that object entities are tracked from the start
+ // This can happen when a query is added with object reference that have not
+ // yet been referenced as annotation and therefore do not recognized as
+ // value annotation
+ if ( $oid < 1 && ( ( $oid = $this->createId( $dependency ) ) < 1 ) ) {
+ continue;
+ }
+
+ $inserts[$sid . $oid] = [
+ 's_id' => $sid,
+ 'o_id' => $oid
+ ];
+ }
+
+ if ( $inserts === [] ) {
+ return $connection->endAtomicTransaction( __METHOD__ );
+ }
+
+ // MW's multi-array insert needs a numeric dimensional array but the key
+ // was used with a hash to avoid duplicate entries hence the re-copy
+ $inserts = array_values( $inserts );
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'id' => $sid
+ ];
+
+ $this->logger->info( '[QueryDependency] Table insert: {id} ID', $context );
+
+ $connection->insert(
+ SQLStore::QUERY_LINKS_TABLE,
+ $inserts,
+ __METHOD__
+ );
+
+ $connection->endAtomicTransaction( __METHOD__ );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIWikiPage $subject, $subobjectName
+ * @param string $subobjectName
+ */
+ public function getId( DIWikiPage $subject, $subobjectName = '' ) {
+
+ if ( $subobjectName !== '' ) {
+ $subject = new DIWikiPage(
+ $subject->getDBkey(),
+ $subject->getNamespace(),
+ $subject->getInterwiki(),
+ $subobjectName
+ );
+ }
+
+ $id = $this->store->getObjectIds()->getId(
+ $subject
+ );
+
+ return $id;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param DIWikiPage $subject, $subobjectName
+ * @param string $subobjectName
+ */
+ public function createId( DIWikiPage $subject, $subobjectName = '' ) {
+
+ $id = $this->store->getObjectIds()->makeSMWPageID(
+ $subject->getDBkey(),
+ $subject->getNamespace(),
+ $subject->getInterwiki(),
+ $subobjectName,
+ false
+ );
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'id' => $id,
+ 'origin' => $subject->getHash() . $subobjectName
+
+ ];
+
+ $this->logger->info( '[QueryDependency] Table update: new {id} ID; {origin}', $context );
+
+ return $id;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/DependencyLinksUpdateJournal.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/DependencyLinksUpdateJournal.php
new file mode 100644
index 00000000..ff9d8526
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/DependencyLinksUpdateJournal.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace SMW\SQLStore\QueryDependency;
+
+use Onoi\Cache\Cache;
+use Psr\Log\LoggerAwareTrait;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Deferred\CallableUpdate;
+use Title;
+
+/**
+ * Temporary storage of entities that are expected to be refreshed (or updated)
+ * during an article view due to being a dependency of an altered query.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class DependencyLinksUpdateJournal {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var string
+ */
+ const VERSION = '0.1';
+
+ /**
+ * Namespace for the cache instance
+ */
+ const CACHE_NAMESPACE = 'smw:update:qdep';
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var CallableUpdate
+ */
+ private $callableUpdate;
+
+ /**
+ * @since 3.0
+ *
+ * @param Cache $cache
+ * @param callableUpdate $callableUpdate
+ */
+ public function __construct( Cache $cache, CallableUpdate $callableUpdate ) {
+ $this->cache = $cache;
+ $this->callableUpdate = $callableUpdate;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return string
+ */
+ public static function makeKey( $subject ) {
+
+ $segments = [];
+
+ if ( $subject instanceof DIWikiPage || $subject instanceof Title ) {
+ $segments = [ $subject->getDBKey(), $subject->getNamespace(), $subject->getInterwiki(), '' ];
+ }
+
+ return smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [
+ implode( '#', $segments ),
+ self::VERSION
+ ]
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $hashList
+ * @param integer|true $revID
+ */
+ public function updateFromList( array $hashList, $revID = true ) {
+
+ foreach ( $hashList as $hash ) {
+
+ $key = smwfCacheKey(
+ self::CACHE_NAMESPACE,
+ [
+ $hash,
+ self::VERSION
+ ]
+ );
+
+ $this->cache->save( $key, $revID );
+ }
+
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage|Title $subject
+ * @param integer|true $revID
+ */
+ public function update( $subject, $revID = true ) {
+ $this->cache->save( self::makeKey( $subject ), $revID );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage|Title $subject
+ */
+ public function has( $subject ) {
+ return $this->cache->contains( self::makeKey( $subject ) ) === true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ */
+ public function delete( $subject ) {
+
+ if ( !$subject instanceof Title && !$subject instanceof DIWikiPage ) {
+ throw new RuntimeException( "Invalid subject instance" );
+ }
+
+ // Avoid interference with any other process during a preOutputCommit
+ // stage especially when CACHE_DB is used as instance
+ $this->callableUpdate->setCallback( function() use( $subject ) {
+ $this->cache->delete( self::makeKey( $subject ) );
+ } );
+
+ $this->callableUpdate->setOrigin(
+ [
+ __METHOD__,
+ $subject->getDBKey() . '#' . $subject->getNamespace() . '#' . $subject->getInterwiki()
+ ]
+ );
+
+ $this->callableUpdate->pushUpdate();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/EntityIdListRelevanceDetectionFilter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/EntityIdListRelevanceDetectionFilter.php
new file mode 100644
index 00000000..ab103fb2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/EntityIdListRelevanceDetectionFilter.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace SMW\SQLStore\QueryDependency;
+
+use Psr\Log\LoggerAwareTrait;
+use SMW\SQLStore\ChangeOp\ChangeOp;
+use SMW\Store;
+use SMW\Utils\Timer;
+
+/**
+ * This class filters entities recorded in the ChangeOp
+ * and applies a relevance rule set by:
+ *
+ * - Remove exempted properties (not relevant)
+ * - Add properties that are affiliated on a relational change
+ *
+ * By affiliation implies that a property listed is not directly related to a query
+ * dependency, yet it is monitored and can, if altered trigger a dependency update
+ * that normally is only reserved to dependent properties.
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class EntityIdListRelevanceDetectionFilter {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Store
+ */
+ private $store = null;
+
+ /**
+ * @var ChangeOp
+ */
+ private $changeOp = null;
+
+ /**
+ * @var array
+ */
+ private $propertyExemptionList = [];
+
+ /**
+ * @var array
+ */
+ private $affiliatePropertyDetectionList = [];
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ * @param ChangeOp $changeOp
+ */
+ public function __construct( Store $store, ChangeOp $changeOp ) {
+ $this->store = $store;
+ $this->changeOp = $changeOp;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return DIWikiPage
+ */
+ public function getSubject() {
+ return $this->changeOp->getSubject();
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array $propertyExemptionList
+ */
+ public function setPropertyExemptionList( array $propertyExemptionList ) {
+ $this->propertyExemptionList = array_flip(
+ str_replace( ' ', '_', $propertyExemptionList )
+ );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param array $affiliatePropertyDetectionList
+ */
+ public function setAffiliatePropertyDetectionList( array $affiliatePropertyDetectionList ) {
+ $this->affiliatePropertyDetectionList = array_flip(
+ str_replace( ' ', '_', $affiliatePropertyDetectionList )
+ );
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return array
+ */
+ public function getFilteredIdList() {
+
+ Timer::start( __CLASS__ );
+
+ $changedEntityIdSummaryList = array_flip(
+ $this->changeOp->getChangedEntityIdSummaryList()
+ );
+
+ $affiliateEntityList = [];
+ $tableChangeOps = $this->changeOp->getTableChangeOps();
+
+ foreach ( $tableChangeOps as $tableChangeOp ) {
+ $this->applyFilterToTableChangeOp(
+ $tableChangeOp,
+ $affiliateEntityList,
+ $changedEntityIdSummaryList
+ );
+ }
+
+ $filteredIdList = array_merge(
+ array_keys( $changedEntityIdSummaryList ),
+ array_keys( $affiliateEntityList )
+ );
+
+ $this->logger->info(
+ [
+ 'QueryDependency',
+ 'EntityIdListRelevanceDetectionFilter',
+ 'Filter changeOp list',
+ 'procTime in sec: {procTime}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'procTime' => Timer::getElapsedTime( __CLASS__, 6 )
+ ]
+ );
+
+ return $filteredIdList;
+ }
+
+ private function applyFilterToTableChangeOp( $tableChangeOp, &$affiliateEntityList, &$changedEntityIdSummaryList ) {
+
+ foreach ( $tableChangeOp->getFieldChangeOps( 'insert' ) as $insertFieldChangeOp ) {
+
+ // Copy fields temporarily
+ if ( $tableChangeOp->isFixedPropertyOp() ) {
+ $insertFieldChangeOp->set( 'p_id', $tableChangeOp->getFixedPropertyValueBy( 'p_id' ) );
+ $insertFieldChangeOp->set( 'key', $tableChangeOp->getFixedPropertyValueBy( 'key' ) );
+ }
+
+ $this->modifyEntityList( $insertFieldChangeOp, $affiliateEntityList, $changedEntityIdSummaryList );
+ }
+
+ foreach ( $tableChangeOp->getFieldChangeOps( 'delete' ) as $deleteFieldChangeOp ) {
+
+ if ( $tableChangeOp->isFixedPropertyOp() ) {
+ $deleteFieldChangeOp->set( 'p_id', $tableChangeOp->getFixedPropertyValueBy( 'p_id' ) );
+ $deleteFieldChangeOp->set( 'key', $tableChangeOp->getFixedPropertyValueBy( 'key' ) );
+ }
+
+ $this->modifyEntityList( $deleteFieldChangeOp, $affiliateEntityList, $changedEntityIdSummaryList );
+ }
+ }
+
+ private function modifyEntityList( $fieldChangeOp, &$affiliateEntityList, &$changedEntityIdSummaryList ) {
+ $key = '';
+
+ if ( $fieldChangeOp->has( 'key' ) ) {
+ $key = $fieldChangeOp->get( 'key' );
+ } elseif ( $fieldChangeOp->has( 'p_id' ) ) {
+ $dataItem = $this->store->getObjectIds()->getDataItemById( $fieldChangeOp->get( 'p_id' ) );
+ $key = $dataItem !== null ? $dataItem->getDBKey() : null;
+ }
+
+ // Exclusion before inclusion
+ if ( isset( $this->propertyExemptionList[$key]) ) {
+ $this->unsetEntityList( $fieldChangeOp, $changedEntityIdSummaryList );
+ return;
+ }
+
+ if ( isset( $this->affiliatePropertyDetectionList[$key] ) && $fieldChangeOp->has( 's_id' ) ) {
+ $affiliateEntityList[$fieldChangeOp->get( 's_id' )] = true;
+ }
+ }
+
+ private function unsetEntityList( $fieldChangeOp, &$changedEntityIdSummaryList ) {
+ // Remove matched blacklisted property reference
+ if ( $fieldChangeOp->has( 'p_id' ) ) {
+ unset( $changedEntityIdSummaryList[$fieldChangeOp->get( 'p_id' )] );
+ }
+
+ // Remove associated subject ID's
+ if ( $fieldChangeOp->has( 's_id' ) ) {
+ unset( $changedEntityIdSummaryList[$fieldChangeOp->get( 's_id' )] );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryDependencyLinksStore.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryDependencyLinksStore.php
new file mode 100644
index 00000000..7c1cc359
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryDependencyLinksStore.php
@@ -0,0 +1,567 @@
+<?php
+
+namespace SMW\SQLStore\QueryDependency;
+
+use Psr\Log\LoggerAwareTrait;
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Jobs\ParserCachePurgeJob;
+use SMW\RequestOptions;
+use SMW\SQLStore\ChangeOp\ChangeOp;
+use SMW\SQLStore\SQLStore;
+use SMW\Store;
+use SMW\Utils\Timer;
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class QueryDependencyLinksStore {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var DependencyLinksTableUpdater
+ */
+ private $dependencyLinksTableUpdater;
+
+ /**
+ * @var QueryResultDependencyListResolver
+ */
+ private $queryResultDependencyListResolver;
+
+ /**
+ * @var NamespaceExaminer
+ */
+ private $namespaceExaminer;
+
+ /**
+ * @var boolean
+ */
+ private $isEnabled = true;
+
+ /**
+ * @var boolean
+ */
+ private $isCommandLineMode = false;
+
+ /**
+ * @var boolean
+ */
+ private $isPrimary = false;
+
+ /**
+ * Time factor to be used to determine whether an update should actually occur
+ * or not. The comparison is made against the page_touched timestamp (updated
+ * by the ParserCachePurgeJob) to a previous update to avoid unnecessary DB
+ * transactions if it takes place within the computed time frame.
+ *
+ * @var integer
+ */
+ private $skewFactorForDependencyUpdateInSeconds = 10;
+
+ /**
+ * @since 2.3
+ *
+ * @param QueryResultDependencyListResolver $queryResultDependencyListResolver
+ * @param DependencyLinksTableUpdater $dependencyLinksTableUpdater
+ */
+ public function __construct( QueryResultDependencyListResolver $queryResultDependencyListResolver, DependencyLinksTableUpdater $dependencyLinksTableUpdater ) {
+ $this->queryResultDependencyListResolver = $queryResultDependencyListResolver;
+ $this->dependencyLinksTableUpdater = $dependencyLinksTableUpdater;
+ $this->store = $this->dependencyLinksTableUpdater->getStore();
+ $this->namespaceExaminer = ApplicationFactory::getInstance()->getNamespaceExaminer();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ */
+ public function setStore( Store $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @see https://www.mediawiki.org/wiki/Manual:$wgCommandLineMode
+ * Indicates whether MW is running in command-line mode.
+ *
+ * @since 2.5
+ *
+ * @param boolean $isCommandLineMode
+ */
+ public function isCommandLineMode( $isCommandLineMode ) {
+ $this->isCommandLineMode = $isCommandLineMode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isPrimary
+ */
+ public function isPrimary( $isPrimary ) {
+ $this->isPrimary = $isPrimary;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return boolean
+ */
+ public function isEnabled() {
+ return $this->isEnabled;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param boolean $isEnabled
+ */
+ public function setEnabled( $isEnabled ) {
+ $this->isEnabled = (bool)$isEnabled;
+ }
+
+ /**
+ * This method is called from the `SMW::SQLStore::AfterDataUpdateComplete` hook and
+ * removes outdated query ID's from the table if the diff contains a `delete`
+ * entry for the _ask table.
+ *
+ * @since 2.3
+ *
+ * @param ChangeOp $changeOp
+ */
+ public function pruneOutdatedTargetLinks( ChangeOp $changeOp ) {
+
+ if ( !$this->isEnabled() ) {
+ return null;
+ }
+
+ Timer::start( __METHOD__ );
+ $hash = null;
+
+ $tableName = $this->store->getPropertyTableInfoFetcher()->findTableIdForProperty(
+ new DIProperty( '_ASK' )
+ );
+
+ $tableChangeOps = $changeOp->getTableChangeOps( $tableName );
+
+ // Remove any dependency for queries that are no longer used
+ foreach ( $tableChangeOps as $tableChangeOp ) {
+
+ if ( !$tableChangeOp->hasChangeOp( 'delete' ) ) {
+ continue;
+ }
+
+ $deleteIdList = [];
+
+ foreach ( $tableChangeOp->getFieldChangeOps( 'delete' ) as $fieldChangeOp ) {
+ $deleteIdList[] = $fieldChangeOp->get( 'o_id' );
+ }
+
+ $this->dependencyLinksTableUpdater->deleteDependenciesFromList( $deleteIdList );
+ }
+
+ if ( ( $subject = $changeOp->getSubject() ) !== null ) {
+ $hash = $subject->getHash();
+ }
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $hash,
+ 'procTime' => Timer::getElapsedTime( __METHOD__, 7 )
+ ];
+
+ $this->logger->info(
+ '[QueryDependency] Prune links completed: {origin} (procTime in sec: {procTime})',
+ $context
+ );
+
+ return true;
+ }
+
+ /**
+ * Build the ParserCachePurgeJob parameters on filtered entities to minimize
+ * necessary update work.
+ *
+ * @since 2.3
+ *
+ * @param EntityIdListRelevanceDetectionFilter $entityIdListRelevanceDetectionFilter
+ */
+ public function pushParserCachePurgeJob( EntityIdListRelevanceDetectionFilter $entityIdListRelevanceDetectionFilter ) {
+
+ if ( !$this->isEnabled() ) {
+ return;
+ }
+
+ $filteredIdList = $entityIdListRelevanceDetectionFilter->getFilteredIdList();
+
+ if ( $filteredIdList === [] ) {
+ return;
+ }
+
+ $parserCachePurgeJob = ApplicationFactory::getInstance()->newJobFactory()->newParserCachePurgeJob(
+ $entityIdListRelevanceDetectionFilter->getSubject()->getTitle(),
+ [
+ 'idlist' => $filteredIdList,
+ 'exec.mode' => ParserCachePurgeJob::EXEC_JOURNAL
+ ]
+ );
+
+ if ( $this->isPrimary || $this->isCommandLineMode ) {
+ $parserCachePurgeJob->run();
+ } else {
+ $parserCachePurgeJob->lazyPush();
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return array
+ */
+ public function findEmbeddedQueryIdListBySubject( DIWikiPage $subject, RequestOptions $requestOptions = null ) {
+
+ $embeddedQueryIdList = [];
+
+ $dataItems = $this->store->getPropertyValues(
+ $subject,
+ new DIProperty( '_ASK' ),
+ $requestOptions
+ );
+
+ foreach ( $dataItems as $dataItem ) {
+ $embeddedQueryIdList[$dataItem->getHash()] = $this->dependencyLinksTableUpdater->getId( $dataItem );
+ }
+
+ return $embeddedQueryIdList;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ * @param RequestOptions $requestOptions
+ *
+ * @return array
+ */
+ public function findDependencyTargetLinksForSubject( DIWikiPage $subject, RequestOptions $requestOptions ) {
+ return $this->findDependencyTargetLinks(
+ [ $this->dependencyLinksTableUpdater->getId( $subject ) ],
+ $requestOptions
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer|array $id
+ *
+ * @return integer
+ */
+ public function countDependencies( $id ) {
+
+ $count = 0;
+ $ids = !is_array( $id ) ? (array)$id : $id;
+
+ if ( $ids === [] || !$this->isEnabled() ) {
+ return $count;
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $row = $connection->selectRow(
+ SQLStore::QUERY_LINKS_TABLE,
+ [
+ 'COUNT(s_id) AS count'
+ ],
+ [
+ 'o_id' => $ids
+ ],
+ __METHOD__
+ );
+
+ $count = $row ? $row->count : $count;
+
+ return (int)$count;
+ }
+
+ /**
+ * Finds a partial list (given limit and offset) of registered subjects that
+ * that represent a dependency on something like a subject in a query list,
+ * a property, or a printrequest.
+ *
+ * `s_id` contains the subject id that links to the query that fulfills one
+ * of the conditions cited above.
+ *
+ * Prefetched Ids are turned into a hash list that can later be split into
+ * chunks to work either in online or batch mode without creating a huge memory
+ * foothold.
+ *
+ * @note Select a list is crucial for performance as any selectRow would /
+ * single Id select would strain the system on large list connected to a
+ * query
+ *
+ * @since 2.3
+ *
+ * @param array $idlist
+ * @param RequestOptions $requestOptions
+ *
+ * @return array
+ */
+ public function findDependencyTargetLinks( array $idlist, RequestOptions $requestOptions ) {
+
+ if ( $idlist === [] || !$this->isEnabled() ) {
+ return [];
+ }
+
+ $options = [
+ 'LIMIT' => $requestOptions->getLimit(),
+ 'OFFSET' => $requestOptions->getOffset(),
+ ] + [ 'DISTINCT' ];
+
+ $conditions = [
+ 'o_id' => $idlist
+ ];
+
+ foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
+ $conditions[] = $extraCondition;
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $rows = $connection->select(
+ SQLStore::QUERY_LINKS_TABLE,
+ [ 's_id' ],
+ $conditions,
+ __METHOD__,
+ $options
+ );
+
+ $targetLinksIdList = [];
+
+ foreach ( $rows as $row ) {
+ $targetLinksIdList[] = $row->s_id;
+ }
+
+ if ( $targetLinksIdList === [] ) {
+ return [];
+ }
+
+ // Return the expected count of targets
+ $requestOptions->setOption( 'links.count', count( $targetLinksIdList ) );
+
+ $poolRequestOptions = new RequestOptions();
+
+ $poolRequestOptions->addExtraCondition(
+ 'smw_iw !=' . $connection->addQuotes( SMW_SQL3_SMWREDIIW ) . ' AND '.
+ 'smw_iw !=' . $connection->addQuotes( SMW_SQL3_SMWDELETEIW )
+ );
+
+ return $this->store->getObjectIds()->getDataItemPoolHashListFor(
+ $targetLinksIdList,
+ $poolRequestOptions
+ );
+ }
+
+ /**
+ * This method is called from the `SMW::Store::AfterQueryResultLookupComplete` hook
+ * to resolve and update dependencies fetched from an embedded query and its
+ * QueryResult object.
+ *
+ * @since 2.3
+ *
+ * @param QueryResult|string $queryResult
+ */
+ public function updateDependencies( $queryResult ) {
+
+ if ( !$this->canUpdateDependencies( $queryResult ) ) {
+ return null;
+ }
+
+ Timer::start( __METHOD__ );
+
+ $subject = $queryResult->getQuery()->getContextPage();
+ $hash = $queryResult->getQuery()->getQueryId();
+
+ $sid = $this->dependencyLinksTableUpdater->getId(
+ $subject,
+ $hash
+ );
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'id' => $sid
+ ];
+
+ if ( $this->isRegistered( $sid, $subject ) ) {
+ return $this->logger->info(
+ '[QueryDependency] Skipping update: {id} (already registered, no dependency update)',
+ $context
+ );
+ }
+
+ // Executed as DeferredTransactionalUpdate
+ $callback = function() use( $queryResult, $subject, $sid, $hash ) {
+ $this->doUpdate( $queryResult, $subject, $sid, $hash );
+ };
+
+ $deferredTransactionalUpdate = ApplicationFactory::getInstance()->newDeferredTransactionalCallableUpdate(
+ $callback
+ );
+
+ $origin = $subject->getHash();
+
+ $deferredTransactionalUpdate->setOrigin( [ __METHOD__, $origin ] );
+ $deferredTransactionalUpdate->markAsPending( $this->isCommandLineMode );
+ $deferredTransactionalUpdate->setFingerprint( $hash );
+
+ $deferredTransactionalUpdate->enabledDeferredUpdate( true );
+ $deferredTransactionalUpdate->waitOnTransactionIdle();
+
+ $deferredTransactionalUpdate->pushUpdate();
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $origin,
+ 'procTime' => Timer::getElapsedTime( __METHOD__, 7 )
+ ];
+
+ $this->logger->info(
+ '[QueryDependency] Update dependencies registered: {origin} (procTime in sec: {procTime})',
+ $context
+ );
+
+ return true;
+ }
+
+ private function doUpdate( $queryResult, $subject, $sid, $hash ) {
+
+ $dependencyList = $this->queryResultDependencyListResolver->getDependencyListFrom(
+ $queryResult
+ );
+
+ // Add extra dependencies which we only get "late" after the QueryResult
+ // object as been resolved by the ResultPrinter, this is done to
+ // avoid having to process the QueryResult recursively on its own
+ // (which would carry a performance penalty)
+ $dependencyListByLateRetrieval = $this->queryResultDependencyListResolver->getDependencyListByLateRetrievalFrom(
+ $queryResult
+ );
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'origin' => $hash
+ ];
+
+ if ( $dependencyList === [] && $dependencyListByLateRetrieval === [] ) {
+ return $this->logger->info(
+ '[QueryDependency] no update: {origin} (no dependency list available)',
+ $context
+ );
+ }
+
+ // SID < 0 means the storage update/process has not been finalized
+ // (new object hasn't been registered)
+ if ( $sid < 1 || ( $sid = $this->dependencyLinksTableUpdater->getId( $subject, $hash ) ) < 1 ) {
+ $sid = $this->dependencyLinksTableUpdater->createId( $subject, $hash );
+ }
+
+ $this->dependencyLinksTableUpdater->addToUpdateList(
+ $sid,
+ $dependencyList
+ );
+
+ $this->dependencyLinksTableUpdater->addToUpdateList(
+ $sid,
+ $dependencyListByLateRetrieval
+ );
+
+ $this->dependencyLinksTableUpdater->doUpdate();
+ }
+
+ private function canUpdateDependencies( $queryResult ) {
+
+ if ( !$this->isEnabled() || !$queryResult instanceof QueryResult ) {
+ return false;
+ }
+
+ $query = $queryResult->getQuery();
+
+ $actions = [
+ // #2484 Avoid any update activities during a stashedit API access
+ 'stashedit',
+
+ // Avoid update on `submit` during a preview
+ 'submit',
+
+ // Avoid update on `parse` during a wikieditor preview
+ 'parse'
+ ];
+
+ if ( in_array( $query->getOption( 'request.action' ), $actions ) ) {
+ return false;
+ }
+
+ if ( $query === null || $query->getContextPage() === null ) {
+ return false;
+ }
+
+ // Make sure that when a query is embedded in a not supported NS to bail
+ // out
+ if ( !$this->namespaceExaminer->isSemanticEnabled( $query->getContextPage()->getNamespace() ) ) {
+ return false;
+ }
+
+ return $query->getLimit() > 0 && $query->getOption( Query::NO_DEPENDENCY_TRACE ) !== true;
+ }
+
+ private function isRegistered( $sid, $subject ) {
+
+ static $suppressUpdateCache = [];
+ $hash = $subject->getHash();
+
+ if ( $sid < 1 ) {
+ return false;
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $row = $connection->selectRow(
+ SQLStore::QUERY_LINKS_TABLE,
+ [
+ 's_id'
+ ],
+ [ 's_id' => $sid ],
+ __METHOD__
+ );
+
+ $title = $subject->getTitle();
+
+ // https://phabricator.wikimedia.org/T167943
+ if ( !isset( $suppressUpdateCache[$hash] ) && $title !== null ) {
+ $suppressUpdateCache[$hash] = wfTimestamp( TS_MW, $title->getTouched() ) + $this->skewFactorForDependencyUpdateInSeconds;
+ }
+
+ // Check whether the query has already been registered and only then
+ // check for a possible divergent time
+ return $row !== false && $suppressUpdateCache[$hash] > wfTimestamp( TS_MW );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryReferenceBacklinks.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryReferenceBacklinks.php
new file mode 100644
index 00000000..b683627c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryReferenceBacklinks.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace SMW\SQLStore\QueryDependency;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Message;
+use SMW\RequestOptions;
+use SMW\SemanticData;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class QueryReferenceBacklinks {
+
+ /**
+ * @var QueryDependencyLinksStore
+ */
+ private $queryDependencyLinksStore = null;
+
+ /**
+ * @since 2.5
+ *
+ * @param QueryDependencyLinksStore $queryDependencyLinksStore
+ */
+ public function __construct( QueryDependencyLinksStore $queryDependencyLinksStore ) {
+ $this->queryDependencyLinksStore = $queryDependencyLinksStore;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param SemanticData $semanticData
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return boolean
+ */
+ public function addReferenceLinksTo( SemanticData $semanticData, RequestOptions $requestOptions = null ) {
+
+ if ( !$this->queryDependencyLinksStore->isEnabled() ) {
+ return false;
+ }
+
+ // Don't display a reference where the requesting page is
+ // part of the list that contains queries (suppress self-embedded queries)
+ foreach ( $this->queryDependencyLinksStore->findEmbeddedQueryIdListBySubject( $semanticData->getSubject() ) as $key => $qid ) {
+ $requestOptions->addExtraCondition( 's_id!=' . $qid );
+ }
+
+ $referenceLinks = $this->findReferenceLinks( $semanticData->getSubject(), $requestOptions );
+
+ $property = new DIProperty(
+ '_ASK'
+ );
+
+ foreach ( $referenceLinks as $subject ) {
+ $semanticData->addPropertyObjectValue( $property, DIWikiPage::doUnserialize( $subject ) );
+ }
+
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIWikiPage $subject
+ * @param integer $limit
+ * @param integer $offset
+ *
+ * @return array
+ */
+ public function findReferenceLinks( DIWikiPage $subject, RequestOptions $requestOptions = null ) {
+
+ $queryTargetLinksHashList = $this->queryDependencyLinksStore->findDependencyTargetLinksForSubject(
+ $subject,
+ $requestOptions
+ );
+
+ return $queryTargetLinksHashList;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ * @param DIWikiPage $subject
+ *
+ * @return boolean
+ */
+ public function doesRequireFurtherLink( DIProperty $property, DIWikiPage $subject, &$html ) {
+
+ if ( $property->getKey() !== '_ASK' ) {
+ return true;
+ }
+
+ $localURL = \SpecialPage::getSafeTitleFor( 'SearchByProperty' )->getLocalURL(
+ [
+ 'property' => $property->getLabel(),
+ 'value' => $subject->getTitle()->getPrefixedText()
+ ]
+ );
+
+ $html .= \Html::element(
+ 'a',
+ [ 'href' => $localURL ],
+ Message::get( 'smw_browse_more' )
+ );
+
+ // Return false in order to stop the link creation process the replace the
+ // generate link
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryResultDependencyListResolver.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryResultDependencyListResolver.php
new file mode 100644
index 00000000..a56c4103
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependency/QueryResultDependencyListResolver.php
@@ -0,0 +1,285 @@
+<?php
+
+namespace SMW\SQLStore\QueryDependency;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\HierarchyLookup;
+use SMW\Query\Language\ClassDescription;
+use SMW\Query\Language\ConceptDescription;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ThingDescription;
+use SMW\Query\Language\ValueDescription;
+use SMWQueryResult as QueryResult;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class QueryResultDependencyListResolver {
+
+ /**
+ * @var HierarchyLookup
+ */
+ private $hierarchyLookup;
+
+ /**
+ * Specifies a list of property keys to be excluded from the detection
+ * process.
+ *
+ * @var array
+ */
+ private $propertyDependencyExemptionlist = [];
+
+ /**
+ * @since 2.3
+ *
+ * @param $queryResult Can be a string for when format=Debug
+ * @param HierarchyLookup $hierarchyLookup
+ */
+ public function __construct( HierarchyLookup $hierarchyLookup ) {
+ $this->hierarchyLookup = $hierarchyLookup;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param array $propertyDependencyExemptionlist
+ */
+ public function setPropertyDependencyExemptionlist( array $propertyDependencyExemptionlist ) {
+ // Make sure that user defined properties are correctly normalized and flip
+ // to build an index based map
+ $this->propertyDependencyExemptionlist = array_flip(
+ str_replace( ' ', '_', $propertyDependencyExemptionlist )
+ );
+ }
+
+ /**
+ * At the point where the QueryResult instantiates results by means of the
+ * ResultArray, record the objects with the help of the ResolverJournal.
+ *
+ * When the `... updateDependencies` is executed in deferred mode it allows
+ * a "late" access to track dependencies of column/row entities without having
+ * to resolve the QueryResult object on its own, see
+ * ResultArray::getNextDataValue/ResultArray::getNextDataItem.
+ *
+ * @since 2.4
+ *
+ * @param QueryResult|string $queryResult
+ *
+ * @return DIWikiPage[]|[]
+ */
+ public function getDependencyListByLateRetrievalFrom( $queryResult ) {
+
+ if ( !$this->canResolve( $queryResult ) ) {
+ return [];
+ }
+
+ $resolverJournal = $queryResult->getResolverJournal();
+
+ $dependencyList = $resolverJournal->getEntityList();
+ $resolverJournal->prune();
+
+ return $dependencyList;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param QueryResult|string $queryResult
+ *
+ * @return DIWikiPage[]|[]
+ */
+ public function getDependencyListFrom( $queryResult ) {
+
+ if ( !$this->canResolve( $queryResult ) ) {
+ return [];
+ }
+
+ $description = $queryResult->getQuery()->getDescription();
+
+ $dependencySubjectList = [
+ $queryResult->getQuery()->getContextPage()
+ ];
+
+ // Find entities described by the query
+ $this->doResolveDependenciesFromDescription(
+ $dependencySubjectList,
+ $queryResult->getStore(),
+ $description
+ );
+
+ $this->doResolveDependenciesFromPrintRequest(
+ $dependencySubjectList,
+ $description->getPrintRequests()
+ );
+
+ $dependencySubjectList = array_merge(
+ $dependencySubjectList,
+ $queryResult->getResults()
+ );
+
+ $queryResult->reset();
+
+ return $dependencySubjectList;
+ }
+
+ /**
+ * Resolving dependencies for non-embedded queries or limit=0 (which only
+ * links to Special:Ask via further results) is not required
+ */
+ private function canResolve( $queryResult ) {
+ return $queryResult instanceof QueryResult && $queryResult->getQuery() !== null && $queryResult->getQuery()->getContextPage() !== null && $queryResult->getQuery()->getLimit() > 0;
+ }
+
+ private function doResolveDependenciesFromDescription( &$subjects, $store, $description ) {
+
+ // Ignore entities that use a comparator other than SMW_CMP_EQ
+ // [[Has page::~Foo*]] or similar is going to be ignored
+ if ( $description instanceof ValueDescription &&
+ $description->getDataItem() instanceof DIWikiPage &&
+ $description->getComparator() === SMW_CMP_EQ ) {
+ $subjects[] = $description->getDataItem();
+ }
+
+ if ( $description instanceof ConceptDescription && $concept = $description->getConcept() ) {
+ if ( $concept === null || !isset( $subjects[$concept->getHash()] ) ) {
+ $subjects[$concept->getHash()] = $concept;
+ $this->doResolveDependenciesFromDescription(
+ $subjects,
+ $store,
+ $this->getConceptDescription( $store, $concept )
+ );
+ }
+ }
+
+ if ( $description instanceof ClassDescription ) {
+ foreach ( $description->getCategories() as $category ) {
+
+ if ( $this->hierarchyLookup->hasSubcategory( $category ) ) {
+ $this->doMatchSubcategory( $subjects, $category );
+ }
+
+ $subjects[] = $category;
+ }
+ }
+
+ if ( $description instanceof SomeProperty ) {
+ $this->doResolveDependenciesFromDescription( $subjects, $store, $description->getDescription() );
+ $this->doMatchProperty( $subjects, $description->getProperty() );
+ }
+
+ if ( $description instanceof Conjunction || $description instanceof Disjunction ) {
+ foreach ( $description->getDescriptions() as $description ) {
+ $this->doResolveDependenciesFromDescription( $subjects, $store, $description );
+ }
+ }
+ }
+
+ private function doMatchProperty( &$subjects, DIProperty $property ) {
+
+ if ( $property->isInverse() ) {
+ $property = new DIProperty( $property->getKey() );
+ }
+
+ $subject = $property->getCanonicalDiWikiPage();
+
+ if ( $this->hierarchyLookup->hasSubproperty( $property ) ) {
+ $this->doMatchSubproperty( $subjects, $subject, $property );
+ }
+
+ // Use the key here do match against pre-defined properties (e.g. _MDAT)
+ $key = str_replace( ' ', '_', $property->getKey() );
+
+ if ( !isset( $this->propertyDependencyExemptionlist[$key] ) ) {
+ $subjects[$subject->getHash()] = $subject;
+ }
+ }
+
+ private function doMatchSubcategory( &$subjects, DIWikiPage $category ) {
+
+ $hash = $category->getHash();
+ $subcategories = [];
+
+ // #1713
+ // Safeguard against a possible category (or redirect thereof) to point
+ // to itself by relying on tracking the hash of already inserted objects
+ if ( !isset( $subjects[$hash] ) ) {
+ $subcategories = $this->hierarchyLookup->getConsecutiveHierarchyList( $category );
+ }
+
+ foreach ( $subcategories as $subcategory ) {
+ $subjects[$subcategory->getHash()] = $subcategory;
+
+ if ( $this->hierarchyLookup->hasSubcategory( $subcategory ) ) {
+ $this->doMatchSubcategory( $subjects, $subcategory );
+ }
+ }
+ }
+
+ private function doMatchSubproperty( &$subjects, $subject, DIProperty $property ) {
+
+ $subproperties = [];
+
+ // Using the DBKey as short-cut, as we don't expect to match sub-properties for
+ // pre-defined properties instead it should be sufficient for user-defined
+ // properties to rely on the normalized DBKey (e.g Has_page)
+ if (
+ !isset( $subjects[$subject->getHash()] ) &&
+ !isset( $this->propertyDependencyExemptionlist[$subject->getDBKey()] ) ) {
+ $subproperties = $this->hierarchyLookup->getConsecutiveHierarchyList( $property );
+ }
+
+ foreach ( $subproperties as $subproperty ) {
+
+ if ( isset( $this->propertyDependencyExemptionlist[$subproperty->getKey()] ) ) {
+ continue;
+ }
+
+ $subject = $subproperty->getCanonicalDiWikiPage();
+ $subjects[$subject->getHash()] = $subject;
+
+ $this->doMatchProperty( $subjects, $subproperty );
+ }
+ }
+
+ private function doResolveDependenciesFromPrintRequest( &$subjects, array $printRequests ) {
+
+ foreach ( $printRequests as $printRequest ) {
+ $data = $printRequest->getData();
+
+ if ( $data instanceof \SMWPropertyValue ) {
+ $subjects[] = $data->getDataItem()->getCanonicalDiWikiPage();
+ }
+
+ // Category
+ if ( $data instanceof \Title ) {
+ $subjects[] = DIWikiPage::newFromTitle( $data );
+ }
+ }
+ }
+
+ private function getConceptDescription( $store, DIWikiPage $concept ) {
+
+ $value = $store->getPropertyValues(
+ $concept,
+ new DIProperty( '_CONC' )
+ );
+
+ if ( $value === null || $value === [] ) {
+ return new ThingDescription();
+ }
+
+ $value = end( $value );
+
+ return ApplicationFactory::getInstance()->newQueryParser()->getQueryDescription(
+ $value->getConceptQuery()
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependencyLinksStoreFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependencyLinksStoreFactory.php
new file mode 100644
index 00000000..d557a8a4
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryDependencyLinksStoreFactory.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\ApplicationFactory;
+use SMW\Site;
+use SMW\SQLStore\ChangeOp\ChangeOp;
+use SMW\SQLStore\QueryDependency\DependencyLinksTableUpdater;
+use SMW\SQLStore\QueryDependency\DependencyLinksUpdateJournal;
+use SMW\SQLStore\QueryDependency\EntityIdListRelevanceDetectionFilter;
+use SMW\SQLStore\QueryDependency\QueryDependencyLinksStore;
+use SMW\SQLStore\QueryDependency\QueryReferenceBacklinks;
+use SMW\SQLStore\QueryDependency\QueryResultDependencyListResolver;
+use SMW\Store;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class QueryDependencyLinksStoreFactory {
+
+ /**
+ * @since 3.0
+ *
+ * @return DependencyLinksUpdateJournal
+ */
+ public function newDependencyLinksUpdateJournal() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $dependencyLinksUpdateJournal = new DependencyLinksUpdateJournal(
+ $applicationFactory->getCache(),
+ $applicationFactory->newDeferredCallableUpdate()
+ );
+
+ $dependencyLinksUpdateJournal->setLogger(
+ $applicationFactory->getMediaWikiLogger()
+ );
+
+ return $dependencyLinksUpdateJournal;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return QueryResultDependencyListResolver
+ */
+ public function newQueryResultDependencyListResolver() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $queryResultDependencyListResolver = new QueryResultDependencyListResolver(
+ $applicationFactory->newHierarchyLookup()
+ );
+
+ $queryResultDependencyListResolver->setPropertyDependencyExemptionlist(
+ $applicationFactory->getSettings()->get( 'smwgQueryDependencyPropertyExemptionList' )
+ );
+
+ return $queryResultDependencyListResolver;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ *
+ * @return QueryDependencyLinksStore
+ */
+ public function newQueryDependencyLinksStore( Store $store ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $logger = $applicationFactory->getMediaWikiLogger();
+
+ $dependencyLinksTableUpdater = new DependencyLinksTableUpdater(
+ $store
+ );
+
+ $dependencyLinksTableUpdater->setLogger(
+ $logger
+ );
+
+ $queryDependencyLinksStore = new QueryDependencyLinksStore(
+ $this->newQueryResultDependencyListResolver(),
+ $dependencyLinksTableUpdater
+ );
+
+ $queryDependencyLinksStore->setLogger(
+ $logger
+ );
+
+ $queryDependencyLinksStore->setEnabled(
+ $applicationFactory->getSettings()->get( 'smwgEnabledQueryDependencyLinksStore' )
+ );
+
+ $queryDependencyLinksStore->isCommandLineMode(
+ Site::isCommandLineMode()
+ );
+
+ return $queryDependencyLinksStore;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @param Store $store
+ * @param ChangeOp $changeOp
+ *
+ * @return EntityIdListRelevanceDetectionFilter
+ */
+ public function newEntityIdListRelevanceDetectionFilter( Store $store, ChangeOp $changeOp ) {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $entityIdListRelevanceDetectionFilter = new EntityIdListRelevanceDetectionFilter(
+ $store,
+ $changeOp
+ );
+
+ $entityIdListRelevanceDetectionFilter->setLogger(
+ ApplicationFactory::getInstance()->getMediaWikiLogger()
+ );
+
+ $entityIdListRelevanceDetectionFilter->setPropertyExemptionList(
+ $settings->get( 'smwgQueryDependencyPropertyExemptionList' )
+ );
+
+ $entityIdListRelevanceDetectionFilter->setAffiliatePropertyDetectionList(
+ $settings->get( 'smwgQueryDependencyAffiliatePropertyDetectionList' )
+ );
+
+ return $entityIdListRelevanceDetectionFilter;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ *
+ * @return QueryReferenceBacklinks
+ */
+ public function newQueryReferenceBacklinks( Store $store ) {
+ return new QueryReferenceBacklinks( $this->newQueryDependencyLinksStore( $store ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/ConceptQuerySegmentBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/ConceptQuerySegmentBuilder.php
new file mode 100644
index 00000000..107c6b3b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/ConceptQuerySegmentBuilder.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use RuntimeException;
+use SMW\Query\Parser as QueryParser;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class ConceptQuerySegmentBuilder {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @var QuerySegmentListProcessor
+ */
+ private $querySegmentListProcessor;
+
+ /**
+ * @var QueryParser
+ */
+ private $queryParser;
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ * @param QuerySegmentListProcessor $querySegmentListProcessor
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder, QuerySegmentListProcessor $querySegmentListProcessor ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ $this->querySegmentListProcessor = $querySegmentListProcessor;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param QueryParser $queryParser
+ */
+ public function setQueryParser( QueryParser $queryParser ) {
+ $this->queryParser = $queryParser;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $conceptDescriptionText
+ *
+ * @return QuerySegment|null
+ */
+ public function getQuerySegmentFrom( $conceptDescriptionText ) {
+
+ QuerySegment::$qnum = 0;
+
+ $querySegmentListBuilder = $this->querySegmentListBuilder;
+ $querySegmentListBuilder->setSortKeys( [] );
+
+ if ( $this->queryParser === null ) {
+ throw new RuntimeException( 'Missing a QueryParser instance' );
+ }
+
+ $querySegmentListBuilder->getQuerySegmentFrom(
+ $this->queryParser->getQueryDescription( $conceptDescriptionText )
+ );
+
+ $qid = $querySegmentListBuilder->getLastQuerySegmentId();
+ $querySegmentList = $querySegmentListBuilder->getQuerySegmentList();
+
+ if ( $qid < 0 ) {
+ return null;
+ }
+
+ // execute query tree, resolve all dependencies
+ $this->querySegmentListProcessor->setQueryMode(
+ Query::MODE_INSTANCES
+ );
+
+ $this->querySegmentListProcessor->setQuerySegmentList(
+ $querySegmentList
+ );
+
+ $this->querySegmentListProcessor->process( $qid );
+
+ return $querySegmentList[$qid];
+ }
+
+ /**
+ * @since 2.2
+ */
+ public function cleanUp() {
+ $this->querySegmentListProcessor->setQueryMode( Query::MODE_INSTANCES );
+ $this->querySegmentListProcessor->cleanUp();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->querySegmentListBuilder->getErrors();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreter.php
new file mode 100644
index 00000000..e5c5241f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreter.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use SMW\Query\Language\Description;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+interface DescriptionInterpreter {
+
+ /**
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description );
+
+ /**
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return QuerySegment
+ */
+ public function interpretDescription( Description $description );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreterFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreterFactory.php
new file mode 100644
index 00000000..391553a8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreterFactory.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use SMW\ApplicationFactory;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreters\ClassDescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreters\ConceptDescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreters\DisjunctionConjunctionInterpreter;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreters\DispatchingDescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreters\NamespaceDescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreters\SomePropertyInterpreter;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreters\ThingDescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreters\ValueDescriptionInterpreter;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class DescriptionInterpreterFactory {
+
+ /**
+ * @since 2.4
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ *
+ * @return DispatchingDescriptionInterpreter
+ */
+ public function newDispatchingDescriptionInterpreter( QuerySegmentListBuilder $querySegmentListBuilder ) {
+
+ $pplicationFactory = ApplicationFactory::getInstance();
+ $dispatchingDescriptionInterpreter = new DispatchingDescriptionInterpreter();
+
+ $dispatchingDescriptionInterpreter->addDefaultInterpreter(
+ new ThingDescriptionInterpreter( $querySegmentListBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new SomePropertyInterpreter( $querySegmentListBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new DisjunctionConjunctionInterpreter( $querySegmentListBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new NamespaceDescriptionInterpreter( $querySegmentListBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new ClassDescriptionInterpreter( $querySegmentListBuilder )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ new ValueDescriptionInterpreter( $querySegmentListBuilder )
+ );
+
+ $conceptDescriptionInterpreter = new ConceptDescriptionInterpreter(
+ $querySegmentListBuilder
+ );
+
+ $conceptDescriptionInterpreter->setQueryParser(
+ $pplicationFactory->getQueryFactory()->newQueryParser(
+ $pplicationFactory->getSettings()->get( 'smwgQConceptFeatures' )
+ )
+ );
+
+ $dispatchingDescriptionInterpreter->addInterpreter(
+ $conceptDescriptionInterpreter
+ );
+
+ return $dispatchingDescriptionInterpreter;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php
new file mode 100644
index 00000000..e936bf94
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ClassDescriptionInterpreter.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\DIProperty;
+use SMW\Query\Language\ClassDescription;
+use SMW\Query\Language\Description;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\QuerySegment;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class ClassDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof ClassDescription;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return QuerySegment
+ */
+ public function interpretDescription( Description $description ) {
+
+ $query = new QuerySegment();
+
+ $cqid = QuerySegment::$qnum;
+ $cquery = new QuerySegment();
+ $cquery->type = QuerySegment::Q_CLASS_HIERARCHY;
+ $cquery->joinfield = [];
+ $cquery->depth = $description->getHierarchyDepth();
+
+ foreach ( $description->getCategories() as $category ) {
+
+ $categoryId = $this->querySegmentListBuilder->getStore()->getObjectIds()->getSMWPageID(
+ $category->getDBkey(),
+ NS_CATEGORY,
+ $category->getInterwiki(),
+ ''
+ );
+
+ if ( $categoryId != 0 ) {
+ $cquery->joinfield[] = $categoryId;
+ }
+ }
+
+ if ( count( $cquery->joinfield ) == 0 ) { // Empty result.
+ $query->type = QuerySegment::Q_VALUE;
+ $query->joinTable = '';
+ $query->joinfield = '';
+ } else { // Instance query with disjunction of classes (categories)
+ $query->joinTable = $this->querySegmentListBuilder->getStore()->findPropertyTableID( new DIProperty( '_INST' ) );
+ $query->joinfield = "$query->alias.s_id";
+ $query->components[$cqid] = "$query->alias.o_id";
+
+ $this->querySegmentListBuilder->addQuerySegment( $cquery );
+ }
+
+ return $query;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ComparatorMapper.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ComparatorMapper.php
new file mode 100644
index 00000000..e32ac8c2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ComparatorMapper.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use RuntimeException;
+use SMW\Query\Language\ValueDescription;
+use SMWDIUri as DIUri;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class ComparatorMapper {
+
+ /**
+ * @since 2.2
+ *
+ * @param ValueDescription $description
+ * @param string &$value
+ *
+ * @return string
+ * @throws RuntimeException
+ */
+ public function mapComparator( ValueDescription $description, &$value ) {
+
+ $comparatorMap = [
+ SMW_CMP_EQ => '=',
+ SMW_CMP_LESS => '<',
+ SMW_CMP_GRTR => '>',
+ SMW_CMP_LEQ => '<=',
+ SMW_CMP_GEQ => '>=',
+ SMW_CMP_NEQ => '!=',
+ SMW_CMP_LIKE => ' LIKE ',
+ SMW_CMP_PRIM_LIKE => ' LIKE ',
+ SMW_CMP_NLKE => ' NOT LIKE ',
+ SMW_CMP_PRIM_NLKE => ' NOT LIKE '
+ ];
+
+ $comparator = $description->getComparator();
+
+ if ( !isset( $comparatorMap[$comparator] ) ) {
+ throw new RuntimeException( "Unsupported comparator $comparator in value description." );
+ }
+
+ if ( $comparator === SMW_CMP_LIKE || $comparator === SMW_CMP_NLKE || $comparator === SMW_CMP_PRIM_LIKE || $comparator === SMW_CMP_PRIM_NLKE ) {
+
+ if ( $description->getDataItem() instanceof DIUri ) {
+ $value = str_replace( [ 'http://', 'https://', '%2A' ], [ '*', '*', '*' ], $value );
+ }
+
+ // Escape to prepare string matching:
+ $value = str_replace(
+ [ '\\', '%', '_', '*', '?' ],
+ [ '\\\\', '\%', '\_', '%', '_' ],
+ $value
+ );
+ }
+
+ return $comparatorMap[$comparator];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php
new file mode 100644
index 00000000..82955af1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ConceptDescriptionInterpreter.php
@@ -0,0 +1,197 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use RuntimeException;
+use SMW\Query\Language\ConceptDescription;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Parser as QueryParser;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\QuerySegment;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuilder;
+use SMWSQLStore3;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class ConceptDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @var QueryParser
+ */
+ private $queryParser;
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof ConceptDescription;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param QueryParser $queryParser
+ */
+ public function setQueryParser( QueryParser $queryParser ) {
+ $this->queryParser = $queryParser;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return QuerySegment
+ */
+ public function interpretDescription( Description $description ) {
+
+ $query = new QuerySegment();
+ $concept = $description->getConcept();
+
+ $conceptId = $this->querySegmentListBuilder->getStore()->getObjectIds()->getSMWPageID(
+ $concept->getDBkey(),
+ SMW_NS_CONCEPT,
+ '',
+ ''
+ );
+
+ $hash = 'concept-' . $conceptId;
+
+ $this->querySegmentListBuilder->getCircularReferenceGuard()->mark( $hash );
+
+ if ( $this->querySegmentListBuilder->getCircularReferenceGuard()->isCircular( $hash ) ) {
+
+ $this->querySegmentListBuilder->addError(
+ [ 'smw-query-condition-circular', $description->getQueryString() ]
+ );
+
+ return $query;
+ }
+
+ $db = $this->querySegmentListBuilder->getStore()->getConnection( 'mw.db.queryengine' );
+ $row = $this->getConceptForId( $db, $conceptId );
+
+ // No description found, concept does not exist.
+ if ( $row === false ) {
+ $this->querySegmentListBuilder->getCircularReferenceGuard()->unmark( 'concept-' . $conceptId );
+ // keep the above query object, it yields an empty result
+ // TODO: announce an error here? (maybe not, since the query processor can check for
+ // non-existing concept pages which is probably the main reason for finding nothing here)
+ return $query;
+ };
+
+ global $smwgQConceptCaching, $smwgQMaxSize, $smwgQMaxDepth, $smwgQFeatures, $smwgQConceptCacheLifetime;
+
+ $may_be_computed = ( $smwgQConceptCaching == CONCEPT_CACHE_NONE ) ||
+ ( ( $smwgQConceptCaching == CONCEPT_CACHE_HARD ) && ( ( ~( ~( $row->concept_features + 0 ) | $smwgQFeatures ) ) == 0 ) &&
+ ( $smwgQMaxSize >= $row->concept_size ) && ( $smwgQMaxDepth >= $row->concept_depth ) );
+
+ if ( $row->cache_date &&
+ ( ( $row->cache_date > ( strtotime( "now" ) - $smwgQConceptCacheLifetime * 60 ) ) ||
+ !$may_be_computed ) ) { // Cached concept, use cache unless it is dead and can be revived.
+
+ $query->joinTable = SMWSQLStore3::CONCEPT_CACHE_TABLE;
+ $query->joinfield = "$query->alias.s_id";
+ $query->where = "$query->alias.o_id=" . $db->addQuotes( $conceptId );
+ } elseif ( $row->concept_txt ) { // Parse description and process it recursively.
+ if ( $may_be_computed ) {
+ $description = $this->getConceptQueryDescriptionFrom( $row->concept_txt );
+
+ $this->findCircularDescription(
+ $concept,
+ $description
+ );
+
+ $qid = $this->querySegmentListBuilder->getQuerySegmentFrom( $description );
+
+ if ($qid != -1) {
+ $query = $this->querySegmentListBuilder->findQuerySegment( $qid );
+ } else { // somehow the concept query is no longer valid; maybe some syntax changed (upgrade) or global settings were modified since storing it
+ $this->querySegmentListBuilder->addError( 'smw_emptysubquery' ); // not the right message, but this case is very rare; let us not make detailed messages for this
+ }
+ } else {
+ $this->querySegmentListBuilder->addError(
+ [ 'smw_concept_cache_miss', $concept->getDBkey() ]
+ );
+ }
+ } // else: no cache, no description (this may happen); treat like empty concept
+
+ $this->querySegmentListBuilder->getCircularReferenceGuard()->unmark( $hash );
+
+ return $query;
+ }
+
+ /**
+ * We bypass the storage interface here (which is legal as we control it,
+ * and safe if we are careful with changes ...)
+ *
+ * This should be faster, but we must implement the unescaping that concepts
+ * do on getWikiValue
+ */
+ private function getConceptForId( $db, $id ) {
+ return $db->selectRow(
+ 'smw_fpt_conc',
+ [ 'concept_txt', 'concept_features', 'concept_size', 'concept_depth', 'cache_date' ],
+ [ 's_id' => $id ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * No defaultnamespaces here; If any, these are already in the concept.
+ * Unescaping is the same as in SMW_DV_Conept's getWikiValue().
+ */
+ private function getConceptQueryDescriptionFrom( $conceptQuery ) {
+
+ if ( $this->queryParser === null ) {
+ throw new RuntimeException( 'Missing a QueryParser instance' );
+ }
+
+ return $this->queryParser->getQueryDescription(
+ str_replace( [ '&lt;', '&gt;', '&amp;' ], [ '<', '>', '&' ], $conceptQuery )
+ );
+ }
+
+ private function findCircularDescription( $concept, &$description ) {
+
+ if ( $description instanceof ConceptDescription ) {
+ if ( $description->getConcept()->equals( $concept ) ) {
+ $this->querySegmentListBuilder->addError(
+ [ 'smw-query-condition-circular', $description->getQueryString() ]
+ );
+ return;
+ }
+ }
+
+ if ( $description instanceof Conjunction || $description instanceof Disjunction ) {
+ foreach ( $description->getDescriptions() as $desc ) {
+ $this->findCircularDescription( $concept, $desc );
+ }
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/DisjunctionConjunctionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/DisjunctionConjunctionInterpreter.php
new file mode 100644
index 00000000..9f2286d8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/DisjunctionConjunctionInterpreter.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\QuerySegment;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class DisjunctionConjunctionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof Conjunction || $description instanceof Disjunction;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return QuerySegment
+ */
+ public function interpretDescription( Description $description ) {
+
+ $query = new QuerySegment();
+ $query->type = $description instanceof Conjunction ? QuerySegment::Q_CONJUNCTION : QuerySegment::Q_DISJUNCTION;
+
+ foreach ( $description->getDescriptions() as $subDescription ) {
+
+ $subQueryId = $this->querySegmentListBuilder->getQuerySegmentFrom( $subDescription );
+
+ if ( $subQueryId >= 0 ) {
+ $query->components[$subQueryId] = true;
+ }
+ }
+
+ // All subconditions failed, drop this as well.
+ if ( count( $query->components ) == 0 ) {
+ $query->type = QuerySegment::Q_NOQUERY;
+ }
+
+ return $query;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/DispatchingDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/DispatchingDescriptionInterpreter.php
new file mode 100644
index 00000000..1bfe71a6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/DispatchingDescriptionInterpreter.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Description;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreter;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class DispatchingDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var DescriptionInterpreter[]
+ */
+ private $interpreters = [];
+
+ /**
+ * @var DescriptionInterpreter
+ */
+ private $defaultInterpreter = null;
+
+ /**
+ * @param Description $description
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+
+ foreach ( $this->interpreters as $interpreter ) {
+ if ( $interpreter->canInterpretDescription( $description ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param Description $description
+ *
+ * @return QuerySegment
+ * @throws InvalidArgumentException
+ */
+ public function interpretDescription( Description $description ) {
+
+ foreach ( $this->interpreters as $interpreter ) {
+ if ( $interpreter->canInterpretDescription( $description ) ) {
+ return $interpreter->interpretDescription( $description );
+ }
+ }
+
+ // Instead of throwing an exception we return a ThingDescriptionInterpreter
+ // for all unregistered/unknown descriptions
+ return $this->defaultInterpreter->interpretDescription( $description );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param DescriptionInterpreter $defaultInterpreter
+ */
+ public function addInterpreter( DescriptionInterpreter $interpreter ) {
+ $this->interpreters[] = $interpreter;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param DescriptionInterpreter $defaultInterpreter
+ */
+ public function addDefaultInterpreter( DescriptionInterpreter $defaultInterpreter ) {
+ $this->defaultInterpreter = $defaultInterpreter;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php
new file mode 100644
index 00000000..b7b52d24
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/NamespaceDescriptionInterpreter.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Description;
+use SMW\Query\Language\NamespaceDescription;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\QuerySegment;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuilder;
+use SMWSql3SmwIds;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class NamespaceDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof NamespaceDescription;
+ }
+
+ /**
+ * TODO: One instance of the SMW IDs table on s_id always suffices (swm_id is KEY)! Doable in execution ... (PERFORMANCE)
+ *
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return QuerySegment
+ */
+ public function interpretDescription( Description $description ) {
+
+ $db = $this->querySegmentListBuilder->getStore()->getConnection( 'mw.db.queryengine' );
+
+ $query = new QuerySegment();
+ $query->joinTable = SMWSql3SmwIds::TABLE_NAME;
+ $query->joinfield = "$query->alias.smw_id";
+ $query->where = "$query->alias.smw_namespace=" . $db->addQuotes( $description->getNamespace() );
+
+ return $query;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php
new file mode 100644
index 00000000..39dfa3ab
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/SomePropertyInterpreter.php
@@ -0,0 +1,319 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use RuntimeException;
+use SMW\DataTypeRegistry;
+use SMW\Query\Language\Conjunction;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\Disjunction;
+use SMW\Query\Language\SomeProperty;
+use SMW\Query\Language\ThingDescription;
+use SMW\Query\Language\ValueDescription;
+use SMW\SQLStore\EntityStore\DataItemHandler;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\FulltextSearchTableFactory;
+use SMW\SQLStore\QueryEngine\QuerySegment;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuilder;
+use SMWDataItem as DataItem;
+use SMWSql3SmwIds;
+use SMWSQLStore3Table;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class SomePropertyInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @var ComparatorMapper
+ */
+ private $comparatorMapper;
+
+ /**
+ * @var FulltextSearchTableFactory
+ */
+ private $fulltextSearchTableFactory;
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ $this->comparatorMapper = new ComparatorMapper();
+ $this->fulltextSearchTableFactory = new FulltextSearchTableFactory();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof SomeProperty;
+ }
+
+ /**
+ * @todo The case of nominal classes (top-level ValueDescription) still
+ * makes some assumptions about the table structure, especially about the
+ * name of the joinfield (o_id). Better extend
+ * compilePropertyValueDescription to deal with this case.
+ *
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return QuerySegment
+ */
+ public function interpretDescription( Description $description ) {
+
+ $query = new QuerySegment();
+
+ $this->interpretPropertyConditionForDescription(
+ $query,
+ $description
+ );
+
+ return $query;
+ }
+
+ /**
+ * Modify the given query object to account for some property condition for
+ * the given property. If it is not possible to generate a query for the
+ * given data, the query type is changed to QueryContainer::Q_NOQUERY. Callers need
+ * to check for this and discard the query in this case.
+ *
+ * @note This method does not support sortkey (_SKEY) property queries,
+ * since they do not have a normal property table. This should not be a
+ * problem since comparators on sortkeys are supported indirectly when
+ * using comparators on wikipages. There is no reason to create any
+ * query with _SKEY ad users cannot do so either (no user label).
+ *
+ * @since 1.8
+ */
+ private function interpretPropertyConditionForDescription( QuerySegment $query, SomeProperty $description ) {
+
+ $db = $this->querySegmentListBuilder->getStore()->getConnection( 'mw.db.queryengine' );
+
+ $property = $description->getProperty();
+
+ $tableid = $this->querySegmentListBuilder->getStore()->findPropertyTableID( $property );
+
+ if ( $tableid === '' ) { // Give up
+ $query->type = QuerySegment::Q_NOQUERY;
+ return;
+ }
+
+ $proptables = $this->querySegmentListBuilder->getStore()->getPropertyTables();
+ $proptable = $proptables[$tableid];
+
+ if ( !$proptable->usesIdSubject() ) {
+ // no queries with such tables
+ // (only redirects are affected in practice)
+ $query->type = QuerySegment::Q_NOQUERY;
+ return;
+ }
+
+ $typeid = $property->findPropertyTypeID();
+ $diType = DataTypeRegistry::getInstance()->getDataItemId( $typeid );
+
+ if ( $property->isInverse() && $diType !== DataItem::TYPE_WIKIPAGE ) {
+ // can only invert properties that point to pages
+ $query->type = QuerySegment::Q_NOQUERY;
+ return;
+ }
+
+ $diHandler = $this->querySegmentListBuilder->getStore()->getDataItemHandlerForDIType( $diType );
+ $indexField = $diHandler->getIndexField();
+
+ // TODO: strictly speaking, the DB key is not what we want here,
+ // since sortkey is based on a "wiki value"
+ $sortkey = $property->getKey();
+
+ // *** Now construct the query ... ***//
+ $query->joinTable = $proptable->getName();
+ $query->depth = $description->getHierarchyDepth();
+
+ // *** Add conditions for selecting rows for this property ***//
+ if ( !$proptable->isFixedPropertyTable() ) {
+ $pid = $this->querySegmentListBuilder->getStore()->getObjectIds()->getSMWPropertyID( $property );
+
+ // Construct property hierarchy:
+ $pqid = QuerySegment::$qnum;
+ $pquery = new QuerySegment();
+ $pquery->type = QuerySegment::Q_PROP_HIERARCHY;
+ $pquery->joinfield = [ $pid ];
+ $pquery->depth = $description->getHierarchyDepth();
+ $query->components[$pqid] = "{$query->alias}.p_id";
+
+ $this->querySegmentListBuilder->addQuerySegment( $pquery );
+
+ // Alternative code without property hierarchies:
+ // $query->where = "{$query->alias}.p_id=" . $this->m_dbs->addQuotes( $pid );
+ } // else: no property column, no hierarchy queries
+
+ // *** Add conditions on the value of the property ***//
+ if ( $diType === DataItem::TYPE_WIKIPAGE ) {
+ $o_id = $indexField;
+ if ( $property->isInverse() ) {
+ $s_id = $o_id;
+ $o_id = 's_id';
+ } else {
+ $s_id = 's_id';
+ }
+ $query->joinfield = "{$query->alias}.{$s_id}";
+
+ // process page description like main query
+ $sub = $this->querySegmentListBuilder->getQuerySegmentFrom(
+ $description->getDescription()
+ );
+
+ if ( $sub >= 0 ) {
+ $subQuery = $this->querySegmentListBuilder->findQuerySegment(
+ $sub
+ );
+
+ $o_id = $subQuery->indexField !== '' ? $subQuery->indexField : $o_id;
+ $query->components[$sub] = "{$query->alias}.{$o_id}";
+ }
+
+ if ( array_key_exists( $sortkey, $this->querySegmentListBuilder->getSortKeys() ) ) {
+ // TODO: This SMW IDs table is possibly duplicated in the query.
+ // Example: [[has capital::!Berlin]] with sort=has capital
+ // Can we prevent that? (PERFORMANCE)
+ $query->from = ' INNER JOIN ' . $db->tableName( SMWSql3SmwIds::TABLE_NAME ) .
+ " AS ids{$query->alias} ON ids{$query->alias}.smw_id={$query->alias}.{$o_id}";
+ $query->sortfields[$sortkey] = "ids{$query->alias}.smw_sort";
+ }
+ } else { // non-page value description
+ $query->joinfield = "{$query->alias}.s_id";
+ $this->compilePropertyValueDescription( $query, $description->getDescription(), $proptable, $diHandler, 'AND' );
+ if ( array_key_exists( $sortkey, $this->querySegmentListBuilder->getSortKeys() ) ) {
+ $query->sortfields[$sortkey] = isset( $query->sortIndexField ) ? $query->sortIndexField : "{$query->alias}.{$indexField}";
+ }
+ }
+ }
+
+ /**
+ * Given an Description that is just a conjunction or disjunction of
+ * ValueDescription objects, create and return a plain WHERE condition
+ * string for it.
+ *
+ * @param $query
+ * @param Description $description
+ * @param SMWSQLStore3Table $proptable
+ * @param DataItemHandler $diHandler for that table
+ * @param string $operator SQL operator "AND" or "OR"
+ */
+ private function compilePropertyValueDescription(
+ $query, Description $description, SMWSQLStore3Table $proptable, DataItemHandler $diHandler, $operator ) {
+
+ if ( $description instanceof ValueDescription ) {
+ $this->mapValueDescription( $query, $description, $diHandler, $operator );
+ } elseif ( ( $description instanceof Conjunction ) ||
+ ( $description instanceof Disjunction ) ) {
+ $op = ( $description instanceof Conjunction ) ? 'AND' : 'OR';
+
+ // #556 ensure correct parentheses are applied for something
+ // like "(a OR b OR c) AND d AND e"
+ if ( $query->where && substr( $query->where, -1 ) != '(' ) {
+ $query->where .= " $operator ";
+ }
+
+ $query->where .= "(";
+
+ foreach ( $description->getDescriptions() as $subdesc ) {
+ $this->compilePropertyValueDescription( $query, $subdesc, $proptable, $diHandler, $op );
+ }
+
+ $query->where .= ")";
+
+ } elseif ( $description instanceof ThingDescription ) {
+ // nothing to do
+ } else {
+ throw new RuntimeException( "Cannot process this type of Description." );
+ }
+ }
+
+ /**
+ * Given an Description that is just a conjunction or disjunction of
+ * ValueDescription objects, create and return a plain WHERE condition
+ * string for it.
+ *
+ * @param $query
+ * @param ValueDescription $description
+ * @param DataItemHandler $diHandler for that table
+ * @param string $operator SQL operator "AND" or "OR"
+ */
+ private function mapValueDescription(
+ $query, ValueDescription $description, DataItemHandler $diHandler, $operator ) {
+
+ $where = '';
+ $dataItem = $description->getDataItem();
+ $db = $this->querySegmentListBuilder->getStore()->getConnection( 'mw.db.queryengine' );
+
+ $valueMatchConditionBuilder = $this->fulltextSearchTableFactory->newValueMatchConditionBuilderByType(
+ $this->querySegmentListBuilder->getStore()
+ );
+
+ // TODO Better get the handle from the property type
+ // Some comparators (e.g. LIKE) could use DI values of
+ // a different type; we care about the property table, not
+ // about the value
+
+ // Do not support smw_id joined data for now.
+ $indexField = $diHandler->getIndexField();
+
+ //Hack to get to the field used as index
+ $keys = $diHandler->getWhereConds( $dataItem );
+ $value = $keys[$indexField];
+
+ // See if the getSQLCondition method exists and call it if this is the case.
+ // Invoked by SMAreaValueDescription, SMGeoCoordsValueDescription
+ if ( method_exists( $description, 'getSQLCondition' ) ) {
+ $fields = $diHandler->getTableFields();
+
+ $where = $description->getSQLCondition(
+ $query->alias,
+ array_keys( $fields ),
+ $this->querySegmentListBuilder->getStore()->getConnection( DB_SLAVE )
+ );
+ }
+
+ if ( $where == '' && $valueMatchConditionBuilder->canApplyFulltextSearchMatchCondition( $description ) ) {
+ $query->joinTable = $valueMatchConditionBuilder->getTableName();
+ $query->sortIndexField = $valueMatchConditionBuilder->getSortIndexField( $query->alias );
+ $query->components = [];
+ $where = $valueMatchConditionBuilder->getWhereCondition( $description, $query->alias );
+ } elseif ( $where == '' ) {
+
+ $comparator = $this->comparatorMapper->mapComparator(
+ $description,
+ $value
+ );
+
+ $where = "$query->alias.{$indexField}{$comparator}" . $db->addQuotes( $value );
+ }
+
+ if ( $where !== '' ) {
+
+ if ( $query->where && substr( $query->where, -1 ) != '(' ) {
+ $query->where .= " $operator ";
+ }
+
+ $query->where .= "($where)";
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ThingDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ThingDescriptionInterpreter.php
new file mode 100644
index 00000000..e4d3dde7
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ThingDescriptionInterpreter.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\Query\Language\Description;
+use SMW\Query\Language\ThingDescription;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\QuerySegment;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class ThingDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof ThingDescription;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return QuerySegment
+ */
+ public function interpretDescription( Description $description ) {
+
+ $query = new QuerySegment();
+ $query->type = QuerySegment::Q_NOQUERY;
+
+ return $query;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php
new file mode 100644
index 00000000..0ea4ec12
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/DescriptionInterpreters/ValueDescriptionInterpreter.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\DescriptionInterpreters;
+
+use SMW\DIWikiPage;
+use SMW\Query\Language\Description;
+use SMW\Query\Language\ValueDescription;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreter;
+use SMW\SQLStore\QueryEngine\FulltextSearchTableFactory;
+use SMW\SQLStore\QueryEngine\QuerySegment;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuilder;
+use SMWDIBlob as DIBlob;
+use SMWSql3SmwIds;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class ValueDescriptionInterpreter implements DescriptionInterpreter {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @var ComparatorMapper
+ */
+ private $comparatorMapper;
+
+ /**
+ * @var FulltextSearchTableFactory
+ */
+ private $fulltextSearchTableFactory;
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ $this->comparatorMapper = new ComparatorMapper();
+ $this->fulltextSearchTableFactory = new FulltextSearchTableFactory();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return boolean
+ */
+ public function canInterpretDescription( Description $description ) {
+ return $description instanceof ValueDescription;
+ }
+
+ /**
+ * Only type '_wpg' objects can appear on query level (essentially as nominal classes)
+ *
+ * @since 2.2
+ *
+ * @param Description $description
+ *
+ * @return QuerySegment
+ */
+ public function interpretDescription( Description $description ) {
+
+ $query = new QuerySegment();
+
+ if ( !$description->getDataItem() instanceof DIWikiPage ) {
+ return $query;
+ }
+
+ $comparator = $description->getComparator();
+ $property = $description->getProperty();
+ $value = $description->getDataItem()->getSortKey();
+
+ // A simple value match using the `~~Foo` will initiate a fulltext
+ // search without being bound to a property allowing a broad match
+ // search
+ if ( ( $comparator === SMW_CMP_LIKE || $comparator === SMW_CMP_NLKE ) ) {
+
+ $fulltextSearchSupport = $this->addFulltextSearchCondition(
+ $description,
+ $query,
+ $comparator,
+ $value
+ );
+
+ if ( $fulltextSearchSupport ) {
+ return $query;
+ }
+ }
+
+ if ( $comparator === SMW_CMP_EQ ) {
+ $query->type = QuerySegment::Q_VALUE;
+
+ $oid = $this->querySegmentListBuilder->getStore()->getObjectIds()->getSMWPageID(
+ $description->getDataItem()->getDBkey(),
+ $description->getDataItem()->getNamespace(),
+ $description->getDataItem()->getInterwiki(),
+ $description->getDataItem()->getSubobjectName()
+ );
+
+ $query->joinfield = [ $oid ];
+ } else { // Join with SMW IDs table needed for other comparators (apply to title string).
+ $query->joinTable = SMWSql3SmwIds::TABLE_NAME;
+ $query->joinfield = "{$query->alias}.smw_id";
+
+ $comparator = $this->comparatorMapper->mapComparator(
+ $description,
+ $value
+ );
+
+ $db = $this->querySegmentListBuilder->getStore()->getConnection( 'mw.db.queryengine' );
+
+ $query->where = "{$query->alias}.smw_sortkey$comparator" . $db->addQuotes( $value );
+ }
+
+ return $query;
+ }
+
+ private function addFulltextSearchCondition( $description, $query, $comparator, &$value ) {
+
+ // Uses ~~ wide proximity?
+ $usesWidePromixity = false;
+
+ // If a remaining ~ is present then the user searched with a ~~ string
+ // where the Comparator already matched/removed the first one
+ if ( substr( $value, 0, 1 ) === '~' ) {
+ $value = substr( $value, 1 );
+ $usesWidePromixity = true;
+ }
+
+ // If it is not a wide proximity search and it doesn't have a property then
+ // don't try to match using the fulltext index (redirect [[~Foo]] to LIKE)
+ if ( !$usesWidePromixity && $description->getProperty() === null ) {
+ return false;
+ }
+
+ $valueMatchConditionBuilder = $this->fulltextSearchTableFactory->newValueMatchConditionBuilderByType(
+ $this->querySegmentListBuilder->getStore()
+ );
+
+ if ( !$valueMatchConditionBuilder->isEnabled() || !$valueMatchConditionBuilder->hasMinTokenLength( $value ) ) {
+ return false;
+ }
+
+ if ( !$usesWidePromixity && !$valueMatchConditionBuilder->canApplyFulltextSearchMatchCondition( $description ) ) {
+ return false;
+ }
+
+ $query->joinTable = $valueMatchConditionBuilder->getTableName();
+ $query->joinfield = "{$query->alias}.s_id";
+ $query->indexField = 's_id';
+ $query->components = [];
+
+ $query->where = $valueMatchConditionBuilder->getWhereCondition(
+ new ValueDescription( new DIBlob( $value ), null, $comparator ),
+ $query->alias
+ );
+
+ return $query;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/EngineOptions.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/EngineOptions.php
new file mode 100644
index 00000000..55558454
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/EngineOptions.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use SMW\Options;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class EngineOptions extends Options {
+
+ /**
+ * @since 2.2
+ */
+ public function __construct() {
+ parent::__construct( [
+ 'smwgIgnoreQueryErrors' => $GLOBALS['smwgIgnoreQueryErrors'],
+ 'smwgQSortFeatures' => $GLOBALS['smwgQSortFeatures'],
+ 'smwgQFilterDuplicates' => $GLOBALS['smwgQFilterDuplicates']
+ ] );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/MySQLValueMatchConditionBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/MySQLValueMatchConditionBuilder.php
new file mode 100644
index 00000000..9580e2d5
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/MySQLValueMatchConditionBuilder.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\Fulltext;
+
+use SMW\Query\Language\ValueDescription;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class MySQLValueMatchConditionBuilder extends ValueMatchConditionBuilder {
+
+ /**
+ * @see ValueMatchConditionBuilder::canApplyFulltextSearchMatchCondition
+ * @since 2.5
+ *
+ * @param ValueDescription $description
+ *
+ * @return boolean
+ */
+ public function canApplyFulltextSearchMatchCondition( ValueDescription $description ) {
+
+ if ( !$this->isEnabled() ) {
+ return false;
+ }
+
+ if ( $description->getProperty() !== null && $this->isExemptedProperty( $description->getProperty() ) ) {
+ return false;
+ }
+
+ if ( !$this->searchTable->isValidByType( $description->getDataItem()->getDiType() ) ) {
+ return false;
+ }
+
+ $matchableText = $this->getMatchableTextFromDescription(
+ $description
+ );
+
+ $comparator = $description->getComparator();
+
+ if ( $matchableText && ( $comparator === SMW_CMP_LIKE || $comparator === SMW_CMP_NLKE ) ) {
+
+ // http://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html
+ // innodb_ft_min_token_size and innodb_ft_max_token_size are used
+ // for InnoDB search indexes. ft_min_word_len and ft_max_word_len
+ // are used for MyISAM search indexes
+
+ // Don't count any wildcard
+ return $this->hasMinTokenLength( str_replace( '*', '', $matchableText ) );
+ }
+
+ return false;
+ }
+
+ /**
+ * @see ValueMatchConditionBuilder::getWhereCondition
+ * @since 2.5
+ *
+ * @param ValueDescription $description
+ * @param string $temporaryTable
+ *
+ * @return string
+ */
+ public function getWhereCondition( ValueDescription $description, $temporaryTable = '' ) {
+
+ $affix = '';
+ $matchableText = $this->getMatchableTextFromDescription(
+ $description
+ );
+
+ // Any query modifier? Take care of it before any tokenizer or ngrams
+ // distort the marker
+ if (
+ ( $pos = strrpos( $matchableText, '&BOL' ) ) !== false ||
+ ( $pos = strrpos( $matchableText, '&INL' ) ) !== false ||
+ ( $pos = strrpos( $matchableText, '&QEX' ) ) !== false ) {
+ $affix = mb_strcut( $matchableText, $pos );
+ $matchableText = str_replace( $affix, '', $matchableText );
+ }
+
+ $value = $this->textSanitizer->sanitize(
+ $matchableText,
+ true
+ );
+
+ $value .= $affix;
+
+ // A leading or trailing minus sign indicates that this word must not
+ // be present in any of the rows that are returned.
+ // InnoDB only supports leading minus signs.
+ if ( $description->getComparator() === SMW_CMP_NLKE ) {
+ $value = '-' . $value;
+ }
+
+ $temporaryTable = $temporaryTable !== '' ? $temporaryTable . '.' : '';
+ $column = $temporaryTable . $this->searchTable->getIndexField();
+
+ $property = $description->getProperty();
+ $propertyCondition = '';
+
+ // Full text is collected in a single table therefore limit the match
+ // process by adding the PID as an additional condition
+ if ( $property !== null ) {
+ $propertyCondition = 'AND ' . $temporaryTable . 'p_id=' . $this->searchTable->getIdByProperty( $property );
+ }
+
+ $querySearchModifier = $this->getQuerySearchModifier(
+ $value
+ );
+
+ return "MATCH($column) AGAINST (" . $this->searchTable->addQuotes( $value ) . " $querySearchModifier) $propertyCondition";
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string &$value
+ *
+ * @return string
+ */
+ public function getQuerySearchModifier( &$value ) {
+
+ // @see http://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html
+ // "MySQL can perform boolean full-text searches using the IN BOOLEAN
+ // MODE modifier. With this modifier, certain characters have special
+ // meaning at the beginning or end of words ..."
+ if ( strpos( $value, '&BOL' ) !== false ) {
+ $value = str_replace( '&BOL', '', $value );
+ return 'IN BOOLEAN MODE';
+ }
+
+ if ( strpos( $value, '&INL' ) !== false ) {
+ $value = str_replace( '&INL', '', $value );
+ return 'IN NATURAL LANGUAGE MODE';
+ }
+
+ if ( strpos( $value, '&QEX' ) !== false ) {
+ $value = str_replace( '&QEX', '', $value );
+ return 'WITH QUERY EXPANSION';
+ }
+
+ return 'IN BOOLEAN MODE';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SQLiteValueMatchConditionBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SQLiteValueMatchConditionBuilder.php
new file mode 100644
index 00000000..5357da68
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SQLiteValueMatchConditionBuilder.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\Fulltext;
+
+use SMW\Query\Language\ValueDescription;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SQLiteValueMatchConditionBuilder extends ValueMatchConditionBuilder {
+
+ /**
+ * @see ValueMatchConditionBuilder::canApplyFulltextSearchMatchCondition
+ * @since 2.5
+ *
+ * @param ValueDescription $description
+ *
+ * @return boolean
+ */
+ public function canApplyFulltextSearchMatchCondition( ValueDescription $description ) {
+
+ if ( !$this->isEnabled() ) {
+ return false;
+ }
+
+ if ( $description->getProperty() !== null && $this->isExemptedProperty( $description->getProperty() ) ) {
+ return false;
+ }
+
+ if ( !$this->searchTable->isValidByType( $description->getDataItem()->getDiType() ) ) {
+ return false;
+ }
+
+ $matchableText = $this->getMatchableTextFromDescription(
+ $description
+ );
+
+ $comparator = $description->getComparator();
+
+ if ( $matchableText && ( $comparator === SMW_CMP_LIKE || $comparator === SMW_CMP_NLKE ) ) {
+ return $this->hasMinTokenLength( str_replace( '*', '', $matchableText ) );
+ }
+
+ return false;
+ }
+
+ /**
+ * @see ValueMatchConditionBuilder::getWhereCondition
+ * @since 2.5
+ *
+ * @param ValueDescription $description
+ * @param string $temporaryTable
+ *
+ * @return string
+ */
+ public function getWhereCondition( ValueDescription $description, $temporaryTable = '' ) {
+
+ $matchableText = $this->getMatchableTextFromDescription(
+ $description
+ );
+
+ $value = $this->textSanitizer->sanitize(
+ $matchableText,
+ true
+ );
+
+ // A leading or trailing minus sign indicates that this word must not
+ // be present in any of the rows that are returned.
+ // InnoDB only supports leading minus signs.
+ if ( $description->getComparator() === SMW_CMP_NLKE ) {
+ $value = '-' . $value;
+ }
+
+ // Something like [[Has text::!~database]] will cause a
+ // "malformed MATCH expression" due to "An FTS query may not consist
+ // entirely of terms or term-prefix queries with unary "-" operators
+ // attached to them." and doing "NOT database" will result in an empty
+ // result set
+
+ $temporaryTable = $temporaryTable !== '' ? $temporaryTable . '.' : '';
+ $column = $temporaryTable . $this->searchTable->getIndexField();
+
+ $property = $description->getProperty();
+ $propertyCondition = '';
+
+ // Full text is collected in a single table therefore limit the match
+ // process by adding the PID as an additional condition
+ if ( $property !== null ) {
+ $propertyCondition = ' AND ' . $temporaryTable . 'p_id=' . $this->searchTable->addQuotes(
+ $this->searchTable->getIdByProperty( $property )
+ );
+ }
+
+ return $column . " MATCH " . $this->searchTable->addQuotes( $value ) . "$propertyCondition";
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTable.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTable.php
new file mode 100644
index 00000000..3a62be87
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTable.php
@@ -0,0 +1,281 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\Fulltext;
+
+use SMW\DataTypeRegistry;
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\MediaWiki\Database;
+use SMW\SQLStore\SQLStore;
+use SMWDataItem as DataItem;
+use SMW\Exception\PredefinedPropertyLabelMismatchException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SearchTable {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var boolean
+ */
+ private $isEnabled = false;
+
+ /**
+ * @var integer
+ */
+ private $minTokenSize = 3;
+
+ /**
+ * @var integer
+ */
+ private $indexableDataTypes = 0;
+
+ /**
+ * @var array
+ */
+ private $propertyExemptionList = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ $this->connection = $store->getConnection( 'mw.db.queryengine' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $propertyExemptionList
+ */
+ public function setPropertyExemptionList( array $propertyExemptionList ) {
+ $this->propertyExemptionList = array_flip(
+ str_replace( ' ', '_', $propertyExemptionList )
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $indexableDataTypes
+ */
+ public function setIndexableDataTypes( $indexableDataTypes ) {
+ $this->indexableDataTypes = $indexableDataTypes;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getPropertyExemptionList() {
+ return array_keys( $this->propertyExemptionList );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $id
+ *
+ * @return boolean
+ */
+ public function isExemptedPropertyById( $id ) {
+
+ $dataItem = $this->getDataItemById( $id );
+
+ if ( !$dataItem instanceof DIWikiPage || $dataItem->getDBKey() === '' ) {
+ return false;
+ }
+
+ try {
+ $property = DIProperty::newFromUserLabel(
+ $dataItem->getDBKey()
+ );
+ } catch( PredefinedPropertyLabelMismatchException $e ) {
+ // The property no longer exists (or is no longer available) therefore
+ // exempt it.
+ return true;
+ }
+
+ return $this->isExemptedProperty( $property );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return boolean
+ */
+ public function isExemptedProperty( DIProperty $property ) {
+
+ $dataItemTypeId = DataTypeRegistry::getInstance()->getDataItemId(
+ $property->findPropertyTypeID()
+ );
+
+ // Property does not belong to a valid type which means to be exempted
+ if ( !$this->isValidByType( $dataItemTypeId ) ) {
+ return true;
+ }
+
+ return isset( $this->propertyExemptionList[$property->getKey()] );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return boolean
+ */
+ public function isValidByType( $type ) {
+
+ $indexType = SMW_FT_NONE;
+
+ if ( $type === DataItem::TYPE_BLOB ) {
+ $indexType = SMW_FT_BLOB;
+ }
+
+ if ( $type === DataItem::TYPE_URI ) {
+ $indexType = SMW_FT_URI;
+ }
+
+ if ( $type === DataItem::TYPE_WIKIPAGE ) {
+ $indexType = SMW_FT_WIKIPAGE;
+ }
+
+ return ( $this->indexableDataTypes & $indexType ) != 0;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $enabled
+ */
+ public function setEnabled( $enabled ) {
+ $this->isEnabled = (bool)$enabled;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function isEnabled() {
+ return $this->isEnabled;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return SQLStore::FT_SEARCH_TABLE;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getIndexField() {
+ return 'o_text';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getSortField() {
+ return 'o_sort';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return integer
+ */
+ public function getMinTokenSize() {
+ return $this->minTokenSize;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return integer $minTokenSize
+ */
+ public function setMinTokenSize( $minTokenSize ) {
+ $this->minTokenSize = (int)$minTokenSize;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $token
+ *
+ * @return boolean
+ */
+ public function hasMinTokenLength( $token ) {
+ return mb_strlen( $token ) >= $this->minTokenSize;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DIProperty $property
+ *
+ * @return integer
+ */
+ public function getIdByProperty( DIProperty $property ) {
+ return $this->store->getObjectIds()->getId( $property->getCanonicalDiWikiPage() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $id
+ *
+ * @return DIWikiPage|null
+ */
+ public function getDataItemById( $id ) {
+ return $this->store->getObjectIds()->getDataItemById( $id );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getPropertyTables() {
+ return $this->store->getPropertyTables();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function addQuotes( $value ) {
+ return $this->connection->addQuotes( $value );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTableRebuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTableRebuilder.php
new file mode 100644
index 00000000..e4368052
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTableRebuilder.php
@@ -0,0 +1,335 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\Fulltext;
+
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\MessageReporterFactory;
+use SMW\DIProperty;
+use SMW\MediaWiki\Database;
+use SMWDataItem as DataItem;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SearchTableRebuilder {
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var SearchTableUpdater
+ */
+ private $searchTableUpdater;
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @var boolean
+ */
+ private $reportVerbose = false;
+
+ /**
+ * @var boolean
+ */
+ private $optimization = false;
+
+ /**
+ * @var array
+ */
+ private $skippedTables = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param SearchTableUpdater $searchTableUpdater
+ * @param Database $connection
+ */
+ public function __construct( Database $connection, SearchTableUpdater $searchTableUpdater ) {
+ $this->connection = $connection;
+ $this->searchTableUpdater = $searchTableUpdater;
+ $this->messageReporter = MessageReporterFactory::getInstance()->newNullMessageReporter();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return SearchTable
+ */
+ public function getSearchTable() {
+ return $this->searchTableUpdater->getSearchTable();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param MessageReporter $messageReporter
+ */
+ public function setMessageReporter( MessageReporter $messageReporter ) {
+ $this->messageReporter = $messageReporter;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $reportVerbose
+ */
+ public function reportVerbose( $reportVerbose ) {
+ $this->reportVerbose = (bool)$reportVerbose;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $optimization
+ */
+ public function requestOptimization( $optimization ) {
+ $this->optimization = (bool)$optimization;
+ }
+
+ /**
+ * @see RebuildFulltextSearchTable::execute
+ *
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function rebuild() {
+
+ if ( !$this->searchTableUpdater->isEnabled() ) {
+ return $this->reportMessage( "\n" . "FullText search indexing is not enabled or supported." ."\n" );
+ }
+
+ if ( $this->optimization ) {
+ return $this->doOptimize();
+ }
+
+ $this->doRebuild();
+
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function flushTable() {
+ if ( $this->searchTableUpdater->isEnabled() ) {
+ $this->searchTableUpdater->flushTable();
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getQualifiedTableList() {
+
+ $tableList = [];
+
+ if ( !$this->searchTableUpdater->isEnabled() ) {
+ return $tableList;
+ }
+
+ foreach ( $this->searchTableUpdater->getPropertyTables() as $proptable ) {
+
+ if ( !$this->getSearchTable()->isValidByType( $proptable->getDiType() ) ) {
+ continue;
+ }
+
+ $tableList[] = $proptable->getName();
+ }
+
+ return $tableList;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $tableName
+ */
+ public function rebuildByTable( $tableName ) {
+ foreach ( $this->searchTableUpdater->getPropertyTables() as $proptable ) {
+ if ( $proptable->getName() === $tableName && $this->getSearchTable()->isValidByType( $proptable->getDiType() ) ) {
+ $this->doRebuildByPropertyTable( $proptable );
+ }
+ }
+ }
+
+ private function doOptimize() {
+
+ $this->reportMessage( "\nOptimization ...\n" );
+
+ $this->reportMessage(
+ "\nRunning table optimization (Depending on the SQL back-end " .
+ "\nthis operation may lock the table and suspend any inserts or" .
+ "\ndeletes during the process.)\n"
+ );
+
+ if ( $this->searchTableUpdater->optimize() ) {
+ $this->reportMessage( "\n ... optimization has finished.\n" );
+ } else {
+ $this->reportMessage( "\nThe SQL back-end does not support this operation.\n" );
+ }
+
+ return true;
+ }
+
+ private function doRebuild() {
+
+ $this->reportMessage(
+ "\nThe entire index table is going to be purged first and it may\n" .
+ "take a moment before the rebuild is completed due to varying\n" .
+ "table contents.\n"
+ );
+
+ $this->reportMessage( "\nIndex process ..." );
+ $this->reportMessage( "\n" . " ... purging the index table ..." );
+
+ $this->searchTableUpdater->flushTable();
+ $this->reportMessage( "\n" . " ... rebuilding (finished/expected) ..." );
+
+ foreach ( $this->searchTableUpdater->getPropertyTables() as $proptable ) {
+
+ // Only care for Blob/Uri tables
+ if ( !$this->getSearchTable()->isValidByType( $proptable->getDiType() ) ) {
+ $this->skippedTables[$proptable->getName()] = 'Not a valid DI type';
+ continue;
+ }
+
+ $this->doRebuildByPropertyTable( $proptable );
+ }
+
+ $this->reportMessage( "\n ... done." );
+ $this->reportMessage( "\n ... report unindexed table(s) ...", $this->reportVerbose );
+
+ foreach ( $this->skippedTables as $tableName => $reason ) {
+ $this->reportMessage( "\n". sprintf( "%-38s%s", " ... {$tableName}", $reason ), $this->reportVerbose );
+ }
+
+ $this->reportMessage( "\n" );
+ }
+
+ private function doRebuildByPropertyTable( $proptable ) {
+
+ $searchTable = $this->getSearchTable();
+
+ if ( $proptable->getDiType() === DataItem::TYPE_URI ) {
+ $fetchFields = [ 's_id', 'p_id', 'o_blob', 'o_serialized' ];
+ } elseif ( $proptable->getDiType() === DataItem::TYPE_WIKIPAGE ) {
+ $fetchFields = [ 's_id', 'p_id', 'o_id' ];
+ } else {
+ $fetchFields = [ 's_id', 'p_id', 'o_blob', 'o_hash' ];
+ }
+
+ $table = $proptable->getName();
+ $pid = '';
+
+ // Fixed tables don't have a p_id column therefore get it
+ // from the ID TABLE
+ if ( $proptable->isFixedPropertyTable() ) {
+ unset( $fetchFields[1] ); // p_id
+
+ $property = new DIProperty( $proptable->getFixedProperty() );
+
+ if ( $property->getLabel() === '' ) {
+ return $this->skippedTables[$table] = 'Fixed property, ' . $property->getKey() . ' is invalid';
+ }
+
+ $pid = $searchTable->getIdByProperty(
+ $property
+ );
+
+ if ( $searchTable->isExemptedPropertyById( $pid ) ) {
+ return $this->skippedTables[$table] = 'Fixed property table, belongs to exempted ' . $proptable->getFixedProperty() . ' property';
+ }
+ }
+
+ $rows = $this->connection->select(
+ $table,
+ $fetchFields,
+ [],
+ __METHOD__
+ );
+
+ if ( $rows === false || $rows === null ) {
+ return $this->skippedTables[$table] = 'Empty table';
+ }
+
+ $this->doRebuildFromRows( $searchTable, $table, $pid, $rows );
+ }
+
+ private function doRebuildFromRows( $searchTable, $table, $pid, $rows ) {
+
+ $i = 0;
+ $expected = $rows->numRows();
+
+ if ( $expected == 0 ) {
+ return $this->skippedTables[$table] = 'Empty table';
+ }
+
+ $this->reportMessage( "\n" );
+
+ foreach ( $rows as $row ) {
+ $i++;
+
+ $sid = $row->s_id;
+ $pid = !isset( $row->p_id ) ? $pid : $row->p_id;
+
+ $indexableText = $this->getIndexableTextFromRow(
+ $searchTable,
+ $row
+ );
+
+ if ( $searchTable->isExemptedPropertyById( $pid ) || !$searchTable->hasMinTokenLength( $indexableText ) ) {
+ continue;
+ }
+
+ $this->reportMessage(
+ "\r". sprintf( "%-38s%s", " ... {$table}", sprintf( "%4.0f%% (%s/%s)", ( $i / $expected ) * 100, $i, $expected ) )
+ );
+
+ $text = $this->searchTableUpdater->read( $sid, $pid );
+
+ // Unknown, so let's create the row
+ if ( $text === false ) {
+ $this->searchTableUpdater->insert( $sid, $pid );
+ }
+
+ $this->searchTableUpdater->update( $sid, $pid, trim( $text ) . ' ' . $indexableText );
+ }
+ }
+
+ private function reportMessage( $message, $verbose = true ) {
+ if ( $verbose ) {
+ $this->messageReporter->reportMessage( $message );
+ }
+ }
+
+ private function getIndexableTextFromRow( $searchTable, $row ) {
+
+ $indexableText = '';
+
+ // Page, Uri, or blob?
+ if ( isset( $row->o_id ) ) {
+ $dataItem = $searchTable->getDataItemById( $row->o_id );
+ $indexableText = $dataItem instanceof DataItem ? $dataItem->getSortKey() : '';
+ } elseif ( isset( $row->o_serialized ) ) {
+ $indexableText = $row->o_blob === null ? $row->o_serialized : $row->o_blob;
+ } elseif ( isset( $row->o_blob ) ) {
+ $indexableText = $row->o_blob;
+ } elseif ( isset( $row->o_hash ) ) {
+ $indexableText = $row->o_hash;
+ }
+
+ return trim( $indexableText );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTableUpdater.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTableUpdater.php
new file mode 100644
index 00000000..a329d664
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/SearchTableUpdater.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\Fulltext;
+
+use SMW\MediaWiki\Database;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class SearchTableUpdater {
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var SearchTable
+ */
+ private $searchTable;
+
+ /**
+ * @var TextSanitizer
+ */
+ private $textSanitizer;
+
+ /**
+ * @since 2.5
+ *
+ * @param Database $connection
+ * @param SearchTable $searchTable
+ * @param TextSanitizer $textSanitizer
+ */
+ public function __construct( Database $connection, SearchTable $searchTable, TextSanitizer $textSanitizer ) {
+ $this->connection = $connection;
+ $this->searchTable = $searchTable;
+ $this->textSanitizer = $textSanitizer;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return SearchTable
+ */
+ public function getSearchTable() {
+ return $this->searchTable;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function isEnabled() {
+ return $this->searchTable->isEnabled();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getPropertyTables() {
+ return $this->searchTable->getPropertyTables();
+ }
+
+ /**
+ * @see http://dev.mysql.com/doc/refman/5.7/en/fulltext-fine-tuning.html
+ * @see http://dev.mysql.com/doc/refman/5.7/en/optimize-table.html
+ *
+ * "Running OPTIMIZE TABLE on a table with a full-text index rebuilds the
+ * full-text index, removing deleted Document IDs and consolidating multiple
+ * entries for the same word, where possible."
+ *
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function optimize() {
+
+ if ( !$this->connection->isType( 'mysql' ) ) {
+ return false;
+ }
+
+ $this->connection->query(
+ "OPTIMIZE TABLE " . $this->searchTable->getTableName(),
+ __METHOD__
+ );
+
+ return true;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $sid
+ * @param integer $pid
+ *
+ * @return boolean
+ */
+ public function exists( $sid, $pid ) {
+
+ $row = $this->connection->selectRow(
+ $this->searchTable->getTableName(),
+ [ 's_id' ],
+ [
+ 's_id' => (int)$sid,
+ 'p_id' => (int)$pid
+ ],
+ __METHOD__
+ );
+
+ return $row !== false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $sid
+ * @param integer $pid
+ *
+ * @return false|string
+ */
+ public function read( $sid, $pid ) {
+ $row = $this->connection->selectRow(
+ $this->searchTable->getTableName(),
+ [ 'o_text' ],
+ [
+ 's_id' => (int)$sid,
+ 'p_id' => (int)$pid
+ ],
+ __METHOD__
+ );
+
+ if ( $row === false ) {
+ return false;
+ }
+
+ return $this->textSanitizer->sanitize( $row->o_text );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $sid
+ * @param integer $pid
+ * @param string $text
+ */
+ public function update( $sid, $pid, $text ) {
+
+ if ( trim( $text ) === '' || ( $indexableText = $this->textSanitizer->sanitize( $text ) ) === '' ) {
+ return $this->delete( $sid, $pid );
+ }
+
+ $this->connection->update(
+ $this->searchTable->getTableName(),
+ [
+ 'o_text' => $indexableText,
+ 'o_sort' => mb_substr( $text, 0, 32 )
+ ],
+ [
+ 's_id' => (int)$sid,
+ 'p_id' => (int)$pid
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $sid
+ * @param integer $pid
+ */
+ public function insert( $sid, $pid ) {
+ $this->connection->insert(
+ $this->searchTable->getTableName(),
+ [
+ 's_id' => (int)$sid,
+ 'p_id' => (int)$pid,
+ 'o_text' => ''
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $sid
+ * @param integer $pid
+ */
+ public function delete( $sid, $pid ) {
+ $this->connection->delete(
+ $this->searchTable->getTableName(),
+ [
+ 's_id' => (int)$sid,
+ 'p_id' => (int)$pid
+ ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function flushTable() {
+ $this->connection->delete(
+ $this->searchTable->getTableName(),
+ '*',
+ __METHOD__
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/TextChangeUpdater.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/TextChangeUpdater.php
new file mode 100644
index 00000000..25e94d07
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/TextChangeUpdater.php
@@ -0,0 +1,312 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\Fulltext;
+
+use Onoi\Cache\Cache;
+use Psr\Log\LoggerAwareTrait;
+use SMW\MediaWiki\Database;
+use SMW\ApplicationFactory;
+use SMW\DIWikiPage;
+use SMW\SQLStore\ChangeOp\ChangeDiff;
+use SMW\SQLStore\ChangeOp\ChangeOp;
+use SMW\SQLStore\ChangeOp\TableChangeOp;
+use SMW\Utils\Timer;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class TextChangeUpdater {
+
+ use LoggerAwareTrait;
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var SearchTableUpdater
+ */
+ private $searchTableUpdater;
+
+ /**
+ * @var boolean
+ */
+ private $asDeferredUpdate = true;
+
+ /**
+ * @var boolean
+ */
+ private $isCommandLineMode = false;
+
+ /**
+ * @var boolean
+ */
+ private $isPrimary = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param Database $connection
+ * @param Cache $cache
+ * @param SearchTableUpdater $searchTableUpdater
+ * @param TextSanitizer $textSanitizer
+ */
+ public function __construct( Database $connection, Cache $cache, SearchTableUpdater $searchTableUpdater ) {
+ $this->connection = $connection;
+ $this->cache = $cache;
+ $this->searchTableUpdater = $searchTableUpdater;
+ }
+
+ /**
+ * @note See comments in the DefaultSettings.php on the smwgFulltextDeferredUpdate setting
+ *
+ * @since 2.5
+ *
+ * @param boolean $asDeferredUpdate
+ */
+ public function asDeferredUpdate( $asDeferredUpdate ) {
+ $this->asDeferredUpdate = (bool)$asDeferredUpdate;
+ }
+
+ /**
+ * When running from commandLine, push updates directly to avoid overhead when
+ * it is known that within that mode transactions are FIFO (i.e. the likelihood
+ * for race conditions of unfinished updates are diminishable).
+ *
+ * @since 2.5
+ *
+ * @param boolean $isCommandLineMode
+ */
+ public function isCommandLineMode( $isCommandLineMode ) {
+ $this->isCommandLineMode = (bool)$isCommandLineMode;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isPrimary
+ */
+ public function isPrimary( $isPrimary ) {
+ $this->isPrimary = $isPrimary;
+ }
+
+ /**
+ * @see SMW::SQLStore::AfterDataUpdateComplete hook
+ *
+ * @since 2.5
+ *
+ * @param ChangeOp $changeOp
+ */
+ public function pushUpdates( ChangeOp $changeOp ) {
+
+ if ( !$this->searchTableUpdater->isEnabled() ) {
+ return;
+ }
+
+ Timer::start( __METHOD__ );
+
+ // Update within the same transaction as started by SMW::SQLStore::AfterDataUpdateComplete
+ if ( !$this->asDeferredUpdate || $this->isCommandLineMode || $this->isPrimary ) {
+ return $this->doUpdateFromChangeDiff( $changeOp->newChangeDiff() );
+ }
+
+ if ( !$this->canPostUpdate( $changeOp ) ) {
+ return;
+ }
+
+ $fulltextSearchTableUpdateJob = ApplicationFactory::getInstance()->newJobFactory()->newFulltextSearchTableUpdateJob(
+ $changeOp->getSubject()->getTitle(),
+ [
+ 'slot:id' => $changeOp->getSubject()->getHash()
+ ]
+ );
+
+ $fulltextSearchTableUpdateJob->lazyPush();
+
+ $this->logger->info(
+ [
+ 'Fulltext',
+ 'TextChangeUpdater',
+ 'Table update (as job) scheduled',
+ 'procTime in sec: {procTime}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'procTime' => Timer::getElapsedTime( __METHOD__, 5 )
+ ]
+ );
+ }
+
+ /**
+ * @see SearchTableUpdateJob::run
+ *
+ * @since 2.5
+ *
+ * @param array|boolan $parameters
+ */
+ public function pushUpdatesFromJobParameters( $parameters ) {
+
+ if ( !$this->searchTableUpdater->isEnabled() || !isset( $parameters['slot:id'] ) || $parameters['slot:id'] === false ) {
+ return;
+ }
+
+ $subject = DIWikiPage::doUnserialize( $parameters['slot:id'] );
+ $changeDiff = ChangeDiff::fetch( $this->cache, $subject );
+
+ if ( $changeDiff !== false ) {
+ return $this->doUpdateFromChangeDiff( $changeDiff );
+ }
+
+ $this->logger->info(
+ [
+ 'Fulltext',
+ 'TextChangeUpdater',
+ 'Failed update (ChangeDiff) on {id}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'id' => $parameters['slot:id']
+ ]
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ChangeOp $changeOp
+ */
+ public function doUpdateFromChangeDiff( ChangeDiff $changeDiff ) {
+
+ if ( !$this->searchTableUpdater->isEnabled() ) {
+ return;
+ }
+
+ Timer::start( __METHOD__ );
+
+ $textItems = $changeDiff->getTextItems();
+ $diffChangeOps = $changeDiff->getTableChangeOps();
+
+ $changeList = $changeDiff->getChangeListByType( 'insert' );
+ $updates = [];
+
+ // Ensure that any delete operation is being accounted for to avoid that
+ // removed value annotation remain
+ if ( $diffChangeOps !== [] ) {
+ $this->doDeleteFromTableChangeOps( $diffChangeOps );
+ }
+
+ // Build a composite of replacements where a change occurred, this my
+ // contain some false positives
+ foreach ( $textItems as $sid => $textItem ) {
+
+ if ( !isset( $changeList[$sid] ) ) {
+ continue;
+ }
+
+ $this->collectUpdates( $sid, $textItem, $changeList, $updates );
+ }
+
+ foreach ( $updates as $key => $value ) {
+ list( $sid, $pid ) = explode( ':', $key, 2 );
+
+ if ( $this->searchTableUpdater->exists( $sid, $pid ) === false ) {
+ $this->searchTableUpdater->insert( $sid, $pid );
+ }
+
+ $this->searchTableUpdater->update(
+ $sid,
+ $pid,
+ $value
+ );
+ }
+
+ $this->logger->info(
+ [
+ 'Fulltext',
+ 'TextChangeUpdater',
+ 'Table update completed',
+ 'procTime in sec: {procTime}'
+ ],
+ [
+ 'method' => __METHOD__,
+ 'role' => 'developer',
+ 'procTime' => Timer::getElapsedTime( __METHOD__, 5 )
+ ]
+ );
+ }
+
+ private function collectUpdates( $sid, array $textItem, $changeList, &$updates ) {
+
+ $searchTable = $this->searchTableUpdater->getSearchTable();
+
+ foreach ( $textItem as $pid => $text ) {
+
+ // Exempted property -> out
+ if ( $searchTable->isExemptedPropertyById( $pid ) ) {
+ continue;
+ }
+
+ $text = implode( ' ', $text );
+ $key = $sid . ':' . $pid;
+
+ $updates[$key] = !isset( $updates[$key] ) ? $text : $updates[$key] . ' ' . $text;
+ }
+ }
+
+ private function doDeleteFromTableChangeOps( array $tableChangeOps ) {
+ foreach ( $tableChangeOps as $tableChangeOp ) {
+ $this->doDeleteFromTableChangeOp( $tableChangeOp );
+ }
+ }
+
+ private function doDeleteFromTableChangeOp( TableChangeOp $tableChangeOp ) {
+
+ foreach ( $tableChangeOp->getFieldChangeOps( 'delete' ) as $fieldChangeOp ) {
+
+ // Replace s_id for subobjects etc. with the o_id
+ if ( $tableChangeOp->isFixedPropertyOp() ) {
+ $fieldChangeOp->set( 's_id', $fieldChangeOp->has( 'o_id' ) ? $fieldChangeOp->get( 'o_id' ) : $fieldChangeOp->get( 's_id' ) );
+ $fieldChangeOp->set( 'p_id', $tableChangeOp->getFixedPropertyValueBy( 'p_id' ) );
+ }
+
+ if ( !$fieldChangeOp->has( 'p_id' ) ) {
+ continue;
+ }
+
+ $this->searchTableUpdater->delete(
+ $fieldChangeOp->get( 's_id' ),
+ $fieldChangeOp->get( 'p_id' )
+ );
+ }
+ }
+
+ private function canPostUpdate( $changeOp ) {
+
+ $searchTable = $this->searchTableUpdater->getSearchTable();
+ $canPostUpdate = false;
+
+ // Find out whether we should actual initiate an update
+ foreach ( $changeOp->getChangedEntityIdSummaryList() as $id ) {
+ if ( ( $dataItem = $searchTable->getDataItemById( $id ) ) instanceof DIWikiPage && $dataItem->getNamespace() === SMW_NS_PROPERTY ) {
+ if ( !$searchTable->isExemptedPropertyById( $id ) ) {
+ $canPostUpdate = true;
+ break;
+ }
+ }
+ }
+
+ return $canPostUpdate;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/TextSanitizer.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/TextSanitizer.php
new file mode 100644
index 00000000..54c54208
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/TextSanitizer.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\Fulltext;
+
+use Onoi\Tesa\Normalizer;
+use Onoi\Tesa\Sanitizer;
+use Onoi\Tesa\SanitizerFactory;
+use Onoi\Tesa\Tokenizer\Tokenizer;
+use Onoi\Tesa\Transliterator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class TextSanitizer {
+
+ /**
+ * @var SanitizerFactory
+ */
+ private $sanitizerFactory;
+
+ /**
+ * @var array
+ */
+ private $languageDetection = [];
+
+ /**
+ * @var integer
+ */
+ private $minTokenSize = 3;
+
+ /**
+ * @since 2.5
+ *
+ * @param SanitizerFactory $sanitizerFactory
+ */
+ public function __construct( SanitizerFactory $sanitizerFactory ) {
+ $this->sanitizerFactory = $sanitizerFactory;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getVersions() {
+
+ $languageDetector = '(Disabled)';
+
+ if ( isset( $this->languageDetection['TextCatLanguageDetector'] ) ) {
+ $languageDetector = 'TextCatLanguageDetector (' . implode(', ', $this->languageDetection['TextCatLanguageDetector'] ) . ')';
+ }
+
+ return [
+ 'ICU (Intl) PHP-extension' => ( extension_loaded( 'intl' ) ? INTL_ICU_VERSION : '(Disabled)' ),
+ 'Tesa::Sanitizer' => Sanitizer::VERSION,
+ 'Tesa::Transliterator' => Transliterator::VERSION,
+ 'Tesa::LanguageDetector' => $languageDetector
+ ];
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $languageDetection
+ */
+ public function setLanguageDetection( array $languageDetection ) {
+ $this->languageDetection = $languageDetection;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $minTokenSize
+ */
+ public function setMinTokenSize( $minTokenSize ) {
+ $this->minTokenSize = $minTokenSize;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ * @param boolean $isSearchTerm
+ *
+ * @return string
+ */
+ public function sanitize( $text, $isSearchTerm = false ) {
+ $start = microtime( true );
+ $text = rawurldecode( trim( $text ) );
+
+ $exemptionList = '';
+
+ // Those have special meaning when running a match search against
+ // the fulltext index (wildcard, phrase matching markers etc.)
+ if ( $isSearchTerm ) {
+ $exemptionList = [ '*', '"', '+', '-', '&', ',', '@', '~' ];
+ }
+
+ $sanitizer = $this->sanitizerFactory->newSanitizer( $text );
+ $sanitizer->toLowercase();
+ $sanitizer->applyTransliteration();
+ $sanitizer->convertDoubleWidth();
+
+ $sanitizer->replace(
+ [ 'http://', 'https://', 'mailto:', '%2A', '_', '&#x005B;', '&#91;', "\n", "\t" ],
+ [ '', '', '', '*', ' ', '[', '[', "", "" ]
+ );
+
+ $language = $this->predictLanguage( $text );
+
+ $sanitizer->setOption(
+ Sanitizer::WHITELIST,
+ $exemptionList
+ );
+
+ $sanitizer->setOption(
+ Sanitizer::MIN_LENGTH,
+ $this->minTokenSize
+ );
+
+ $tokenizer = $this->sanitizerFactory->newPreferredTokenizerByLanguage(
+ $text,
+ $language
+ );
+
+ $tokenizer->setOption(
+ Tokenizer::REGEX_EXEMPTION,
+ $exemptionList
+ );
+
+ $text = $sanitizer->sanitizeWith(
+ $tokenizer,
+ $this->sanitizerFactory->newStopwordAnalyzerByLanguage( $language ),
+ $this->sanitizerFactory->newSynonymizerByLanguage( $language )
+ );
+
+ // Remove possible spaces added by the tokenizer
+ $text = str_replace(
+ [ ' *', '* ', ' "', '" ', '+ ', '- ', '@ ', '~ ', '*+', '*-', '*~' ],
+ [ '*', '*', '"', '"', '+', '-', '@', '~' ,'* +', '* -', '* ~' ],
+ $text
+ );
+
+ //var_dump( $language, $text, (microtime( true ) - $start ) );
+ return $text;
+ }
+
+ private function predictLanguage( $text ) {
+
+ if ( $this->languageDetection === [] ) {
+ return null;
+ }
+
+ $languageDetector = $this->sanitizerFactory->newNullLanguageDetector();
+
+ if ( isset( $this->languageDetection['TextCatLanguageDetector'] ) ) {
+ $languageDetector = $this->sanitizerFactory->newTextCatLanguageDetector();
+ $languageDetector->setLanguageCandidates( $this->languageDetection['TextCatLanguageDetector'] );
+ }
+
+ return $languageDetector->detect(
+ Normalizer::reduceLengthTo( $text, 200 )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/ValueMatchConditionBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/ValueMatchConditionBuilder.php
new file mode 100644
index 00000000..56e6fa7a
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/Fulltext/ValueMatchConditionBuilder.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine\Fulltext;
+
+use SMW\DIProperty;
+use SMW\DIWikiPage;
+use SMW\Query\Language\ValueDescription;
+use SMWDIBlob as DIBlob;
+use SMWDIUri as DIUri;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ValueMatchConditionBuilder {
+
+ /**
+ * @var TextSanitizer
+ */
+ protected $textSanitizer;
+
+ /**
+ * @var SearchTable
+ */
+ protected $searchTable;
+
+ /**
+ * @since 2.5
+ *
+ * @param TextSanitizer $textSanitizer
+ * @param SearchTable $searchTable
+ */
+ public function __construct( TextSanitizer $textSanitizer, SearchTable $searchTable ) {
+ $this->textSanitizer = $textSanitizer;
+ $this->searchTable = $searchTable;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return boolean
+ */
+ public function isEnabled() {
+ return $this->searchTable->isEnabled();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getTableName() {
+ return $this->searchTable->getTableName();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $value
+ *
+ * @return boolean
+ */
+ public function hasMinTokenLength( $value ) {
+ return $this->searchTable->hasMinTokenLength( $value );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $property
+ *
+ * @return boolean
+ */
+ public function isExemptedProperty( DIProperty $property ) {
+ return $this->searchTable->isExemptedProperty( $property );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $temporaryTable
+ *
+ * @return string
+ */
+ public function getSortIndexField( $temporaryTable = '' ) {
+ return ( $temporaryTable !== '' ? $temporaryTable . '.' : '' ) . $this->searchTable->getSortField();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ValueDescription $description
+ *
+ * @return boolean
+ */
+ public function canApplyFulltextSearchMatchCondition( ValueDescription $description ) {
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param ValueDescription $description
+ * @param string $temporaryTable
+ *
+ * @return string
+ */
+ public function getWhereCondition( ValueDescription $description, $temporaryTable = '' ) {
+ return '';
+ }
+
+ protected function getMatchableTextFromDescription( ValueDescription $description ) {
+
+ $matchableText = false;
+
+ if ( $description->getDataItem() instanceof DIBlob ) {
+ $matchableText = $description->getDataItem()->getString();
+ }
+
+ if ( $description->getDataItem() instanceof DIUri || $description->getDataItem() instanceof DIWikiPage ) {
+ $matchableText = $description->getDataItem()->getSortKey();
+ }
+
+ return $matchableText;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/FulltextSearchTableFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/FulltextSearchTableFactory.php
new file mode 100644
index 00000000..1e7403f2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/FulltextSearchTableFactory.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use Onoi\Tesa\SanitizerFactory;
+use SMW\ApplicationFactory;
+use SMW\SQLStore\QueryEngine\Fulltext\MySQLValueMatchConditionBuilder;
+use SMW\SQLStore\QueryEngine\Fulltext\SearchTable;
+use SMW\SQLStore\QueryEngine\Fulltext\SearchTableRebuilder;
+use SMW\SQLStore\QueryEngine\Fulltext\SearchTableUpdater;
+use SMW\SQLStore\QueryEngine\Fulltext\SQLiteValueMatchConditionBuilder;
+use SMW\SQLStore\QueryEngine\Fulltext\TextChangeUpdater;
+use SMW\SQLStore\QueryEngine\Fulltext\TextSanitizer;
+use SMW\SQLStore\QueryEngine\Fulltext\ValueMatchConditionBuilder;
+use SMW\Store;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class FulltextSearchTableFactory {
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ *
+ * @return ValueMatchConditionBuilder
+ */
+ public function newValueMatchConditionBuilderByType( Store $store ) {
+
+ $type = $store->getConnection( 'mw.db' )->getType();
+
+ switch ( $type ) {
+ case 'mysql':
+ return new MySQLValueMatchConditionBuilder(
+ $this->newTextSanitizer(),
+ $this->newSearchTable( $store )
+ );
+ break;
+ case 'sqlite':
+ return new SQLiteValueMatchConditionBuilder(
+ $this->newTextSanitizer(),
+ $this->newSearchTable( $store )
+ );
+ break;
+ }
+
+ return new ValueMatchConditionBuilder( $this->newTextSanitizer(), $this->newSearchTable( $store ) );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ *
+ * @return SearchTable
+ */
+ public function newTextSanitizer() {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $textSanitizer = new TextSanitizer(
+ new SanitizerFactory()
+ );
+
+ $textSanitizer->setLanguageDetection(
+ $settings->get( 'smwgFulltextLanguageDetection' )
+ );
+
+ $textSanitizer->setMinTokenSize(
+ $settings->get( 'smwgFulltextSearchMinTokenSize' )
+ );
+
+ return $textSanitizer;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ *
+ * @return SearchTable
+ */
+ public function newSearchTable( Store $store ) {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $searchTable = new SearchTable(
+ $store
+ );
+
+ $searchTable->setEnabled(
+ $settings->get( 'smwgEnabledFulltextSearch' )
+ );
+
+ $searchTable->setPropertyExemptionList(
+ $settings->get( 'smwgFulltextSearchPropertyExemptionList' )
+ );
+
+ $searchTable->setMinTokenSize(
+ $settings->get( 'smwgFulltextSearchMinTokenSize' )
+ );
+
+ $searchTable->setIndexableDataTypes(
+ $settings->get( 'smwgFulltextSearchIndexableDataTypes' )
+ );
+
+ return $searchTable;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ *
+ * @return SearchTableUpdater
+ */
+ public function newSearchTableUpdater( Store $store ) {
+ return new SearchTableUpdater(
+ $store->getConnection( 'mw.db' ),
+ $this->newSearchTable( $store ),
+ $this->newTextSanitizer()
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ *
+ * @return TextChangeUpdater
+ */
+ public function newTextChangeUpdater( Store $store ) {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+
+ $textChangeUpdater = new TextChangeUpdater(
+ $store->getConnection( 'mw.db' ),
+ $applicationFactory->getCache(),
+ $this->newSearchTableUpdater( $store )
+ );
+
+ $textChangeUpdater->setLogger(
+ $applicationFactory->getMediaWikiLogger()
+ );
+
+ $textChangeUpdater->asDeferredUpdate(
+ $settings->get( 'smwgFulltextDeferredUpdate' )
+ );
+
+ // https://www.mediawiki.org/wiki/Manual:$wgCommandLineMode
+ $textChangeUpdater->isCommandLineMode(
+ $GLOBALS['wgCommandLineMode']
+ );
+
+ return $textChangeUpdater;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ *
+ * @return SearchTableRebuilder
+ */
+ public function newSearchTableRebuilder( Store $store ) {
+ return new SearchTableRebuilder(
+ $store->getConnection( 'mw.db' ),
+ $this->newSearchTableUpdater( $store )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/HierarchyTempTableBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/HierarchyTempTableBuilder.php
new file mode 100644
index 00000000..8f0edf37
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/HierarchyTempTableBuilder.php
@@ -0,0 +1,204 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use RuntimeException;
+use SMW\MediaWiki\Database;
+use SMW\SQLStore\TableBuilder\TemporaryTableBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class HierarchyTempTableBuilder {
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var TemporaryTableBuilder
+ */
+ private $temporaryTableBuilder;
+
+ /**
+ * Cache of computed hierarchy queries for reuse ("catetgory/property value
+ * string" => "tablename").
+ *
+ * @var string[]
+ */
+ private $hierarchyCache = [];
+
+ /**
+ * @var array
+ */
+ private $hierarchyTypeTable = [];
+
+ /**
+ * @since 2.3
+ *
+ * @param Database $connection
+ * @param TemporaryTableBuilder $temporaryTableBuilder
+ */
+ public function __construct( Database $connection, TemporaryTableBuilder $temporaryTableBuilder ) {
+ $this->connection = $connection;
+ $this->temporaryTableBuilder = $temporaryTableBuilder;
+ }
+
+ /**
+ * @since 2.3
+ */
+ public function emptyHierarchyCache() {
+ $this->hierarchyCache = [];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return array
+ */
+ public function getHierarchyCache() {
+ return $this->hierarchyCache;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $table
+ * @param integer $depth
+ */
+ public function setPropertyHierarchyTableDefinition( $table, $depth ) {
+ $this->hierarchyTypeTable['property'] = [ $this->connection->tableName( $table ), $depth ];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $table
+ * @param integer $depth
+ */
+ public function setClassHierarchyTableDefinition( $table, $depth ) {
+ $this->hierarchyTypeTable['class'] = [ $this->connection->tableName( $table ), $depth ];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $type
+ *
+ * @return array
+ * @throws RuntimeException
+ */
+ public function getHierarchyTableDefinitionForType( $type ) {
+
+ if ( !isset( $this->hierarchyTypeTable[$type] ) ) {
+ throw new RuntimeException( "$type is unknown" );
+ }
+
+ return $this->hierarchyTypeTable[$type];
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @param string $type
+ * @param string $tablename
+ * @param string $valueComposite
+ * @param integer|null $depth
+ *
+ * @throws RuntimeException
+ */
+ public function createHierarchyTempTableFor( $type, $tablename, $valueComposite, $depth = null ) {
+
+ $this->temporaryTableBuilder->create( $tablename );
+
+ list( $smwtable, $d ) = $this->getHierarchyTableDefinitionForType( $type );
+
+ if ( $depth === null ) {
+ $depth = $d;
+ }
+
+ if ( array_key_exists( $valueComposite, $this->hierarchyCache ) ) { // Just copy known result.
+
+ $this->connection->query(
+ "INSERT INTO $tablename (id) SELECT id" . ' FROM ' . $this->hierarchyCache[$valueComposite],
+ __METHOD__
+ );
+
+ return;
+ }
+
+ $this->buildTempTable( $tablename, $valueComposite, $smwtable, $depth );
+ }
+
+ /**
+ * @note we use two helper tables. One holds the results of each new iteration, one holds the
+ * results of the previous iteration. One could of course do with only the above result table,
+ * but then every iteration would use all elements of this table, while only the new ones
+ * obtained in the previous step are relevant. So this is a performance measure.
+ */
+ private function buildTempTable( $tablename, $values, $smwtable, $depth ) {
+
+ $db = $this->connection;
+
+ $tmpnew = 'smw_new';
+ $tmpres = 'smw_res';
+
+ $this->temporaryTableBuilder->create( $tmpnew );
+ $this->temporaryTableBuilder->create( $tmpres );
+
+ // Adding multiple values for the same column in sqlite is not supported
+ foreach ( explode( ',', $values ) as $value ) {
+
+ $db->query(
+ "INSERT " . "IGNORE" . " INTO $tablename (id) VALUES $value",
+ __METHOD__
+ );
+
+ $db->query(
+ "INSERT " . "IGNORE" . " INTO $tmpnew (id) VALUES $value",
+ __METHOD__
+ );
+ }
+
+ for ( $i = 0; $i < $depth; $i++ ) {
+ $db->query(
+ "INSERT " . 'IGNORE ' . "INTO $tmpres (id) SELECT s_id" . '@INT' . " FROM $smwtable, $tmpnew WHERE o_id=id",
+ __METHOD__
+ );
+
+ if ( $db->affectedRows() == 0 ) { // no change, exit loop
+ break;
+ }
+
+ $db->query(
+ "INSERT " . 'IGNORE ' . "INTO $tablename (id) SELECT $tmpres.id FROM $tmpres",
+ __METHOD__
+ );
+
+ if ( $db->affectedRows() == 0 ) { // no change, exit loop
+ break;
+ }
+
+ // empty "new" table
+ $db->query(
+ 'TRUNCATE TABLE ' . $tmpnew,
+ __METHOD__
+ );
+
+ $tmpname = $tmpnew;
+ $tmpnew = $tmpres;
+ $tmpres = $tmpname;
+ }
+
+ $this->hierarchyCache[$values] = $tablename;
+
+ $this->temporaryTableBuilder->drop( $tmpnew );
+ $this->temporaryTableBuilder->drop( $tmpres );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/OrderCondition.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/OrderCondition.php
new file mode 100644
index 00000000..10e974a6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/OrderCondition.php
@@ -0,0 +1,252 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use RuntimeException;
+use SMW\DataValueFactory;
+use SMW\DataValues\PropertyChainValue;
+use SMW\Query\DescriptionFactory;
+use SMW\Query\Language\Description;
+
+/**
+ * Modifies a given query object at $qid to account for all ordering conditions
+ * in the Query $query. It is always required that $qid is the id of a query
+ * that joins with the SMW ID_TABELE so that the field alias.smw_title is
+ * available for default sorting.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ */
+class OrderCondition {
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @var DescriptionFactory
+ */
+ private $descriptionFactory;
+
+ /**
+ * Array of sorting requests ("Property_name" => "ASC"/"DESC"). Used during query
+ * processing (where these property names are searched while compiling the query
+ * conditions).
+ *
+ * @var string[]
+ */
+ private $sortKeys = [];
+
+ /**
+ * @var boolean
+ */
+ private $isSupported = true;
+
+ /**
+ * @var boolean
+ */
+ private $asUnconditional = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ */
+ public function __construct( QuerySegmentListBuilder $querySegmentListBuilder ) {
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ $this->descriptionFactory = new DescriptionFactory();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $sortKeys
+ */
+ public function setSortKeys( $sortKeys ) {
+ $this->sortKeys = $sortKeys;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string[]
+ */
+ public function getSortKeys() {
+ return $this->sortKeys;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->querySegmentListBuilder->getErrors();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $isSupported
+ */
+ public function isSupported( $isSupported ) {
+ $this->isSupported = $isSupported;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $asUnconditional
+ */
+ public function asUnconditional( $asUnconditional ) {
+ $this->asUnconditional = $asUnconditional;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $qid
+ *
+ * @return QuerySegment[]
+ */
+ public function apply( $qid ) {
+
+ if ( !$this->isSupported ) {
+ return $this->querySegmentListBuilder->getQuerySegmentList();
+ }
+
+ $querySegment = $this->querySegmentListBuilder->findQuerySegment(
+ $qid
+ );
+
+ $extraDescriptions = $this->collectExtraDescriptionsFromSortKeys(
+ $querySegment
+ );
+
+ if ( $extraDescriptions !== [] ) {
+ $this->addConjunctionFromExtraDescriptions( $querySegment, $extraDescriptions );
+ }
+
+ return $this->querySegmentListBuilder->getQuerySegmentList();
+ }
+
+ private function collectExtraDescriptionsFromSortKeys( $querySegment ) {
+
+ $extraDescriptions = [];
+
+ foreach ( $this->sortKeys as $label => $order ) {
+
+ if ( !is_string( $label ) ) {
+ throw new RuntimeException( "Expected a string value as sortkey" );
+ }
+
+ if ( ( $description = $this->findExtraDescriptionBy( $querySegment, $label, $order ) ) instanceof Description ) {
+ $extraDescriptions[] = $description;
+ }
+ }
+
+ return $extraDescriptions;
+ }
+
+ private function findExtraDescriptionBy( $querySegment, $label, $order ) {
+
+ $description = null;
+
+ // Is assigned, leave ...
+ if ( array_key_exists( $label, $querySegment->sortfields ) ) {
+ return $description;
+ }
+
+ // Find missing property to sort by.
+ if ( $label === '' ) { // Sort by first result column (page titles).
+ $querySegment->sortfields[$label] = "$querySegment->alias.smw_sort";
+ } elseif ( $label === '#' ) { // Sort by first result column (page titles).
+ // PHP7 showed a rather erratic behaviour where in cases
+ // the sortkey contains the same string for comparison, the
+ // result returned from the DB was mixed in order therefore
+ // using # as indicator to search for additional fields if
+ // no specific property is given (see test cases in #1534)
+ $querySegment->sortfields[$label] = "$querySegment->alias.smw_sort,$querySegment->alias.smw_title,$querySegment->alias.smw_subobject";
+ } elseif ( PropertyChainValue::isChained( $label ) ) { // Try to extend query.
+ $propertyChainValue = DataValueFactory::getInstance()->newDataValueByType( PropertyChainValue::TYPE_ID );
+ $propertyChainValue->setUserValue( $label );
+
+ if ( !$propertyChainValue->isValid() ) {
+ return $description;
+ }
+
+ $lastDataItem = $propertyChainValue->getLastPropertyChainValue()->getDataItem();
+
+ $description = $this->descriptionFactory->newSomeProperty(
+ $lastDataItem,
+ $this->descriptionFactory->newThingDescription()
+ );
+
+ // #2176, Set a different membership in case duplicate detection is
+ // enabled, the fingerprint will be distinguishable from a condition
+ // with another ThingDescription for the same property that would
+ // otherwise create a "Error: 1066 Not unique table/alias: 't3'"
+ $description->setMembership( $label );
+
+ foreach ( $propertyChainValue->getPropertyChainValues() as $val ) {
+ $description = $this->descriptionFactory->newSomeProperty(
+ $val->getDataItem(),
+ $description
+ );
+ }
+
+ // Add and replace Foo.Bar=asc with Bar=asc as we ultimately only
+ // order to the result of the last element
+ $this->sortKeys[$lastDataItem->getKey()] = $order;
+ unset( $this->sortKeys[$label] );
+ } else { // Try to extend query.
+ $sortprop = DataValueFactory::getInstance()->newPropertyValueByLabel( $label );
+
+ if ( $sortprop->isValid() ) {
+ $description = $this->descriptionFactory->newSomeProperty(
+ $sortprop->getDataItem(),
+ $this->descriptionFactory->newThingDescription()
+ );
+ }
+ }
+
+ return $description;
+ }
+
+ private function addConjunctionFromExtraDescriptions( $querySegment, array $extraDescriptions ) {
+
+ $this->querySegmentListBuilder->setSortKeys(
+ $this->sortKeys
+ );
+
+ $this->querySegmentListBuilder->getQuerySegmentFrom(
+ $this->descriptionFactory->newConjunction( $extraDescriptions )
+ );
+
+ // This is always an QuerySegment::Q_CONJUNCTION ...
+ $newQuerySegment = $this->querySegmentListBuilder->findQuerySegment(
+ $this->querySegmentListBuilder->getLastQuerySegmentId()
+ );
+
+ // ... so just re-wire its dependencies
+ foreach ( $newQuerySegment->components as $cid => $field ) {
+ $querySegment->components[$cid] = $querySegment->joinfield;
+
+ if ( $this->asUnconditional ) {
+ $this->querySegmentListBuilder->findQuerySegment( $cid )->joinType = 'LEFT OUTER';
+ }
+
+ $querySegment->sortfields = array_merge(
+ $querySegment->sortfields,
+ $this->querySegmentListBuilder->findQuerySegment( $cid )->sortfields
+ );
+ }
+
+ $this->querySegmentListBuilder->addQuerySegment( $querySegment );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QueryEngine.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QueryEngine.php
new file mode 100644
index 00000000..9f9c7b8c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QueryEngine.php
@@ -0,0 +1,573 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+use SMW\DIWikiPage;
+use SMW\Exception\PredefinedPropertyLabelMismatchException;
+use SMW\Query\DebugFormatter;
+use SMW\Query\Language\ThingDescription;
+use SMW\QueryEngine as QueryEngineInterface;
+use SMW\QueryFactory;
+use SMWDataItem as DataItem;
+use SMWQuery as Query;
+use SMWQueryResult as QueryResult;
+use SMWSQLStore3 as SQLStore;
+
+/**
+ * Class that implements query answering for SQLStore.
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class QueryEngine implements QueryEngineInterface, LoggerAwareInterface {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * Query mode copied from given query. Some submethods act differently when
+ * in Query::MODE_DEBUG.
+ *
+ * @var int
+ */
+ private $queryMode;
+
+ /**
+ * Array of generated QuerySegment query descriptions (index => object)
+ *
+ * @var QuerySegment[]
+ */
+ private $querySegmentList = [];
+
+ /**
+ * Array of sorting requests ("Property_name" => "ASC"/"DESC"). Used during
+ * query processing (where these property names are searched while compiling
+ * the query conditions).
+ *
+ * @var string[]
+ */
+ private $sortKeys;
+
+ /**
+ * Local collection of error strings, passed on to callers if possible.
+ *
+ * @var string[]
+ */
+ private $errors = [];
+
+ /**
+ * @var QuerySegmentListBuildManager
+ */
+ private $querySegmentListBuildManager;
+
+ /**
+ * @var QuerySegmentListProcessor
+ */
+ private $querySegmentListProcessor;
+
+ /**
+ * @var EngineOptions
+ */
+ private $engineOptions;
+
+ /**
+ * @var QueryFactory
+ */
+ private $queryFactory;
+
+ /**
+ * @since 2.2
+ *
+ * @param SQLStore $store
+ * @param QuerySegmentListBuildManager $querySegmentListBuildManager
+ * @param QuerySegmentListProcessor $querySegmentListProcessor
+ * @param EngineOptions $engineOptions
+ */
+ public function __construct( SQLStore $store, QuerySegmentListBuildManager $querySegmentListBuildManager, QuerySegmentListProcessor $querySegmentListProcessor, EngineOptions $engineOptions ) {
+ $this->store = $store;
+ $this->querySegmentListBuildManager = $querySegmentListBuildManager;
+ $this->querySegmentListProcessor = $querySegmentListProcessor;
+ $this->engineOptions = $engineOptions;
+ $this->queryFactory = new QueryFactory();
+ }
+
+ /**
+ * @see LoggerAwareInterface::setLogger
+ *
+ * @since 2.5
+ *
+ * @param LoggerInterface $logger
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * The new SQL store's implementation of query answering. This function
+ * works in two stages: First, the nested conditions of the given query
+ * object are preprocessed to compute an abstract representation of the
+ * SQL query that is to be executed. Since query conditions correspond to
+ * joins with property tables in most cases, this abstract representation
+ * is essentially graph-like description of how property tables are joined.
+ * Moreover, this graph is tree-shaped, since all query conditions are
+ * tree-shaped. Each part of this abstract query structure is represented
+ * by an QuerySegment object in the array querySegmentList.
+ *
+ * As a second stage of processing, the thus prepared SQL query is actually
+ * executed. Typically, this means that the joins are collapsed into one
+ * SQL query to retrieve results. In some cases, such as in dbug mode, the
+ * execution might be restricted and not actually perform the whole query.
+ *
+ * The two-stage process helps to separate tasks, and it also allows for
+ * better optimisations: it is left to the execution engine how exactly the
+ * query result is to be obtained. For example, one could pre-compute
+ * partial suib-results in temporary tables (or even cache them somewhere),
+ * instead of passing one large join query to the DB (of course, it might
+ * be large only if the configuration of SMW allows it). For some DBMS, a
+ * step-wise execution of the query might lead to better performance, since
+ * it exploits the tree-structure of the joins, which is important for fast
+ * processing -- not all DBMS might be able in seeing this by themselves.
+ *
+ * @param Query $query
+ *
+ * @return mixed depends on $query->querymode
+ */
+ public function getQueryResult( Query $query ) {
+
+ if ( ( !$this->engineOptions->get( 'smwgIgnoreQueryErrors' ) || $query->getDescription() instanceof ThingDescription ) &&
+ $query->querymode != Query::MODE_DEBUG &&
+ count( $query->getErrors() ) > 0 ) {
+ return $this->queryFactory->newQueryResult( $this->store, $query, [], false );
+ // NOTE: we check this here to prevent unnecessary work, but we check
+ // it after query processing below again in case more errors occurred.
+ } elseif ( $query->querymode == Query::MODE_NONE || $query->getLimit() < 1 ) {
+ // don't query, but return something to printer
+ return $this->queryFactory->newQueryResult( $this->store, $query, [], true );
+ }
+
+ $connection = $this->store->getConnection( 'mw.db.queryengine' );
+
+ $this->queryMode = $query->querymode;
+ $this->querySegmentList = [];
+
+ $this->errors = [];
+ QuerySegment::$qnum = 0;
+ $this->sortKeys = $query->sortkeys;
+
+ $rootid = $this->querySegmentListBuildManager->getQuerySegmentFrom(
+ $query
+ );
+
+ $this->querySegmentList = $this->querySegmentListBuildManager->getQuerySegmentList();
+ $this->sortKeys = $this->querySegmentListBuildManager->getSortKeys();
+ $this->errors = $this->querySegmentListBuildManager->getErrors();
+
+ // Possibly stop if new errors happened:
+ if ( !$this->engineOptions->get( 'smwgIgnoreQueryErrors' ) &&
+ $query->querymode != Query::MODE_DEBUG &&
+ count( $this->errors ) > 0 ) {
+ $query->addErrors( $this->errors );
+ return $this->queryFactory->newQueryResult( $this->store, $query, [], false );
+ }
+
+ // *** Now execute the computed query ***//
+ $this->querySegmentListProcessor->setQueryMode( $this->queryMode );
+ $this->querySegmentListProcessor->setQuerySegmentList( $this->querySegmentList );
+
+ // execute query tree, resolve all dependencies
+ $this->querySegmentListProcessor->process(
+ $rootid
+ );
+
+ $this->applyExtraWhereCondition(
+ $connection,
+ $rootid
+ );
+
+ // #835
+ // SELECT DISTINCT and ORDER BY RANDOM causes an issue for postgres
+ // Disable RANDOM support for postgres
+ if ( $connection->isType( 'postgres' ) ) {
+ $this->engineOptions->set(
+ 'smwgQSortFeatures',
+ $this->engineOptions->get( 'smwgQSortFeatures' ) & ~SMW_QSORT_RANDOM
+ );
+ }
+
+ switch ( $query->querymode ) {
+ case Query::MODE_DEBUG:
+ $result = $this->getDebugQueryResult( $query, $rootid );
+ break;
+ case Query::MODE_COUNT:
+ $result = $this->getCountQueryResult( $query, $rootid );
+ break;
+ default:
+ $result = $this->getInstanceQueryResult( $query, $rootid );
+ break;
+ }
+
+ $this->querySegmentListProcessor->cleanUp();
+ $query->addErrors( $this->errors );
+
+ return $result;
+ }
+
+ /**
+ * Using a preprocessed internal query description referenced by $rootid, compute
+ * the proper debug output for the given query.
+ *
+ * @param Query $query
+ * @param integer $rootid
+ *
+ * @return string
+ */
+ private function getDebugQueryResult( Query $query, $rootid ) {
+
+ $qobj = $this->querySegmentList[$rootid];
+ $entries = [];
+
+ $sqlOptions = $this->getSQLOptions( $query, $rootid );
+
+ $entries['SQL Query'] = '';
+ $entries['SQL Explain'] = '';
+
+ $this->doExecuteDebugQueryResult( $qobj, $sqlOptions, $entries );
+ $auxtables = '';
+
+ foreach ( $this->querySegmentListProcessor->getExecutedQueries() as $table => $log ) {
+ $auxtables .= "<li>Temporary table $table";
+ foreach ( $log as $q ) {
+ $auxtables .= "<br />&#160;&#160;<tt>$q</tt>";
+ }
+ $auxtables .= '</li>';
+ }
+
+ if ( $auxtables ) {
+ $entries['Auxilliary Tables'] = "<ul>$auxtables</ul>";
+ } else {
+ $entries['Auxilliary Tables'] = 'No auxilliary tables used.';
+ }
+
+ return DebugFormatter::getStringFrom( 'SQLStore', $entries, $query );
+ }
+
+ private function doExecuteDebugQueryResult( $qobj, $sqlOptions, &$entries ) {
+
+ if ( !isset( $qobj->joinfield ) || $qobj->joinfield === '' ) {
+ return $entries['SQL Query'] = 'Empty result, no SQL query created.';
+ }
+
+ $connection = $this->store->getConnection( 'mw.db.queryengine' );
+ list( $startOpts, $useIndex, $tailOpts ) = $connection->makeSelectOptions( $sqlOptions );
+
+ $sortfields = implode( $qobj->sortfields, ',' );
+ $sortfields = $sortfields ? ', ' . $sortfields : '';
+
+ $format = DebugFormatter::getFormat(
+ $connection->getType()
+ );
+
+ $sql = "SELECT DISTINCT ".
+ "$qobj->alias.smw_id AS id," .
+ "$qobj->alias.smw_title AS t," .
+ "$qobj->alias.smw_namespace AS ns," .
+ "$qobj->alias.smw_iw AS iw," .
+ "$qobj->alias.smw_subobject AS so," .
+ "$qobj->alias.smw_sortkey AS sortkey" .
+ "$sortfields " .
+ "FROM " .
+ $connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from .
+ ( $qobj->where === '' ? '':' WHERE ' ) . $qobj->where . "$tailOpts $startOpts $useIndex ".
+ "LIMIT " . $sqlOptions['LIMIT'] . ' ' .
+ "OFFSET " . $sqlOptions['OFFSET'];
+
+ $res = $connection->query(
+ "EXPLAIN $format $sql",
+ __METHOD__
+ );
+
+ $entries['SQL Explain'] = DebugFormatter::prettifyExplain( $connection->getType(), $res );
+ $entries['SQL Query'] = DebugFormatter::prettifySql( $sql, $qobj->alias );
+
+ $connection->freeResult( $res );
+ }
+
+ /**
+ * Using a preprocessed internal query description referenced by $rootid, compute
+ * the proper counting output for the given query.
+ *
+ * @param Query $query
+ * @param integer $rootid
+ *
+ * @return integer
+ */
+ private function getCountQueryResult( Query $query, $rootid ) {
+
+ $queryResult = $this->queryFactory->newQueryResult(
+ $this->store,
+ $query,
+ [],
+ false
+ );
+
+ $queryResult->setCountValue( 0 );
+
+ $qobj = $this->querySegmentList[$rootid];
+
+ if ( $qobj->joinfield === '' ) { // empty result, no query needed
+ return $queryResult;
+ }
+
+ $connection = $this->store->getConnection( 'mw.db.queryengine' );
+
+ $sql_options = [ 'LIMIT' => $query->getLimit() + 1, 'OFFSET' => $query->getOffset() ];
+
+ $res = $connection->select(
+ $connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from,
+ "COUNT(DISTINCT $qobj->alias.smw_id) AS count",
+ $qobj->where,
+ __METHOD__,
+ $sql_options
+ );
+
+ $row = $connection->fetchObject( $res );
+ $count = 0;
+
+ if ( $row !== false ) {
+ $count = $row->count;
+ }
+
+ $connection->freeResult( $res );
+
+ $queryResult->setCountValue( $count );
+
+ return $queryResult;
+ }
+
+ /**
+ * Using a preprocessed internal query description referenced by $rootid,
+ * compute the proper result instance output for the given query.
+ * @todo The SQL standard requires us to select all fields by which we sort, leading
+ * to wrong results regarding the given limit: the user expects limit to be applied to
+ * the number of distinct pages, but we can use DISTINCT only to whole rows. Thus, if
+ * rows contain sortfields, then pages with multiple values for that field are distinct
+ * and appear multiple times in the result. Filtering duplicates in post processing
+ * would still allow such duplicates to push aside wanted values, leading to less than
+ * "limit" results although there would have been "limit" really distinct results. For
+ * this reason, we select sortfields only for POSTGRES. MySQL is able to perform what
+ * we want here. It would be nice if we could eliminate the bug in POSTGRES as well.
+ *
+ * @param Query $query
+ * @param integer $rootid
+ *
+ * @return QueryResult
+ */
+ private function getInstanceQueryResult( Query $query, $rootid ) {
+
+ $connection = $this->store->getConnection( 'mw.db.queryengine' );
+ $qobj = $this->querySegmentList[$rootid];
+
+ // Empty result, no query needed
+ if ( $qobj->joinfield === '' ) {
+ return $this->queryFactory->newQueryResult(
+ $this->store,
+ $query,
+ [],
+ false
+ );
+ }
+
+ $sql_options = $this->getSQLOptions( $query, $rootid );
+
+ // Selecting those is required in standard SQL (but MySQL does not require it).
+ $sortfields = implode( $qobj->sortfields, ',' );
+ $sortfields = $sortfields ? ',' . $sortfields : '';
+
+ $res = $connection->select(
+ $connection->tableName( $qobj->joinTable ) . " AS $qobj->alias" . $qobj->from,
+ "DISTINCT ".
+ "$qobj->alias.smw_id AS id," .
+ "$qobj->alias.smw_title AS t," .
+ "$qobj->alias.smw_namespace AS ns," .
+ "$qobj->alias.smw_iw AS iw," .
+ "$qobj->alias.smw_subobject AS so," .
+ "$qobj->alias.smw_sortkey AS sortkey" .
+ "$sortfields",
+ $qobj->where,
+ __METHOD__,
+ $sql_options
+ );
+
+ $results = [];
+ $dataItemCache = [];
+
+ $logToTable = [];
+ $hasFurtherResults = false;
+
+ // Number of fetched results ( != number of valid results in
+ // array $results)
+ $count = 0;
+ $missedCount = 0;
+
+ $diHandler = $this->store->getDataItemHandlerForDIType(
+ DataItem::TYPE_WIKIPAGE
+ );
+
+ while ( ( $count < $query->getLimit() ) && ( $row = $connection->fetchObject( $res ) ) ) {
+ if ( $row->iw === '' || $row->iw{0} != ':' ) {
+
+ // Catch exception for non-existing predefined properties that
+ // still registered within non-updated pages (@see bug 48711)
+ try {
+ $dataItem = $diHandler->dataItemFromDBKeys( [
+ $row->t,
+ intval( $row->ns ),
+ $row->iw,
+ '',
+ $row->so
+ ] );
+
+ // Register the ID in an event the post-proceesing
+ // fails (namespace no longer valid etc.)
+ $dataItem->setId( $row->id );
+ } catch ( PredefinedPropertyLabelMismatchException $e ) {
+ $logToTable[$row->t] = "issue creating a {$row->t} dataitem from a database row";
+ $this->log( __METHOD__ . ' ' . $e->getMessage() );
+ $dataItem = '';
+ }
+
+ if ( $dataItem instanceof DIWikiPage && !isset( $dataItemCache[$dataItem->getHash()] ) ) {
+ $count++;
+ $dataItemCache[$dataItem->getHash()] = true;
+ $results[] = $dataItem;
+ // These IDs are usually needed for displaying the page (esp. if more property values are displayed):
+ $this->store->smwIds->setCache( $row->t, $row->ns, $row->iw, $row->so, $row->id, $row->sortkey );
+ } else {
+ $missedCount++;
+ $logToTable[$row->t] = "skip result for {$row->t} existing cache entry / query " . $query->getHash();
+ }
+ } else {
+ $missedCount++;
+ $logToTable[$row->t] = "skip result for {$row->t} due to an internal `{$row->iw}` pointer / query " . $query->getHash();
+ }
+ }
+
+ if ( $connection->fetchObject( $res ) ) {
+ $count++;
+ }
+
+ if ( $logToTable !== [] ) {
+ $this->log( __METHOD__ . ' ' . implode( ',', $logToTable ) );
+ }
+
+ if ( $count > $query->getLimit() || ( $count + $missedCount ) > $query->getLimit() ) {
+ $hasFurtherResults = true;
+ };
+
+ $connection->freeResult( $res );
+
+ $queryResult = $this->queryFactory->newQueryResult(
+ $this->store,
+ $query,
+ $results,
+ $hasFurtherResults
+ );
+
+ return $queryResult;
+ }
+
+ private function applyExtraWhereCondition( $connection, $qid ) {
+
+ if ( !isset( $this->querySegmentList[$qid] ) ) {
+ return null;
+ }
+
+ $qobj = $this->querySegmentList[$qid];
+
+ // Filter elements that should never appear in a result set
+ $extraWhereCondition = [
+ 'del' => "$qobj->alias.smw_iw!=" . $connection->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) . " AND $qobj->alias.smw_iw!=" . $connection->addQuotes( SMW_SQL3_SMWDELETEIW ),
+ 'redi' => "$qobj->alias.smw_iw!=" . $connection->addQuotes( SMW_SQL3_SMWREDIIW )
+ ];
+
+ if ( strpos( $qobj->where, SMW_SQL3_SMWIW_OUTDATED ) === false ) {
+ $qobj->where .= $qobj->where === '' ? $extraWhereCondition['del'] : " AND " . $extraWhereCondition['del'];
+ }
+
+ if ( strpos( $qobj->where, SMW_SQL3_SMWREDIIW ) === false ) {
+ $qobj->where .= $qobj->where === '' ? $extraWhereCondition['redi'] : " AND " . $extraWhereCondition['redi'];
+ }
+
+ $this->querySegmentList[$qid] = $qobj;
+ }
+
+ /**
+ * Get a SQL option array for the given query and preprocessed query object at given id.
+ *
+ * @param Query $query
+ * @param integer $rootId
+ *
+ * @return array
+ */
+ private function getSQLOptions( Query $query, $rootId ) {
+
+ $result = [
+ 'LIMIT' => $query->getLimit() + 5,
+ 'OFFSET' => $query->getOffset()
+ ];
+
+ if ( !$this->engineOptions->isFlagSet( 'smwgQSortFeatures', SMW_QSORT ) ) {
+ return $result;
+ }
+
+ // Build ORDER BY options using discovered sorting fields.
+ $qobj = $this->querySegmentList[$rootId];
+
+ foreach ( $this->sortKeys as $propkey => $order ) {
+
+ if ( !is_string( $propkey ) ) {
+ throw new RuntimeException( "Expected a string value as sortkey" );
+ }
+
+ if ( ( $order != 'RANDOM' ) && array_key_exists( $propkey, $qobj->sortfields ) ) { // Field was successfully added.
+
+ $list = $qobj->sortfields[$propkey];
+
+ // Contains a compound list of sortfields without order?
+ if ( strpos( $list, ',' ) !== false && strpos( $list, $order ) === false ) {
+ $list = str_replace( ',', " $order,", $list );
+ }
+
+ $result['ORDER BY'] = ( array_key_exists( 'ORDER BY', $result ) ? $result['ORDER BY'] . ', ' : '' ) . $list . " $order ";
+ } elseif ( ( $order == 'RANDOM' ) && $this->engineOptions->isFlagSet( 'smwgQSortFeatures', SMW_QSORT_RANDOM ) ) {
+ $result['ORDER BY'] = ( array_key_exists( 'ORDER BY', $result ) ? $result['ORDER BY'] . ', ' : '' ) . ' RAND() ';
+ }
+ }
+
+ return $result;
+ }
+
+ private function log( $message, $context = [] ) {
+
+ if ( $this->logger === null ) {
+ return;
+ }
+
+ $this->logger->info( $message, $context );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegment.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegment.php
new file mode 100644
index 00000000..ce176410
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegment.php
@@ -0,0 +1,169 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+/**
+ * Class for representing a single (sub)query description.
+ *
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ */
+class QuerySegment {
+
+ /**
+ * Type of empty query without usable condition, dropped as soon as
+ * discovered. This is used only during preparing the query (no
+ * queries of this type should ever be added).
+ */
+ const Q_NOQUERY = 0;
+
+ /**
+ * Type of query that is a join with a query (jointable: internal
+ * table name; joinfield/components/where use alias.fields;
+ * from uses external table names, components interpreted
+ * conjunctively (JOIN)).
+ */
+ const Q_TABLE = 1;
+
+ /**
+ * Type of query that matches a constant value (joinfield is a
+ * disjunctive array of unquoted values, jointable empty, components
+ * empty).
+ */
+ const Q_VALUE = 2;
+
+ /**
+ * Type of query that is a disjunction of other queries
+ * (joinfield/jointable empty; only components relevant)
+ */
+ const Q_DISJUNCTION = 3;
+
+ /**
+ * Type of query that is a conjunction of other queries
+ * (joinfield/jointable empty; only components relevant).
+ */
+ const Q_CONJUNCTION = 4;
+
+ /**
+ * Type of query that creates a temporary table of all superclasses
+ * of given classes (only joinfield relevant: (disjunctive) array of
+ * unquoted values).
+ */
+ const Q_CLASS_HIERARCHY = 5;
+
+ /**
+ * Type of query that creates a temporary table of all superproperties
+ * of given properties (only joinfield relevant: (disjunctive) array
+ * of unquoted values).
+ */
+ const Q_PROP_HIERARCHY = 6;
+
+ /**
+ * @var integer
+ */
+ public $type = self::Q_TABLE;
+
+ /**
+ * @var integer|null
+ */
+ public $depth;
+
+ /**
+ * @var string
+ */
+ public $fingerprint = '';
+
+ /**
+ * @var boolean
+ */
+ public $null = false;
+
+ /**
+ * @var boolean
+ */
+ public $not = false;
+
+ /**
+ * @var string
+ */
+ public $joinType = '';
+
+ /**
+ * @var string
+ */
+ public $joinTable = '';
+
+ /**
+ * @var string|array
+ */
+ public $joinfield = '';
+
+ /**
+ * Allows to define an index field, for example in case when a sub-query rewires
+ * a match condition.
+ *
+ * @var string
+ */
+ public $indexField = '';
+
+ /**
+ * @var string
+ */
+ public $from = '';
+
+ /**
+ * @var string
+ */
+ public $where = '';
+
+ /**
+ * @var string[]
+ */
+ public $components = [];
+
+ /**
+ * The alias to be used for jointable; read-only after construct!
+ * @var string
+ */
+ public $alias;
+
+ /**
+ * property dbkey => db field; passed down during query execution.
+ * @var string[]
+ */
+ public $sortfields = [];
+
+ /**
+ * @var integer
+ */
+ public $queryNumber;
+
+ /**
+ * @var integer
+ */
+ public static $qnum = 0;
+
+ /**
+ * @since 2.2
+ */
+ public function __construct() {
+ $this->queryNumber = self::$qnum;
+ $this->alias = 't' . self::$qnum;
+ self::$qnum++;
+ }
+
+ /**
+ * @since 2.2
+ */
+ public function reset() {
+ self::$qnum = 0;
+
+ $this->queryNumber = self::$qnum;
+ $this->alias = 't' . self::$qnum;
+ self::$qnum++;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListBuildManager.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListBuildManager.php
new file mode 100644
index 00000000..fd9c8577
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListBuildManager.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use SMW\MediaWiki\Database;
+use SMW\SQLStore\SQLStore;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class QuerySegmentListBuildManager {
+
+ /**
+ * @var QuerySegment[]
+ */
+ private $querySegmentList = [];
+
+ /**
+ * @var string[]
+ */
+ private $errors = [];
+
+ /**
+ * @var string[]
+ */
+ private $sortKeys;
+
+ /**
+ * @var QuerySegmentListBuilder
+ */
+ private $querySegmentListBuilder;
+
+ /**
+ * @var OrderCondition
+ */
+ private $orderCondition;
+
+ /**
+ * @since 2.5
+ *
+ * @param Database $connection
+ * @param QuerySegmentListBuilder $querySegmentListBuilder
+ * @param OrderCondition $orderCondition
+ */
+ public function __construct( Database $connection, QuerySegmentListBuilder $querySegmentListBuilder, OrderCondition $orderCondition ) {
+ $this->connection = $connection;
+ $this->querySegmentListBuilder = $querySegmentListBuilder;
+ $this->orderCondition = $orderCondition;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string[]
+ */
+ public function getSortKeys() {
+ return $this->sortKeys;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getQuerySegmentList() {
+ return $this->querySegmentList;
+ }
+
+ /**
+ * Compute abstract representation of the query (compilation)
+ *
+ * @param Query $query
+ *
+ * @return integer
+ */
+ public function getQuerySegmentFrom( Query $query ) {
+
+ $this->sortKeys = $query->sortkeys;
+
+ // Anchor IT_TABLE as root element
+ $rootSegmentNumber = QuerySegment::$qnum;
+ $rootSegment = new QuerySegment();
+ $rootSegment->joinTable = SQLStore::ID_TABLE;
+ $rootSegment->joinfield = "$rootSegment->alias.smw_id";
+
+ $this->querySegmentListBuilder->addQuerySegment(
+ $rootSegment
+ );
+
+ $this->querySegmentListBuilder->setSortKeys(
+ $this->sortKeys
+ );
+
+ // compile query, build query "plan"
+ $this->querySegmentListBuilder->getQuerySegmentFrom(
+ $query->getDescription()
+ );
+
+ $qid = $this->querySegmentListBuilder->getLastQuerySegmentId();
+ $this->querySegmentList = $this->querySegmentListBuilder->getQuerySegmentList();
+ $this->errors = $this->querySegmentListBuilder->getErrors();
+
+ // no valid/supported condition; ensure that at least only proper pages
+ // are delivered
+ if ( $qid < 0 ) {
+ $qid = $rootSegmentNumber;
+ $qobj = $this->querySegmentList[$rootSegmentNumber];
+ $qobj->where = "$qobj->alias.smw_iw!=" . $this->connection->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) .
+ " AND $qobj->alias.smw_iw!=" . $this->connection->addQuotes( SMW_SQL3_SMWREDIIW ) .
+ " AND $qobj->alias.smw_iw!=" . $this->connection->addQuotes( SMW_SQL3_SMWBORDERIW ) .
+ " AND $qobj->alias.smw_iw!=" . $this->connection->addQuotes( SMW_SQL3_SMWINTDEFIW );
+ $this->querySegmentListBuilder->addQuerySegment( $qobj );
+ }
+
+ if ( isset( $this->querySegmentList[$qid]->joinTable ) && $this->querySegmentList[$qid]->joinTable != SQLStore::ID_TABLE ) {
+ // manually make final root query (to retrieve namespace,title):
+ $rootid = $rootSegmentNumber;
+ $qobj = $this->querySegmentList[$rootSegmentNumber];
+ $qobj->components = [ $qid => "$qobj->alias.smw_id" ];
+ $qobj->sortfields = $this->querySegmentList[$qid]->sortfields;
+ $this->querySegmentListBuilder->addQuerySegment( $qobj );
+ } else { // not such a common case, but worth avoiding the additional inner join:
+ $rootid = $qid;
+ }
+
+ $this->orderCondition->setSortKeys(
+ $this->sortKeys
+ );
+
+ // Include order conditions (may extend query if needed for sorting):
+ $this->querySegmentList = $this->orderCondition->apply(
+ $rootid
+ );
+
+ $this->sortKeys = $this->orderCondition->getSortKeys();
+ $this->errors = $this->orderCondition->getErrors();
+
+ return $rootid;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListBuilder.php
new file mode 100644
index 00000000..dbe02754
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListBuilder.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use InvalidArgumentException;
+use OutOfBoundsException;
+use SMW\Message;
+use SMW\Query\Language\Conjuncton;
+use SMW\Query\Language\Description;
+use SMW\Store;
+use SMW\Utils\CircularReferenceGuard;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class QuerySegmentListBuilder {
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var DispatchingDescriptionInterpreter
+ */
+ private $dispatchingDescriptionInterpreter = null;
+
+ /**
+ * @var boolean
+ */
+ private $isFilterDuplicates = true;
+
+ /**
+ * Array of generated QueryContainer query descriptions (index => object).
+ *
+ * @var QuerySegment[]
+ */
+ private $querySegments = [];
+
+ /**
+ * Array of sorting requests ("Property_name" => "ASC"/"DESC"). Used during query
+ * processing (where these property names are searched while compiling the query
+ * conditions).
+ *
+ * @var string[]
+ */
+ private $sortKeys = [];
+
+ /**
+ * @var string[]
+ */
+ private $errors = [];
+
+ /**
+ * @var integer
+ */
+ private $lastQuerySegmentId = -1;
+
+ /**
+ * @since 2.2
+ *
+ * @param Store $store
+ * @param DescriptionInterpreterFactory $descriptionInterpreterFactory
+ */
+ public function __construct( Store $store, DescriptionInterpreterFactory $descriptionInterpreterFactory ) {
+ $this->store = $store;
+ $this->dispatchingDescriptionInterpreter = $descriptionInterpreterFactory->newDispatchingDescriptionInterpreter( $this );
+ $this->circularReferenceGuard = new CircularReferenceGuard( 'sql-query' );
+ $this->circularReferenceGuard->setMaxRecursionDepth( 2 );
+
+ QuerySegment::$qnum = 0;
+ }
+
+ /**
+ * Filter dulicate segments that represent the same query and to be identified
+ * by the same hash.
+ *
+ * @since 2.5
+ *
+ * @param boolean $isFilterDuplicates
+ */
+ public function isFilterDuplicates( $isFilterDuplicates ) {
+ $this->isFilterDuplicates = (bool)$isFilterDuplicates;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return Store
+ */
+ public function getStore() {
+ return $this->store;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param array $sortKeys
+ *
+ * @return $this
+ */
+ public function setSortKeys( $sortKeys ) {
+ $this->sortKeys = $sortKeys;
+ return $this;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return string[]
+ */
+ public function getSortKeys() {
+ return $this->sortKeys;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return CircularReferenceGuard
+ */
+ public function getCircularReferenceGuard() {
+ return $this->circularReferenceGuard;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param int $id
+ *
+ * @return QuerySegment
+ * @throws InvalidArgumentException
+ * @throws OutOfBoundsException
+ */
+ public function findQuerySegment( $id ) {
+
+ if ( !is_int( $id ) ) {
+ throw new InvalidArgumentException( '$id needs to be an integer' );
+ }
+
+ if ( !array_key_exists( $id, $this->querySegments ) ) {
+ throw new OutOfBoundsException( 'There is no query segment with id ' . $id );
+ }
+
+ return $this->querySegments[$id];
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return QuerySegment[]
+ */
+ public function getQuerySegmentList() {
+ return $this->querySegments;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param QuerySegment $query
+ */
+ public function addQuerySegment( QuerySegment $query ) {
+ $this->querySegments[$query->queryNumber] = $query;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return integer
+ */
+ public function getLastQuerySegmentId() {
+ return $this->lastQuerySegmentId;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $error
+ */
+ public function addError( $error, $type = Message::TEXT ) {
+ $this->errors[Message::getHash( $error, $type )] = Message::encode( $error, $type );
+ }
+
+ /**
+ * Create a new QueryContainer object that can be used to obtain results
+ * for the given description. The result is stored in $this->queries
+ * using a numeric key that is returned as a result of the function.
+ * Returns -1 if no query was created.
+ *
+ * @param Description $description
+ *
+ * @return integer
+ */
+ public function getQuerySegmentFrom( Description $description ) {
+
+ $fingerprint = $description->getFingerprint();
+
+ // Get membership of descriptions that are resolved recursively
+ if ( $description->getMembership() !== '' ) {
+ $fingerprint = $fingerprint . $description->getMembership();
+ }
+
+ if ( ( $querySegment = $this->findDuplicates( $fingerprint ) ) ) {
+ return $querySegment;
+ }
+
+ $querySegment = $this->dispatchingDescriptionInterpreter->interpretDescription(
+ $description
+ );
+
+ $querySegment->fingerprint = $fingerprint;
+ //$querySegment->membership = $description->getMembership();
+ //$querySegment->queryString = $description->getQueryString();
+
+ $this->lastQuerySegmentId = $this->registerQuerySegment(
+ $querySegment
+ );
+
+ return $this->lastQuerySegmentId;
+ }
+
+ /**
+ * Register a query object to the internal query list, if the query is
+ * valid. Also make sure that sortkey information is propagated down
+ * from subqueries of this query.
+ *
+ * @param QuerySegment $query
+ */
+ private function registerQuerySegment( QuerySegment $query ) {
+ if ( $query->type === QuerySegment::Q_NOQUERY ) {
+ return -1;
+ }
+
+ $this->addQuerySegment( $query );
+
+ // Propagate sortkeys from subqueries:
+ if ( $query->type !== QuerySegment::Q_DISJUNCTION ) {
+ // Sortkeys are killed by disjunctions (not all parts may have them),
+ // NOTE: preprocessing might try to push disjunctions downwards to safe sortkey, but this seems to be minor
+ foreach ( $query->components as $cid => $field ) {
+ $query->sortfields = array_merge( $this->findQuerySegment( $cid )->sortfields, $query->sortfields );
+ }
+ }
+
+ return $query->queryNumber;
+ }
+
+ private function findDuplicates( $fingerprint ) {
+
+ if ( $this->errors !== [] || $this->isFilterDuplicates === false ) {
+ return false;
+ }
+
+ foreach ( $this->querySegments as $querySegment ) {
+ if ( $querySegment->fingerprint === $fingerprint ) {
+ return $querySegment->queryNumber;
+ };
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php
new file mode 100644
index 00000000..ad1f65a6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/QuerySegmentListProcessor.php
@@ -0,0 +1,349 @@
+<?php
+
+namespace SMW\SQLStore\QueryEngine;
+
+use RuntimeException;
+use SMW\MediaWiki\Database;
+use SMW\SQLStore\TableBuilder\TemporaryTableBuilder;
+use SMWQuery as Query;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Markus Krötzsch
+ * @author Jeroen De Dauw
+ * @author mwjames
+ */
+class QuerySegmentListProcessor {
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var TemporaryTableBuilder
+ */
+ private $temporaryTableBuilder;
+
+ /**
+ * @var HierarchyTempTableBuilder
+ */
+ private $hierarchyTempTableBuilder;
+
+ /**
+ * Array of arrays of executed queries, indexed by the temporary table names
+ * results were fed into.
+ *
+ * @var array
+ */
+ private $executedQueries = [];
+
+ /**
+ * Query mode copied from given query. Some submethods act differently when
+ * in Query::MODE_DEBUG.
+ *
+ * @var int
+ */
+ private $queryMode;
+
+ /**
+ * @var array
+ */
+ private $querySegmentList = [];
+
+ /**
+ * @param Database $connection
+ * @param TemporaryTableBuilder $temporaryTableBuilder
+ * @param HierarchyTempTableBuilder $hierarchyTempTableBuilder
+ */
+ public function __construct( Database $connection, TemporaryTableBuilder $temporaryTableBuilder, HierarchyTempTableBuilder $hierarchyTempTableBuilder ) {
+ $this->connection = $connection;
+ $this->temporaryTableBuilder = $temporaryTableBuilder;
+ $this->hierarchyTempTableBuilder = $hierarchyTempTableBuilder;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return array
+ */
+ public function getExecutedQueries() {
+ return $this->executedQueries;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param &$querySegmentList
+ */
+ public function setQuerySegmentList( &$querySegmentList ) {
+ $this->querySegmentList =& $querySegmentList;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param integer
+ */
+ public function setQueryMode( $queryMode ) {
+ $this->queryMode = $queryMode;
+ }
+
+ /**
+ * Process stored queries and change store accordingly. The query obj is modified
+ * so that it contains non-recursive description of a select to execute for getting
+ * the actual result.
+ *
+ * @param integer $id
+ * @throws RuntimeException
+ */
+ public function process( $id ) {
+
+ $this->hierarchyTempTableBuilder->emptyHierarchyCache();
+ $this->executedQueries = [];
+
+ // Should never happen
+ if ( !isset( $this->querySegmentList[$id] ) ) {
+ throw new RuntimeException( "$id doesn't exist" );
+ }
+
+ $this->resolve( $this->querySegmentList[$id] );
+ }
+
+ private function resolve( QuerySegment &$query ) {
+
+ switch ( $query->type ) {
+ case QuerySegment::Q_TABLE: // Normal query with conjunctive subcondition.
+ foreach ( $query->components as $qid => $joinField ) {
+ $subQuery = $this->querySegmentList[$qid];
+ $this->resolve( $subQuery );
+
+ if ( $subQuery->joinTable !== '' ) { // Join with jointable.joinfield
+ $op = $subQuery->not ? '!' : '';
+
+ $joinType = $subQuery->joinType ? $subQuery->joinType : 'INNER';
+ $t = $this->connection->tableName( $subQuery->joinTable ) ." AS $subQuery->alias";
+
+ if ( $subQuery->from ) {
+ $t = "($t $subQuery->from)";
+ }
+
+ $query->from .= " $joinType JOIN $t ON $joinField$op=" . $subQuery->joinfield;
+
+ if ( $joinType === 'LEFT' ) {
+ $query->where .= ( ( $query->where === '' ) ? '' : ' AND ' ) . '(' . $subQuery->joinfield . ' IS NULL)';
+ }
+
+ } elseif ( $subQuery->joinfield !== '' ) { // Require joinfield as "value" via WHERE.
+ $condition = '';
+
+ if ( $subQuery->null === true ) {
+ $condition .= ( $condition ? ' OR ': '' ) . "$joinField IS NULL";
+ } else {
+ foreach ( $subQuery->joinfield as $value ) {
+ $op = $subQuery->not ? '!' : '';
+ $condition .= ( $condition ? ' OR ': '' ) . "$joinField$op=" . $this->connection->addQuotes( $value );
+ }
+ }
+
+ if ( count( $subQuery->joinfield ) > 1 ) {
+ $condition = "($condition)";
+ }
+
+ $query->where .= ( ( $query->where === '' || $subQuery->where === null ) ? '' : ' AND ' ) . $condition;
+ $query->from .= $subQuery->from;
+ } else { // interpret empty joinfields as impossible condition (empty result)
+ $query->joinfield = ''; // make whole query false
+ $query->joinTable = '';
+ $query->where = '';
+ $query->from = '';
+ break;
+ }
+
+ if ( $subQuery->where !== '' && $subQuery->where !== null ) {
+ if ( $subQuery->joinType === 'LEFT' || $subQuery->joinType == 'LEFT OUTER' ) {
+ $query->from .= ' AND (' . $subQuery->where . ')';
+ } else {
+ $query->where .= ( ( $query->where === '' ) ? '' : ' AND ' ) . '(' . $subQuery->where . ')';
+ }
+ }
+ }
+
+ $query->components = [];
+ break;
+ case QuerySegment::Q_CONJUNCTION:
+ reset( $query->components );
+ $key = false;
+
+ // Pick one subquery as anchor point ...
+ foreach ( $query->components as $qkey => $qid ) {
+ $key = $qkey;
+
+ if ( $this->querySegmentList[$qkey]->joinTable !== '' ) {
+ break;
+ }
+ }
+
+ $result = $this->querySegmentList[$key];
+ unset( $query->components[$key] );
+
+ // Execute it first (may change jointable and joinfield, e.g. when making temporary tables)
+ $this->resolve( $result );
+
+ // ... and append to this query the remaining queries.
+ foreach ( $query->components as $qid => $joinfield ) {
+ $result->components[$qid] = $result->joinfield;
+ }
+
+ // Second execute, now incorporating remaining conditions.
+ $this->resolve( $result );
+ $query = $result;
+ break;
+ case QuerySegment::Q_DISJUNCTION:
+ if ( $this->queryMode !== Query::MODE_NONE ) {
+ $this->temporaryTableBuilder->create( $this->connection->tableName( $query->alias ) );
+ }
+
+ $this->executedQueries[$query->alias] = [];
+
+ foreach ( $query->components as $qid => $joinField ) {
+ $subQuery = $this->querySegmentList[$qid];
+ $this->resolve( $subQuery );
+ $sql = '';
+
+ if ( $subQuery->joinTable !== '' ) {
+ $sql = 'INSERT ' . 'IGNORE ' . 'INTO ' .
+ $this->connection->tableName( $query->alias ) .
+ " SELECT DISTINCT $subQuery->joinfield FROM " . $this->connection->tableName( $subQuery->joinTable ) .
+ " AS $subQuery->alias $subQuery->from" . ( $subQuery->where ? " WHERE $subQuery->where":'' );
+ } elseif ( $subQuery->joinfield !== '' ) {
+ // NOTE: this works only for single "unconditional" values without further
+ // WHERE or FROM. The execution must take care of not creating any others.
+ $values = '';
+
+ // This produces an error on postgres with
+ // pg_query(): Query failed: ERROR: duplicate key value violates
+ // unique constraint "sunittest_t3_pkey" DETAIL: Key (id)=(274) already exists.
+
+ foreach ( $subQuery->joinfield as $value ) {
+ $values .= ( $values ? ',' : '' ) . '(' . $this->connection->addQuotes( $value ) . ')';
+ }
+
+ $sql = 'INSERT ' . 'IGNORE ' . 'INTO ' . $this->connection->tableName( $query->alias ) . " (id) VALUES $values";
+ } // else: // interpret empty joinfields as impossible condition (empty result), ignore
+ if ( $sql ) {
+ $this->executedQueries[$query->alias][] = $sql;
+
+ if ( $this->queryMode !== Query::MODE_NONE ) {
+ $this->connection->query(
+ $sql,
+ __METHOD__
+ );
+ }
+ }
+ }
+
+ $query->type = QuerySegment::Q_TABLE;
+ $query->where = '';
+ $query->components = [];
+
+ $query->joinTable = $query->alias;
+ $query->joinfield = "$query->alias.id";
+ $query->sortfields = []; // Make sure we got no sortfields.
+ // TODO: currently this eliminates sortkeys, possibly keep them (needs different temp table format though, maybe not such a good thing to do)
+ break;
+ case QuerySegment::Q_PROP_HIERARCHY:
+ case QuerySegment::Q_CLASS_HIERARCHY: // make a saturated hierarchy
+ $this->resolveHierarchy( $query );
+ break;
+ case QuerySegment::Q_VALUE:
+ break; // nothing to do
+ }
+ }
+
+ /**
+ * Find subproperties or subcategories. This may require iterative computation,
+ * and temporary tables are used in many cases.
+ *
+ * @param QuerySegment $query
+ */
+ private function resolveHierarchy( QuerySegment &$query ) {
+
+ switch ( $query->type ) {
+ case QuerySegment::Q_PROP_HIERARCHY:
+ $type = 'property';
+ break;
+ case QuerySegment::Q_CLASS_HIERARCHY:
+ $type = 'class';
+ break;
+ }
+
+ list( $smwtable, $depth ) = $this->hierarchyTempTableBuilder->getHierarchyTableDefinitionForType(
+ $type
+ );
+
+ // An individual depth was annotated as part of the query
+ if ( $query->depth !== null ) {
+ $depth = $query->depth;
+ }
+
+ if ( $depth <= 0 ) { // treat as value, no recursion
+ $query->type = QuerySegment::Q_VALUE;
+ return;
+ }
+
+ $values = '';
+ $valuecond = '';
+
+ foreach ( $query->joinfield as $value ) {
+ $values .= ( $values ? ',':'' ) . '(' . $this->connection->addQuotes( $value ) . ')';
+ $valuecond .= ( $valuecond ? ' OR ':'' ) . 'o_id=' . $this->connection->addQuotes( $value );
+ }
+
+ // Try to safe time (SELECT is cheaper than creating/dropping 3 temp tables):
+ $res = $this->connection->select(
+ $smwtable,
+ 's_id',
+ $valuecond,
+ __METHOD__,
+ [ 'LIMIT' => 1 ]
+ );
+
+ if ( !$this->connection->fetchObject( $res ) ) { // no subobjects, we are done!
+ $this->connection->freeResult( $res );
+ $query->type = QuerySegment::Q_VALUE;
+ return;
+ }
+
+ $this->connection->freeResult( $res );
+ $tablename = $this->connection->tableName( $query->alias );
+ $this->executedQueries[$query->alias] = [
+ "Recursively computed hierarchy for element(s) $values.",
+ "SELECT s_id FROM $smwtable WHERE $valuecond LIMIT 1"
+ ];
+
+ $query->joinTable = $query->alias;
+ $query->joinfield = "$query->alias.id";
+
+ $this->hierarchyTempTableBuilder->createHierarchyTempTableFor(
+ $type,
+ $tablename,
+ $values,
+ $depth
+ );
+ }
+
+ /**
+ * After querying, make sure no temporary database tables are left.
+ * @todo I might be better to keep the tables and possibly reuse them later
+ * on. Being temporary, the tables will vanish with the session anyway.
+ */
+ public function cleanUp() {
+ foreach ( $this->executedQueries as $table => $log ) {
+ $this->temporaryTableBuilder->drop( $this->connection->tableName( $table ) );
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/README.md b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/README.md
new file mode 100644
index 00000000..a6d9dc21
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngine/README.md
@@ -0,0 +1,61 @@
+# QueryEngine
+
+The `QueryEngine` handles the transformation of the `ask` query language into a `SQL` construct and is also
+responsible to return query results from the `SQL` back-end with the help of the following components:
+
+- The `QuerySegmentListBuilder` transforms `ask` descriptions into individual `QuerySegment`'s (aka `QuerySegmentList`)
+- The `DescriptionInterpreter` interface describes classes that are responsible to interpret a specific
+ `Description` object and turn it into an abstract `SQL` construct (a `QuerySegment`)
+- The `QuerySegmentListProcessor` flattens and transforms a list of `QuerySegment`'s into a non-recursive
+ tree of `SQL` statements (including resolving of property/category hierarchies)
+- The `ConceptQueryResolver` encapsulates query processing of a concept description in connection
+ with the `ConceptCache` class
+
+## Overview
+
+![image](https://cloud.githubusercontent.com/assets/1245473/10050078/ca42ff12-621a-11e5-84c3-5fd04d945c6c.png)
+
+### Examples
+```php
+/**
+ * Equivalent to [[Category:Foo]]
+ */
+$classDescription = new ClassDescription(
+ new DIWikiPage( 'Foo', NS_CATEGORY )
+);
+
+/**
+ * Equivalent to [[:+]]
+ */
+$namespaceDescription = new NamespaceDescription(
+ NS_MAIN
+);
+
+/**
+ * Equivalent to [[Foo::+]]
+ */
+$someProperty = new SomeProperty(
+ new DIProperty( 'Foo' ),
+ new ThingDescription()
+);
+
+/**
+ * Equivalent to [[:+]][[Category:Foo]][[Foo::+]]
+ */
+$description = new Conjunction( [
+ $namespaceDescription,
+ $classDescription,
+ $someProperty
+] );
+```
+```php
+$query = new Query( $description );
+$query->setLimit( 10 );
+
+$sqlStorefactory = new SQLStoreFactory(
+ new SQLStore()
+);
+
+$queryEngine = $sqlStorefactory->newMasterQueryEngine();
+$queryResult = $queryEngine->getQueryResult( $query );
+```
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngineFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngineFactory.php
new file mode 100644
index 00000000..185399aa
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/QueryEngineFactory.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\ApplicationFactory;
+use SMW\DIProperty;
+use SMW\SQLStore\QueryEngine\ConceptQuerySegmentBuilder;
+use SMW\SQLStore\QueryEngine\DescriptionInterpreterFactory;
+use SMW\SQLStore\QueryEngine\EngineOptions;
+use SMW\SQLStore\QueryEngine\HierarchyTempTableBuilder;
+use SMW\SQLStore\QueryEngine\OrderCondition;
+use SMW\SQLStore\QueryEngine\QueryEngine;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuilder;
+use SMW\SQLStore\QueryEngine\QuerySegmentListBuildManager;
+use SMW\SQLStore\QueryEngine\QuerySegmentListProcessor;
+use SMW\SQLStore\TableBuilder\TemporaryTableBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.4
+ *
+ * @author mwjames
+ */
+class QueryEngineFactory {
+
+ /**
+ * @var SMWSQLStore3
+ */
+ private $store;
+
+ /**
+ * @since 2.4
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return QuerySegmentListBuilder
+ */
+ public function newQuerySegmentListBuilder() {
+
+ $querySegmentListBuilder = new QuerySegmentListBuilder(
+ $this->store,
+ new DescriptionInterpreterFactory()
+ );
+
+ $querySegmentListBuilder->isFilterDuplicates(
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgQFilterDuplicates' )
+ );
+
+ return $querySegmentListBuilder;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return QuerySegmentListProcessor
+ */
+ public function newQuerySegmentListProcessor() {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $connection = $this->store->getConnection( 'mw.db.queryengine' );
+ $temporaryTableBuilder = $this->newTemporaryTableBuilder();
+
+ $hierarchyTempTableBuilder = new HierarchyTempTableBuilder(
+ $connection,
+ $temporaryTableBuilder
+ );
+
+ $hierarchyTempTableBuilder->setPropertyHierarchyTableDefinition(
+ $this->store->findPropertyTableID( new DIProperty( '_SUBP' ) ),
+ $settings->get( 'smwgQSubpropertyDepth' )
+ );
+
+ $hierarchyTempTableBuilder->setClassHierarchyTableDefinition(
+ $this->store->findPropertyTableID( new DIProperty( '_SUBC' ) ),
+ $settings->get( 'smwgQSubcategoryDepth' )
+ );
+
+ $querySegmentListProcessor = new QuerySegmentListProcessor(
+ $connection,
+ $temporaryTableBuilder,
+ $hierarchyTempTableBuilder
+ );
+
+ return $querySegmentListProcessor;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return QueryEngine
+ */
+ public function newQueryEngine() {
+
+ $querySegmentListBuilder = $this->newQuerySegmentListBuilder();
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $settings = $applicationFactory->getSettings();
+
+ $orderCondition = new OrderCondition(
+ $querySegmentListBuilder
+ );
+
+ $orderCondition->isSupported(
+ $settings->isFlagSet( 'smwgQSortFeatures', SMW_QSORT )
+ );
+
+ $orderCondition->asUnconditional(
+ $settings->isFlagSet( 'smwgQSortFeatures', SMW_QSORT_UNCONDITIONAL )
+ );
+
+ $querySegmentListBuildManager = new QuerySegmentListBuildManager(
+ $this->store->getConnection( 'mw.db.queryengine' ),
+ $querySegmentListBuilder,
+ $orderCondition
+ );
+
+ $queryEngine = new QueryEngine(
+ $this->store,
+ $querySegmentListBuildManager,
+ $this->newQuerySegmentListProcessor(),
+ new EngineOptions()
+ );
+
+ $queryEngine->setLogger(
+ $applicationFactory->getMediaWikiLogger()
+ );
+
+ return $queryEngine;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return ConceptQuerySegmentBuilder
+ */
+ public function newConceptQuerySegmentBuilder() {
+
+ $pplicationFactory = ApplicationFactory::getInstance();
+
+ $conceptQuerySegmentBuilder = new ConceptQuerySegmentBuilder(
+ $this->newQuerySegmentListBuilder(),
+ $this->newQuerySegmentListProcessor()
+ );
+
+ $conceptQuerySegmentBuilder->setQueryParser(
+ $pplicationFactory->getQueryFactory()->newQueryParser(
+ $pplicationFactory->getSettings()->get( 'smwgQConceptFeatures' )
+ )
+ );
+
+ return $conceptQuerySegmentBuilder;
+ }
+
+ private function newTemporaryTableBuilder() {
+
+ $temporaryTableBuilder = new TemporaryTableBuilder(
+ $this->store->getConnection( 'mw.db.queryengine' )
+ );
+
+ $temporaryTableBuilder->setAutoCommitFlag(
+ ApplicationFactory::getInstance()->getSettings()->get( 'smwgQTemporaryTablesAutoCommitMode' )
+ );
+
+ return $temporaryTableBuilder;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/README.md b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/README.md
new file mode 100644
index 00000000..6c938074
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/README.md
@@ -0,0 +1,12 @@
+# SQLStore
+
+The `SQLStore` consists of a storage and query engine to manage semantic data structures with the help of a `SQL` back-end.
+
+## Overview
+
+![image](https://cloud.githubusercontent.com/assets/1245473/20031413/49b336f6-a377-11e6-90f5-9fbe25b27812.png)
+
+## See also
+
+- [Installer](https://github.com/SemanticMediaWiki/SemanticMediaWiki/blob/master/docs/technical/sqlstore.installer.md)
+- [QueryEngine](https://github.com/SemanticMediaWiki/SemanticMediaWiki/blob/master/src/SQLStore/QueryEngine/README.md) \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/RedirectStore.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/RedirectStore.php
new file mode 100644
index 00000000..11065976
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/RedirectStore.php
@@ -0,0 +1,274 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use Onoi\Cache\Cache;
+use SMW\HashBuilder;
+use SMW\InMemoryPoolCache;
+use SMW\MediaWiki\Jobs\UpdateJob;
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMW\Store;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.1
+ *
+ * @author mwjames
+ */
+class RedirectStore {
+
+ const TABLE_NAME = 'smw_fpt_redi';
+
+ /**
+ * @var Store
+ */
+ private $store;
+
+ /**
+ * @var Cache
+ */
+ private $cache;
+
+ /**
+ * @var boolean
+ */
+ private $hasEqualitySupport = false;
+
+ /**
+ * @since 2.1
+ *
+ * @param Store $store
+ * @param Cache|null $cache
+ */
+ public function __construct( Store $store, Cache $cache = null ) {
+ $this->store = $store;
+ $this->cache = $cache;
+
+ if ( $this->cache === null ) {
+ $this->cache = InMemoryPoolCache::getInstance()->getPoolCacheById( 'sql.store.redirect.infostore' );
+ }
+
+ $this->setEqualitySupportFlag( $GLOBALS['smwgQEqualitySupport'] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $equalitySupport
+ */
+ public function setEqualitySupportFlag( $equalitySupport ) {
+ $this->hasEqualitySupport = $equalitySupport != SMW_EQ_NONE;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $title DB key
+ * @param integer $namespace
+ *
+ * @return boolean
+ */
+ public function isRedirect( $title, $namespace ) {
+ return $this->findRedirect( $title, $namespace ) != 0;
+ }
+
+ /**
+ * Returns an id for a redirect if no redirect is found 0 is returned
+ *
+ * @since 2.1
+ *
+ * @param string $title DB key
+ * @param integer $namespace
+ *
+ * @return integer
+ */
+ public function findRedirect( $title, $namespace ) {
+
+ $hash = HashBuilder::createHashIdFromSegments(
+ $title,
+ $namespace
+ );
+
+ if ( $this->cache->contains( $hash ) ) {
+ return $this->cache->fetch( $hash );
+ }
+
+ $id = $this->select( $title, $namespace );
+
+ $this->cache->save( $hash, $id );
+
+ return $id;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param integer $id
+ * @param string $title
+ * @param integer $namespace
+ */
+ public function addRedirect( $id, $title, $namespace ) {
+
+ $this->insert( $id, $title, $namespace );
+
+ $hash = HashBuilder::createHashIdFromSegments(
+ $title,
+ $namespace
+ );
+
+ $this->cache->save( $hash, $id );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ * @param string $title
+ * @param integer $namespace
+ */
+ public function updateRedirect( $id, $title, $namespace ) {
+
+ $this->deleteRedirect( $title, $namespace );
+
+ if ( !$this->canCreateUpdateJobs() || !$this->hasEqualitySupport ) {
+ return;
+ }
+
+ // Entries that refer to old target may in fact refer to subject,
+ // but we don't know which: schedule affected pages for update
+ $propertyTables = $this->store->getPropertyTables();
+ $connection = $this->store->getConnection( 'mw.db' );
+ $jobs = [];
+
+ foreach ( $propertyTables as $proptable ) {
+
+ // Can be skipped safely
+ if ( $proptable->getName() == self::TABLE_NAME ) {
+ continue;
+ }
+
+ $query = [
+ 'from' => '',
+ 'fields' => ''
+ ];
+
+ $query['condition'] = [ 'p_id' => $id ];
+
+ if ( $proptable->usesIdSubject() ) {
+ $query['from'] .= $connection->tableName( $proptable->getName() );
+ $query['from'] .= ' INNER JOIN ';
+ $query['from'] .= $connection->tableName( SQLStore::ID_TABLE ) . ' ON s_id=smw_id';
+ $query['fields'] = 'DISTINCT smw_title AS t,smw_namespace AS ns';
+ } else {
+ $query['from'] = $connection->tableName( $proptable->getName() );
+ $query['fields'] = 'DISTINCT s_title AS t,s_namespace AS ns';
+ }
+
+ if ( $namespace === SMW_NS_PROPERTY && !$proptable->isFixedPropertyTable() ) {
+ $this->findUpdateJobs( $connection, $query, $jobs );
+ }
+
+ foreach ( $proptable->getFields( $this->store ) as $fieldName => $fieldType ) {
+
+ if ( $fieldType !== FieldType::FIELD_ID ) {
+ continue;
+ }
+
+ $query['condition'] = [ $fieldName => $id ];
+ $this->findUpdateJobs( $connection, $query, $jobs );
+ }
+ }
+
+ foreach ( $jobs as $job ) {
+ $job->insert();
+ }
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $title
+ * @param integer $namespace
+ */
+ public function deleteRedirect( $title, $namespace ) {
+
+ $this->delete( $title, $namespace );
+
+ $hash = HashBuilder::createHashIdFromSegments(
+ $title,
+ $namespace
+ );
+
+ $this->cache->delete( $hash );
+ }
+
+ private function select( $title, $namespace ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $row = $connection->selectRow(
+ self::TABLE_NAME,
+ 'o_id',
+ [
+ 's_title' => $title,
+ 's_namespace' => $namespace
+ ],
+ __METHOD__
+ );
+
+ return $row !== false && isset( $row->o_id ) ? (int)$row->o_id : 0;
+ }
+
+ private function insert( $id, $title, $namespace ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $connection->insert(
+ self::TABLE_NAME,
+ [
+ 's_title' => $title,
+ 's_namespace' => $namespace,
+ 'o_id' => $id ],
+ __METHOD__
+ );
+ }
+
+ private function delete( $title, $namespace ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $connection->delete(
+ self::TABLE_NAME,
+ [
+ 's_title' => $title,
+ 's_namespace' => $namespace ],
+ __METHOD__
+ );
+ }
+
+ private function canCreateUpdateJobs() {
+ return $this->store->getOption( Store::OPT_CREATE_UPDATE_JOB, true ) && $this->store->getOption( 'smwgEnableUpdateJobs' );
+ }
+
+ private function findUpdateJobs( $connection, $query, &$jobs ) {
+
+ $res = $connection->select(
+ $query['from'],
+ $query['fields'],
+ $query['condition'],
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ $title = Title::makeTitleSafe( $row->ns, $row->t );
+
+ if ( $title !== null ) {
+ $jobs[] = new UpdateJob( $title );
+ }
+ }
+
+ $connection->freeResult( $res );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/RequestOptionsProc.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/RequestOptionsProc.php
new file mode 100644
index 00000000..8a8cc949
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/RequestOptionsProc.php
@@ -0,0 +1,304 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\DIWikiPage;
+use SMW\Store;
+use SMWDIBlob as DIBlob;
+use SMWRequestOptions as RequestOptions;
+use SMWStringCondition as StringCondition;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author Markus Krötzsch
+ * @author mwjames
+ */
+class RequestOptionsProc {
+
+ /**
+ * Transform input parameters into a suitable array of SQL options.
+ * The parameter $valuecol defines the string name of the column to which
+ * sorting requests etc. are to be applied.
+ *
+ * @since 1.8
+ *
+ * @param RequestOptions|null $requestOptions
+ * @param string $valueCol
+ *
+ * @return array
+ */
+ public static function getSQLOptions( RequestOptions $requestOptions = null, $valueCol = '' ) {
+ $sqlConds = [];
+
+ if ( $requestOptions === null ) {
+ return $sqlConds;
+ }
+
+ if ( $requestOptions->getLimit() > 0 ) {
+ $sqlConds['LIMIT'] = $requestOptions->getLimit();
+ }
+
+ if ( $requestOptions->getOffset() > 0 ) {
+ $sqlConds['OFFSET'] = $requestOptions->getOffset();
+ }
+
+ if ( ( $valueCol !== '' ) && ( $requestOptions->sort ) ) {
+ $sqlConds['ORDER BY'] = $requestOptions->ascending ? $valueCol : $valueCol . ' DESC';
+ }
+
+ if ( $requestOptions->getOption( 'GROUP BY' ) ) {
+ $sqlConds['GROUP BY'] = $requestOptions->getOption( 'GROUP BY' );
+ }
+
+ if ( $requestOptions->getOption( 'DISTINCT' ) ) {
+ $sqlConds['DISTINCT'] = $requestOptions->getOption( 'DISTINCT' );
+ }
+
+ // Avoid a possible filesort (likely caused by ORDER BY) when limit is
+ // less than 2
+ if ( $requestOptions->limit < 2 || $requestOptions->getOption( 'ORDER BY' ) === false ) {
+ unset( $sqlConds['ORDER BY'] );
+ }
+
+ return $sqlConds;
+ }
+
+ /**
+ * Transform input parameters into a suitable string of additional SQL
+ * conditions. The parameter $valuecol defines the string name of the
+ * column to which value restrictions etc. are to be applied.
+ *
+ * @since 1.8
+ *
+ * @param Store $store
+ * @param RequestOptions|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 static function getSQLConditions( Store $store, RequestOptions $requestOptions = null, $valueCol = '', $labelCol = '', $addAnd = true ) {
+ $sqlConds = '';
+
+ if ( $requestOptions === null ) {
+ return $sqlConds;
+ }
+
+ $connection = $store->getConnection( 'mw.db' );
+
+ // Apply value boundary
+ if ( ( $valueCol !== '' ) && ( $requestOptions->boundary !== null ) ) {
+
+ if ( $requestOptions->ascending ) {
+ $op = $requestOptions->include_boundary ? ' >= ' : ' > ';
+ } else {
+ $op = $requestOptions->include_boundary ? ' <= ' : ' < ';
+ }
+
+ $sqlConds .= ( $addAnd ? ' AND ' : '' ) . $valueCol . $op . $connection->addQuotes( $requestOptions->boundary );
+ }
+
+ // Apply string conditions
+ if ( $labelCol !== '' ) {
+ foreach ( $requestOptions->getStringConditions() as $strcond ) {
+ $string = str_replace( '_', '\_', $strcond->string );
+ $condition = 'LIKE';
+
+ switch ( $strcond->condition ) {
+ case StringCondition::COND_PRE:
+ $string .= '%';
+ break;
+ case StringCondition::COND_POST:
+ $string = '%' . $string;
+ break;
+ case StringCondition::COND_MID:
+ $string = '%' . $string . '%';
+ break;
+ case StringCondition::COND_EQ:
+ $string = $strcond->string;
+ $condition = '=';
+ break;
+ }
+
+ $conditionOperator = $strcond->isOr ? ' OR ' : ' AND ';
+
+ if ( $strcond->isNot ) {
+ $sqlConds = " ($sqlConds) AND ($labelCol NOT $condition ". $connection->addQuotes( $string ) . ") ";
+ } else {
+ $sqlConds .= ( ( $addAnd || ( $sqlConds !== '' ) ) ? $conditionOperator : '' ) . "$labelCol $condition " . $connection->addQuotes( $string );
+ }
+ }
+ }
+
+ foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
+
+ $expr = $addAnd ? 'AND' : '';
+
+ if ( is_array( $extraCondition ) ) {
+ foreach ( $extraCondition as $k => $v ) {
+ $expr = $k;
+ $extraCondition = $v;
+ }
+ }
+
+ $sqlConds .= ( ( $addAnd || ( $sqlConds !== '' ) ) ? " $expr " : '' ) . $extraCondition;
+ }
+
+ return $sqlConds;
+ }
+
+ /**
+ * Not in all cases can requestoptions be forwarded to the DB using
+ * getSQLConditions() and getSQLOptions(): some data comes from caches
+ * that do not respect the options yet. This method takes an array of
+ * results (SMWDataItem objects) *of the same type* and applies the
+ * given requestoptions as appropriate.
+ *
+ * @since 1.8
+ *
+ * @param Store $store
+ * @param array $data array of SMWDataItem objects
+ * @param SMWRequestOptions|null $requestoptions
+ *
+ * @return SMWDataItem[]
+ */
+ public static function applyRequestOptions( Store $store, array $data, RequestOptions $requestOptions = null ) {
+
+ if ( $data === [] || $requestOptions === null ) {
+ return $data;
+ }
+
+ $result = [];
+ $sortres = [];
+
+ $sampleDataItem = reset( $data );
+ $isNumeric = is_numeric( $sampleDataItem->getSortKey() );
+
+ $i = 0;
+
+ foreach ( $data as $item ) {
+
+ list( $label, $value ) = self::getSortKeyForItem( $store, $item );
+
+ $keepDataValue = self::applyBoundaryConditions( $requestOptions, $value, $isNumeric );
+ $keepDataValue = self::applyStringConditions( $requestOptions, $label, $keepDataValue );
+
+ if ( $keepDataValue ) {
+ $result[$i] = $item;
+ $sortres[$i] = $value;
+ $i++;
+ }
+ }
+
+ self::applySortRestriction( $requestOptions, $result, $sortres, $isNumeric );
+ self::applyLimitRestriction( $requestOptions, $result );
+
+ return $result;
+ }
+
+ private static function applyStringConditions( $requestOptions, $label, $keepDataValue ) {
+
+ foreach ( $requestOptions->getStringConditions() as $strcond ) { // apply string conditions
+ switch ( $strcond->condition ) {
+ case StringCondition::STRCOND_PRE:
+ $keepDataValue = $keepDataValue && ( strpos( $label, $strcond->string ) === 0 );
+ break;
+ case StringCondition::STRCOND_POST:
+ $keepDataValue = $keepDataValue && ( strpos( strrev( $label ), strrev( $strcond->string ) ) === 0 );
+ break;
+ case StringCondition::STRCOND_MID:
+ $keepDataValue = $keepDataValue && ( strpos( $label, $strcond->string ) !== false );
+ break;
+ }
+ }
+
+ return $keepDataValue;
+ }
+
+ private static function applyBoundaryConditions( $requestOptions, $value, $isNumeric ) {
+ $keepDataValue = true; // keep datavalue only if this remains true
+
+ if ( $requestOptions->boundary === null ) {
+ return $keepDataValue;
+ }
+
+ // apply value boundary
+ $strc = $isNumeric ? 0 : strcmp( $value, $requestOptions->boundary );
+
+ if ( $requestOptions->ascending ) {
+ if ( $requestOptions->include_boundary ) {
+ $keepDataValue = $isNumeric ? ( $value >= $requestOptions->boundary ) : ( $strc >= 0 );
+ } else {
+ $keepDataValue = $isNumeric ? ( $value > $requestOptions->boundary ) : ( $strc > 0 );
+ }
+ } else {
+ if ( $requestOptions->include_boundary ) {
+ $keepDataValue = $isNumeric ? ( $value <= $requestOptions->boundary ) : ( $strc <= 0 );
+ } else {
+ $keepDataValue = $isNumeric ? ( $value < $requestOptions->boundary ) : ( $strc < 0 );
+ }
+ }
+
+ return $keepDataValue;
+ }
+
+ private static function getSortKeyForItem( $store, $item ) {
+
+ if ( $item instanceof DIWikiPage ) {
+ $label = $store->getWikiPageSortKey( $item );
+ $value = $label;
+ } else {
+ $label = ( $item instanceof DIBlob ) ? $item->getString() : '';
+ $value = $item->getSortKey();
+ }
+
+ return [ $label, $value ];
+ }
+
+ private static function applySortRestriction( $requestOptions, &$result, $sortres, $isNumeric ) {
+
+ if ( !$requestOptions->sort ) {
+ return null;
+ }
+
+ $flag = $isNumeric ? SORT_NUMERIC : SORT_LOCALE_STRING;
+
+ // SORT_NATURAL is selected on n-asc, n-desc
+ if ( isset( $requestOptions->natural ) ) {
+ $flag = SORT_NATURAL;
+ }
+
+ if ( $requestOptions->ascending ) {
+ asort( $sortres, $flag );
+ } else {
+ arsort( $sortres, $flag );
+ }
+
+ $newres = [];
+
+ foreach ( $sortres as $key => $value ) {
+ $newres[] = $result[$key];
+ }
+
+ $result = $newres;
+ }
+
+ private static function applyLimitRestriction( $requestOptions, &$result ) {
+
+ // In case of a `conditionConstraint` the restriction is set forth by the
+ // SELECT statement.
+ if ( isset( $requestOptions->conditionConstraint ) ) {
+ return $result;
+ }
+
+ if ( $requestOptions->limit > 0 ) {
+ return $result = array_slice( $result, $requestOptions->offset, $requestOptions->limit );
+ }
+
+ $result = array_slice( $result, $requestOptions->offset );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/SQLStoreFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/SQLStoreFactory.php
new file mode 100644
index 00000000..0cba4d0e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/SQLStoreFactory.php
@@ -0,0 +1,762 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use Onoi\Cache\Cache;
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\NullMessageReporter;
+use SMW\ApplicationFactory;
+use SMW\ChangePropListener;
+use SMW\DIWikiPage;
+use SMW\Options;
+use SMW\Site;
+use SMW\SQLStore\ChangeOp\ChangeOp;
+use SMW\SQLStore\EntityStore\CachingEntityLookup;
+use SMW\SQLStore\EntityStore\CachingSemanticDataLookup;
+use SMW\SQLStore\EntityStore\DataItemHandlerDispatcher;
+use SMW\SQLStore\EntityStore\IdCacheManager;
+use SMW\SQLStore\EntityStore\IdEntityFinder;
+use SMW\SQLStore\EntityStore\IdChanger;
+use SMW\SQLStore\EntityStore\UniquenessLookup;
+use SMW\SQLStore\EntityStore\NativeEntityLookup;
+use SMW\SQLStore\EntityStore\SemanticDataLookup;
+use SMW\SQLStore\EntityStore\SubobjectListFinder;
+use SMW\SQLStore\EntityStore\TraversalPropertyLookup;
+use SMW\SQLStore\EntityStore\PropertySubjectsLookup;
+use SMW\SQLStore\EntityStore\PropertiesLookup;
+use SMW\SQLStore\Lookup\CachedListLookup;
+use SMW\SQLStore\Lookup\ListLookup;
+use SMW\SQLStore\Lookup\PropertyUsageListLookup;
+use SMW\SQLStore\Lookup\RedirectTargetLookup;
+use SMW\SQLStore\Lookup\UndeclaredPropertyListLookup;
+use SMW\SQLStore\Lookup\UnusedPropertyListLookup;
+use SMW\SQLStore\Lookup\UsageStatisticsListLookup;
+use SMW\SQLStore\Lookup\ProximityPropertyValueLookup;
+use SMW\SQLStore\TableBuilder\TableBuilder;
+use SMW\SQLStore\TableBuilder\Examiner\HashField;
+use SMW\Utils\CircularReferenceGuard;
+use SMWRequestOptions as RequestOptions;
+use SMWSql3SmwIds as EntityIdManager;
+use SMW\Services\ServicesContainer;
+use SMW\RequestData;
+use SMWSQLStore3;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author mwjames
+ */
+class SQLStoreFactory {
+
+ /**
+ * @var SMWSQLStore3
+ */
+ private $store;
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @var QueryEngineFactory
+ */
+ private $queryEngineFactory;
+
+ /**
+ * @since 2.2
+ *
+ * @param SMWSQLStore3 $store
+ * @param MessageReporter|null $messageReporter
+ */
+ public function __construct( SMWSQLStore3 $store, MessageReporter $messageReporter = null ) {
+ $this->store = $store;
+ $this->messageReporter = $messageReporter;
+
+ if ( $this->messageReporter === null ) {
+ $this->messageReporter = new NullMessageReporter();
+ }
+
+ $this->queryEngineFactory = new QueryEngineFactory( $store );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return QueryEngine
+ */
+ public function newMasterQueryEngine() {
+ return $this->queryEngineFactory->newQueryEngine();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return QueryEngine
+ */
+ public function newSlaveQueryEngine() {
+ return $this->newMasterQueryEngine();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return EntityIdManager
+ */
+ public function newEntityTable() {
+ return new EntityIdManager( $this->store, $this );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return PropertyTableUpdater
+ */
+ public function newPropertyTableUpdater() {
+ return new PropertyTableUpdater( $this->store, $this->newPropertyStatisticsStore() );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return ConceptCache
+ */
+ public function newMasterConceptCache() {
+
+ $conceptCache = new ConceptCache(
+ $this->store,
+ $this->queryEngineFactory->newConceptQuerySegmentBuilder()
+ );
+
+ $conceptCache->setUpperLimit(
+ $GLOBALS['smwgQMaxLimit']
+ );
+
+ return $conceptCache;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return ConceptCache
+ */
+ public function newSlaveConceptCache() {
+ return $this->newMasterConceptCache();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return ListLookup
+ */
+ public function newUsageStatisticsCachedListLookup() {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $usageStatisticsListLookup = new UsageStatisticsListLookup(
+ $this->store,
+ $this->newPropertyStatisticsStore()
+ );
+
+ return $this->newCachedListLookup(
+ $usageStatisticsListLookup,
+ $settings->safeGet( 'special.statistics' ),
+ $settings->safeGet( 'special.statistics' )
+ );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return CachedListLookup
+ */
+ public function newPropertyUsageCachedListLookup( RequestOptions $requestOptions = null ) {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $propertyUsageListLookup = new PropertyUsageListLookup(
+ $this->store,
+ $this->newPropertyStatisticsStore(),
+ $requestOptions
+ );
+
+ return $this->newCachedListLookup(
+ $propertyUsageListLookup,
+ $settings->safeGet( 'special.properties' ),
+ $settings->safeGet( 'special.properties' )
+ );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return CachedListLookup
+ */
+ public function newUnusedPropertyCachedListLookup( RequestOptions $requestOptions = null ) {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $unusedPropertyListLookup = new UnusedPropertyListLookup(
+ $this->store,
+ $this->newPropertyStatisticsStore(),
+ $requestOptions
+ );
+
+ return $this->newCachedListLookup(
+ $unusedPropertyListLookup,
+ $settings->safeGet( 'special.unusedproperties' ),
+ $settings->safeGet( 'special.unusedproperties' )
+ );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return CachedListLookup
+ */
+ public function newUndeclaredPropertyCachedListLookup( RequestOptions $requestOptions = null ) {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $undeclaredPropertyListLookup = new UndeclaredPropertyListLookup(
+ $this->store,
+ $settings->get( 'smwgPDefaultType' ),
+ $requestOptions
+ );
+
+ return $this->newCachedListLookup(
+ $undeclaredPropertyListLookup,
+ $settings->safeGet( 'special.wantedproperties' ),
+ $settings->safeGet( 'special.wantedproperties' )
+ );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param ListLookup $listLookup
+ * @param boolean $useCache
+ * @param integer $cacheExpiry
+ *
+ * @return ListLookup
+ */
+ public function newCachedListLookup( ListLookup $listLookup, $useCache, $cacheExpiry ) {
+
+ $cacheFactory = ApplicationFactory::getInstance()->newCacheFactory();
+
+ if ( is_int( $useCache ) ) {
+ $useCache = true;
+ }
+
+ $cacheOptions = $cacheFactory->newCacheOptions( [
+ 'useCache' => $useCache,
+ 'ttl' => $cacheExpiry
+ ] );
+
+ $cachedListLookup = new CachedListLookup(
+ $listLookup,
+ $cacheFactory->newMediaWikiCompositeCache( $cacheFactory->getMainCacheType() ),
+ $cacheOptions
+ );
+
+ $cachedListLookup->setCachePrefix( $cacheFactory->getCachePrefix() );
+
+ return $cachedListLookup;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return DeferredCallableUpdate
+ */
+ public function newDeferredCallableCachedListLookupUpdate() {
+
+ $deferredTransactionalUpdate = ApplicationFactory::getInstance()->newDeferredTransactionalCallableUpdate( function() {
+ $this->newPropertyUsageCachedListLookup()->deleteCache();
+ $this->newUnusedPropertyCachedListLookup()->deleteCache();
+ $this->newUndeclaredPropertyCachedListLookup()->deleteCache();
+ $this->newUsageStatisticsCachedListLookup()->deleteCache();
+ } );
+
+ return $deferredTransactionalUpdate;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return EntityRebuildDispatcher
+ */
+ public function newEntityRebuildDispatcher() {
+ return new EntityRebuildDispatcher(
+ $this->store,
+ ApplicationFactory::getInstance()->newTitleFactory()
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return EntityLookup
+ */
+ public function newEntityLookup() {
+
+ $applicationFactory = ApplicationFactory::getInstance();
+ $settings = $applicationFactory->getSettings();
+ $nativeEntityLookup = new NativeEntityLookup( $this->store );
+
+ if ( $settings->get( 'smwgEntityLookupCacheType' ) === CACHE_NONE ) {
+ return $nativeEntityLookup;
+ }
+
+ $circularReferenceGuard = new CircularReferenceGuard( 'store:entitylookup' );
+ $circularReferenceGuard->setMaxRecursionDepth( 2 );
+
+ $cacheFactory = $applicationFactory->newCacheFactory();
+
+ $blobStore = $cacheFactory->newBlobStore(
+ 'smw:store:entitylookup:',
+ $settings->get( 'smwgEntityLookupCacheType' ),
+ $settings->get( 'smwgEntityLookupCacheLifetime' )
+ );
+
+ $cachingEntityLookup = new CachingEntityLookup(
+ $nativeEntityLookup,
+ new RedirectTargetLookup( $this->store, $circularReferenceGuard ),
+ $blobStore
+ );
+
+ $cachingEntityLookup->setLookupFeatures(
+ $settings->get( 'smwgEntityLookupFeatures' )
+ );
+
+ return $cachingEntityLookup;
+ }
+
+ /**
+ * @since 2.3
+ *
+ * @return PropertyTableInfoFetcher
+ */
+ public function newPropertyTableInfoFetcher() {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $propertyTableInfoFetcher = new PropertyTableInfoFetcher(
+ new PropertyTypeFinder( $this->store->getConnection( 'mw.db' ) )
+ );
+
+ $propertyTableInfoFetcher->setCustomFixedPropertyList(
+ $settings->get( 'smwgFixedProperties' )
+ );
+
+ $propertyTableInfoFetcher->setCustomSpecialPropertyList(
+ $settings->get( 'smwgPageSpecialProperties' )
+ );
+
+ return $propertyTableInfoFetcher;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return PropertyTableIdReferenceFinder
+ */
+ public function newPropertyTableIdReferenceFinder() {
+
+ $propertyTableIdReferenceFinder = new PropertyTableIdReferenceFinder(
+ $this->store
+ );
+
+ $propertyTableIdReferenceFinder->isCapitalLinks(
+ Site::isCapitalLinks()
+ );
+
+ return $propertyTableIdReferenceFinder;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return Installer
+ */
+ public function newInstaller() {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $tableBuilder = TableBuilder::factory(
+ $this->store->getConnection( DB_MASTER )
+ );
+
+ $tableBuilder->setMessageReporter(
+ $this->messageReporter
+ );
+
+ $tableIntegrityExaminer = new TableIntegrityExaminer(
+ $this->store,
+ new HashField( $this->store )
+ );
+
+ $tableSchemaManager = new TableSchemaManager(
+ $this->store
+ );
+
+ $tableSchemaManager->setFeatureFlags(
+ $settings->get( 'smwgFieldTypeFeatures' )
+ );
+
+ $installer = new Installer(
+ $tableSchemaManager,
+ $tableBuilder,
+ $tableIntegrityExaminer
+ );
+
+ $installer->setMessageReporter(
+ $this->messageReporter
+ );
+
+ $installer->setOptions(
+ $this->store->getOptions()->filter(
+ [
+ Installer::OPT_TABLE_OPTIMIZE,
+ Installer::OPT_IMPORT,
+ Installer::OPT_SCHEMA_UPDATE,
+ Installer::OPT_SUPPLEMENT_JOBS
+ ]
+ )
+ );
+
+ return $installer;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return DataItemHandlerDispatcher
+ */
+ public function newDataItemHandlerDispatcher() {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+
+ $dataItemHandlerDispatcher = new DataItemHandlerDispatcher(
+ $this->store
+ );
+
+ $dataItemHandlerDispatcher->setFieldTypeFeatures(
+ $settings->get( 'smwgFieldTypeFeatures' )
+ );
+
+ return $dataItemHandlerDispatcher;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return LoggerInterface
+ */
+ public function getLogger() {
+ return ApplicationFactory::getInstance()->getMediaWikiLogger();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return TraversalPropertyLookup
+ */
+ public function newTraversalPropertyLookup() {
+ return new TraversalPropertyLookup( $this->store );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return PropertySubjectsLookup
+ */
+ public function newPropertySubjectsLookup() {
+ return new PropertySubjectsLookup( $this->store );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return PropertiesLookup
+ */
+ public function newPropertiesLookup() {
+ return new PropertiesLookup( $this->store );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return PropertyStatisticsStore
+ */
+ public function newPropertyStatisticsStore() {
+
+ $propertyStatisticsStore = new PropertyStatisticsStore(
+ $this->store->getConnection( 'mw.db' )
+ );
+
+ $propertyStatisticsStore->setLogger(
+ $this->getLogger()
+ );
+
+ $propertyStatisticsStore->isCommandLineMode(
+ Site::isCommandLineMode()
+ );
+
+ return $propertyStatisticsStore;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return IdCacheManager
+ */
+ public function newIdCacheManager( $id, array $config ) {
+
+ $inMemoryPoolCache = ApplicationFactory::getInstance()->getInMemoryPoolCache();
+ $caches = [];
+
+ foreach ( $config as $key => $cacheSize ) {
+ $inMemoryPoolCache->resetPoolCacheById(
+ "$id.$key"
+ );
+
+ $caches[$key] = $inMemoryPoolCache->getPoolCacheById(
+ "$id.$key",
+ $cacheSize
+ );
+ }
+
+ return new IdCacheManager( $caches );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return PropertyTableRowDiffer
+ */
+ public function newPropertyTableRowDiffer() {
+
+ $propertyTableRowMapper = new PropertyTableRowMapper(
+ $this->store
+ );
+
+ $propertyTableRowDiffer = new PropertyTableRowDiffer(
+ $this->store,
+ $propertyTableRowMapper
+ );
+
+ return $propertyTableRowDiffer;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param IdCacheManager $idCacheManager
+ *
+ * @return IdEntityFinder
+ */
+ public function newIdEntityFinder( IdCacheManager $idCacheManager ) {
+
+ $idMatchFinder = new IdEntityFinder(
+ $this->store,
+ $this->getIteratorFactory(),
+ $idCacheManager
+ );
+
+ return $idMatchFinder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return IdChanger
+ */
+ public function newIdChanger() {
+
+ $idChanger = new IdChanger(
+ $this->store
+ );
+
+ return $idChanger;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return UniquenessLookup
+ */
+ public function newUniquenessLookup() {
+
+ $uniquenessLookup = new UniquenessLookup(
+ $this->store,
+ $this->getIteratorFactory()
+ );
+
+ return $uniquenessLookup;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return HierarchyLookup
+ */
+ public function newHierarchyLookup() {
+ return ApplicationFactory::getInstance()->newHierarchyLookup();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return SubobjectListFinder
+ */
+ public function newSubobjectListFinder() {
+
+ $subobjectListFinder = new SubobjectListFinder(
+ $this->store,
+ $this->getIteratorFactory()
+ );
+
+ return $subobjectListFinder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return SemanticDataLookup
+ */
+ public function newSemanticDataLookup() {
+
+ $semanticDataLookup = new SemanticDataLookup(
+ $this->store
+ );
+
+ $semanticDataLookup->setLogger(
+ $this->getLogger()
+ );
+
+ $cachingSemanticDataLookup = new CachingSemanticDataLookup(
+ $semanticDataLookup,
+ ApplicationFactory::getInstance()->getCache()
+ );
+
+ return $cachingSemanticDataLookup;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return TableFieldUpdater
+ */
+ public function newTableFieldUpdater() {
+
+ $tableFieldUpdater = new TableFieldUpdater(
+ $this->store
+ );
+
+ return $tableFieldUpdater;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return RedirectStore
+ */
+ public function newRedirectStore() {
+
+ $redirectStore = new RedirectStore(
+ $this->store
+ );
+
+ return $redirectStore;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return ChangePropListener
+ */
+ public function newChangePropListener() {
+ return new ChangePropListener();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $subject
+ *
+ * @return ChangeOp
+ */
+ public function newChangeOp( DIWikiPage $subject ) {
+
+ $settings = ApplicationFactory::getInstance()->getSettings();
+ $changeOp = new ChangeOp( $subject );
+
+ $changeOp->setTextItemsFlag(
+ $settings->get( 'smwgEnabledFulltextSearch' )
+ );
+
+ return $changeOp;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return ProximityPropertyValueLookup
+ */
+ public function newProximityPropertyValueLookup() {
+ return new ProximityPropertyValueLookup( $this->store );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return EntityValueUniquenessConstraintChecker
+ */
+ public function newEntityValueUniquenessConstraintChecker() {
+ return new EntityValueUniquenessConstraintChecker(
+ $this->store,
+ $this->getIteratorFactory()
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return ServicesContainer
+ */
+ public function newServicesContainer() {
+
+ $servicesContainer = new ServicesContainer(
+ [
+ 'ProximityPropertyValueLookup' => [
+ '_service' => [ $this, 'newProximityPropertyValueLookup' ],
+ '_type' => ProximityPropertyValueLookup::class
+ ],
+ 'EntityValueUniquenessConstraintChecker' => [
+ '_service' => [ $this, 'newEntityValueUniquenessConstraintChecker' ],
+ '_type' => EntityValueUniquenessConstraintChecker::class
+ ],
+ 'PropertyTableIdReferenceFinder' => function() {
+ static $singleton;
+ return $singleton = $singleton === null ? $this->newPropertyTableIdReferenceFinder() : $singleton;
+ }
+ ]
+ );
+
+ return $servicesContainer;
+ }
+
+ private function getIteratorFactory() {
+ return ApplicationFactory::getInstance()->getIteratorFactory();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder.php
new file mode 100644
index 00000000..c87c82ca
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\SQLStore\TableBuilder\Table;
+
+/**
+ * @private
+ *
+ * Provides generic creation and updating function for database tables. A builder
+ * that implements this interface is expected to define Database specific
+ * operations and allowing it to be executed on a specific RDBMS back-end.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+interface TableBuilder {
+
+ /**
+ * Common prefix used by all Semantic MediaWiki tables
+ */
+ const TABLE_PREFIX = 'smw_';
+
+ /**
+ * Processing field activity status
+ */
+ const PROC_FIELD_NEW = 'field.new';
+
+ /**
+ * Processing field activity status
+ */
+ const PROC_FIELD_UPD = 'field.update';
+
+ /**
+ * Processing field activity status
+ */
+ const PROC_FIELD_DROP = 'field.drop';
+
+ /**
+ * On before the creation of tables and indices
+ */
+ const PRE_CREATION = 'pre.creation';
+
+ /**
+ * On after creation of all tables
+ */
+ const POST_CREATION = 'post.creation';
+
+ /**
+ * On after dropping all tables
+ */
+ const POST_DESTRUCTION = 'post.destruction';
+
+ /**
+ * Generic creation and updating function for database tables. Ideally, it
+ * would be able to modify a table's signature in arbitrary ways, but it will
+ * fail for some changes. Its string-based interface is somewhat too
+ * impoverished for a permanent solution. It would be possible to go for update
+ * scripts (specific to each change) in the style of MediaWiki instead.
+ *
+ * Make sure the table of the given name has the given fields, provided
+ * as an array with entries fieldname => typeparams. typeparams should be
+ * in a normalised form and order to match to existing values.
+ *
+ * The function returns an array that includes all columns that have been
+ * changed. For each such column, the array contains an entry
+ * columnname => action, where action is one of 'up', 'new', or 'del'
+ *
+ * @note The function partly ignores the order in which fields are set up.
+ * Only if the type of some field changes will its order be adjusted explicitly.
+ *
+ * @since 2.5
+ *
+ * @param Table $table
+ */
+ public function create( Table $table );
+
+ /**
+ * Removes a table from the RDBMS backend.
+ *
+ * @since 2.5
+ *
+ * @param Table $table
+ */
+ public function drop( Table $table );
+
+ /**
+ * Performs analysis on a key distribution and stores the distribution so
+ * that the query planner can use these statistics to help determine the
+ * most efficient execution plans for queries.
+ *
+ * @since 3.0
+ *
+ * @param Table $table
+ */
+ public function optimize( Table $table );
+
+ /**
+ * Database backends often have different types that need to be used
+ * repeatedly in (Semantic) MediaWiki. This function provides the
+ * preferred type (as a string) for various common kinds of columns.
+ * The input is one of the following strings: 'id' (page id numbers or
+ * similar), 'title' (title strings or similar), 'namespace' (namespace
+ * numbers), 'blob' (longer text blobs), 'iw' (interwiki prefixes).
+ *
+ * @since 2.5
+ *
+ * @param string|FieldType $fieldType
+ *
+ * @return string|false SQL type declaration
+ */
+ public function getStandardFieldType( $fieldType );
+
+ /**
+ * Returns a list of process activities
+ *
+ * @since 3.0
+ *
+ * @param array
+ */
+ public function getLog();
+
+ /**
+ * Allows to check and validate the build on specific events
+ *
+ * @since 2.5
+ *
+ * @param string $event
+ */
+ public function checkOn( $event );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/Examiner/HashField.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/Examiner/HashField.php
new file mode 100644
index 00000000..0e866368
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/Examiner/HashField.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder\Examiner;
+
+use Onoi\MessageReporter\MessageReporterAwareTrait;
+use SMW\SQLStore\SQLStore;
+use SMW\Maintenance\PopulateHashField;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.1
+ *
+ * @author mwjames
+ */
+class HashField {
+
+ use MessageReporterAwareTrait;
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var PopulateHashField
+ */
+ private $populateHashField;
+
+ /**
+ * @since 3.1
+ *
+ * @param SQLStore $store
+ * @param PopulateHashField|null $populateHashField
+ */
+ public function __construct( SQLStore $store, PopulateHashField $populateHashField = null ) {
+ $this->store = $store;
+ $this->populateHashField = $populateHashField;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @return integer
+ */
+ public static function threshold() {
+ return PopulateHashField::COUNT_SCRIPT_EXECUTION_THRESHOLD;
+ }
+
+ /**
+ * @since 3.1
+ *
+ * @param array $opts
+ */
+ public function check( array $opts = [] ) {
+
+ $this->messageReporter->reportMessage( "Checking smw_hash field consistency ...\n" );
+ require_once $GLOBALS['smwgMaintenanceDir'] . "/populateHashField.php";
+
+ if ( $this->populateHashField === null ) {
+ $this->populateHashField = new PopulateHashField();
+ }
+
+ $this->populateHashField->setStore( $this->store );
+ $this->populateHashField->setMessageReporter( $this->messageReporter );
+
+ $rows = $this->populateHashField->fetchRows();
+ $count = 0;
+
+ if ( $rows !== null ) {
+ $count = $rows->numRows();
+ }
+
+ if ( $count > self::threshold() ) {
+ $this->messageReporter->reportMessage( " ... missing $count rows ...\n" );
+ $this->messageReporter->reportMessage( " ... skipping the `smw_hash` field population ...\n" );
+
+ $this->populateHashField->setComplete( false );
+ } elseif ( $count != 0 ) {
+ $this->populateHashField->populate( $rows );
+ } else {
+ $this->populateHashField->setComplete( true );
+ }
+
+ $this->messageReporter->reportMessage( " ... done.\n" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/FieldType.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/FieldType.php
new file mode 100644
index 00000000..8b5ba88f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/FieldType.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class FieldType {
+
+ /**
+ * @var string
+ */
+ const FIELD_ID = 'id';
+
+ /**
+ * @var string
+ */
+ const FIELD_ID_PRIMARY = 'id_primary';
+
+ /**
+ * @var string
+ */
+ const FIELD_ID_UNSIGNED = 'id_unsigned';
+
+ /**
+ * @var string
+ */
+ const FIELD_TITLE = 'title';
+
+ /**
+ * @var string
+ */
+ const FIELD_HASH = 'hash';
+
+ /**
+ * @var string
+ */
+ const FIELD_NAMESPACE = 'namespace';
+
+ /**
+ * @var string
+ */
+ const FIELD_INTERWIKI = 'interwiki';
+
+ /**
+ * @var string
+ */
+ const FIELD_USAGE_COUNT = 'usage_count';
+
+ /**
+ * @var string
+ */
+ const TYPE_CHAR_NOCASE = 'char_nocase';
+
+ /**
+ * @var string
+ */
+ const TYPE_CHAR_LONG = 'char_long';
+
+ /**
+ * @var string
+ */
+ const TYPE_CHAR_LONG_NOCASE = 'char_long_nocase';
+
+ /**
+ * @var integer
+ */
+ const CHAR_LONG_LENGTH = 300;
+
+ /**
+ * @var string
+ */
+ const TYPE_BOOL = 'boolean';
+
+ /**
+ * @var string
+ */
+ const TYPE_INT = 'integer';
+
+ /**
+ * @var string
+ */
+ const TYPE_INT_UNSIGNED = 'integer_unsigned';
+
+ /**
+ * @var string
+ */
+ const TYPE_TEXT = 'text';
+
+ /**
+ * @var string
+ */
+ const TYPE_BLOB = 'blob';
+
+ /**
+ * @var string
+ */
+ const TYPE_DOUBLE = 'double';
+
+ /**
+ * @since 2.5
+ *
+ * @param string|array $type
+ * @param array $fieldTypes
+ */
+ public static function mapType( $type, $fieldTypes = [] ) {
+
+ $fieldType = $type;
+ $auxilary = '';
+
+ // [ FieldType::FIELD_ID, 'NOT NULL' ]
+ if ( is_array( $type ) && count( $type ) > 1 ) {
+ $fieldType = $type[0];
+ $auxilary = ' ' . $type[1];
+ } elseif ( is_array( $type ) ) {
+ $fieldType = $type[0];
+ }
+
+ if ( isset( $fieldTypes[$fieldType] ) ) {
+ $fieldType = $fieldTypes[$fieldType];
+ }
+
+ return $fieldType . $auxilary;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php
new file mode 100644
index 00000000..7c9a5ebc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/MySQLTableBuilder.php
@@ -0,0 +1,402 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ * @author Marcel Gsteiger
+ * @author Jeroen De Dauw
+ */
+class MySQLTableBuilder extends TableBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getStandardFieldType( $fieldType ) {
+
+ $charLongLength = FieldType::CHAR_LONG_LENGTH;
+
+ $fieldTypes = [
+ // like page_id in MW page table
+ 'id' => 'INT(11) UNSIGNED',
+ // like page_id in MW page table
+ 'id_primary' => 'INT(11) UNSIGNED NOT NULL KEY AUTO_INCREMENT',
+
+ // (see postgres on the difference)
+ 'id_unsigned' => 'INT(11) UNSIGNED',
+
+ // like page_namespace in MW page table
+ 'namespace' => 'INT(11)',
+ // like page_title in MW page table
+ 'title' => 'VARBINARY(255)',
+ // like iw_prefix in MW interwiki table
+ 'interwiki' => 'VARBINARY(32)',
+ 'iw' => 'VARBINARY(32)',
+ 'hash' => 'VARBINARY(40)',
+ // larger blobs of character data, usually not subject to SELECT conditions
+ 'blob' => 'MEDIUMBLOB',
+ 'text' => 'TEXT',
+ 'boolean' => 'TINYINT(1)',
+ 'double' => 'DOUBLE',
+ 'integer' => 'INT(8)',
+ 'char_long' => "VARBINARY($charLongLength)",
+ 'char_nocase' => 'VARCHAR(255) CHARSET utf8 COLLATE utf8_general_ci',
+ 'char_long_nocase' => "VARCHAR($charLongLength) CHARSET utf8 COLLATE utf8_general_ci",
+ 'usage_count' => 'INT(8) UNSIGNED',
+ 'integer_unsigned' => 'INT(8) UNSIGNED'
+ ];
+
+ return FieldType::mapType( $fieldType, $fieldTypes );
+ }
+
+ /** Create */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doCreateTable( $tableName, array $attributes = null ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $sql = '';
+
+ $fieldSql = [];
+ $fields = $attributes['fields'];
+
+ foreach ( $fields as $fieldName => $fieldType ) {
+ $fieldSql[] = "$fieldName " . $this->getStandardFieldType( $fieldType );
+ }
+
+ // @see $wgDBname
+ $dbName = isset( $this->config['wgDBname'] ) ? "`". $this->config['wgDBname'] . "`." : '';
+
+ $sql .= 'CREATE TABLE ' . $dbName . $tableName . ' (' . implode( ',', $fieldSql ) . ') ';
+ $sql .= $this->sql_from( $attributes );
+
+ $this->connection->query( $sql, __METHOD__ );
+ }
+
+ private function sql_from( array $attributes ) {
+
+ // $smwgFulltextSearchTableOptions can define:
+ // - 'mysql' => array( 'ENGINE=MyISAM, DEFAULT CHARSET=utf8' )
+ // - 'mysql' => array( 'ENGINE=MyISAM, DEFAULT CHARSET=utf8', 'WITH PARSER ngram' )
+ if ( isset( $attributes['fulltextSearchTableOptions']['mysql'] ) ) {
+
+ $tableOption = $attributes['fulltextSearchTableOptions']['mysql'];
+
+ // By convention the first index has table specific relevance
+ if ( is_array( $tableOption ) ) {
+ $tableOption = isset( $tableOption[0] ) ? $tableOption[0] : '';
+ }
+
+ return $tableOption;
+ }
+
+ // @see $wgDBTableOptions, This replacement is needed for compatibility,
+ // http://bugs.mysql.com/bug.php?id=17501
+ if ( isset( $this->config['wgDBTableOptions'] ) ) {
+ return str_replace( 'TYPE', 'ENGINE', $this->config['wgDBTableOptions'] );
+ }
+ }
+
+ /** Update */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doUpdateTable( $tableName, array $attributes = null ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $currentFields = $this->getCurrentFields( $tableName );
+
+ $fields = $attributes['fields'];
+ $position = 'FIRST';
+
+ // Loop through all the field definitions, and handle each definition
+ foreach ( $fields as $fieldName => $fieldType ) {
+ $this->doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, $attributes );
+
+ $position = "AFTER $fieldName";
+ $currentFields[$fieldName] = false;
+ }
+
+ // The updated fields have their value set to false, so if a field has a value
+ // that differs from false, it's an obsolete one that should be removed.
+ foreach ( $currentFields as $fieldName => $value ) {
+ if ( $value !== false ) {
+ $this->doDropField( $tableName, $fieldName );
+ }
+ }
+ }
+
+ private function getCurrentFields( $tableName ) {
+
+ $sql = 'DESCRIBE ' . $tableName;
+
+ $res = $this->connection->query( $sql, __METHOD__ );
+ $currentFields = [];
+
+ foreach ( $res as $row ) {
+ $type = strtoupper( $row->Type );
+
+ if ( substr( $type, 0, 8 ) == 'VARCHAR(' ) {
+ $type .= ' binary'; // just assume this to be the case for VARCHAR, though DESCRIBE will not tell us
+ }
+
+ if ( $row->Null != 'YES' ) {
+ $type .= ' NOT NULL';
+ }
+
+ if ( $row->Key == 'PRI' ) { /// FIXME: updating "KEY" is not possible, the below query will fail in this case.
+ $type .= ' KEY';
+ }
+
+ if ( $row->Extra == 'auto_increment' ) {
+ $type .= ' AUTO_INCREMENT';
+ }
+
+ $currentFields[$row->Field] = $type;
+ }
+
+ return $currentFields;
+ }
+
+ private function doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, array $attributes ) {
+
+ if ( !isset( $this->activityLog[$tableName] ) ) {
+ $this->activityLog[$tableName] = [];
+ }
+
+ $fieldType = $this->getStandardFieldType( $fieldType );
+ $default = '';
+
+ if ( isset( $attributes['defaults'][$fieldName] ) ) {
+ $default = "DEFAULT '" . $attributes['defaults'][$fieldName] . "'";
+ }
+
+ if ( !array_key_exists( $fieldName, $currentFields ) ) {
+ $this->doCreateField( $tableName, $fieldName, $position, $fieldType, $default );
+ } elseif ( $currentFields[$fieldName] != $fieldType ) {
+ $this->doUpdateFieldType( $tableName, $fieldName, $position, $currentFields[$fieldName], $fieldType );
+ } else {
+ $this->reportMessage( " ... field $fieldName is fine.\n" );
+ }
+ }
+
+ private function doCreateField( $tableName, $fieldName, $position, $fieldType, $default ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_NEW;
+
+ $this->reportMessage( " ... creating field $fieldName ... " );
+ $this->connection->query( "ALTER TABLE $tableName ADD `$fieldName` $fieldType $default $position", __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doUpdateFieldType( $tableName, $fieldName, $position, $oldFieldType, $newFieldType ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_UPD;
+
+ // Continue to alter the type but silence the output since we cannot get
+ // any better information from MySQL about the types hence we a hack the
+ // message
+ if ( strpos( $oldFieldType, 'binary' ) !== false && strpos( $newFieldType, 'CHARSET utf8 COLLATE utf8_general_ci' ) !== false ) {
+ $this->reportMessage( " ... changing to a CHARSET utf8 field type ... " );
+ } else {
+ $this->reportMessage( " ... changing type of field $fieldName from '$oldFieldType' to '$newFieldType' ... " );
+ }
+
+ // To avoid Error: 1068 Multiple primary key defined when a PRIMARY is involved
+ if ( strpos( $newFieldType, 'AUTO_INCREMENT' ) !== false ) {
+ $this->connection->query( "ALTER TABLE $tableName DROP PRIMARY KEY", __METHOD__ );
+ }
+
+ $this->connection->query( "ALTER TABLE $tableName CHANGE `$fieldName` `$fieldName` $newFieldType $position", __METHOD__ );
+
+ // http://stackoverflow.com/questions/1873085/how-to-convert-from-varbinary-to-char-varchar-in-mysql
+ // http://bugs.mysql.com/bug.php?id=34564
+ if ( strpos( $oldFieldType, 'VARBINARY' ) !== false && strpos( $newFieldType, 'VARCHAR' ) !== false ) {
+ // $this->connection->query( "SELECT CAST($fieldName AS CHAR) from $tableName", __METHOD__ );
+ }
+
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doDropField( $tableName, $fieldName ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_DROP;
+
+ $this->reportMessage( " ... deleting obsolete field $fieldName ... " );
+ $this->connection->query( "ALTER TABLE $tableName DROP COLUMN `$fieldName`", __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ /** Index */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doCreateIndices( $tableName, array $indexOptions = null ) {
+
+ $indices = $indexOptions['indices'];
+
+ // First remove possible obsolete indices
+ $this->doDropObsoleteIndices( $tableName, $indices );
+
+ // Add new indexes.
+ foreach ( $indices as $indexName => $index ) {
+ // If the index is an array, it contains the column
+ // name as first element, and index type as second one.
+ if ( is_array( $index ) ) {
+ $columns = $index[0];
+ $indexType = count( $index ) > 1 ? $index[1] : 'INDEX';
+ } else {
+ $columns = $index;
+ $indexType = 'INDEX';
+ }
+
+ $this->doCreateIndex( $tableName, $indexType, $indexName, $columns, $indexOptions );
+ }
+ }
+
+ private function doDropObsoleteIndices( $tableName, array &$indices ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $currentIndices = $this->getIndexInfo( $tableName );
+
+ $idx = [];
+
+ // #2717
+ // The index info doesn't return length information (...idx1(200),idx2...)
+ // for an index hence to avoid a constant remove/create cycle we eliminate
+ // the length information from the temporary mirror when comparing new and
+ // old; of course we won't detect length changes!
+ foreach ( $indices as $k => $columns ) {
+ $idx[$k] = preg_replace("/\([^)]+\)/", "", $columns );
+ }
+
+ foreach ( $currentIndices as $indexName => $indexColumn ) {
+ // Indices may contain something like array( 'id', 'UNIQUE INDEX' )
+ $id = $this->recursive_array_search( $indexColumn, $idx );
+ if ( $id !== false || $indexName == 'PRIMARY' ) {
+ $this->reportMessage( " ... index $indexColumn is fine.\n" );
+
+ if ( $id !== false ) {
+ unset( $indices[$id] );
+ unset( $idx[$id] );
+ }
+
+ } else { // Duplicate or unrequired index.
+ $this->doDropIndex( $tableName, $indexName, $indexColumn );
+ }
+ }
+ }
+
+ /**
+ * Get the information about all indexes of a table. The result is an
+ * array of format indexname => indexcolumns. The latter is a comma
+ * separated list.
+ *
+ * @return array indexname => columns
+ */
+ private function getIndexInfo( $tableName ) {
+
+ $indices = [];
+
+ $res = $this->connection->query( 'SHOW INDEX FROM ' . $tableName, __METHOD__ );
+
+ if ( !$res ) {
+ return $indices;
+ }
+
+ foreach ( $res as $row ) {
+ if ( !array_key_exists( $row->Key_name, $indices ) ) {
+ $indices[$row->Key_name] = $row->Column_name;
+ } else {
+ $indices[$row->Key_name] .= ',' . $row->Column_name;
+ }
+ }
+
+ return $indices;
+ }
+
+ private function doDropIndex( $tableName, $indexName, $columns ) {
+ $this->reportMessage( " ... removing index $columns ..." );
+ $this->connection->query( 'DROP INDEX ' . $indexName . ' ON ' . $tableName, __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doCreateIndex( $tableName, $indexType, $indexName, $columns, array $indexOptions ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $indexOption = '';
+
+ $this->reportMessage( " ... creating new index $columns ..." );
+
+ // @see MySQLTableBuilder::createExtraSQLFromattributes
+ // @see https://dev.mysql.com/doc/refman/5.7/en/fulltext-search-ngram.html
+ if ( isset( $indexOptions['fulltextSearchTableOptions']['mysql'] ) ) {
+ $indexOption = $indexOptions['fulltextSearchTableOptions']['mysql'];
+
+ // By convention the second index has index specific relevance
+ if ( is_array( $indexOption ) ) {
+ $indexOption = isset( $indexOption[1] ) ? $indexOption[1] : '';
+ }
+ }
+
+ if ( $indexType === 'FULLTEXT' ) {
+ $this->connection->query( "ALTER TABLE $tableName ADD $indexType $columns ($columns) $indexOption", __METHOD__ );
+ } else {
+ $this->connection->query( "ALTER TABLE $tableName ADD $indexType ($columns)", __METHOD__ );
+ }
+
+ $this->reportMessage( "done.\n" );
+ }
+
+ /** Drop */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doDropTable( $tableName ) {
+ $this->connection->query( 'DROP TABLE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ protected function doOptimize( $tableName ) {
+
+ $this->reportMessage( "Checking table $tableName ...\n" );
+
+ // https://dev.mysql.com/doc/refman/5.7/en/analyze-table.html
+ // Performs a key distribution analysis and stores the distribution for
+ // the named table or tables
+ $this->reportMessage( " ... analyze" );
+ $this->connection->query( 'ANALYZE TABLE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+
+ // https://dev.mysql.com/doc/refman/5.7/en/optimize-table.html
+ // Reorganizes the physical storage of table data and associated index data,
+ // to reduce storage space and improve I/O efficiency
+ $this->reportMessage( ", optimize " );
+ $this->connection->query( 'OPTIMIZE TABLE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+
+ $this->reportMessage( "done.\n" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/PostgresTableBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/PostgresTableBuilder.php
new file mode 100644
index 00000000..b0547c34
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/PostgresTableBuilder.php
@@ -0,0 +1,423 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder;
+
+use SMW\SQLStore\SQLStore;
+use SMW\MediaWiki\Connection\Sequence;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ * @author Marcel Gsteiger
+ * @author Jeroen De Dauw
+ */
+class PostgresTableBuilder extends TableBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getStandardFieldType( $fieldType ) {
+
+ // serial is a 4 bytes autoincrementing integer (1 to 2147483647)
+
+ $fieldTypes = [
+ // like page_id in MW page table
+ 'id' => 'SERIAL',
+ // like page_id in MW page table
+ 'id_primary' => 'SERIAL NOT NULL PRIMARY KEY',
+
+ // not autoincrementing integer
+ 'id_unsigned' => 'INTEGER',
+
+ // like page_namespace in MW page table
+ 'namespace' => 'BIGINT',
+ // like page_title in MW page table
+ 'title' => 'TEXT',
+ // like iw_prefix in MW interwiki table
+ 'interwiki' => 'TEXT',
+ 'iw' => 'TEXT',
+ 'hash' => 'TEXT',
+ // larger blobs of character data, usually not subject to SELECT conditions
+ 'blob' => 'BYTEA',
+ 'text' => 'TEXT',
+ 'boolean' => 'BOOLEAN',
+ 'double' => 'DOUBLE PRECISION',
+ 'integer' => 'bigint',
+ 'char_long' => 'TEXT',
+ // Requires citext extension
+ 'char_nocase' => 'citext NOT NULL',
+ 'char_long_nocase' => 'citext NOT NULL',
+ 'usage_count' => 'bigint',
+ 'integer_unsigned' => 'INTEGER'
+ ];
+
+ return FieldType::mapType( $fieldType, $fieldTypes );
+ }
+
+ /** Create */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doCreateTable( $tableName, array $attributes = null ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+
+ $fieldSql = [];
+ $fields = $attributes['fields'];
+
+ foreach ( $fields as $fieldName => $fieldType ) {
+ $fieldSql[] = "$fieldName " . $this->getStandardFieldType( $fieldType );
+ }
+
+ $sql = 'CREATE TABLE ' . $tableName . ' (' . implode( ',', $fieldSql ) . ') ';
+
+ $this->connection->query( $sql, __METHOD__ );
+ }
+
+ /** Update */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doUpdateTable( $tableName, array $attributes = null ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $currentFields = $this->getCurrentFields( $tableName );
+
+ $fields = $attributes['fields'];
+ $position = 'FIRST';
+
+ if ( !isset( $this->activityLog[$tableName] ) ) {
+ $this->activityLog[$tableName] = [];
+ }
+
+ // Loop through all the field definitions, and handle each definition
+ foreach ( $fields as $fieldName => $fieldType ) {
+ $this->doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, $attributes );
+
+ $position = "AFTER $fieldName";
+ $currentFields[$fieldName] = false;
+ }
+
+ // The updated fields have their value set to false, so if a field has a value
+ // that differs from false, it's an obsolete one that should be removed.
+ foreach ( $currentFields as $fieldName => $value ) {
+ if ( $value !== false ) {
+ $this->doDropField( $tableName, $fieldName );
+ }
+ }
+ }
+
+ private function getCurrentFields( $tableName ) {
+
+ $tableName = str_replace( '"', '', $tableName );
+ // Use the data dictionary in postgresql to get an output comparable to DESCRIBE.
+/*
+ $sql = <<<EOT
+SELECT
+ a.attname as "Field",
+ upper(pg_catalog.format_type(a.atttypid, a.atttypmod)) as "Type",
+ (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)
+ FROM pg_catalog.pg_attrdef d
+ WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as "Extra",
+ case when a.attnotnull THEN 'NO'::text else 'YES'::text END as "Null", a.attnum
+ FROM pg_catalog.pg_attribute a
+ WHERE a.attrelid = (
+ SELECT c.oid
+ FROM pg_catalog.pg_class c
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relname ~ '^($tableName)$'
+ AND pg_catalog.pg_table_is_visible(c.oid)
+ LIMIT 1
+ ) AND a.attnum > 0 AND NOT a.attisdropped
+ ORDER BY a.attnum
+EOT;
+*/
+
+ $sql = "SELECT a.attname as \"Field\","
+ . " upper(pg_catalog.format_type(a.atttypid, a.atttypmod)) as \"Type\","
+ . " (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)"
+ . " FROM pg_catalog.pg_attrdef d"
+ . " WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) as \"Extra\", "
+ . " case when a.attnotnull THEN 'NO'::text else 'YES'::text END as \"Null\", a.attnum"
+ . " FROM pg_catalog.pg_attribute a"
+ . " WHERE a.attrelid = (SELECT c.oid"
+ . " FROM pg_catalog.pg_class c"
+ . " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace"
+ . " WHERE c.relname ~ '^(" . $tableName . ")$'"
+ . " AND pg_catalog.pg_table_is_visible(c.oid)"
+ . " LIMIT 1) AND a.attnum > 0 AND NOT a.attisdropped"
+ . " ORDER BY a.attnum";
+
+ $res = $this->connection->query( $sql, __METHOD__ );
+ $currentFields = [];
+
+ foreach ( $res as $row ) {
+ $type = strtoupper( $row->Type );
+
+ if ( preg_match( '/^nextval\\(.+\\)/i', $row->Extra ) ) {
+ $type = 'SERIAL NOT NULL';
+ } elseif ( $row->Null != 'YES' ) {
+ $type .= ' NOT NULL';
+ }
+
+ $currentFields[$row->Field] = $type;
+ }
+
+ return $currentFields;
+ }
+
+ private function doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, array $attributes ) {
+
+ $fieldType = $this->getStandardFieldType( $fieldType );
+ $keypos = strpos( $fieldType, ' PRIMARY KEY' );
+
+ if ( $keypos > 0 ) {
+ $fieldType = substr( $fieldType, 0, $keypos );
+ }
+
+ $fieldType = strtoupper( $fieldType );
+ $default = '';
+
+ if ( isset( $attributes['defaults'][$fieldName] ) ) {
+ $default = "DEFAULT '" . $attributes['defaults'][$fieldName] . "'";
+ }
+
+ if ( !array_key_exists( $fieldName, $currentFields ) ) {
+ $this->doCreateField( $tableName, $fieldName, $position, $fieldType, $default );
+ } elseif ( $currentFields[$fieldName] != $fieldType ) {
+ $this->reportMessage( " ... changing type of field $fieldName from '$currentFields[$fieldName]' to '$fieldType' ... " );
+
+ $notnullposnew = strpos( $fieldType, ' NOT NULL' );
+
+ if ( $notnullposnew > 0 ) {
+ $fieldType = substr( $fieldType, 0, $notnullposnew );
+ }
+
+ $notnullposold = strpos( $currentFields[$fieldName], ' NOT NULL' );
+ $typeold = strtoupper( ( $notnullposold > 0 ) ? substr( $currentFields[$fieldName], 0, $notnullposold ) : $currentFields[$fieldName] );
+
+ // Added USING statement to avoid
+ // "Query: ALTER TABLE "smw_object_ids" ALTER COLUMN "smw_proptable_hash" TYPE BYTEA ...
+ // Error: 42804 ERROR: column "smw_proptable_hash" cannot be cast automatically to type bytea
+ // HINT: You might need to specify "USING smw_proptable_hash::bytea"."
+
+ if ( $typeold != $fieldType ) {
+ $sql = "ALTER TABLE " . $tableName . " ALTER COLUMN \"" . $fieldName . "\" TYPE " . $fieldType . " USING \"$fieldName\"::$fieldType";
+ $this->connection->query( $sql, __METHOD__ );
+ }
+
+ if ( $notnullposold != $notnullposnew ) {
+ $sql = "ALTER TABLE " . $tableName . " ALTER COLUMN \"" . $fieldName . "\" " . ( $notnullposnew > 0 ? 'SET' : 'DROP' ) . " NOT NULL";
+ $this->connection->query( $sql, __METHOD__ );
+ }
+
+ $this->reportMessage( "done.\n" );
+ } else {
+ $this->reportMessage( " ... field $fieldName is fine.\n" );
+ }
+ }
+
+ private function doCreateField( $tableName, $fieldName, $position, $fieldType, $default ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_NEW;
+
+ $this->reportMessage( " ... creating field $fieldName ... " );
+ $this->connection->query( "ALTER TABLE $tableName ADD \"" . $fieldName . "\" $fieldType $default", __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doDropField( $tableName, $fieldName ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_DROP;
+
+ $this->reportMessage( " ... deleting obsolete field $fieldName ... " );
+ $this->connection->query( 'ALTER TABLE ' . $tableName . ' DROP COLUMN "' . $fieldName . '"', __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ /** Index */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doCreateIndices( $tableName, array $indexOptions = null ) {
+
+ $indices = $indexOptions['indices'];
+ $ix = [];
+
+ // In case an index has a length restriction indexZ(200), remove it since
+ // Postgres doesn't know such syntax
+ foreach ( $indices as $k => $columns ) {
+ $ix[$k] = preg_replace("/\([^)]+\)/", "", $columns );
+ }
+
+ $indices = $ix;
+
+ // First remove possible obsolete indices
+ $this->doDropObsoleteIndices( $tableName, $indices );
+
+ // Add new indexes.
+ foreach ( $indices as $indexName => $index ) {
+ // If the index is an array, it contains the column
+ // name as first element, and index type as second one.
+ if ( is_array( $index ) ) {
+ $columns = $index[0];
+ $indexType = count( $index ) > 1 ? $index[1] : 'INDEX';
+ } else {
+ $columns = $index;
+ $indexType = 'INDEX';
+ }
+
+ $this->doCreateIndex( $tableName, $indexType, $indexName, $columns, $indexOptions );
+ }
+ }
+
+ private function doDropObsoleteIndices( $tableName, array &$indices ) {
+
+ $tableName = $this->connection->tableName( $tableName, 'raw' );
+ $currentIndices = $this->getIndexInfo( $tableName );
+
+ foreach ( $currentIndices as $indexName => $indexColumn ) {
+ // Indices may contain something like array( 'id', 'UNIQUE INDEX' )
+ $id = $this->recursive_array_search( $indexColumn, $indices );
+ if ( $id !== false || $indexName == 'PRIMARY' ) {
+ $this->reportMessage( " ... index $indexColumn is fine.\n" );
+
+ if ( $id !== false ) {
+ unset( $indices[$id] );
+ }
+
+ } else { // Duplicate or unrequired index.
+ $this->doDropIndex( $tableName, $indexName, $indexColumn );
+ }
+ }
+ }
+
+ private function doCreateIndex( $tableName, $indexType, $indexName, $columns, array $indexOptions ) {
+
+ if ( $indexType === 'FULLTEXT' ) {
+ return $this->reportMessage( " ... skipping the fulltext index creation ..." );
+ }
+
+ $tableName = $this->connection->tableName( $tableName, 'raw' );
+ $indexName = $this->getCumulatedIndexName( $tableName, $columns );
+
+ $this->reportMessage( " ... creating new index $columns ..." );
+
+ if ( $this->connection->indexInfo( $tableName, $indexName ) === false ) {
+ $this->connection->query( "CREATE $indexType $indexName ON $tableName ($columns)", __METHOD__ );
+ }
+
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function getCumulatedIndexName( $tableName, $columns ) {
+ // Identifiers -- table names, column names, constraint names,
+ // etc. -- are limited to a maximum length of 63 bytes
+ return str_replace( '__' , '_', "{$tableName}_idx_" . str_replace( [ '_', 'smw', ',' ], [ '', '_', '_' ], $columns ) );
+ }
+
+ private function getIndexInfo( $tableName ) {
+
+ $indices = [];
+
+ $sql = "SELECT i.relname AS indexname,"
+ . " pg_get_indexdef(i.oid) AS indexdef, "
+ . " replace(substring(pg_get_indexdef(i.oid) from E'\\\\((.*)\\\\)'), ' ' , '') AS indexcolumns"
+ . " FROM pg_index x"
+ . " JOIN pg_class c ON c.oid = x.indrelid"
+ . " JOIN pg_class i ON i.oid = x.indexrelid"
+ . " LEFT JOIN pg_namespace n ON n.oid = c.relnamespace"
+ . " LEFT JOIN pg_tablespace t ON t.oid = i.reltablespace"
+ . " WHERE c.relkind = 'r'::\"char\" AND i.relkind = 'i'::\"char\""
+ . " AND c.relname = '" . $tableName . "'"
+ . " AND NOT pg_get_indexdef(i.oid) ~ '^CREATE UNIQUE INDEX'";
+
+ $res = $this->connection->query( $sql, __METHOD__ );
+
+ if ( !$res ) {
+ return [];
+ }
+
+ foreach ( $res as $row ) {
+ $indices[$row->indexname] = $row->indexcolumns;
+ }
+
+ return $indices;
+ }
+
+ private function doDropIndex( $tableName, $indexName, $columns ) {
+ $this->reportMessage( " ... removing index $columns ..." );
+ $this->connection->query( 'DROP INDEX IF EXISTS ' . $indexName, __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ /** Drop */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doDropTable( $tableName ) {
+ // Function: SMW\SQLStore\TableBuilder\PostgresTableBuilder::doDropTable
+ // Error: 2BP01 ERROR: cannot drop table smw_object_ids because other objects depend on it
+ // DETAIL: default for table sunittest_smw_object_ids column smw_id depends on sequence smw_object_ids_smw_id_seq
+ // HINT: Use DROP ... CASCADE to drop the dependent objects too.
+ $this->connection->query( 'DROP TABLE IF EXISTS ' . $this->connection->tableName( $tableName ) . ' CASCADE', __METHOD__ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ protected function doOptimize( $tableName ) {
+
+ $this->reportMessage( "Checking table $tableName ...\n" );
+
+ // https://www.postgresql.org/docs/9.0/static/sql-analyze.html
+ $this->reportMessage( " ... analyze " );
+ $this->connection->query( 'ANALYZE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+
+ $this->reportMessage( "done.\n" );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function checkOn( $event ) {
+ if ( $event === self::POST_CREATION ) {
+ $this->doCheckOnPostCreation();
+ }
+ }
+
+ private function doCheckOnPostCreation() {
+
+ $sequence = new Sequence( $this->connection );
+
+ // To avoid things like:
+ // "Error: 23505 ERROR: duplicate key value violates unique constraint "smw_object_ids_pkey""
+ $seq_num = $sequence->restart( SQLStore::ID_TABLE, 'smw_id' );
+
+ $this->reportMessage( "Checking `smw_id` sequence consistency ...\n" );
+ $this->reportMessage( " ... setting sequence to {$seq_num} ...\n" );
+ $this->reportMessage( " ... done.\n" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/SQLiteTableBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/SQLiteTableBuilder.php
new file mode 100644
index 00000000..acd7e3c9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/SQLiteTableBuilder.php
@@ -0,0 +1,373 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ * @author Markus Krötzsch
+ * @author Marcel Gsteiger
+ * @author Jeroen De Dauw
+ */
+class SQLiteTableBuilder extends TableBuilder {
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getStandardFieldType( $fieldType ) {
+
+ $charLongLength = FieldType::CHAR_LONG_LENGTH;
+
+ $fieldTypes = [
+ // like page_id in MW page table
+ 'id' => 'INTEGER',
+ 'id_primary' => 'INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT',
+
+ // (see postgres, mysql on the difference)
+ 'id_unsigned' => 'INTEGER',
+
+ // like page_namespace in MW page table
+ 'namespace' => 'INT(11)',
+ // like page_title in MW page table
+ 'title' => 'VARBINARY(255)',
+ // like iw_prefix in MW interwiki table
+ 'interwiki' => 'TEXT',
+ 'iw' => 'TEXT',
+ 'hash' => 'VARBINARY(40)',
+ // larger blobs of character data, usually not subject to SELECT conditions
+ 'blob' => 'MEDIUMBLOB',
+ 'text' => 'TEXT',
+ 'boolean' => 'TINYINT(1)',
+ 'double' => 'DOUBLE',
+ 'integer' => 'INT(8)',
+ 'char_long' => "VARBINARY($charLongLength)",
+ 'char_nocase' => 'VARCHAR(255) NOT NULL COLLATE NOCASE',
+ 'char_long_nocase' => "VARCHAR($charLongLength) NOT NULL COLLATE NOCASE",
+ 'usage_count' => 'INT(8)',
+ 'integer_unsigned' => 'INTEGER'
+ ];
+
+ return FieldType::mapType( $fieldType, $fieldTypes );
+ }
+
+ /** Create */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doCreateTable( $tableName, array $attributes = null ) {
+
+ $mode = '';
+ $option = '';
+
+ $ftsOptions = null;
+ $tableName = $this->connection->tableName( $tableName );
+
+ if ( isset( $attributes['fulltextSearchTableOptions']['sqlite'] ) ) {
+ $ftsOptions = $attributes['fulltextSearchTableOptions']['sqlite'];
+ }
+
+ // Filter extra module options
+ // @see https://www.sqlite.org/fts3.html#fts4_options
+ //
+ // $smwgFulltextSearchTableOptions can define:
+ // - 'sqlite' => array( 'FTS4' )
+ // - 'sqlite' => array( 'FTS4', 'tokenize=porter' )
+ if ( $ftsOptions !== null && is_array( $ftsOptions ) ) {
+ $mode = isset( $ftsOptions[0] ) ? $ftsOptions[0] : '';
+ $option = isset( $ftsOptions[1] ) ? $ftsOptions[1] : '';
+ } elseif ( $ftsOptions !== null ) {
+ $mode = $ftsOptions;
+ }
+
+ $fieldSql = [];
+ $fields = $attributes['fields'];
+
+ foreach ( $fields as $fieldName => $fieldType ) {
+ $fieldSql[] = "$fieldName " . $this->getStandardFieldType( $fieldType );
+ }
+
+ if ( $mode === '' ) {
+ $sql = 'CREATE TABLE ' . $tableName .'(' . implode( ',', $fieldSql ) . ') ';
+ } else {
+ $sql = 'CREATE VIRTUAL TABLE ' . $tableName . ' USING ' . strtolower( $mode ) .'(' . implode( ',', $fieldSql ) . $option . ') ';
+ }
+
+ $this->connection->query( $sql, __METHOD__ );
+ }
+
+ /** Update */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doUpdateTable( $tableName, array $attributes = null ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $currentFields = $this->getCurrentFields( $tableName );
+
+ $fields = $attributes['fields'];
+ $position = 'FIRST';
+
+ // Loop through all the field definitions, and handle each definition for either postgres or MySQL.
+ foreach ( $fields as $fieldName => $fieldType ) {
+ $this->doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, $attributes );
+
+ $position = "AFTER $fieldName";
+ $currentFields[$fieldName] = false;
+ }
+
+ // The updated fields have their value set to false, so if a field has a value
+ // that differs from false, it's an obsolete one that should be removed.
+ foreach ( $currentFields as $fieldName => $value ) {
+ if ( $value !== false ) {
+ $this->doDropField( $tableName, $fieldName, $attributes );
+ }
+ }
+ }
+
+ private function getCurrentFields( $tableName ) {
+
+ $sql = 'PRAGMA table_info(' . $tableName . ')';
+
+ $res = $this->connection->query( $sql, __METHOD__ );
+ $currentFields = [];
+
+ foreach ( $res as $row ) {
+ $row->Field = $row->name;
+ $row->Type = $row->type;
+ $type = $row->type;
+
+ if ( $row->notnull == '1' ) {
+ $type .= ' NOT NULL';
+ }
+
+ if ( $row->pk == '1' ) {
+ $type .= ' PRIMARY KEY AUTOINCREMENT';
+ }
+
+ $currentFields[$row->Field] = $type;
+ }
+
+ return $currentFields;
+ }
+
+ private function doUpdateField( $tableName, $fieldName, $fieldType, $currentFields, $position, array $attributes ) {
+
+ if ( !isset( $this->activityLog[$tableName] ) ) {
+ $this->activityLog[$tableName] = [];
+ }
+
+ $fieldType = $this->getStandardFieldType( $fieldType );
+ $default = '';
+
+ if ( isset( $attributes['defaults'][$fieldName] ) ) {
+ $default = "DEFAULT '" . $attributes['defaults'][$fieldName] . "'";
+ }
+
+ if ( !array_key_exists( $fieldName, $currentFields ) ) {
+ $this->doCreateField( $tableName, $fieldName, $position, $fieldType, $default );
+ } elseif ( $currentFields[$fieldName] != $fieldType ) {
+ $this->doUpdateFieldType( $tableName, $fieldName, $position, $currentFields[$fieldName], $fieldType );
+ } else {
+ $this->reportMessage( " ... field $fieldName is fine.\n" );
+ }
+ }
+
+ private function doCreateField( $tableName, $fieldName, $position, $fieldType, $default ) {
+
+ if ( strpos( $tableName, 'ft_search' ) !== false ) {
+ return $this->reportMessage( " ... virtual tables can not be altered in SQLite ...\n" );
+ }
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_NEW;
+
+ if ( $default === '' ) {
+ // @see https://www.sqlite.org/lang_altertable.html states that
+ // "If a NOT NULL constraint is specified, then the column must have a default value other than NULL."
+ $default = "DEFAULT NULL";
+
+ // Add DEFAULT '' to avoid
+ // Query: ALTER TABLE sunittest_rdbms_test ADD `t_num` INT(8) NOT NULL
+ // Function: SMW\SQLStore\TableBuilder\SQLiteTableBuilder::doCreateField
+ // Error: 1 Cannot add a NOT NULL column with default value NULL
+ if ( strpos( $fieldType, 'NOT NULL' ) !== false ) {
+ $default = "DEFAULT ''";
+ }
+ }
+
+ $this->reportMessage( " ... creating field $fieldName ... " );
+ $this->connection->query( "ALTER TABLE $tableName ADD `$fieldName` $fieldType $default", __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doUpdateFieldType( $tableName, $fieldName, $position, $oldFieldType, $newFieldType ) {
+ $this->reportMessage( " ... changing field type is not supported in SQLite (http://www.sqlite.org/omitted.html) \n" );
+ $this->reportMessage( " Please delete and reinitialize the tables to remove obsolete data, or just keep it.\n" );
+ }
+
+ private function doDropField( $tableName, $fieldName, $attributes ) {
+
+ $this->activityLog[$tableName][$fieldName] = self::PROC_FIELD_DROP;
+
+ $fields = $attributes['fields'];
+ $temp_table = "{$tableName}_temp";
+
+ // https://stackoverflow.com/questions/5938048/delete-column-from-sqlite-table
+ // Deleting obsolete fields is not possible in SQLite therefore create a
+ // temp table, copy the content, remove the table with obsolete/ fields,
+ // and rename the temp table
+ $field_def = [];
+ $field_list = [];
+
+ foreach ( $fields as $field => $type ) {
+ $field_def[] = "$field " . $this->getStandardFieldType( $type );
+ $field_list[] = $field;
+ }
+
+ $this->reportMessage( " ... field $fieldName is obsolete ...\n" );
+ $this->reportMessage( " ... creating a temporary table ...\n" );
+ $this->connection->query( 'DROP TABLE IF EXISTS ' . $temp_table, __METHOD__ );
+ $this->connection->query( 'CREATE TABLE ' . $temp_table .' (' . implode( ',', $field_def ) . ') ', __METHOD__ );
+ $this->reportMessage( " ... copying table contents ...\n" );
+ $this->connection->query( 'INSERT INTO ' . $temp_table . ' SELECT ' . implode( ',', $field_list ) . ' FROM ' . $tableName, __METHOD__ );
+ $this->reportMessage( " ... dropping table with obsolete field definitions ...\n" );
+ $this->connection->query( 'DROP TABLE IF EXISTS ' . $tableName, __METHOD__ );
+ $this->reportMessage( " ... renaming temporary table to $tableName ...\n" );
+ $this->connection->query( 'ALTER TABLE ' . $temp_table . ' RENAME TO ' . $tableName, __METHOD__ );
+ $this->reportMessage( " ... done.\n" );
+ }
+
+ /** Index */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doCreateIndices( $tableName, array $indexOptions = null ) {
+
+ $indices = $indexOptions['indices'];
+ $ix = [];
+
+ // In case an index has a length restriction indexZ(200), remove it since
+ // SQLite doesn't know such syntax
+ foreach ( $indices as $k => $columns ) {
+ $ix[$k] = preg_replace("/\([^)]+\)/", "", $columns );
+ }
+
+ $indices = $ix;
+
+ // First remove possible obsolete indices
+ $this->doDropObsoleteIndices( $tableName, $indices );
+
+ // Add new indexes.
+ foreach ( $indices as $indexName => $index ) {
+ // If the index is an array, it contains the column
+ // name as first element, and index type as second one.
+ if ( is_array( $index ) ) {
+ $columns = $index[0];
+ $indexType = count( $index ) > 1 ? $index[1] : 'INDEX';
+ } else {
+ $columns = $index;
+ $indexType = 'INDEX';
+ }
+
+ $this->doCreateIndex( $tableName, $indexType, $indexName, $columns, $indexOptions );
+ }
+ }
+
+ private function doDropObsoleteIndices( $tableName, array &$indices ) {
+
+ $currentIndices = $this->getIndexInfo( $tableName );
+
+ // TODO We do not currently get the right column definitions in
+ // SQLite; hence we can only drop all indexes. Wasteful.
+ foreach ( $currentIndices as $indexName => $indexColumn ) {
+ $this->doDropIndex( $tableName, $indexName, $indexColumn );
+ }
+ }
+
+ private function getIndexInfo( $tableName ) {
+
+ $tableName = $this->connection->tableName( $tableName );
+ $indices = [];
+
+ $res = $this->connection->query( 'PRAGMA index_list(' . $tableName . ')', __METHOD__ );
+
+ if ( !$res ) {
+ return [];
+ }
+
+ foreach ( $res as $row ) {
+ /// FIXME The value should not be $row->name below?!
+ if ( !array_key_exists( $row->name, $indices ) ) {
+ $indices[$row->name] = $row->name;
+ } else {
+ $indices[$row->name] .= ',' . $row->name;
+ }
+ }
+
+ return $indices;
+ }
+
+ private function doDropIndex( $tableName, $indexName, $columns ) {
+ $this->reportMessage( " ... removing index $columns ..." );
+ $this->connection->query( 'DROP INDEX ' . $indexName, __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ private function doCreateIndex( $tableName, $indexType, $indexName, $columns, array $indexOptions ) {
+
+ if ( $indexType === 'FULLTEXT' ) {
+ return $this->reportMessage( " ... skipping the fulltext index creation ..." );
+ }
+
+ if ( strpos( $tableName, 'ft_search' ) !== false ) {
+ return $this->reportMessage( " ... virtual tables can not be altered in SQLite ...\n" );
+ }
+
+ $tableName = $this->connection->tableName( $tableName );
+ $indexName = "{$tableName}_index{$indexName}";
+
+ $this->reportMessage( " ... creating new index $columns ..." );
+ $this->connection->query( "CREATE $indexType $indexName ON $tableName ($columns)", __METHOD__ );
+ $this->reportMessage( "done.\n" );
+ }
+
+ /** Drop */
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ protected function doDropTable( $tableName ) {
+ $this->connection->query( 'DROP TABLE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ protected function doOptimize( $tableName ) {
+
+ $this->reportMessage( "Checking table $tableName ...\n" );
+
+ // https://sqlite.org/lang_analyze.html
+ $this->reportMessage( " ... analyze " );
+ $this->connection->query( 'ANALYZE ' . $this->connection->tableName( $tableName ), __METHOD__ );
+
+ $this->reportMessage( "done.\n" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/Table.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/Table.php
new file mode 100644
index 00000000..5565bda9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/Table.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder;
+
+use RuntimeException;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class Table {
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var array
+ */
+ private $attributes = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param string $name
+ */
+ public function __construct( $name ) {
+ $this->name = $name;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string
+ */
+ public function getHash() {
+ return json_encode( $this->attributes );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array
+ */
+ public function getAttributes() {
+ return $this->attributes;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @param mixed
+ */
+ public function get( $key ) {
+
+ if ( !isset( $this->attributes[$key] ) ) {
+ throw new RuntimeException( "$key is a reserved option key." );
+ }
+
+ return $this->attributes[$key];
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $fieldName
+ * @param string|array $fieldType
+ */
+ public function addColumn( $fieldName, $fieldType ) {
+ $this->attributes['fields'][$fieldName] = $fieldType;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|array $index
+ * @param string|null $key
+ */
+ public function addIndex( $index, $key = null ) {
+ if ( $key !== null ) {
+ $this->attributes['indices'][$key] = $index;
+ } else {
+ $this->attributes['indices'][] = $index;
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $fieldName
+ * @param string|int $default
+ */
+ public function addDefault( $fieldName, $default ) {
+ $this->attributes['defaults'][$fieldName] = $default;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $key
+ * @param string|array $option
+ *
+ * @throws RuntimeException
+ */
+ public function addOption( $key, $option ) {
+
+ if ( $key === 'fields' || $key === 'indices' || $key === 'defaults' ) {
+ throw new RuntimeException( "$key is a reserved option key." );
+ }
+
+ $this->attributes[$key] = $option;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/TableBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/TableBuilder.php
new file mode 100644
index 00000000..9f39a22c
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/TableBuilder.php
@@ -0,0 +1,246 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder;
+
+use DatabaseBase;
+use Onoi\MessageReporter\MessageReporter;
+use Onoi\MessageReporter\MessageReporterAware;
+use RuntimeException;
+use SMW\SQLStore\TableBuilder as TableBuilderInterface;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+abstract class TableBuilder implements TableBuilderInterface, MessageReporterAware, MessageReporter {
+
+ /**
+ * @var DatabaseBase
+ */
+ protected $connection;
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @var array
+ */
+ protected $config = [];
+
+ /**
+ * @var array
+ */
+ protected $activityLog = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param DatabaseBase $connection
+ */
+ protected function __construct( DatabaseBase $connection ) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DatabaseBase $connection
+ *
+ * @return TableBuilder
+ * @throws RuntimeException
+ */
+ public static function factory( DatabaseBase $connection ) {
+
+ $instance = null;
+
+ switch ( $connection->getType() ) {
+ case 'mysql':
+ $instance = new MySQLTableBuilder( $connection );
+ break;
+ case 'sqlite':
+ $instance = new SQLiteTableBuilder( $connection );
+ break;
+ case 'postgres':
+ $instance = new PostgresTableBuilder( $connection );
+ break;
+ }
+
+ if ( $instance === null ) {
+ throw new RuntimeException( "Unknown or unsupported DB type " . $connection->getType() );
+ }
+
+ $instance->addConfig( 'wgDBname', $GLOBALS['wgDBname'] );
+ $instance->addConfig( 'wgDBTableOptions', $GLOBALS['wgDBTableOptions'] );
+
+ return $instance;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|integer $key
+ * @param mixed
+ */
+ public function addConfig( $key, $value ) {
+ $this->config[$key] = $value;
+ }
+
+ /**
+ * @see MessageReporterAware::setMessageReporter
+ *
+ * @since 2.5
+ *
+ * @param MessageReporter $messageReporter
+ */
+ public function setMessageReporter( MessageReporter $messageReporter ) {
+ $this->messageReporter = $messageReporter;
+ }
+
+ /**
+ * @see MessageReporter::reportMessage
+ *
+ * @since 2.5
+ *
+ * @param string $message
+ */
+ public function reportMessage( $message ) {
+
+ if ( $this->messageReporter === null ) {
+ return;
+ }
+
+ $this->messageReporter->reportMessage( $message );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function getStandardFieldType( $fieldType ) {
+ return false;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function create( Table $table ) {
+
+ $attributes = $table->getAttributes();
+ $tableName = $table->getName();
+
+ $this->reportMessage( "Checking table $tableName ...\n" );
+
+ if ( $this->connection->tableExists( $tableName ) === false ) { // create new table
+ $this->reportMessage( " Table not found, now creating...\n" );
+ $this->doCreateTable( $tableName, $attributes );
+ } else {
+ $this->reportMessage( " Table already exists, checking structure ...\n" );
+ $this->doUpdateTable( $tableName, $attributes );
+ }
+
+ $this->reportMessage( " ... done.\n" );
+
+ if ( !isset( $attributes['indices'] ) ) {
+ return $this->reportMessage( "No index structures for table $tableName ...\n" );
+ }
+
+ $this->reportMessage( "Checking index structures for table $tableName ...\n" );
+ $this->doCreateIndices( $tableName, $attributes );
+
+ $this->reportMessage( " ... done.\n" );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * {@inheritDoc}
+ */
+ public function drop( Table $table ) {
+
+ $tableName = $table->getName();
+
+ if ( $this->connection->tableExists( $tableName ) === false ) { // create new table
+ return $this->reportMessage( " ... $tableName not found, skipping removal.\n" );
+ }
+
+ $this->doDropTable( $tableName );
+ $this->reportMessage( " ... dropped table $tableName.\n" );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function optimize( Table $table ) {
+ $this->doOptimize( $table->getName() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $event
+ */
+ public function checkOn( $event ) {
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function getLog() {
+ return $this->activityLog;
+ }
+
+ /**
+ * @param string $tableName
+ * @param array $tableOptions
+ */
+ abstract protected function doCreateTable( $tableName, array $tableOptions = null);
+
+ /**
+ * @param string $tableName
+ * @param array $tableOptions
+ */
+ abstract protected function doUpdateTable( $tableName, array $tableOptions = null );
+
+ /**
+ * @param string $tableName
+ * @param array $indexOptions
+ */
+ abstract protected function doCreateIndices( $tableName, array $indexOptions = null );
+
+ /**
+ * @param string $tableName
+ */
+ abstract protected function doDropTable( $tableName );
+
+ /**
+ * @param string $tableName
+ */
+ abstract protected function doOptimize( $tableName );
+
+ // #1978
+ // http://php.net/manual/en/function.array-search.php
+ protected function recursive_array_search( $needle, $haystack ) {
+ foreach( $haystack as $key => $value ) {
+ $current_key = $key;
+
+ if ( $needle === $value or ( is_array( $value ) && $this->recursive_array_search( $needle, $value ) !== false ) ) {
+ return $current_key;
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/TemporaryTableBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/TemporaryTableBuilder.php
new file mode 100644
index 00000000..fac1bd90
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableBuilder/TemporaryTableBuilder.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace SMW\SQLStore\TableBuilder;
+
+use SMW\MediaWiki\Database;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ * @author mwjames
+ */
+class TemporaryTableBuilder {
+
+ /**
+ * @var Database
+ */
+ private $connection;
+
+ /**
+ * @var boolean
+ */
+ private $autoCommitFlag = false;
+
+ /**
+ * @since 2.3
+ *
+ * @param Database $connection
+ */
+ public function __construct( Database $connection ) {
+ $this->connection = $connection;
+ }
+
+ /**
+ * @see $smwgQTemporaryTablesWithAutoCommit
+ * @since 2.5
+ *
+ * @param boolean $autoCommitFlag
+ */
+ public function setAutoCommitFlag( $autoCommitFlag ) {
+ $this->autoCommitFlag = (bool)$autoCommitFlag;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $tableName
+ */
+ public function create( $tableName ) {
+
+ if ( $this->autoCommitFlag ) {
+ $this->connection->setFlag( Database::AUTO_COMMIT );
+ }
+
+ $this->connection->query(
+ $this->getSQLCodeFor( $tableName ),
+ __METHOD__,
+ false
+ );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $tableName
+ */
+ public function drop( $tableName ) {
+
+ if ( $this->autoCommitFlag ) {
+ $this->connection->setFlag( Database::AUTO_COMMIT );
+ }
+
+ $this->connection->query(
+ "DROP TEMPORARY TABLE " . $tableName,
+ __METHOD__,
+ false
+ );
+ }
+
+ /**
+ * Get SQL code suitable to create a temporary table of the given name, used
+ * to store ids.
+ *
+ * MySQL can do that simply by creating new temporary tables. PostgreSQL first
+ * checks if such a table exists, so the code is ready to reuse existing tables
+ * if the code was modified to keep them after query answering. Also, PostgreSQL
+ * tables will use a RULE to achieve built-in duplicate elimination. The latter
+ * is done using INSERT IGNORE in MySQL.
+ *
+ * @param string $tableName
+ *
+ * @return string
+ */
+ private function getSQLCodeFor( $tableName ) {
+ // PostgreSQL: no memory tables, use RULE to emulate INSERT IGNORE
+ if ( $this->connection->isType( 'postgres' ) ) {
+
+ // Remove any double quotes from the name
+ $tableName = str_replace( '"', '', $tableName );
+
+ return "DO \$\$BEGIN "
+ . " IF EXISTS(SELECT NULL FROM pg_tables WHERE tablename='{$tableName}' AND schemaname = ANY (current_schemas(true))) "
+ . " THEN DELETE FROM {$tableName}; "
+ . " ELSE "
+ . " CREATE TEMPORARY TABLE {$tableName} (id SERIAL); "
+ . " CREATE RULE {$tableName}_ignore AS ON INSERT TO {$tableName} WHERE (EXISTS (SELECT 1 FROM {$tableName} "
+ . " WHERE ({$tableName}.id = new.id))) DO INSTEAD NOTHING; "
+ . " END IF; "
+ . "END\$\$";
+ }
+
+ // MySQL_ just a temporary table, use INSERT IGNORE later
+ return "CREATE TEMPORARY TABLE " . $tableName . "( id INT UNSIGNED KEY ) ENGINE=MEMORY";
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableFieldUpdater.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableFieldUpdater.php
new file mode 100644
index 00000000..d9fbcfdb
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableFieldUpdater.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\MediaWiki\Collator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TableFieldUpdater {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var Collator
+ */
+ private $collator;
+
+ /**
+ * @since 3.0
+ *
+ * @param SQLStore $store
+ * @param Collator|null $collator
+ */
+ public function __construct( SQLStore $store, Collator $collator = null ) {
+ $this->store = $store;
+ $this->collator = $collator;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param string $searchKey
+ */
+ public function updateSortField( $id, $searchKey ) {
+
+ if ( $this->collator === null ) {
+ $this->collator = Collator::singleton();
+ }
+
+ $connection = $this->store->getConnection( 'mw.db' );
+ $connection->beginAtomicTransaction( __METHOD__ );
+
+ // #2089 (MySQL 5.7 complained with "Data too long for column")
+ $searchKey = mb_substr( $searchKey, 0, 254 );
+
+ $connection->update(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_sortkey' => $searchKey,
+ 'smw_sort' => $this->collator->getSortKey( $searchKey )
+ ],
+ [ 'smw_id' => $id ],
+ __METHOD__
+ );
+
+ $connection->endAtomicTransaction( __METHOD__ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $id
+ * @param intege $rev_id
+ */
+ public function updateRevField( $sid, $rev_id ) {
+
+ $connection = $this->store->getConnection( 'mw.db' );
+
+ $connection->update(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_rev' => $rev_id
+ ],
+ [
+ 'smw_id' => $sid
+ ],
+ __METHOD__
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableIntegrityExaminer.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableIntegrityExaminer.php
new file mode 100644
index 00000000..690a3d97
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableIntegrityExaminer.php
@@ -0,0 +1,344 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use Onoi\MessageReporter\MessageReporterAwareTrait;
+use Onoi\MessageReporter\NullMessageReporter;
+use SMW\DIProperty;
+use SMW\Exception\PredefinedPropertyLabelMismatchException;
+use SMW\MediaWiki\Collator;
+use SMW\PropertyRegistry;
+use SMW\SQLStore\TableBuilder\Table;
+use SMW\SQLStore\Installer;
+use SMW\SQLStore\TableBuilder\Examiner\HashField;
+use SMWSql3SmwIds;
+
+/**
+ * @private
+ *
+ * Allows to execute SQLStore or table specific examination tasks that are
+ * expected to be part of the installation or removal routine.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class TableIntegrityExaminer {
+
+ use MessageReporterAwareTrait;
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var HashField
+ */
+ private $hashField;
+
+ /**
+ * @var array
+ */
+ private $predefinedProperties = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param SQLStore $store
+ * @param HashField $hashField
+ */
+ public function __construct( SQLStore $store, HashField $hashField ) {
+ $this->store = $store;
+ $this->hashField = $hashField;
+ $this->messageReporter = new NullMessageReporter();
+ $this->setPredefinedPropertyList( PropertyRegistry::getInstance()->getPropertyList() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $propertyList
+ */
+ public function setPredefinedPropertyList( array $propertyList ) {
+
+ $fixedPropertyList = SMWSql3SmwIds::$special_ids;
+ $predefinedPropertyList = [];
+
+ foreach ( $propertyList as $key => $val ) {
+ $predefinedPropertyList[$key] = null;
+
+ if ( isset( $fixedPropertyList[$key] ) ) {
+ $predefinedPropertyList[$key] = $fixedPropertyList[$key];
+ } elseif ( is_integer( $val ) ) {
+ $predefinedPropertyList[$key] = $val;
+ }
+ }
+
+ $this->predefinedPropertyList = $predefinedPropertyList;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param TableBuilder $tableBuilder
+ */
+ public function checkOnPostCreation( TableBuilder $tableBuilder ) {
+
+ $this->checkPredefinedPropertyIndices();
+
+ $this->hashField->setMessageReporter( $this->messageReporter );
+ $this->hashField->check();
+
+ $this->checkSortField( $tableBuilder->getLog() );
+
+ // Call out for RDBMS specific implementations
+ $tableBuilder->checkOn( TableBuilder::POST_CREATION );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param TableBuilder $tableBuilder
+ */
+ public function checkOnPostDestruction( TableBuilder $tableBuilder ) {
+
+ $connection = $this->store->getConnection( DB_MASTER );
+
+ // Find orphaned tables that have not been removed but were produced and
+ // handled by SMW
+ foreach ( $connection->listTables() as $table ) {
+ if ( strpos( $table, TableBuilder::TABLE_PREFIX ) !== false ) {
+
+ // Remove any MW specific prefix at this point which will be
+ // handled by the DB class (abcsmw_foo -> smw_foo)
+ $tableBuilder->drop( new Table( strstr( $table, TableBuilder::TABLE_PREFIX ) ) );
+ }
+ }
+
+ // Call out for RDBMS specific implementations
+ $tableBuilder->checkOn( TableBuilder::POST_DESTRUCTION );
+ }
+
+ /**
+ * Create some initial DB entries for important built-in properties. Having
+ * the DB contents predefined allows us to safe DB calls when certain data
+ * is needed. At the same time, the entries in the DB make sure that DB-based
+ * functions work as with all other properties.
+ */
+ private function checkPredefinedPropertyIndices() {
+
+ $connection = $this->store->getConnection( DB_MASTER );
+
+ $this->messageReporter->reportMessage( "Checking predefined properties ...\n" );
+ $this->checkPredefinedPropertyUpperbound();
+
+ // now write actual properties; do that each time, it is cheap enough
+ // and we can update sortkeys by current language
+ $this->messageReporter->reportMessage( " ... initialize predefined properties ...\n" );
+
+ foreach ( $this->predefinedPropertyList as $prop => $id ) {
+
+ try{
+ $property = new DIProperty( $prop );
+ } catch ( PredefinedPropertyLabelMismatchException $e ) {
+ $property = null;
+ $this->messageReporter->reportMessage( " ... skipping {$prop} due to invalid registration ...\n" );
+ }
+
+ if ( $property === null ) {
+ continue;
+ }
+
+ $this->updatePredefinedProperty( $property, $id );
+ }
+
+ $this->messageReporter->reportMessage( " ... done.\n" );
+ }
+
+ private function checkPredefinedPropertyUpperbound() {
+
+ $connection = $this->store->getConnection( DB_MASTER );
+
+ // Check if we already have this structure
+ $upperbound = SQLStore::FIXED_PROPERTY_ID_UPPERBOUND;
+ $legacyBound = 50;
+
+ $row = $connection->selectRow(
+ SQLStore::ID_TABLE,
+ 'smw_id',
+ 'smw_iw=' . $connection->addQuotes( SMW_SQL3_SMWBORDERIW )
+ );
+
+ if ( $row !== false && $row->smw_id == $upperbound ) {
+ return $this->messageReporter->reportMessage( " ... space for internal properties already allocated.\n" );
+ } elseif ( $row === false ) {
+ $currentUpperbound = $legacyBound;
+ } else {
+ $currentUpperbound = $row->smw_id;
+
+ // Delete the current upperbound to avoid having a duplicate border
+ $connection->delete(
+ SQLStore::ID_TABLE,
+ [ 'smw_id' => $currentUpperbound ],
+ __METHOD__
+ );
+ }
+
+ $this->messageReporter->reportMessage( " ... allocating space for internal properties ...\n" );
+ $this->store->getObjectIds()->moveSMWPageID( $upperbound );
+
+ $connection->insert(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_id' => $upperbound,
+ 'smw_title' => '',
+ 'smw_namespace' => 0,
+ 'smw_iw' => SMW_SQL3_SMWBORDERIW,
+ 'smw_subobject' => '',
+ 'smw_sortkey' => ''
+ ],
+ __METHOD__
+ );
+
+ if ( $currentUpperbound == $upperbound ) {
+ return $this->messageReporter->reportMessage( " ... done.\n" );
+ }
+
+ if ( $currentUpperbound < $upperbound ) {
+ $this->messageReporter->reportMessage( " ... moving from $currentUpperbound to $upperbound upperbound (may take a moment) ..." );
+ $this->messageReporter->reportMessage( " " );
+ }
+
+ for ( $i = $currentUpperbound; $i < $upperbound; $i++ ) {
+
+ if ( ( $i - $currentUpperbound ) % 60 === 0 ) {
+ $this->messageReporter->reportMessage( "\n " );
+ }
+
+ $this->messageReporter->reportMessage( "." );
+ $this->store->getObjectIds()->moveSMWPageID( $i );
+ }
+
+ $this->messageReporter->reportMessage( "\n ... done.\n" );
+ }
+
+ private function checkSortField( $log ) {
+
+ $connection = $this->store->getConnection( DB_MASTER );
+
+ $tableName = $connection->tableName( SQLStore::ID_TABLE );
+ $this->messageReporter->reportMessage( "Checking smw_sortkey, smw_sort fields ...\n" );
+
+ // #2429, copy smw_sortkey content to the new smw_sort field once
+ if ( isset( $log[$tableName]['smw_sort'] ) && $log[$tableName]['smw_sort'] === TableBuilder::PROC_FIELD_NEW ) {
+ $emptyField = 'smw_sort';
+ $copyField = 'smw_sortkey';
+
+ $this->messageReporter->reportMessage( " Table " . SQLStore::ID_TABLE . " ...\n" );
+ $this->messageReporter->reportMessage( " ... copying $copyField to $emptyField ... " );
+ $connection->query( "UPDATE $tableName SET $emptyField = $copyField", __METHOD__ );
+ $this->messageReporter->reportMessage( "done.\n" );
+ }
+
+ $this->messageReporter->reportMessage( " ... done.\n" );
+ }
+
+ private function updatePredefinedProperty( $property, $id ) {
+
+ $connection = $this->store->getConnection( DB_MASTER );
+
+ // Try to find the ID for a non-fixed predefined property
+ if ( $id === null ) {
+ $row = $connection->selectRow(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_id'
+ ],
+ [
+ 'smw_title' => $property->getKey(),
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_subobject' => ''
+ ],
+ __METHOD__
+ );
+
+ if ( $row !== false ) {
+ $id = $row->smw_id;
+ }
+ }
+
+ if ( $id === null ) {
+ return;
+ }
+
+ $label = $property->getCanonicalLabel();
+
+ $iw = $this->store->getObjectIds()->getPropertyInterwiki(
+ $property
+ );
+
+ $row = $connection->selectRow(
+ SQLStore::ID_TABLE,
+ [
+ 'smw_proptable_hash',
+ 'smw_hash'
+ ],
+ [
+ 'smw_id' => $id
+ ],
+ __METHOD__
+ );
+
+ if ( $row === false ) {
+ $row = (object)[ 'smw_proptable_hash' => null, 'smw_hash' => null ];
+ }
+
+ $connection->replace(
+ SQLStore::ID_TABLE,
+ [ 'smw_id' ],
+ [
+ 'smw_id' => $id,
+ 'smw_title' => $property->getKey(),
+ 'smw_namespace' => SMW_NS_PROPERTY,
+ 'smw_iw' => $iw,
+ 'smw_subobject' => '',
+ 'smw_sortkey' => $label,
+ 'smw_sort' => Collator::singleton()->getSortKey( $label ),
+ 'smw_proptable_hash' => $row->smw_proptable_hash,
+ 'smw_hash' => $row->smw_hash
+ ],
+ __METHOD__
+ );
+
+ if ( $id === null ) {
+ return;
+ }
+
+ $row = $connection->selectRow(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ [ 'p_id' ],
+ [ 'p_id' => $id ],
+ __METHOD__
+ );
+
+ // Entry is available therefore don't try to override the count
+ // value
+ if ( $row !== false ) {
+ return;
+ }
+
+ $connection->insert(
+ SQLStore::PROPERTY_STATISTICS_TABLE,
+ [
+ 'p_id' => $id,
+ 'usage_count' => 0,
+ 'null_count' => 0
+ ],
+ __METHOD__
+ );
+ }
+
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableSchemaManager.php b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableSchemaManager.php
new file mode 100644
index 00000000..16be15ca
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SQLStore/TableSchemaManager.php
@@ -0,0 +1,346 @@
+<?php
+
+namespace SMW\SQLStore;
+
+use SMW\SQLStore\TableBuilder\FieldType;
+use SMW\SQLStore\TableBuilder\Table;
+use SMWDataItem as DataItem;
+
+/**
+ * @private
+ *
+ * Database type agnostic table/schema definition manager
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class TableSchemaManager {
+
+ /**
+ * @var SQLStore
+ */
+ private $store;
+
+ /**
+ * @var MessageReporter
+ */
+ private $messageReporter;
+
+ /**
+ * @var Table[]
+ */
+ private $tables = [];
+
+ /**
+ * @var integer
+ */
+ private $featureFlags = false;
+
+ /**
+ * @since 2.5
+ *
+ * @param SQLStore $store
+ */
+ public function __construct( SQLStore $store ) {
+ $this->store = $store;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return string
+ */
+ public function getHash() {
+
+ $hash = [];
+
+ foreach ( $this->getTables() as $table ) {
+ $hash[$table->getName()] = $table->getHash();
+ }
+
+ // Avoid by-chance sorting with an eventual differing hash
+ sort( $hash );
+
+ return md5( json_encode( $hash ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $featureFlags
+ */
+ public function setFeatureFlags( $featureFlags ) {
+ $this->featureFlags = $featureFlags;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $feature
+ *
+ * @return boolean
+ */
+ public function hasFeatureFlag( $feature ) {
+ return ( (int)$this->featureFlags & $feature ) != 0;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $tableName
+ *
+ * @return Table|null
+ */
+ public function findTable( $tableName ) {
+
+ foreach ( $this->getTables() as $table ) {
+ if ( $table->getName() === $tableName ) {
+ return $table;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return Table[]
+ */
+ public function getTables() {
+
+ if ( $this->tables !== [] ) {
+ return $this->tables;
+ }
+
+ $this->addTable( $this->newEntityIdTable() );
+ $this->addTable( $this->newConceptCacheTable() );
+ $this->addTable( $this->newQueryLinksTable() );
+ $this->addTable( $this->newFulltextSearchTable() );
+ $this->addTable( $this->newPropertyStatisticsTable() );
+
+ foreach ( $this->store->getPropertyTables() as $propertyTable ) {
+
+ // Only extensions that aren't setup correctly can force an exception
+ // and to avoid a failure during setup, ensure that standard tables
+ // are correctly initialized otherwise SMW can't recover
+ try {
+ $diHandler = $this->store->getDataItemHandlerForDIType( $propertyTable->getDiType() );
+ } catch ( \Exception $e ) {
+ continue;
+ }
+
+ $this->addTable( $this->newPropertyTable( $propertyTable, $diHandler ) );
+ }
+
+ return $this->tables;
+ }
+
+ private function newEntityIdTable() {
+
+ // ID_TABLE
+ $table = new Table( SQLStore::ID_TABLE );
+
+ $table->addColumn( 'smw_id', FieldType::FIELD_ID_PRIMARY );
+ $table->addColumn( 'smw_namespace', [ FieldType::FIELD_NAMESPACE, 'NOT NULL' ] );
+ $table->addColumn( 'smw_title', [ FieldType::FIELD_TITLE, 'NOT NULL' ] );
+ $table->addColumn( 'smw_iw', [ FieldType::FIELD_INTERWIKI, 'NOT NULL' ] );
+ $table->addColumn( 'smw_subobject', [ FieldType::FIELD_TITLE, 'NOT NULL' ] );
+
+ $table->addColumn( 'smw_sortkey', [
+ $this->hasFeatureFlag( SMW_FIELDT_CHAR_NOCASE ) ? FieldType::TYPE_CHAR_NOCASE : FieldType::FIELD_TITLE,
+ 'NOT NULL'
+ ] );
+
+ $table->addColumn( 'smw_sort', [ FieldType::FIELD_TITLE ] );
+ $table->addColumn( 'smw_proptable_hash', FieldType::TYPE_BLOB );
+ $table->addColumn( 'smw_hash', FieldType::FIELD_HASH );
+ $table->addColumn( 'smw_rev', FieldType::FIELD_ID_UNSIGNED );
+
+ $table->addIndex( 'smw_id' );
+ $table->addIndex( 'smw_id,smw_sortkey' );
+ $table->addIndex( 'smw_hash,smw_id' );
+
+ // IW match lookup
+ $table->addIndex( 'smw_iw' );
+ $table->addIndex( 'smw_iw,smw_id' );
+
+ // ID lookup
+ $table->addIndex( 'smw_title,smw_namespace,smw_iw,smw_subobject' );
+
+ // InProperty lookup
+ // $table->addIndex( 'smw_iw,smw_id,smw_title,smw_sortkey,smw_sort' );
+
+ // Select by sortkey (range queries)
+ $table->addIndex( 'smw_sortkey' );
+
+ // Sort related indices, Store::getPropertySubjects (GROUP BY)
+ // $table->addIndex( 'smw_sort' );
+ $table->addIndex( 'smw_sort,smw_id' );
+
+ // API smwbrowse primary lookup
+ // SMW\MediaWiki\Api\Browse\ListLookup::fetchFromTable
+ $table->addIndex( 'smw_namespace,smw_sortkey' );
+
+ // Interfered with the API lookup index, couldn't find a use case
+ // that would require the this index
+ // $table->addIndex( 'smw_sort,smw_id,smw_iw' );
+
+ $table->addIndex( 'smw_rev,smw_id' );
+
+ return $table;
+ }
+
+ private function newConceptCacheTable() {
+
+ // CONCEPT_CACHE_TABLE (member elements (s)->concepts (o) )
+ $table = new Table( SQLStore::CONCEPT_CACHE_TABLE );
+
+ $table->addColumn( 's_id', [ FieldType::FIELD_ID, 'NOT NULL' ] );
+ $table->addColumn( 'o_id', [ FieldType::FIELD_ID, 'NOT NULL' ] );
+
+ $table->addIndex( 'o_id' );
+
+ return $table;
+ }
+
+ private function newQueryLinksTable() {
+
+ // QUERY_LINKS_TABLE
+ $table = new Table( SQLStore::QUERY_LINKS_TABLE );
+
+ $table->addColumn( 's_id', [ FieldType::FIELD_ID, 'NOT NULL' ] );
+ $table->addColumn( 'o_id', [ FieldType::FIELD_ID, 'NOT NULL' ] );
+
+ $table->addIndex( 's_id' );
+ $table->addIndex( 'o_id' );
+ $table->addIndex( 's_id,o_id' );
+
+ return $table;
+ }
+
+ private function newFulltextSearchTable() {
+
+ // FT_SEARCH_TABLE
+ // TEXT and BLOB is stored off the table with the table just having a pointer
+ // VARCHAR is stored inline with the table
+ $table = new Table( SQLStore::FT_SEARCH_TABLE );
+
+ $table->addColumn( 's_id', [ FieldType::FIELD_ID, 'NOT NULL' ] );
+ $table->addColumn( 'p_id', [ FieldType::FIELD_ID, 'NOT NULL' ] );
+ $table->addColumn( 'o_text', FieldType::TYPE_TEXT );
+ $table->addColumn( 'o_sort', FieldType::FIELD_TITLE );
+
+ $table->addIndex( 's_id' );
+ $table->addIndex( 'p_id' );
+ $table->addIndex( 'o_sort' );
+ $table->addIndex( [ 'o_text', 'FULLTEXT' ] );
+
+ $table->addOption(
+ 'fulltextSearchTableOptions',
+ $GLOBALS['smwgFulltextSearchTableOptions']
+ );
+
+ return $table;
+ }
+
+ private function newPropertyStatisticsTable() {
+
+ // PROPERTY_STATISTICS_TABLE
+ $table = new Table( SQLStore::PROPERTY_STATISTICS_TABLE );
+
+ $table->addColumn( 'p_id', FieldType::FIELD_ID );
+ $table->addColumn( 'usage_count', FieldType::FIELD_USAGE_COUNT );
+ $table->addColumn( 'null_count', FieldType::FIELD_USAGE_COUNT );
+
+ $table->addDefault( 'usage_count', 0 );
+ $table->addDefault( 'null_count', 0 );
+
+ $table->addIndex( [ 'p_id', 'UNIQUE INDEX' ] );
+ $table->addIndex( 'usage_count' );
+ $table->addIndex( 'null_count' );
+
+ return $table;
+ }
+
+ private function newPropertyTable( $propertyTable, $diHandler ) {
+
+ // Prepare indexes. By default, property-value tables
+ // have the following indexes:
+ //
+ // sp: getPropertyValues(), getSemanticData(), getProperties()
+ // po: ask, getPropertySubjects()
+ //
+ // The "p" component is omitted for tables with fixed property.
+ $indexes = [];
+ if ( $propertyTable->usesIdSubject() ) {
+ $fieldarray = [
+ 's_id' => [ FieldType::FIELD_ID, 'NOT NULL' ]
+ ];
+
+ $indexes['sp'] = 's_id';
+ } else {
+ $fieldarray = [
+ 's_title' => [ FieldType::FIELD_TITLE, 'NOT NULL' ],
+ 's_namespace' => [ FieldType::FIELD_NAMESPACE, 'NOT NULL' ]
+ ];
+
+ $indexes['sp'] = 's_title,s_namespace';
+ }
+
+ $indexes['po'] = $diHandler->getIndexField();
+
+ if ( !$propertyTable->isFixedPropertyTable() ) {
+ $fieldarray['p_id'] = [ FieldType::FIELD_ID, 'NOT NULL' ];
+ $indexes['sp'] = $indexes['sp'] . ',p_id';
+ }
+
+ // TODO Special handling; concepts should be handled differently
+ // in the future. See comments in SMW_DIHandler_Concept.php.
+ if ( $propertyTable->getDiType() === DataItem::TYPE_CONCEPT ) {
+ unset( $indexes['po'] );
+ }
+
+ foreach ( $diHandler->getTableIndexes() as $value ) {
+
+ if ( strpos( $value, 'p_id' ) !== false && $propertyTable->isFixedPropertyTable() ) {
+ continue;
+ }
+
+ if ( strpos( $value, 'o_id' ) !== false && !$propertyTable->usesIdSubject() ) {
+ continue;
+ }
+
+ if ( strpos( $value, 's_id' ) !== false && !$propertyTable->usesIdSubject() ) {
+ continue;
+ }
+
+ $indexes = array_merge( $indexes, [ $value ] );
+ }
+
+ $indexes = array_unique( $indexes );
+
+ foreach ( $diHandler->getTableFields() as $fieldname => $fieldType ) {
+ $fieldarray[$fieldname] = $fieldType;
+ }
+
+ $table = new Table( $propertyTable->getName() );
+
+ foreach ( $fieldarray as $fieldName => $fieldType ) {
+ $table->addColumn( $fieldName, $fieldType );
+ }
+
+ foreach ( $indexes as $key => $index ) {
+ $table->addIndex( $index, $key );
+ }
+
+ return $table;
+ }
+
+ private function addTable( Table $table ) {
+ $this->tables[] = $table;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/Content.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/Content.php
new file mode 100644
index 00000000..e5d2a283
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/Content.php
@@ -0,0 +1,275 @@
+<?php
+
+namespace SMW\Schema\Content;
+
+use SMW\Schema\SchemaFactory;
+use SMW\Schema\Exception\SchemaTypeNotFoundException;
+use SMW\Schema\Schema;
+use SMW\ParserData;
+use Symfony\Component\Yaml\Yaml;
+use Symfony\Component\Yaml\Exception\ParseException;
+use JsonContent;
+use Title;
+use User;
+use ParserOptions;
+use ParserOutput;
+use Html;
+
+/**
+ * The content model supports both JSON and YAML (as a superset of JSON), allowing
+ * for its content to be represented in JSON when required while a user may choose
+ * YAML to edit/store the native content (due to improve readability or
+ * aid others with additional inline comments).
+ *
+ * Comments (among other elements) will not be represented in JSON output when
+ * requested by the `Content::toJson` method.
+ *
+ * @see https://en.wikipedia.org/wiki/YAML#Comparison_with_JSON
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Content extends JsonContent {
+
+ /**
+ * @var SchemaFactory
+ */
+ private $schemaFactory;
+
+ /**
+ * @var ContentFormatter
+ */
+ private $contentFormatter;
+
+ /**
+ * @var array
+ */
+ private $parse;
+
+ /**
+ * @var boolean
+ */
+ private $isYaml = false;
+
+ /**
+ * @var boolean
+ */
+ private $isValid;
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function __construct( $text ) {
+ parent::__construct( $text, CONTENT_MODEL_SMW_SCHEMA );
+ }
+
+ /**
+ * `Content::getNativeData` will return the "native" text representation which
+ * in case of YAML is just the text and not a JSON string. Therefore
+ * `getNativeData` preserves the original user input.
+ *
+ * Instead, use this method to retrieve a JSON compatible string for both
+ * JSON and YAML for when the data is valid.
+ *
+ * @since 3.0
+ *
+ * @return null|string
+ */
+ public function toJson() {
+
+ if ( $this->isValid() ) {
+ return json_encode( $this->parse );
+ }
+
+ return null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean
+ */
+ public function isYaml() {
+
+ if ( $this->isValid() ) {
+ return $this->isYaml;
+ }
+
+ return false;
+ }
+
+ /**
+ * @see Content::isValid
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function isValid() {
+
+ if ( $this->isValid === null ) {
+ $this->decode_content();
+ }
+
+ return $this->isValid;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function fillParserOutput( Title $title, $revId, ParserOptions $options, $generateHtml, ParserOutput &$output ) {
+
+ if ( !$generateHtml || !$this->isValid() ) {
+ return;
+ }
+
+ $this->initServices();
+
+ $output->addModuleStyles(
+ $this->contentFormatter->getModuleStyles()
+ );
+
+ $parserData = new ParserData( $title, $output );
+ $schema = null;
+
+ try {
+ $schema = $this->schemaFactory->newSchema(
+ $title->getDBKey(),
+ $this->toJson()
+ );
+ } catch ( SchemaTypeNotFoundException $e ) {
+
+ $this->contentFormatter->setUnknownType(
+ $e->getType()
+ );
+
+ $output->setText(
+ $this->contentFormatter->getText( $this->mText, $this->isYaml )
+ );
+
+ $parserData->addError(
+ [ [ 'smw-schema-error-type-unknown', $e->getType() ] ]
+ );
+
+ $parserData->copyToParserOutput();
+ }
+
+ if ( $schema === null ) {
+ return ;
+ }
+
+ $output->setIndicator(
+ 'mw-helplink',
+ $this->contentFormatter->getHelpLink( $schema )
+ );
+
+ $errors = $this->schemaFactory->newSchemaValidator()->validate(
+ $schema
+ );
+
+ $this->contentFormatter->setType(
+ $this->schemaFactory->getType( $schema->get( 'type' ) )
+ );
+
+ $output->setText(
+ $this->contentFormatter->getText( $this->mText, $this->isYaml, $schema, $errors )
+ );
+
+ foreach ( $errors as $error ) {
+ if ( isset( $error['property'] ) && isset( $error['message'] ) ) {
+ $parserData->addError(
+ [ ['smw-schema-error-violation', $error['property'], $error['message'] ] ]
+ );
+ }
+ }
+
+ $parserData->copyToParserOutput();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+ // FIXME: WikiPage::doEditContent invokes PST before validation. As such, native data
+ // may be invalid (though PST result is discarded later in that case).
+ if ( !$this->isValid() ) {
+ return $this;
+ }
+
+ if ( !$this->isYaml ) {
+ $text = self::normalizeLineEndings(
+ json_encode( $this->parse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
+ );
+ } else {
+ $text = self::normalizeLineEndings( $this->mText );
+ }
+
+ return new static( $text );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param SchemaFactory $schemaFactory
+ * @param ContentFormatter $contentFormatter
+ */
+ public function setServices( SchemaFactory $schemaFactory, ContentFormatter $contentFormatter ) {
+ $this->schemaFactory = $schemaFactory;
+ $this->contentFormatter = $contentFormatter;
+ }
+
+ /**
+ * @see TextContent::normalizeLineEndings (MW 1.28+)
+ *
+ * @param $text
+ * @return string
+ */
+ public static function normalizeLineEndings( $text ) {
+ return str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) );
+ }
+
+ private function initServices() {
+
+ if ( $this->schemaFactory === null ) {
+ $this->schemaFactory = new SchemaFactory();
+ }
+
+ if ( $this->contentFormatter === null ) {
+ $this->contentFormatter = new ContentFormatter();
+ }
+ }
+
+ private function decode_content() {
+
+ // Support either JSON or YAML, if the class is available! Do a quick
+ // check on `{ ... }` to decide whether it is a non-JSON string.
+ if ( $this->mText !== '' && $this->mText[0] !== '{' && substr( $this->mText, -1 ) !== '}' && class_exists( '\Symfony\Component\Yaml\Yaml' ) ) {
+
+ try {
+ $this->parse = Yaml::parse( $this->mText );
+ $this->isYaml = true;
+ } catch ( ParseException $e ) {
+ $this->isYaml = false;
+ $this->parse = null;
+ }
+
+ return $this->isValid = $this->isYaml;
+ } elseif ( $this->mText !== '' ) {
+
+ // Note that this parses it without casting objects to associative arrays.
+ // Objects and arrays are kept as distinguishable types in the PHP values.
+ $this->parse = json_decode( $this->mText );
+ $this->isValid = json_last_error() === JSON_ERROR_NONE;
+
+ return $this->isValid;
+ }
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/ContentFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/ContentFormatter.php
new file mode 100644
index 00000000..ef004b61
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/ContentFormatter.php
@@ -0,0 +1,270 @@
+<?php
+
+namespace SMW\Schema\Content;
+
+use SMW\Schema\Schema;
+use SMW\Schema\SchemaFactory;
+use SMW\Message;
+use SMWInfolink as Infolink;
+use Onoi\CodeHighlighter\Highlighter as CodeHighlighter;
+use Onoi\CodeHighlighter\Geshi;
+use Html;
+use Title;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ContentFormatter {
+
+ /**
+ * @var HtmlBuilder
+ */
+ private $htmlBuilder;
+
+ /**
+ * @var []
+ */
+ private $type = [];
+
+ /**
+ * @var string|null
+ */
+ private $unknownType = false;
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function __construct() {
+ $this->htmlBuilder = new HtmlBuilder();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function setType( $type ) {
+ $this->type = $type;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getModuleStyles() {
+ return [ 'mediawiki.helplink', 'smw.content.schema', 'mediawiki.content.json' ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Schema $schema
+ *
+ * @return string
+ */
+ public function getHelpLink( Schema $schema ) {
+
+ $key = [
+ 'smw-schema-type-help-link',
+ $schema->get( Schema::SCHEMA_TYPE )
+ ];
+
+ $params = [
+ 'href' => $this->msg( $key )
+ ];
+
+ return $this->htmlBuilder->build( 'schema_help_link', $params );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ */
+ public function setUnknownType( $type ) {
+ $this->unknownType = $type;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Schema $schema
+ *
+ * @return string
+ */
+ public function getText( $text, $isYaml = false, Schema $schema = null, array $errors = [] ) {
+
+ $methods = [
+ 'head' => [ $schema, $errors ],
+ 'body' => [ $text, $isYaml ],
+ 'footer' => [ $schema ]
+ ];
+
+ $html = '';
+
+ if ( $this->unknownType !== false ) {
+ $html = $this->unknown_type( $this->unknownType );
+ }
+
+ foreach ( $methods as $method => $element ) {
+ $html .= $this->{$method}( ...$element );
+ }
+
+ return $html;
+ }
+
+ private function head( $schema, array $errors ) {
+
+ if ( $schema === null ) {
+ return '';
+ }
+
+ $schema_link = str_replace( '.json', '', substr(
+ $schema->getValidationSchema(),
+ strrpos( $schema->getValidationSchema(), '/' ) + 1
+ ) );
+
+ $errorCount = count( $errors );
+ $error = $this->error_text( $schema_link, $errors );
+
+ $type = $schema->get( 'type', '' );
+ $description = '';
+
+ if ( isset( $this->type['type_description'] ) ) {
+ $description = $this->msg( $this->type['type_description'], Message::PARSE );
+ }
+
+ $params = [
+ 'link' => '',
+ 'description' => $schema->get( Schema::SCHEMA_DESCRIPTION, '' ),
+ 'type_description' => $description,
+ 'schema-title' => $this->msg( 'smw-schema-title' ),
+ 'error' => $error,
+ 'error-title' => $this->msg( [ 'smw-schema-error', $errorCount ] )
+ ];
+
+ return $this->htmlBuilder->build( 'schema_head', $params );
+ }
+
+ private function body( $text, $isYaml ) {
+
+ $codeHighlighter = null;
+
+ if ( class_exists( '\Onoi\CodeHighlighter\Highlighter' ) ) {
+ $codeHighlighter = new CodeHighlighter();
+
+ // `yaml` works well enough for both JSON and YAML
+ $codeHighlighter->setLanguage( 'yaml' );
+ $codeHighlighter->addOption( Geshi::SET_OVERALL_CLASS, 'content-highlight' );
+ }
+
+ if ( $codeHighlighter !== null && $isYaml ) {
+ $text = $codeHighlighter->highlight( $text );
+ } elseif ( $codeHighlighter !== null ) {
+ $codeHighlighter->addOption( Geshi::SET_STRINGS_STYLE, 'color: #000' );
+ $text = $codeHighlighter->highlight( $text );
+ } else {
+ if ( !$isYaml ) {
+ $text = json_encode( json_decode( $text ), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+ }
+
+ $text = Html::rawElement( 'pre', [ 'class' => 'content-no-highlight' ], $text );
+ }
+
+ $params = [
+ 'text' => $text,
+ 'unknown_type' => $this->unknownType
+ ];
+
+ return $this->htmlBuilder->build( 'schema_body', $params );
+ }
+
+ private function footer( $schema ) {
+
+ if ( $schema === null ) {
+ return '';
+ }
+
+ $tags = [];
+
+ if ( ( $tags = $schema->get( Schema::SCHEMA_TAG, [] ) ) !== [] ) {
+ foreach ( $tags as $k => $tag ) {
+ $tags[$k] = Infolink::newPropertySearchLink( $tag, 'Schema tag', $tag, '' )->getHtml();
+ }
+ }
+
+ $type = $schema->get( 'type', '' );
+ $link = Infolink::newPropertySearchLink( $type, 'Schema type', $type, '' );
+
+ $params = [
+ 'href_type' => Title::newFromText( 'Schema type', SMW_NS_PROPERTY )->getLocalUrl(),
+ 'msg_type' => $this->msg( [ 'smw-schema-type' ] ),
+ 'link_type' => $link->getHtml(),
+ 'href_tag' => Title::newFromText( 'Schema tag', SMW_NS_PROPERTY )->getLocalUrl(),
+ 'msg_tag' => $this->msg( [ 'smw-schema-tag', count( $tags ) ] ),
+ 'tags' => $tags
+ ];
+
+ return $this->htmlBuilder->build( 'schema_footer', $params );
+ }
+
+ private function error_text( $validator_schema, array $errors = [] ) {
+
+ if ( $errors === [] ) {
+ return '';
+ }
+
+ $list = [];
+
+ foreach ( $errors as $error ) {
+
+ if ( !isset( $error['property'] ) ) {
+ continue;
+ }
+
+ $params = [
+ 'msg' => $error['message'],
+ 'text' => $error['property']
+ ];
+
+ $list[] = $this->htmlBuilder->build( 'schema_error', $params );
+ }
+
+ if ( $list === [] ) {
+ return '';
+ }
+
+ $params = [
+ 'list' => $list,
+ 'schema' => $this->msg( [ 'smw-schema-error-schema', $validator_schema ], Message::PARSE )
+ ];
+
+ return $this->htmlBuilder->build( 'schema_error_text', $params );
+ }
+
+ private function unknown_type( $type ) {
+
+ if ( $type === '' || $type === null ) {
+ $key = 'smw-schema-error-type-missing';
+ } else {
+ $key = [ 'smw-schema-error-type-unknown', $type ];
+ }
+
+ $params = [
+ 'msg' => $this->msg( $key, Message::PARSE )
+ ];
+
+ return $this->htmlBuilder->build( 'schema_unknown_type', $params );
+ }
+
+ private function msg( $key, $type = Message::TEXT, $lang = Message::USER_LANGUAGE ) {
+ return Message::get( $key, $type, $lang );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/ContentHandler.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/ContentHandler.php
new file mode 100644
index 00000000..82a4ce4e
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/ContentHandler.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace SMW\Schema\Content;
+
+use JsonContentHandler;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ContentHandler extends JsonContentHandler {
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function __construct() {
+ parent::__construct( CONTENT_MODEL_SMW_SCHEMA, [ CONTENT_FORMAT_JSON ] );
+ }
+
+ /**
+ * Returns true, because wikitext supports caching using the
+ * ParserCache mechanism.
+ *
+ * @since 1.21
+ *
+ * @return bool Always true.
+ *
+ * @see ContentHandler::isParserCacheSupported
+ */
+ public function isParserCacheSupported() {
+ return true;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ protected function getContentClass() {
+ return Content::class;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function supportsSections() {
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function supportsCategories() {
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function supportsRedirects() {
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/HtmlBuilder.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/HtmlBuilder.php
new file mode 100644
index 00000000..c014ecf8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Content/HtmlBuilder.php
@@ -0,0 +1,217 @@
+<?php
+
+namespace SMW\Schema\Content;
+
+use Html;
+use SMW\Utils\HtmlTabs;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HtmlBuilder {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param array $params
+ *
+ * @return string
+ */
+ public function build( $key, array $params ) {
+ return $this->{$key}( $params );
+ }
+
+ private function schema_head( $params ) {
+
+ $list = [];
+ $text = '';
+ $type_description = '';
+
+ if ( $params['link'] !== '' ) {
+ $list[] = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'plainlinks'
+ ],
+ $params['link']
+ );
+ }
+
+ if ( isset( $params['type_description'] ) ) {
+ $type_description .= Html::rawElement(
+ 'p',
+ [
+ 'class' => 'smw-schema-type-description plainlinks'
+
+ ],
+ $params['type_description']
+ );
+ }
+
+ if ( $params['description'] !== '' ) {
+ $type_description .= Html::rawElement(
+ 'p',
+ [
+ 'class' => 'smw-schema-description plainlinks'
+ ],
+ $params['description']
+ );
+ }
+
+ $htmlTabs = new HtmlTabs();
+
+ $htmlTabs->setActiveTab(
+ $params['error'] !== '' ? 'schema-error' : 'schema-summary'
+ );
+
+ $htmlTabs->tab( 'schema-summary', $params['schema-title'] );
+ $htmlTabs->tab(
+ 'schema-error',
+ $params['error-title'],
+ [
+ 'hide' => $params['error'] === '', 'class' => 'error-label'
+ ]
+ );
+
+ $htmlTabs->content( 'schema-summary', $text );
+ $htmlTabs->content( 'schema-error', $params['error'] );
+
+ $html = $htmlTabs->buildHTML(
+ [ 'class' => 'smw-schema' ]
+ );
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'schema-head'
+ ],
+ $type_description . $html
+ );
+ }
+
+ private function schema_body( $params ) {
+
+ $class = $params['unknown_type'] !== false ? ' unknown-type' : '';
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'schema-body' . $class
+ ],
+ $params['text']
+ );
+ }
+
+ private function schema_error_text( $params ) {
+
+ $html = Html::rawElement(
+ 'ul',
+ [
+ 'class' => 'smw-schema-validation-error-list'
+ ],
+ '<li>' . implode( '</li><li>', $params['list'] ) . '</li>'
+ );
+
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-schema-validation-error'
+ ],
+ $params['schema']
+ ) . $html;
+
+ return $html;
+ }
+
+ private function schema_error( $params ) {
+
+ $html = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'schema-error'
+ ],
+ $params['text']
+ );
+
+ return $html . '&nbsp;' . Html::rawElement(
+ 'span',
+ [],
+ ":&nbsp;" . $params['msg']
+ );
+ }
+
+ private function schema_footer( $params ) {
+
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'schema-tags'
+ ],
+ Html::rawElement(
+ 'div',
+ [],
+ Html::rawElement(
+ 'a',
+ [
+ 'href' => $params['href_type']
+ ],
+ $params['msg_type']
+ ) . ':&nbsp;' . $params['link_type']
+ )
+ );
+
+ if ( $params['tags'] !== [] ) {
+ $html .= Html::rawElement(
+ 'div',
+ [
+ 'class' => 'schema-tags'
+ ],
+ Html::rawElement(
+ 'div',
+ [],
+ Html::rawElement(
+ 'a',
+ [
+ 'href' => $params['href_tag']
+ ],
+ $params['msg_tag']
+ ) . ':' . '<ul><li>' . implode( '</li><li>', $params['tags'] ) . '</li></ul>'
+ )
+ );
+ }
+
+ return Html::rawElement(
+ 'div',
+ [
+ 'class' => 'schema-footer'
+ ],
+ $html
+ );
+ }
+
+ private function schema_unknown_type( $params ) {
+ return Html::rawElement(
+ 'p',
+ [
+ 'class' => 'smw-callout smw-callout-error plainlinks'
+ ],
+ $params['msg']
+ );
+ }
+
+ private function schema_help_link( $params ) {
+ return Html::rawElement(
+ 'a',
+ [
+ 'href' => $params['href'],
+ 'target' => '_blank',
+ 'class' => 'mw-helplink',
+ ]
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/Exception/SchemaConstructionFailedException.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Exception/SchemaConstructionFailedException.php
new file mode 100644
index 00000000..fc452f4f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Exception/SchemaConstructionFailedException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace SMW\Schema\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SchemaConstructionFailedException extends RuntimeException {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ */
+ public function __construct( $type ) {
+ parent::__construct( "$type couldn't construct a Schema instance!" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/Exception/SchemaTypeNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Exception/SchemaTypeNotFoundException.php
new file mode 100644
index 00000000..9f5b67e3
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Exception/SchemaTypeNotFoundException.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace SMW\Schema\Exception;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SchemaTypeNotFoundException extends RuntimeException {
+
+ /**
+ * @var string
+ */
+ private $type = '';
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ */
+ public function __construct( $type ) {
+ parent::__construct( "$type is an unrecognized schema type." );
+ $this->type = $type;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/README.md b/www/wiki/extensions/SemanticMediaWiki/src/Schema/README.md
new file mode 100644
index 00000000..0dfaf12b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/README.md
@@ -0,0 +1,26 @@
+The objective of the `SMW_NS_SCHEMA` (aka Schema) namespace is to allow for a structured definition of different schemata where types define the interpreter, syntax elements, and constraints.
+
+The namespace expects a JSON format (or if available, YAML as superset of JSON) as input format to ensure that content elements are structured and a validation a [JSON schema][json:schema] can help enforce requirements and constraints for a specific type.
+
+The following properties are provided to make elements of a schema definition discoverable.
+
+* Schema type (`_SCHEMA_TYPE` )
+* Schema definition (`_SCHEMA_DEF`)
+* Schema description (`_SCHEMA_DESC`)
+* Schema tag (`_SCHEMA_TAG`)
+* Schema link (`_SCHEMA_LINK`)
+
+## Registration
+
+Extensibility for new schema types and interpreters is provided by adding a type to the `$smwgSchemaTypes` setting.
+
+<pre>
+$GLOBALS['smwgSchemaTypes'] = [
+ 'LINK_FORMAT_schema' => [
+ 'validator_schema => __DIR__ . '/data/schema/...',
+ 'group' => SMW_schema_GROUP_FORMAT,
+ ]
+];
+</pre>
+
+[json:schema]: http://json-schema.org/
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/Schema.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Schema.php
new file mode 100644
index 00000000..0bd5d218
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/Schema.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace SMW\Schema;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+interface Schema {
+
+ const SCHEMA_TYPE = 'type';
+ const SCHEMA_DESCRIPTION = 'description';
+ const SCHEMA_TAG = 'tags';
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function get( $key, $default = null );
+
+ /**
+ * Returns the name of the schema which is equivalent with the page name
+ * without the namespace prefix.
+ *
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getValidationSchema();
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaDefinition.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaDefinition.php
new file mode 100644
index 00000000..e32443e8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaDefinition.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace SMW\Schema;
+
+use JsonSerializable;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SchemaDefinition implements Schema, JsonSerializable {
+
+ /**
+ * @var string
+ */
+ private $name;
+
+ /**
+ * @var array
+ */
+ protected $definition = [];
+
+ /**
+ * @var string|null
+ */
+ private $validation_schema;
+
+ /**
+ * @since 3.0
+ *
+ * @param string $name
+ * @param array $definition
+ * @param string|null $validation_schema
+ */
+ public function __construct( $name, array $definition, $validation_schema = null ) {
+ $this->name = $name;
+ $this->definition = $definition;
+ $this->validation_schema = $validation_schema;
+ }
+
+ /**
+ * @see Schema::get
+ * @since 3.0
+ *
+ * @return mixed|null
+ */
+ public function get( $key, $default = null ) {
+ return $this->digDeep( $this->definition, $key, $default );
+ }
+
+ /**
+ * @see Schema::getName
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getName() {
+ return str_replace( '_', ' ', $this->name );
+ }
+
+ /**
+ * @see Schema::getValidationSchema
+ * @since 3.0
+ *
+ * @return string|null
+ */
+ public function getValidationSchema() {
+ return $this->validation_schema;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function jsonSerialize() {
+ return json_encode( $this->definition );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->jsonSerialize();
+ }
+
+ private function digDeep( $array, $key, $default ) {
+
+ if ( strpos( $key, '.' ) !== false ) {
+ $list = explode( '.', $key, 2 );
+
+ foreach ( $list as $k => $v ) {
+ if ( isset( $array[$v] ) ) {
+ return $this->digDeep( $array[$v], $list[$k+1], $default );
+ }
+ }
+ }
+
+ if ( isset( $array[$key] ) ) {
+ return $array[$key];
+ }
+
+ return $default;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaFactory.php
new file mode 100644
index 00000000..0dd13dad
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaFactory.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace SMW\Schema;
+
+use RuntimeException;
+use SMW\ApplicationFactory;
+use SMW\Schema\Exception\SchemaTypeNotFoundException;
+use SMW\Schema\Exception\SchemaConstructionFailedException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SchemaFactory {
+
+ /**
+ * @var []
+ */
+ private $schemaTypes = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param array $schemaTypes
+ */
+ public function __construct( array $schemaTypes = [] ) {
+ $this->schemaTypes = $schemaTypes;
+
+ if ( $this->schemaTypes === [] ) {
+ $this->schemaTypes = $GLOBALS['smwgSchemaTypes'];
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return []
+ */
+ public function getType( $type ) {
+ return isset( $this->schemaTypes[$type] ) ? $this->schemaTypes[$type] : [];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $type
+ *
+ * @return boolean
+ */
+ public function isRegisteredType( $type ) {
+ return isset( $this->schemaTypes[$type] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getRegisteredTypes() {
+ return array_keys( $this->schemaTypes );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|array $group
+ *
+ * @return []
+ */
+ public function getRegisteredTypesByGroup( $group ) {
+
+ $registeredTypes = [];
+ $groups = (array)$group;
+
+ foreach ( $this->schemaTypes as $type => $val ) {
+ if ( isset( $val['group'] ) && in_array( $val['group'], $groups ) ) {
+ $registeredTypes[] = $type;
+ }
+ }
+
+ return $registeredTypes;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $name
+ * @param array|string $data
+ *
+ * @return Schema
+ * @throws RuntimeException
+ */
+ public function newSchema( $name, $data ) {
+
+ if ( is_string( $data ) ) {
+ if ( ( $data = json_decode( $data, true ) ) === null || json_last_error() !== JSON_ERROR_NONE ) {
+ throw new RuntimeException( "Invalid JSON format." );
+ }
+ }
+
+ $type = null;
+ $validation_schema = null;
+
+ if ( isset( $data['type'] ) ) {
+ $type = $data['type'];
+ }
+
+ if ( !isset( $this->schemaTypes[$type] ) ) {
+ throw new SchemaTypeNotFoundException( $type );
+ }
+
+ if ( isset( $this->schemaTypes[$type]['validation_schema'] ) ) {
+ $validation_schema = $this->schemaTypes[$type]['validation_schema'];
+ }
+
+ if ( isset( $this->schemaTypes[$type]['__factory'] ) && is_callable( $this->schemaTypes[$type]['__factory'] ) ) {
+ $schema = $this->schemaTypes[$type]['__factory']( $name, $data );
+ } else {
+ $schema = new SchemaDefinition( $name, $data, $validation_schema );
+ }
+
+ if ( !$schema instanceof Schema ) {
+ throw new SchemaConstructionFailedException( $type );
+ }
+
+ return $schema;
+ }
+
+ public static function newTest( $name, $data ) {
+ return '';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return SchemaValidator
+ */
+ public function newSchemaValidator() {
+ return new SchemaValidator(
+ ApplicationFactory::getInstance()->create( 'JsonSchemaValidator' )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaValidator.php
new file mode 100644
index 00000000..a37094f8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Schema/SchemaValidator.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace SMW\Schema;
+
+use SMW\Utils\JsonSchemaValidator;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class SchemaValidator {
+
+ /**
+ * @var JsonSchemaValidator
+ */
+ private $validator;
+
+ /**
+ * @since 3.0
+ *
+ * @param JsonSchemaValidator $validator
+ */
+ public function __construct( JsonSchemaValidator $validator ) {
+ $this->validator = $validator;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param Schema|null $schema
+ *
+ * @return []
+ */
+ public function validate( Schema $schema = null ) {
+
+ if ( $schema === null || !is_string( $schema->getValidationSchema() ) ) {
+ return [];
+ }
+
+ $this->validator->validate(
+ $schema,
+ $schema->getValidationSchema()
+ );
+
+ if ( $this->validator->isValid() ) {
+ return [];
+ }
+
+ return $this->validator->getErrors();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/SerializerFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/SerializerFactory.php
new file mode 100644
index 00000000..56c322d6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/SerializerFactory.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace SMW;
+
+use Deserializers\Deserializer;
+use OutOfBoundsException;
+use Serializers\Serializer;
+use SMW\Deserializers\ExpDataDeserializer;
+use SMW\Deserializers\SemanticDataDeserializer;
+use SMW\Serializers\ExpDataSerializer;
+use SMW\Serializers\QueryResultSerializer;
+use SMW\Serializers\SemanticDataSerializer;
+use SMWExpData as ExpData;
+use SMWQueryResult as QueryResult;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class SerializerFactory {
+
+ /**
+ * Method that assigns registered serializers to an object
+ *
+ * @since 2.2
+ *
+ * @param mixed $object
+ *
+ * @return Serializer
+ */
+ public function getSerializerFor( $object ) {
+
+ $serializer = null;
+
+ if ( $object instanceof SemanticData ) {
+ $serializer = $this->newSemanticDataSerializer();
+ } elseif ( $object instanceof QueryResult ) {
+ $serializer = $this->newQueryResultSerializer();
+ } elseif ( $object instanceof ExpData ) {
+ $serializer = $this->newExpDataSerializer();
+ }
+
+ if ( !$serializer instanceof Serializer ) {
+ throw new OutOfBoundsException( 'No serializer can be matched to the object' );
+ }
+
+ return $serializer;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param array $serialization
+ *
+ * @return Deserializer
+ */
+ public function getDeserializerFor( array $serialization ) {
+
+ $deserializer = null;
+
+ if ( isset( $serialization['serializer'] ) ) {
+
+ switch ( $serialization['serializer'] ) {
+ case 'SMW\Serializers\SemanticDataSerializer':
+ $deserializer = $this->newSemanticDataDeserializer();
+ break;
+ case 'SMW\Serializers\ExpDataSerializer':
+ $deserializer = $this->newExpDataDeserializer();
+ break;
+ }
+ }
+
+ if ( !$deserializer instanceof Deserializer ) {
+ throw new OutOfBoundsException( 'No deserializer can be matched to the serialization format' );
+ }
+
+ return $deserializer;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return SemanticDataSerializer
+ */
+ public function newSemanticDataSerializer() {
+ return new SemanticDataSerializer();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return SemanticDataDeserializer
+ */
+ public function newSemanticDataDeserializer() {
+ return new SemanticDataDeserializer();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return QueryResultSerializer
+ */
+ public function newQueryResultSerializer() {
+ return new QueryResultSerializer();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return ExpDataSerializer
+ */
+ public function newExpDataSerializer() {
+ return new ExpDataSerializer();
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @return ExpDataDeserializer
+ */
+ public function newExpDataDeserializer() {
+ return new ExpDataDeserializer();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Serializers/ExpDataSerializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Serializers/ExpDataSerializer.php
new file mode 100644
index 00000000..1f3a031d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Serializers/ExpDataSerializer.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace SMW\Serializers;
+
+use OutOfBoundsException;
+use Serializers\Serializer;
+use SMW\Exporter\Element\ExpElement;
+use SMWExpData as ExpData;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class ExpDataSerializer implements Serializer {
+
+ /**
+ * @see Serializer::serialize
+ *
+ * @since 2.2
+ */
+ public function serialize( $expData ) {
+
+ if ( !$expData instanceof ExpData ) {
+ throw new OutOfBoundsException( 'Object is not supported' );
+ }
+
+ return $this->doSerialize( $expData ) + [ 'serializer' => __CLASS__, 'version' => 0.1 ];
+ }
+
+ private function doSerialize( $expData ) {
+
+ $serialization = [
+ 'subject' => $expData->getSubject()->getSerialization()
+ ];
+
+ $properties = [];
+
+ foreach ( $expData->getProperties() as $property ) {
+ $properties[$property->getUri()] = [
+ 'property' => $property->getSerialization(),
+ 'children' => $this->doSerializeChildren( $expData->getValues( $property ) )
+ ];
+ }
+
+ return $serialization + [ 'data' => $properties ];
+ }
+
+ private function doSerializeChildren( array $elements ) {
+
+ $children = [];
+
+ if ( $elements === [] ) {
+ return $children;
+ }
+
+ foreach ( $elements as $element ) {
+ $children[] = $element instanceof ExpElement ? $element->getSerialization() : $this->doSerialize( $element );
+ }
+
+ return $children;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Serializers/FlatSemanticDataSerializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Serializers/FlatSemanticDataSerializer.php
new file mode 100644
index 00000000..30f298a2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Serializers/FlatSemanticDataSerializer.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace SMW\Serializers;
+
+/**
+ * Only returns the head of the subobject without serializing associated
+ * dataItems.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class FlatSemanticDataSerializer extends SemanticDataSerializer {
+
+ /**
+ * @see SemanticDataSerializer::doSerializeSubobject
+ *
+ * @return array
+ */
+ protected function doSerializeSubSemanticData( $subSemanticData ) {
+
+ $subobjects = [];
+
+ foreach ( $subSemanticData as $semanticData ) {
+ $subobjects[] = $semanticData->getSubject()->getSerialization();
+ }
+
+ return $subobjects;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Serializers/QueryResultSerializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Serializers/QueryResultSerializer.php
new file mode 100644
index 00000000..ecba7d30
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Serializers/QueryResultSerializer.php
@@ -0,0 +1,290 @@
+<?php
+
+namespace SMW\Serializers;
+
+use OutOfBoundsException;
+use Serializers\DispatchableSerializer;
+use SMW\DataValueFactory;
+use SMW\Query\PrintRequest;
+use SMWDataItem as DataItem;
+use SMWQueryResult as QueryResult;
+use SMWResultArray;
+use Title;
+
+/**
+ * Class for serializing SMWDataItem and SMWQueryResult objects to a context
+ * independent object consisting of arrays and associative arrays, which can
+ * be fed directly to json_encode, the MediaWiki API, and similar serializers.
+ *
+ * This class is distinct from SMWSerializer and the SMWExpData object
+ * it takes, in that here semantic context is lost.
+ *
+ * @ingroup Serializers
+ *
+ * @licence GNU GPL v2+
+ * @since 1.7
+ *
+ * @author Jeroen De Dauw < jeroendedauw@gmail.com >
+ */
+class QueryResultSerializer implements DispatchableSerializer {
+
+ /**
+ * @var integer
+ */
+ private static $version = 2;
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $version
+ */
+ public function version( $version ) {
+ self::$version = (int)$version;
+ }
+
+ /**
+ * @see SerializerInterface::serialize
+ *
+ * @since 1.9
+ *
+ * @return array
+ * @throws OutOfBoundsException
+ */
+ public function serialize( $queryResult ) {
+
+ if ( !( $this->isSerializerFor( $queryResult ) ) ) {
+ throw new OutOfBoundsException( 'Object was not identified as a QueryResult instance' );
+ }
+
+ return $this->getSerializedQueryResult( $queryResult ) + [ 'serializer' => __CLASS__, 'version' => self::$version ];
+ }
+
+ /**
+ * @see Serializers::isSerializerFor
+ *
+ * @since 1.9
+ */
+ public function isSerializerFor( $queryResult ) {
+ return $queryResult instanceof QueryResult;
+ }
+
+ /**
+ * Get the serialization for the provided data item.
+ *
+ * @since 1.7
+ *
+ * @param SMWDataItem $dataItem
+ *
+ * @return mixed
+ */
+ public static function getSerialization( DataItem $dataItem, $printRequest = null ) {
+ switch ( $dataItem->getDIType() ) {
+ case DataItem::TYPE_WIKIPAGE:
+
+ // Support for a deserializable _rec type with 0.6
+ if ( $printRequest !== null && strpos( $printRequest->getTypeID(), '_rec' ) !== false ) {
+ $recordValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem,
+ $printRequest->getData()->getDataItem()
+ );
+
+ $recordDiValues = [];
+
+ foreach ( $recordValue->getPropertyDataItems() as $property ) {
+ $label = $property->getLabel();
+
+ $recordDiValues[$label] = [
+ 'label' => $label,
+ 'key' => $property->getKey(),
+ 'typeid' => $property->findPropertyTypeID(),
+ 'item' => []
+ ];
+
+ foreach ( $recordValue->getDataItem()->getSemanticData()->getPropertyValues( $property ) as $value ) {
+
+ if ( $property->findPropertyTypeID() === '_qty' ) {
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem( $value, $property );
+
+ $recordDiValues[$label]['item'][] = [
+ 'value' => $dataValue->getNumber(),
+ 'unit' => $dataValue->getUnit()
+ ];
+ } else {
+ $recordDiValues[$label]['item'][] = self::getSerialization( $value );
+ }
+ }
+ }
+ $result = $recordDiValues;
+ } else {
+ $title = $dataItem->getTitle();
+
+ $wikiPageValue = DataValueFactory::getInstance()->newDataValueByItem(
+ $dataItem
+ );
+
+ $result = [
+ 'fulltext' => $title->getFullText(),
+ 'fullurl' => $title->getFullUrl(),
+ 'namespace' => $title->getNamespace(),
+ 'exists' => strval( $title->isKnown() ),
+ 'displaytitle' => $wikiPageValue->getDisplayTitle()
+ ];
+ }
+ break;
+ case DataItem::TYPE_NUMBER:
+ // dataitems and datavalues
+ // Quantity is a datavalue type that belongs to dataitem
+ // type number which means in order to identify the correct
+ // unit, we have re-factor the corresponding datavalue otherwise
+ // we will not be able to determine the unit
+ // (unit is part of the datavalue object)
+ if ( $printRequest !== null && $printRequest->getTypeID() === '_qty' ) {
+ $diProperty = $printRequest->getData()->getDataItem();
+
+ if ( $printRequest->isMode( \SMW\Query\PrintRequest::PRINT_CHAIN ) ) {
+ $diProperty = $printRequest->getData()->getLastPropertyChainValue()->getDataItem();
+ }
+
+ $dataValue = DataValueFactory::getInstance()->newDataValueByItem( $dataItem, $diProperty );
+
+ $result = [
+ 'value' => $dataValue->getNumber(),
+ 'unit' => $dataValue->getUnit()
+ ];
+ } else {
+ $result = $dataItem->getNumber();
+ }
+ break;
+ case DataItem::TYPE_GEO:
+ $result = $dataItem->getCoordinateSet();
+ break;
+ case DataItem::TYPE_TIME:
+ $result = [
+ 'timestamp' => $dataItem->getMwTimestamp(),
+ 'raw' => $dataItem->getSerialization()
+ ];
+ break;
+ default:
+ $result = $dataItem->getSerialization();
+ break;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get the serialization for a SMWQueryResult object.
+ *
+ * @since 1.7
+ *
+ * @param SMWQueryResult $result
+ *
+ * @return array
+ */
+ public static function getSerializedQueryResult( QueryResult $queryResult ) {
+ $results = [];
+ $printRequests = [];
+
+ foreach ( $queryResult->getPrintRequests() as $printRequest ) {
+ $printRequests[] = self::serialize_printrequest( $printRequest );
+ }
+
+ /**
+ * @var DIWikiPage $diWikiPage
+ * @var PrintRequest $printRequest
+ */
+ foreach ( $queryResult->getResults() as $diWikiPage ) {
+
+ if ( $diWikiPage === null || !($diWikiPage->getTitle() instanceof Title ) ) {
+ continue;
+ }
+
+ $result = [ 'printouts' => [] ];
+
+ foreach ( $queryResult->getPrintRequests() as $printRequest ) {
+ $resultArray = new SMWResultArray( $diWikiPage, $printRequest, $queryResult->getStore() );
+
+ if ( $printRequest->getMode() === PrintRequest::PRINT_THIS ) {
+ $dataItems = $resultArray->getContent();
+ $result += self::getSerialization( array_shift( $dataItems ), $printRequest );
+ } elseif ( $resultArray->getContent() !== [] ) {
+ $values = [];
+
+ foreach ( $resultArray->getContent() as $dataItem ) {
+ $values[] = self::getSerialization( $dataItem, $printRequest );
+ }
+ $result['printouts'][$printRequest->getLabel()] = $values;
+ } else {
+ // For those objects that are empty return an empty array
+ // to keep the output consistent
+ $result['printouts'][$printRequest->getLabel()] = [];
+ }
+ }
+
+ $id = $diWikiPage->getTitle()->getFullText();
+
+ /**
+ * #3038
+ *
+ * Version 2: ... "results": { "Foo": {} ... }
+ * Version 3: ... "results": [ { "Foo": {} } ... ]
+ */
+ if ( self::$version >= 3 ) {
+ $results[] = [ $id => $result ];
+ } else{
+ $results[$id] = $result;
+ }
+ }
+
+ $serialization = [
+ 'printrequests' => $printRequests,
+ 'results' => $results,
+
+ // If we wanted to be able to deserialize a serialized QueryResult,
+ // we would need to following information as well.
+ // 'ask' => $queryResult->getQuery()->toArray()
+ ];
+
+ return $serialization;
+ }
+
+ private static function serialize_printrequest( $printRequest ) {
+
+ $serialized = [
+ 'label' => $printRequest->getLabel(),
+ 'key' => '',
+ 'redi' => '',
+ 'typeid' => $printRequest->getTypeID(),
+ 'mode' => $printRequest->getMode(),
+ 'format' => $printRequest->getOutputFormat()
+ ];
+
+ $data = $printRequest->getData();
+
+ if ( $printRequest->isMode( PrintRequest::PRINT_CHAIN ) ) {
+ $serialized['chain'] = $data->getDataItem()->getString();
+ $serialized['key'] = $data->getLastPropertyChainValue()->getDataItem()->getKey();
+ }
+
+ if ( !$printRequest->isMode( PrintRequest::PRINT_PROP ) ) {
+ return $serialized;
+ }
+
+ if ( $data === null ) {
+ return $serialized;
+ }
+
+ $serialized['redi'] = '';
+
+ // To match forwarded redirects
+ if ( !$data->getInceptiveProperty()->equals( $data->getDataItem() ) ) {
+ $serialized['redi'] = $data->getInceptiveProperty()->getKey();
+ }
+
+ // To match internal properties like _MDAT
+ $serialized['key'] = $data->getDataItem()->getKey();
+
+ return $serialized;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Serializers/SemanticDataSerializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Serializers/SemanticDataSerializer.php
new file mode 100644
index 00000000..63085584
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Serializers/SemanticDataSerializer.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace SMW\Serializers;
+
+use OutOfBoundsException;
+use Serializers\Serializer;
+use SMW\SemanticData;
+
+/**
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class SemanticDataSerializer implements Serializer {
+
+ /**
+ * @see Serializer::serialize
+ *
+ * @since 1.9
+ */
+ public function serialize( $semanticData ) {
+
+ if ( !$semanticData instanceof SemanticData ) {
+ throw new OutOfBoundsException( 'Object is not supported' );
+ }
+
+ return $this->doSerialize( $semanticData ) + [ 'serializer' => __CLASS__, 'version' => 2 ];
+ }
+
+ private function doSerialize( SemanticData $semanticData ) {
+
+ $data = [
+ 'subject' => $semanticData->getSubject()->getSerialization(),
+ 'data' => $this->doSerializeProperty( $semanticData )
+ ];
+
+ $subobjects = $this->doSerializeSubSemanticData(
+ $semanticData->getSubSemanticData()
+ );
+
+ if ( $subobjects !== [] ) {
+ $data['sobj'] = $subobjects;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Build property and dataItem serialization record
+ *
+ * @return array
+ */
+ private function doSerializeProperty( $semanticData ) {
+
+ $properties = [];
+
+ foreach ( $semanticData->getProperties() as $property ) {
+ $properties[] = [
+ 'property' => $property->getSerialization(),
+ 'dataitem' => $this->doSerializeDataItem( $semanticData, $property )
+ ];
+ }
+
+ return $properties;
+ }
+
+ /**
+ * Returns DataItem serialization
+ *
+ * @note 'type' is added to ensure that during unserialization the type
+ * definition of the requested data is in alignment with the definition found
+ * in the system (type changes that can occur during the time between
+ * serialization and unserialization)
+ *
+ * @return array
+ */
+ private function doSerializeDataItem( $semanticData, $property ) {
+
+ $dataItems = [];
+
+ foreach ( $semanticData->getPropertyValues( $property ) as $dataItem ) {
+ $dataItems[] = [
+ 'type' => $dataItem->getDIType(),
+ 'item' => $dataItem->getSerialization()
+ ];
+ }
+
+ return $dataItems;
+ }
+
+ /**
+ * Returns all subobjects of a SemanticData instance
+ *
+ * @return array
+ */
+ protected function doSerializeSubSemanticData( $subSemanticData ) {
+
+ $subobjects = [];
+
+ foreach ( $subSemanticData as $semanticData ) {
+ $subobjects[] = $this->doSerialize( $semanticData );
+ }
+
+ return $subobjects;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/DataValueServiceFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Services/DataValueServiceFactory.php
new file mode 100644
index 00000000..bcba0db8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/DataValueServiceFactory.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace SMW\Services;
+
+use Onoi\CallbackContainer\ContainerBuilder;
+use SMW\DataValueFactory;
+use SMW\DataValues\InfoLinksProvider;
+use SMW\DataValues\StringValue;
+use SMW\DataValues\ValueFormatters\DispatchingDataValueFormatter;
+use SMW\DataValues\ValueFormatters\NoValueFormatter;
+use SMW\DataValues\ValueFormatters\ValueFormatter;
+use SMW\DataValues\ValueParsers\ValueParser;
+use SMW\DataValues\ValueValidators\ConstraintValueValidator;
+use SMW\PropertyRestrictionExaminer;
+use SMW\PropertySpecificationLookup;
+use SMWDataValue as DataValue;
+use SMWNumberValue as NumberValue;
+use SMWTimeValue as TimeValue;
+
+/**
+ * @private
+ *
+ * This class provides service and factory functions for DataValue objects and
+ * are only to be used for those objects.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class DataValueServiceFactory {
+
+ /**
+ * Indicates a DataValue service
+ */
+ const SERVICE_FILE = 'DataValueServices.php';
+
+ /**
+ * Indicates a DataValue service
+ */
+ const TYPE_INSTANCE = '__dv.';
+
+ /**
+ * Indicates a ValueParser service
+ */
+ const TYPE_PARSER = '__dv.parser.';
+
+ /**
+ * Indicates a ValueFormatter service
+ */
+ const TYPE_FORMATTER = '__dv.formatter.';
+
+ /**
+ * Indicates a ValueValidator service
+ */
+ const TYPE_VALIDATOR = '__dv.validator.';
+
+ /**
+ * Extraneous service
+ */
+ const TYPE_EXT_FUNCTION = '__dv.ext.func.';
+
+ /**
+ * @var ContainerBuilder
+ */
+ private $containerBuilder;
+
+ /**
+ * @var DispatchingDataValueFormatter
+ */
+ private $dispatchingDataValueFormatter = null;
+
+ /**
+ * @since 2.5
+ */
+ public function __construct( ContainerBuilder $containerBuilder ) {
+ $this->containerBuilder = $containerBuilder;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DataValue $dataValue
+ *
+ * @return InfoLinksProvider
+ */
+ public function newInfoLinksProvider( DataValue $dataValue ) {
+ return new InfoLinksProvider( $dataValue, $this->getPropertySpecificationLookup() );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return DataValueFactory
+ */
+ public function getDataValueFactory() {
+ return DataValueFactory::getInstance();
+ }
+
+ /**
+ * Imported functions registered with DataTypeRegistry::registerExtraneousFunction
+ *
+ * @since 2.5
+ *
+ * @param array $extraneousFunctions
+ */
+ public function importExtraneousFunctions( array $extraneousFunctions ) {
+ foreach ( $extraneousFunctions as $serviceName => $calllback ) {
+ $this->containerBuilder->registerCallback( self::TYPE_EXT_FUNCTION . $serviceName, $calllback );
+ }
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $serviceName
+ *
+ * @return mixed
+ */
+ public function newExtraneousFunctionByName( $serviceName ) {
+ return $this->containerBuilder->create( self::TYPE_EXT_FUNCTION . $serviceName );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $typeId
+ * @param string $class
+ *
+ * @return DataValue
+ */
+ public function newDataValueByType( $typeId, $class ) {
+
+ if ( $this->containerBuilder->isRegistered( self::TYPE_INSTANCE . $typeId ) ) {
+ return $this->containerBuilder->create( self::TYPE_INSTANCE . $typeId );
+ }
+
+ // Legacy invocation, for those that have not been defined yet!s
+ return new $class( $typeId );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DataValue $dataValue
+ *
+ * @return ValueParser
+ */
+ public function getValueParser( DataValue $dataValue ) {
+ return $this->containerBuilder->singleton( self::TYPE_PARSER . $dataValue->getTypeID() );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param DataValue $dataValue
+ *
+ * @return ValueFormatter
+ */
+ public function getValueFormatter( DataValue $dataValue ) {
+
+ $id = self::TYPE_FORMATTER . $dataValue->getTypeID();
+
+ if ( $this->containerBuilder->isRegistered( $id ) ) {
+ $dataValueFormatter = $this->containerBuilder->singleton( $id );
+ } else {
+ $dataValueFormatter = $this->getDispatchableValueFormatter( $dataValue );
+ }
+
+ $dataValueFormatter->setDataValue(
+ $dataValue
+ );
+
+ return $dataValueFormatter;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return ConstraintValueValidator
+ */
+ public function getConstraintValueValidator() {
+ return $this->containerBuilder->singleton( self::TYPE_VALIDATOR . 'CompoundConstraintValueValidator' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return PropertySpecificationLookup
+ */
+ public function getPropertySpecificationLookup() {
+ return $this->containerBuilder->singleton( 'PropertySpecificationLookup' );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return PropertyRestrictionExaminer
+ */
+ public function getPropertyRestrictionExaminer() {
+
+ $propertyRestrictionExaminer = $this->containerBuilder->singleton( 'PropertyRestrictionExaminer' );
+
+ $propertyRestrictionExaminer->setUser(
+ $GLOBALS['wgUser']
+ );
+
+ return $propertyRestrictionExaminer;
+ }
+
+ private function getDispatchableValueFormatter( $dataValue ) {
+
+ if ( $this->dispatchingDataValueFormatter === null ) {
+ $this->dispatchingDataValueFormatter = $this->newDispatchingDataValueFormatter();
+ }
+
+ return $this->dispatchingDataValueFormatter->getDataValueFormatterFor( $dataValue );
+ }
+
+ private function newDispatchingDataValueFormatter() {
+
+ $dispatchingDataValueFormatter = new DispatchingDataValueFormatter();
+
+ // To be checked only after DispatchingDataValueFormatter::addDataValueFormatter did
+ // not match any previous registered DataValueFormatters
+ $dispatchingDataValueFormatter->addDefaultDataValueFormatter(
+ $this->containerBuilder->singleton( self::TYPE_FORMATTER . StringValue::TYPE_ID )
+ );
+
+ $dispatchingDataValueFormatter->addDefaultDataValueFormatter(
+ $this->containerBuilder->singleton( self::TYPE_FORMATTER . NumberValue::TYPE_ID )
+ );
+
+ $dispatchingDataValueFormatter->addDefaultDataValueFormatter(
+ $this->containerBuilder->singleton( self::TYPE_FORMATTER . TimeValue::TYPE_ID )
+ );
+
+ $dispatchingDataValueFormatter->addDefaultDataValueFormatter( new NoValueFormatter() );
+
+ return $dispatchingDataValueFormatter;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/DataValueServices.php b/www/wiki/extensions/SemanticMediaWiki/src/Services/DataValueServices.php
new file mode 100644
index 00000000..99b399b9
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/DataValueServices.php
@@ -0,0 +1,310 @@
+<?php
+
+namespace SMW\Services;
+
+use SMW\DataValues\AllowsListValue;
+use SMW\DataValues\AllowsPatternValue;
+use SMW\DataValues\ImportValue;
+use SMW\DataValues\MonolingualTextValue;
+use SMW\DataValues\ReferenceValue;
+use SMW\DataValues\StringValue;
+use SMW\DataValues\ValueFormatters\CodeStringValueFormatter;
+use SMW\DataValues\ValueFormatters\MonolingualTextValueFormatter;
+use SMW\DataValues\ValueFormatters\NumberValueFormatter;
+use SMW\DataValues\ValueFormatters\PropertyValueFormatter;
+use SMW\DataValues\ValueFormatters\ReferenceValueFormatter;
+use SMW\DataValues\ValueFormatters\StringValueFormatter;
+use SMW\DataValues\ValueFormatters\TimeValueFormatter;
+use SMW\DataValues\ValueParsers\AllowsListValueParser;
+use SMW\DataValues\ValueParsers\AllowsPatternValueParser;
+use SMW\DataValues\ValueParsers\ImportValueParser;
+use SMW\DataValues\ValueParsers\MonolingualTextValueParser;
+use SMW\DataValues\ValueParsers\PropertyValueParser;
+use SMW\DataValues\ValueParsers\TimeValueParser;
+use SMW\DataValues\ValueValidators\AllowsListConstraintValueValidator;
+use SMW\DataValues\ValueValidators\CompoundConstraintValueValidator;
+use SMW\DataValues\ValueValidators\PatternConstraintValueValidator;
+use SMW\DataValues\ValueValidators\PropertySpecificationConstraintValueValidator;
+use SMW\DataValues\ValueValidators\UniquenessConstraintValueValidator;
+use SMWNumberValue as NumberValue;
+use SMWPropertyValue as PropertyValue;
+use SMWQuantityValue as QuantityValue;
+use SMWTimeValue as TimeValue;
+use SMW\Site;
+
+/**
+ * @codeCoverageIgnore
+ *
+ * Services defined in this file SHOULD only be accessed via DataValueServiceFactory
+ * with services being expected to require a prefix to match each individual instance
+ * to a specific DataValue.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+return [
+
+ /**
+ * PropertyValueParser
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_PARSER . PropertyValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_PARSER . PropertyValue::TYPE_ID,
+ PropertyValueParser::class
+ );
+
+ $propertyValueParser = new PropertyValueParser();
+
+ $propertyValueParser->setInvalidCharacterList(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgPropertyInvalidCharacterList' )
+ );
+
+ $propertyValueParser->isCapitalLinks(
+ Site::isCapitalLinks()
+ );
+
+ return $propertyValueParser;
+ },
+
+ /**
+ * PropertyValueFormatter
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_FORMATTER . PropertyValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_FORMATTER . PropertyValue::TYPE_ID,
+ PropertyValueFormatter::class
+ );
+
+ return new PropertyValueFormatter( $containerBuilder->singleton( 'PropertySpecificationLookup' ) );
+ },
+
+ /**
+ * AllowsPatternValueParser
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_PARSER . AllowsPatternValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_PARSER . AllowsPatternValue::TYPE_ID,
+ AllowsPatternValueParser::class
+ );
+
+ return new AllowsPatternValueParser( $containerBuilder->singleton( 'MediaWikiNsContentReader' ) );
+ },
+
+ /**
+ * AllowsListValueParser
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_PARSER . AllowsListValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_PARSER . AllowsListValue::TYPE_ID,
+ AllowsListValueParser::class
+ );
+
+ return new AllowsListValueParser( $containerBuilder->singleton( 'MediaWikiNsContentReader' ) );
+ },
+
+ /**
+ * CompoundConstraintValueValidator
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_VALIDATOR . 'CompoundConstraintValueValidator' => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_VALIDATOR . 'CompoundConstraintValueValidator',
+ CompoundConstraintValueValidator::class
+ );
+
+ $compoundConstraintValueValidator = new CompoundConstraintValueValidator();
+
+ // Any registered ConstraintValueValidator becomes weaker(diminished) in the context
+ // of a preceding validator
+ $compoundConstraintValueValidator->registerConstraintValueValidator(
+ new UniquenessConstraintValueValidator(
+ $containerBuilder->singleton( 'Store' ),
+ $containerBuilder->singleton( 'PropertySpecificationLookup' )
+ )
+ );
+
+ $patternConstraintValueValidator = new PatternConstraintValueValidator(
+ $containerBuilder->create( DataValueServiceFactory::TYPE_PARSER . AllowsPatternValue::TYPE_ID )
+ );
+
+ $compoundConstraintValueValidator->registerConstraintValueValidator(
+ $patternConstraintValueValidator
+ );
+
+ $allowsListConstraintValueValidator = new AllowsListConstraintValueValidator(
+ $containerBuilder->create( DataValueServiceFactory::TYPE_PARSER . AllowsListValue::TYPE_ID ),
+ $containerBuilder->singleton( 'PropertySpecificationLookup' )
+ );
+
+ $compoundConstraintValueValidator->registerConstraintValueValidator(
+ $allowsListConstraintValueValidator
+ );
+
+ $compoundConstraintValueValidator->registerConstraintValueValidator(
+ new PropertySpecificationConstraintValueValidator()
+ );
+
+ return $compoundConstraintValueValidator;
+ },
+
+ /**
+ * ImportValueParser
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_PARSER . ImportValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_PARSER . ImportValue::TYPE_ID,
+ ImportValueParser::class
+ );
+
+ return new ImportValueParser( $containerBuilder->singleton( 'MediaWikiNsContentReader' ) );
+ },
+
+ /**
+ * StringValueFormatter
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_FORMATTER . StringValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_FORMATTER . StringValue::TYPE_ID,
+ StringValueFormatter::class
+ );
+
+ $containerBuilder->registerAlias(
+ DataValueServiceFactory::TYPE_FORMATTER . StringValue::TYPE_ID,
+ DataValueServiceFactory::TYPE_FORMATTER . StringValue::TYPE_LEGACY_ID
+ );
+
+ return new StringValueFormatter();
+ },
+
+ /**
+ * CodeStringValueFormatter
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_FORMATTER . StringValue::TYPE_COD_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_FORMATTER . StringValue::TYPE_COD_ID,
+ CodeStringValueFormatter::class
+ );
+
+ return new CodeStringValueFormatter();
+ },
+
+ /**
+ * ReferenceValueFormatter
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_FORMATTER . ReferenceValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_FORMATTER . ReferenceValue::TYPE_ID,
+ ReferenceValueFormatter::class
+ );
+
+ return new ReferenceValueFormatter();
+ },
+
+ /**
+ * MonolingualTextValueParser
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_PARSER . MonolingualTextValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_PARSER . MonolingualTextValue::TYPE_ID,
+ MonolingualTextValueParser::class
+ );
+
+ return new MonolingualTextValueParser();
+ },
+
+ /**
+ * MonolingualTextValueFormatter
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_FORMATTER . MonolingualTextValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_FORMATTER . MonolingualTextValue::TYPE_ID,
+ MonolingualTextValueFormatter::class
+ );
+
+ return new MonolingualTextValueFormatter();
+ },
+
+ /**
+ * NumberValueFormatter
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_FORMATTER . QuantityValue::TYPE_ID => function( $containerBuilder ) {
+ return $containerBuilder->create( DataValueServiceFactory::TYPE_FORMATTER . NumberValue::TYPE_ID );
+ },
+
+ DataValueServiceFactory::TYPE_FORMATTER . NumberValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_FORMATTER . NumberValue::TYPE_ID,
+ NumberValueFormatter::class
+ );
+
+ return new NumberValueFormatter();
+ },
+
+ /**
+ * TimeValueFormatter
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_FORMATTER . TimeValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_FORMATTER . TimeValue::TYPE_ID,
+ TimeValueFormatter::class
+ );
+
+ return new TimeValueFormatter();
+ },
+
+ /**
+ * TimeValueParser
+ *
+ * @return callable
+ */
+ DataValueServiceFactory::TYPE_PARSER . TimeValue::TYPE_ID => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ DataValueServiceFactory::TYPE_PARSER . TimeValue::TYPE_ID,
+ TimeValueParser::class
+ );
+
+ return new TimeValueParser();
+ },
+
+];
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/Exception/ServiceNotFoundException.php b/www/wiki/extensions/SemanticMediaWiki/src/Services/Exception/ServiceNotFoundException.php
new file mode 100644
index 00000000..664ea144
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/Exception/ServiceNotFoundException.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace SMW\Services\Exception;
+
+use InvalidArgumentException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ServiceNotFoundException extends InvalidArgumentException {
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function __construct( $service ) {
+ parent::__construct( "`$service` is not registered as service!" );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/ImporterServiceFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/Services/ImporterServiceFactory.php
new file mode 100644
index 00000000..cc9b71bf
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/ImporterServiceFactory.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace SMW\Services;
+
+use ImportSource;
+use Onoi\CallbackContainer\ContainerBuilder;
+use SMW\Importer\ContentIterator;
+
+/**
+ * @private
+ *
+ * This class provides service and factory functions for Import objects.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ImporterServiceFactory {
+
+ /**
+ * @var ContainerBuilder
+ */
+ private $containerBuilder;
+
+ /**
+ * @since 3.0
+ */
+ public function __construct( ContainerBuilder $containerBuilder ) {
+ $this->containerBuilder = $containerBuilder;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $source
+ *
+ * @return ImportStringSource
+ */
+ public function newImportStringSource( $source ) {
+ return $this->containerBuilder->create( 'ImportStringSource', $source );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $source
+ *
+ * @return ImportStreamSource
+ */
+ public function newImportStreamSource( $source ) {
+ return $this->containerBuilder->create( 'ImportStreamSource', $source );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ImportSource $importSource
+ *
+ * @return WikiImporter
+ */
+ public function newWikiImporter( ImportSource $importSource ) {
+ return $this->containerBuilder->create( 'WikiImporter', $importSource );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param ContentIterator $contentIterator
+ *
+ * @return Importer
+ */
+ public function newImporter( ContentIterator $contentIterator ) {
+ return $this->containerBuilder->create( 'Importer', $contentIterator );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return JsonContentIterator
+ */
+ public function newJsonContentIterator( $importFileDir ) {
+ return $this->containerBuilder->create( 'JsonContentIterator', $importFileDir );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/ImporterServices.php b/www/wiki/extensions/SemanticMediaWiki/src/Services/ImporterServices.php
new file mode 100644
index 00000000..fb8b9b10
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/ImporterServices.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace SMW\Services;
+
+use SMW\Importer\ContentCreators\DispatchingContentCreator;
+use SMW\Importer\ContentCreators\TextContentCreator;
+use SMW\Importer\ContentCreators\XmlContentCreator;
+use SMW\Importer\ContentIterator;
+use SMW\Importer\ContentModeller;
+use SMW\Importer\Importer;
+use SMW\Importer\JsonContentIterator;
+use SMW\Importer\JsonImportContentsFileDirReader;
+
+/**
+ * @codeCoverageIgnore
+ *
+ * Services defined in this file SHOULD only be accessed either via the
+ * ApplicationFactory or a different factory instance.
+ *
+ * @license GNU GPL v2
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+return [
+
+ /**
+ * ImporterServiceFactory
+ *
+ * @return callable
+ */
+ 'ImporterServiceFactory' => function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'ImporterServiceFactory', '\SMW\Services\ImporterServiceFactory' );
+ return new ImporterServiceFactory( $containerBuilder );
+ },
+
+ /**
+ * XmlContentCreator
+ *
+ * @return callable
+ */
+ 'XmlContentCreator' => function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'XmlContentCreator', '\SMW\Importer\ContentCreators\XmlContentCreator' );
+ return new XmlContentCreator( $containerBuilder->create( 'ImporterServiceFactory' ) );
+ },
+
+ /**
+ * TextContentCreator
+ *
+ * @return callable
+ */
+ 'TextContentCreator' => function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'TextContentCreator', '\SMW\Importer\ContentCreators\TextContentCreator' );
+
+ $connectionManager = $containerBuilder->singleton( 'ConnectionManager' );
+
+ $textContentCreator = new TextContentCreator(
+ $containerBuilder->create( 'PageCreator' ),
+ $connectionManager->getConnection( 'mw.db' )
+ );
+
+ return $textContentCreator;
+ },
+
+ /**
+ * Importer
+ *
+ * @return callable
+ */
+ 'Importer' => function( $containerBuilder, ContentIterator $contentIterator ) {
+ $containerBuilder->registerExpectedReturnType( 'Importer', '\SMW\Importer\Importer' );
+
+ $dispatchingContentCreator = new DispatchingContentCreator(
+ [
+ $containerBuilder->create( 'XmlContentCreator' ),
+ $containerBuilder->create( 'TextContentCreator' )
+ ]
+ );
+
+ $importer = new Importer(
+ $contentIterator,
+ $dispatchingContentCreator
+ );
+
+ $importer->setReqVersion(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgImportReqVersion' )
+ );
+
+ return $importer;
+ },
+
+ /**
+ * JsonContentIterator
+ *
+ * @return callable
+ */
+ 'JsonContentIterator' => function( $containerBuilder, $importFileDirs ) {
+ $containerBuilder->registerExpectedReturnType( 'JsonContentIterator', '\SMW\Importer\JsonContentIterator' );
+
+ $jsonImportContentsFileDirReader = new JsonImportContentsFileDirReader(
+ new ContentModeller(),
+ $importFileDirs
+ );
+
+ return new JsonContentIterator( $jsonImportContentsFileDirReader );
+ },
+
+]; \ No newline at end of file
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/MediaWikiServices.php b/www/wiki/extensions/SemanticMediaWiki/src/Services/MediaWikiServices.php
new file mode 100644
index 00000000..1616fe0f
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/MediaWikiServices.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace SMW\Services;
+
+use ImportStreamSource;
+use ImportStringSource;
+use JobQueueGroup;
+use LBFactory;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use Psr\Log\NullLogger;
+use SMW\Utils\Logger;
+use WikiImporter;
+
+/**
+ * @codeCoverageIgnore
+ *
+ * Services defined in this file SHOULD only be accessed either via the
+ * ApplicationFactory or a different factory instance.
+ *
+ * @license GNU GPL v2
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+return [
+
+ /**
+ * ImportStringSource
+ *
+ * @return callable
+ */
+ 'ImportStringSource' => function( $containerBuilder, $source ) {
+ $containerBuilder->registerExpectedReturnType( 'ImportStringSource', '\ImportStringSource' );
+ return new ImportStringSource( $source );
+ },
+
+ /**
+ * ImportStreamSource
+ *
+ * @return callable
+ */
+ 'ImportStreamSource' => function( $containerBuilder, $source ) {
+ $containerBuilder->registerExpectedReturnType( 'ImportStreamSource', '\ImportStreamSource' );
+ return new ImportStreamSource( $source );
+ },
+
+ /**
+ * WikiImporter
+ *
+ * @return callable
+ */
+ 'WikiImporter' => function( $containerBuilder, \ImportSource $importSource ) {
+ $containerBuilder->registerExpectedReturnType( 'WikiImporter', '\WikiImporter' );
+ return new WikiImporter( $importSource, $containerBuilder->create( 'MainConfig' ) );
+ },
+
+ /**
+ * WikiPage
+ *
+ * @return callable
+ */
+ 'WikiPage' => function( $containerBuilder, \Title $title ) {
+ $containerBuilder->registerExpectedReturnType( 'WikiPage', '\WikiPage' );
+ return \WikiPage::factory( $title );
+ },
+
+ /**
+ * Config
+ *
+ * @return callable
+ */
+ 'MainConfig' => function( $containerBuilder ) {
+
+ // > MW 1.27
+ if ( class_exists( '\MediaWiki\MediaWikiServices' ) && method_exists( '\MediaWiki\MediaWikiServices', 'getMainConfig' ) ) {
+ return MediaWikiServices::getInstance()->getMainConfig();
+ }
+
+ return \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+ },
+
+ /**
+ * LBFactory
+ *
+ * @return callable
+ */
+ 'DBLoadBalancerFactory' => function( $containerBuilder ) {
+
+ if ( class_exists( '\Wikimedia\Rdbms\LBFactory' ) ) {
+ $containerBuilder->registerExpectedReturnType( 'DBLoadBalancerFactory', '\Wikimedia\Rdbms\LBFactory' );
+ } else {
+ $containerBuilder->registerExpectedReturnType( 'DBLoadBalancerFactory', '\LBFactory' );
+ }
+
+ // > MW 1.28
+ if ( class_exists( '\MediaWiki\MediaWikiServices' ) && method_exists( '\MediaWiki\MediaWikiServices', 'getDBLoadBalancerFactory' ) ) {
+ return MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ }
+
+ return LBFactory::singleton();
+ },
+
+ /**
+ * DBLoadBalancer
+ *
+ * @return callable
+ */
+ 'DBLoadBalancer' => function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'DBLoadBalancer', '\LoadBalancer' );
+
+ // > MW 1.27
+ if ( class_exists( '\MediaWiki\MediaWikiServices' ) && method_exists( '\MediaWiki\MediaWikiServices', 'getDBLoadBalancer' ) ) {
+ return MediaWikiServices::getInstance()->getDBLoadBalancer();
+ }
+
+ return LBFactory::singleton()->getMainLB();
+ },
+
+ /**
+ * DBLoadBalancer
+ *
+ * @return callable
+ */
+ 'DefaultSearchEngineTypeForDB' => function( $containerBuilder, \IDatabase $db ) {
+
+ // MW > 1.27
+ if ( class_exists( '\MediaWiki\MediaWikiServices' ) && method_exists( 'SearchEngineFactory', 'getSearchEngineClass' ) ) {
+ return MediaWikiServices::getInstance()->getSearchEngineFactory()->getSearchEngineClass( $db );
+ }
+
+ return $db->getSearchEngine();
+ },
+
+ /**
+ * MediaWikiLogger
+ *
+ * @return callable
+ */
+ 'MediaWikiLogger' => function( $containerBuilder, $channel = 'smw', $role = Logger::ROLE_DEVELOPER ) {
+
+ $containerBuilder->registerExpectedReturnType( 'MediaWikiLogger', '\Psr\Log\LoggerInterface' );
+
+ if ( class_exists( '\MediaWiki\Logger\LoggerFactory' ) ) {
+ $logger = LoggerFactory::getInstance( $channel );
+ } else {
+ $logger = new NullLogger();
+ }
+
+ return new Logger( $logger, $role );
+ },
+
+ /**
+ * JobQueueGroup
+ *
+ * @return callable
+ */
+ 'JobQueueGroup' => function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType( 'JobQueueGroup', '\JobQueueGroup' );
+
+ return JobQueueGroup::singleton();
+ },
+
+];
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/README.md b/www/wiki/extensions/SemanticMediaWiki/src/Services/README.md
new file mode 100644
index 00000000..97debc21
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/README.md
@@ -0,0 +1,27 @@
+Services contain object definitions that with the help of a [ContainerBuilder](https://github.com/onoi/callback-container)
+will manage the object build process and provides instance reuse if necessary. Object instances are normally accessed using
+dedicated factory methods.
+
+## Service files and specification
+
+* `DataValueServiceFactory` provides service and factory functions for
+ `DataValue` objects that are specified in `DataValueServices.php`
+* `ImporterServices.php` provides services for the [Importer](https://github.com/SemanticMediaWiki/SemanticMediaWiki/tree/master/src/Importer)
+* `MediaWikiServices.php` isolates MediaWiki specific functions and services
+* `SharedServicesContainer.php` contains common and shared object definitions used
+ throughout the Semantic MediaWiki code base and are accessible via `ApplicationFactory`
+
+## ContainerBuilder
+
+<pre>
+$containerBuilder = new CallbackContainerFactory();
+$containerBuilder = $callbackContainerFactory->newCallbackContainerBuilder();
+
+$containerBuilder->registerCallbackContainer( new SharedServicesContainer() );
+$containerBuilder->registerFromFile(
+ $GLOBALS['smwgServicesFileDir'] . '/' . 'MediaWikiServices.php'
+);
+</pre>
+
+[`$smwgServicesFileDir`](https://www.semantic-mediawiki.org/wiki/Help:$smwgServicesFileDir) describes the location of the
+service directory.
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/ServicesContainer.php b/www/wiki/extensions/SemanticMediaWiki/src/Services/ServicesContainer.php
new file mode 100644
index 00000000..d5d763a2
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/ServicesContainer.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace SMW\Services;
+
+use RuntimeException;
+
+/**
+ * @private
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class ServicesContainer {
+
+ /**
+ * @var callable[]
+ */
+ private $services;
+
+ /**
+ * @since 3.0
+ */
+ public function __construct( array $services = [] ) {
+ $this->services = $services;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ *
+ * @return mixed
+ */
+ public function get( $key, ...$args ) {
+
+ if ( !isset( $this->services[$key] ) ) {
+ throw new RuntimeException( "$key is an unknown service!" );
+ };
+
+ $type = null;
+ $service = $this->services[$key];
+
+ if ( !is_callable( $service ) && isset( $service['_type'] ) && isset( $service['_service'] ) ) {
+ $type = $service['_type'];
+ $service = $service['_service'];
+ }
+
+ if ( !is_callable( $service ) ) {
+ throw new RuntimeException( "$key is not a callable service!" );
+ };
+
+ $instance = $service( ...$args );
+
+ if ( $type !== null && !is_a( $instance, $type ) ) {
+ throw new RuntimeException( "Service $key is not of the expected $type type!" );
+ }
+
+ return $instance;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param callable $service
+ */
+ public function add( $key, callable $service ) {
+ $this->services[$key] = $service;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Services/SharedServicesContainer.php b/www/wiki/extensions/SemanticMediaWiki/src/Services/SharedServicesContainer.php
new file mode 100644
index 00000000..4f703fed
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Services/SharedServicesContainer.php
@@ -0,0 +1,705 @@
+<?php
+
+namespace SMW\Services;
+
+use JsonSchema\Validator as SchemaValidator;
+use Onoi\BlobStore\BlobStore;
+use Onoi\CallbackContainer\CallbackContainer;
+use Onoi\CallbackContainer\ContainerBuilder;
+use SMW\CachedPropertyValuesPrefetcher;
+use SMW\CacheFactory;
+use SMW\ContentParser;
+use SMW\DataItemFactory;
+use SMW\Factbox\FactboxFactory;
+use SMW\HierarchyLookup;
+use SMW\InMemoryPoolCache;
+use SMW\IteratorFactory;
+use SMW\Localizer;
+use SMW\MediaWiki\Database;
+use SMW\Connection\ConnectionManager;
+use SMW\MediaWiki\Connection\ConnectionProvider;
+use SMW\MediaWiki\Deferred\CallableUpdate;
+use SMW\MediaWiki\Deferred\TransactionalCallableUpdate;
+use SMW\MediaWiki\JobQueue;
+use SMW\MediaWiki\JobFactory;
+use SMW\MediaWiki\ManualEntryLogger;
+use SMW\MediaWiki\MediaWikiNsContentReader;
+use SMW\MediaWiki\PageCreator;
+use SMW\MediaWiki\PageUpdater;
+use SMW\MediaWiki\TitleFactory;
+use SMW\MessageFormatter;
+use SMW\NamespaceExaminer;
+use SMW\Parser\LinksProcessor;
+use SMW\PermissionPthValidator;
+use SMW\ParserData;
+use SMW\PostProcHandler;
+use SMW\PropertyAnnotatorFactory;
+use SMW\PropertyLabelFinder;
+use SMW\PropertyRestrictionExaminer;
+use SMW\PropertySpecificationLookup;
+use SMW\Protection\EditProtectionUpdater;
+use SMW\Protection\ProtectionValidator;
+use SMW\Query\QuerySourceFactory;
+use SMW\Query\Result\CachedQueryResultPrefetcher;
+use SMW\Schema\SchemaFactory;
+use SMW\Settings;
+use SMW\Options;
+use SMW\StoreFactory;
+use SMW\Utils\BufferedStatsdCollector;
+use SMW\Utils\JsonSchemaValidator;
+use SMW\Utils\TempFile;
+use SMW\Elastic\ElasticFactory;
+use SMW\SQLStore\QueryDependencyLinksStoreFactory;
+use SMW\QueryFactory;
+use SMW\Query\Processor\QueryCreator;
+use SMW\Query\Processor\ParamListProcessor;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.3
+ *
+ * @author mwjames
+ */
+class SharedServicesContainer implements CallbackContainer {
+
+ /**
+ * @see CallbackContainer::register
+ *
+ * @since 2.3
+ */
+ public function register( ContainerBuilder $containerBuilder ) {
+
+ $containerBuilder->registerCallback( 'Store', [ $this, 'newStore' ] );
+
+ $this->registerCallbackHandlers( $containerBuilder );
+ $this->registerCallableFactories( $containerBuilder );
+ $this->registerCallbackHandlersByConstructedInstance( $containerBuilder );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return Store
+ */
+ public function newStore( $containerBuilder, $storeClass = null ) {
+
+ $containerBuilder->registerExpectedReturnType( 'Store', '\SMW\Store' );
+ $settings = $containerBuilder->singleton( 'Settings' );
+
+ if ( $storeClass === null || $storeClass === '' ) {
+ $storeClass = $settings->get( 'smwgDefaultStore' );
+ }
+
+ $store = StoreFactory::getStore( $storeClass );
+
+ $configs = [
+ 'smwgDefaultStore',
+ 'smwgSemanticsEnabled',
+ 'smwgAutoRefreshSubject',
+ 'smwgEnableUpdateJobs',
+ 'smwgQEqualitySupport',
+ 'smwgElasticsearchConfig'
+ ];
+
+ foreach ( $configs as $config ) {
+ $store->setOption( $config, $settings->get( $config ) );
+ }
+
+ $store->setLogger(
+ $containerBuilder->singleton( 'MediaWikiLogger' )
+ );
+
+ return $store;
+ }
+
+ private function registerCallbackHandlers( $containerBuilder ) {
+
+ $containerBuilder->registerCallback( 'Settings', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'Settings', '\SMW\Settings' );
+ return Settings::newFromGlobals();
+ } );
+
+ /**
+ * ConnectionManager
+ *
+ * @return callable
+ */
+ $containerBuilder->registerCallback( 'ConnectionManager', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'ConnectionManager', ConnectionManager::class );
+ return new ConnectionManager();
+ } );
+
+ $containerBuilder->registerCallback( 'Cache', function( $containerBuilder, $cacheType = null ) {
+ $containerBuilder->registerExpectedReturnType( 'Cache', '\Onoi\Cache\Cache' );
+ return $containerBuilder->create( 'CacheFactory' )->newMediaWikiCompositeCache( $cacheType );
+ } );
+
+ $containerBuilder->registerCallback( 'NamespaceExaminer', function() use ( $containerBuilder ) {
+ return NamespaceExaminer::newFromArray( $containerBuilder->singleton( 'Settings' )->get( 'smwgNamespacesWithSemanticLinks' ) );
+ } );
+
+ $containerBuilder->registerCallback( 'ParserData', function( $containerBuilder, \Title $title, \ParserOutput $parserOutput ) {
+ $containerBuilder->registerExpectedReturnType( 'ParserData', ParserData::class );
+
+ $parserData = new ParserData( $title, $parserOutput );
+
+ $parserData->setLogger(
+ $containerBuilder->singleton( 'MediaWikiLogger' )
+ );
+
+ return $parserData;
+ } );
+
+ $containerBuilder->registerCallback( 'LinksProcessor', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'LinksProcessor', '\SMW\Parser\LinksProcessor' );
+ return new LinksProcessor();
+ } );
+
+ $containerBuilder->registerCallback( 'MessageFormatter', function( $containerBuilder, \Language $language ) {
+ $containerBuilder->registerExpectedReturnType( 'MessageFormatter', '\SMW\MessageFormatter' );
+ return new MessageFormatter( $language );
+ } );
+
+ $containerBuilder->registerCallback( 'MediaWikiNsContentReader', function() use ( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'MediaWikiNsContentReader', '\SMW\MediaWiki\MediaWikiNsContentReader' );
+ return new MediaWikiNsContentReader();
+ } );
+
+ $containerBuilder->registerCallback( 'PageCreator', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'PageCreator', '\SMW\MediaWiki\PageCreator' );
+ return new PageCreator();
+ } );
+
+ $containerBuilder->registerCallback( 'PageUpdater', function( $containerBuilder, $connection, TransactionalCallableUpdate $transactionalCallableUpdate = null ) {
+ $containerBuilder->registerExpectedReturnType( 'PageUpdater', '\SMW\MediaWiki\PageUpdater' );
+ return new PageUpdater( $connection, $transactionalCallableUpdate );
+ } );
+
+ /**
+ * JobQueue
+ *
+ * @return callable
+ */
+ $containerBuilder->registerCallback( 'JobQueue', function( $containerBuilder ) {
+
+ $containerBuilder->registerExpectedReturnType(
+ 'JobQueue',
+ '\SMW\MediaWiki\JobQueue'
+ );
+
+ return new JobQueue(
+ $containerBuilder->create( 'JobQueueGroup' )
+ );
+ } );
+
+ $containerBuilder->registerCallback( 'ManualEntryLogger', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'ManualEntryLogger', '\SMW\MediaWiki\ManualEntryLogger' );
+ return new ManualEntryLogger();
+ } );
+
+ $containerBuilder->registerCallback( 'TitleFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'TitleFactory', '\SMW\MediaWiki\TitleFactory' );
+ return new TitleFactory();
+ } );
+
+ $containerBuilder->registerCallback( 'ContentParser', function( $containerBuilder, \Title $title ) {
+ $containerBuilder->registerExpectedReturnType( 'ContentParser', '\SMW\ContentParser' );
+ return new ContentParser( $title );
+ } );
+
+ $containerBuilder->registerCallback( 'DeferredCallableUpdate', function( $containerBuilder, callable $callback = null ) {
+ $containerBuilder->registerExpectedReturnType( 'DeferredCallableUpdate', '\SMW\MediaWiki\Deferred\CallableUpdate' );
+ $containerBuilder->registerAlias( 'CallableUpdate', CallableUpdate::class );
+
+ return new CallableUpdate( $callback );
+ } );
+
+ $containerBuilder->registerCallback( 'DeferredTransactionalCallableUpdate', function( $containerBuilder, callable $callback = null, Database $connection = null ) {
+ $containerBuilder->registerExpectedReturnType( 'DeferredTransactionalUpdate', '\SMW\MediaWiki\Deferred\TransactionalCallableUpdate' );
+ $containerBuilder->registerAlias( 'DeferredTransactionalUpdate', TransactionalCallableUpdate::class );
+
+ return new TransactionalCallableUpdate( $callback, $connection );
+ } );
+
+ /**
+ * @var InMemoryPoolCache
+ */
+ $containerBuilder->registerCallback( 'InMemoryPoolCache', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'InMemoryPoolCache', '\SMW\InMemoryPoolCache' );
+ return InMemoryPoolCache::getInstance();
+ } );
+
+ /**
+ * @var PropertyAnnotatorFactory
+ */
+ $containerBuilder->registerCallback( 'PropertyAnnotatorFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'PropertyAnnotatorFactory', '\SMW\PropertyAnnotatorFactory' );
+ return new PropertyAnnotatorFactory();
+ } );
+
+ /**
+ * @var ConnectionProvider
+ */
+ $containerBuilder->registerAlias( 'ConnectionProvider', 'DBConnectionProvider' );
+
+ $containerBuilder->registerCallback( 'ConnectionProvider', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'ConnectionProvider', ConnectionProvider::class );
+
+ $connectionProvider = new ConnectionProvider();
+
+ $connectionProvider->setLogger(
+ $containerBuilder->singleton( 'MediaWikiLogger' )
+ );
+
+ return $connectionProvider;
+ } );
+
+ /**
+ * @var TempFile
+ */
+ $containerBuilder->registerCallback( 'TempFile', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'TempFile', '\SMW\Utils\TempFile' );
+ return new TempFile();
+ } );
+
+ /**
+ * @var PostProcHandler
+ */
+ $containerBuilder->registerCallback( 'PostProcHandler', function( $containerBuilder, \ParserOutput $parserOutput ) {
+ $containerBuilder->registerExpectedReturnType( 'PostProcHandler', PostProcHandler::class );
+
+ $settings = $containerBuilder->singleton( 'Settings' );
+
+ $postProcHandler = new PostProcHandler(
+ $parserOutput,
+ $containerBuilder->singleton( 'Cache' )
+ );
+
+ $postProcHandler->setOptions(
+ $settings->get( 'smwgPostEditUpdate' ) +
+ [ 'smwgEnabledQueryDependencyLinksStore' => $settings->get( 'smwgEnabledQueryDependencyLinksStore' ) ] +
+ [ 'smwgEnabledFulltextSearch' => $settings->get( 'smwgEnabledFulltextSearch' ) ]
+ );
+
+ return $postProcHandler;
+ } );
+
+ /**
+ * @var JsonSchemaValidator
+ */
+ $containerBuilder->registerCallback( 'JsonSchemaValidator', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'JsonSchemaValidator', JsonSchemaValidator::class );
+ $containerBuilder->registerAlias( 'JsonSchemaValidator', JsonSchemaValidator::class );
+
+ $schemaValidator = null;
+
+ // justinrainbow/json-schema
+ if ( class_exists( SchemaValidator::class ) ) {
+ $schemaValidator = new SchemaValidator();
+ }
+
+ $jsonSchemaValidator = new JsonSchemaValidator(
+ $schemaValidator
+ );
+
+ return $jsonSchemaValidator;
+ } );
+
+ /**
+ * @var SchemaFactory
+ */
+ $containerBuilder->registerCallback( 'SchemaFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'SchemaFactory', SchemaFactory::class );
+
+ $settings = $containerBuilder->singleton( 'Settings' );
+
+ $schemaFactory = new SchemaFactory(
+ $settings->get( 'smwgSchemaTypes' )
+ );
+
+ return $schemaFactory;
+ } );
+
+ /**
+ * @var ElasticFactory
+ */
+ $containerBuilder->registerCallback( 'ElasticFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'ElasticFactory', ElasticFactory::class );
+ return new ElasticFactory();
+ } );
+
+ /**
+ * @var Creator
+ */
+ $containerBuilder->registerCallback( 'QueryCreator', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'QueryCreator', QueryCreator::class );
+
+ $settings = $containerBuilder->singleton( 'Settings' );
+
+ $queryCreator = new QueryCreator(
+ $containerBuilder->singleton( 'QueryFactory' ),
+ $settings->get( 'smwgQDefaultNamespaces' ),
+ $settings->get( 'smwgQDefaultLimit' )
+ );
+
+ $queryCreator->setQFeatures(
+ $settings->get( 'smwgQFeatures' )
+ );
+
+ $queryCreator->setQConceptFeatures(
+ $settings->get( 'smwgQConceptFeatures' )
+ );
+
+ return $queryCreator;
+ } );
+
+ /**
+ * @var ParamListProcessor
+ */
+ $containerBuilder->registerCallback( 'ParamListProcessor', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'ParamListProcessor', ParamListProcessor::class );
+
+ $paramListProcessor = new ParamListProcessor(
+ //$containerBuilder->singleton( 'PrintRequestFactory' )
+ );
+
+ return $paramListProcessor;
+ } );
+ }
+
+ private function registerCallableFactories( $containerBuilder ) {
+
+ /**
+ * @var CacheFactory
+ */
+ $containerBuilder->registerCallback( 'CacheFactory', function( $containerBuilder, $mainCacheType = null ) {
+ $containerBuilder->registerExpectedReturnType( 'CacheFactory', '\SMW\CacheFactory' );
+ return new CacheFactory( $mainCacheType );
+ } );
+
+ /**
+ * @var IteratorFactory
+ */
+ $containerBuilder->registerCallback( 'IteratorFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'IteratorFactory', '\SMW\IteratorFactory' );
+ return new IteratorFactory();
+ } );
+
+ /**
+ * @var JobFactory
+ */
+ $containerBuilder->registerCallback( 'JobFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'JobFactory', '\SMW\MediaWiki\JobFactory' );
+ return new JobFactory();
+ } );
+
+ /**
+ * @var FactboxFactory
+ */
+ $containerBuilder->registerCallback( 'FactboxFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'FactboxFactory', '\SMW\Factbox\FactboxFactory' );
+ return new FactboxFactory();
+ } );
+
+ /**
+ * @var QuerySourceFactory
+ */
+ $containerBuilder->registerCallback( 'QuerySourceFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'QuerySourceFactory', '\SMW\Query\QuerySourceFactory' );
+
+ return new QuerySourceFactory(
+ $containerBuilder->singleton( 'Store', null ),
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgQuerySources' )
+ );
+ } );
+
+ /**
+ * @var QueryFactory
+ */
+ $containerBuilder->registerCallback( 'QueryFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'QueryFactory', '\SMW\QueryFactory' );
+ return new QueryFactory();
+ } );
+
+ /**
+ * @var DataItemFactory
+ */
+ $containerBuilder->registerCallback( 'DataItemFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'DataItemFactory', '\SMW\DataItemFactory' );
+ return new DataItemFactory();
+ } );
+
+ /**
+ * @var DataValueServiceFactory
+ */
+ $containerBuilder->registerCallback( 'DataValueServiceFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'DataValueServiceFactory', '\SMW\Services\DataValueServiceFactory' );
+
+ $containerBuilder->registerFromFile(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgServicesFileDir' ) . '/' . DataValueServiceFactory::SERVICE_FILE
+ );
+
+ $dataValueServiceFactory = new DataValueServiceFactory(
+ $containerBuilder
+ );
+
+ return $dataValueServiceFactory;
+ } );
+
+ /**
+ * @var QueryDependencyLinksStoreFactory
+ */
+ $containerBuilder->registerCallback( 'QueryDependencyLinksStoreFactory', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'QueryDependencyLinksStoreFactory', '\SMW\SQLStore\QueryDependencyLinksStoreFactory' );
+ return new QueryDependencyLinksStoreFactory();
+ } );
+
+ }
+
+ private function registerCallbackHandlersByConstructedInstance( $containerBuilder ) {
+
+ /**
+ * @var BlobStore
+ */
+ $containerBuilder->registerCallback( 'BlobStore', function( $containerBuilder, $namespace, $cacheType = null, $ttl = 0 ) {
+ $containerBuilder->registerExpectedReturnType( 'BlobStore', '\Onoi\BlobStore\BlobStore' );
+
+ $cacheFactory = $containerBuilder->create( 'CacheFactory' );
+
+ $blobStore = new BlobStore(
+ $namespace,
+ $cacheFactory->newMediaWikiCompositeCache( $cacheType )
+ );
+
+ $blobStore->setNamespacePrefix(
+ $cacheFactory->getCachePrefix()
+ );
+
+ $blobStore->setExpiryInSeconds(
+ $ttl
+ );
+
+ $blobStore->setUsageState(
+ $cacheType !== CACHE_NONE && $cacheType !== false
+ );
+
+ return $blobStore;
+ } );
+
+ /**
+ * @var CachedQueryResultPrefetcher
+ */
+ $containerBuilder->registerCallback( 'CachedQueryResultPrefetcher', function( $containerBuilder, $cacheType = null ) {
+ $containerBuilder->registerExpectedReturnType( 'CachedQueryResultPrefetcher', '\SMW\Query\Result\CachedQueryResultPrefetcher' );
+
+ $settings = $containerBuilder->singleton( 'Settings' );
+ $cacheType = $cacheType === null ? $settings->get( 'smwgQueryResultCacheType' ) : $cacheType;
+
+ $cachedQueryResultPrefetcher = new CachedQueryResultPrefetcher(
+ $containerBuilder->singleton( 'Store', null ),
+ $containerBuilder->singleton( 'QueryFactory' ),
+ $containerBuilder->create(
+ 'BlobStore',
+ CachedQueryResultPrefetcher::CACHE_NAMESPACE,
+ $cacheType,
+ $settings->get( 'smwgQueryResultCacheLifetime' )
+ ),
+ $containerBuilder->singleton(
+ 'BufferedStatsdCollector',
+ CachedQueryResultPrefetcher::STATSD_ID
+ )
+ );
+
+ $cachedQueryResultPrefetcher->setDependantHashIdExtension(
+ // If the mix of dataTypes changes then modify the hash
+ $settings->get( 'smwgFulltextSearchIndexableDataTypes' ) .
+
+ // If the collation is altered then modify the hash as it
+ // is likely that the sort order of results change
+ $settings->get( 'smwgEntityCollation' ) .
+
+ // Changing the sobj has computation should invalidate
+ // existing caches to avoid oudated references SOBJ IDs
+ $settings->get( 'smwgUseComparableContentHash' )
+ );
+
+ $cachedQueryResultPrefetcher->setLogger(
+ $containerBuilder->singleton( 'MediaWikiLogger' )
+ );
+
+ $cachedQueryResultPrefetcher->setNonEmbeddedCacheLifetime(
+ $settings->get( 'smwgQueryResultNonEmbeddedCacheLifetime' )
+ );
+
+ return $cachedQueryResultPrefetcher;
+ } );
+
+ /**
+ * @var CachedPropertyValuesPrefetcher
+ */
+ $containerBuilder->registerCallback( 'CachedPropertyValuesPrefetcher', function( $containerBuilder, $cacheType = null, $ttl = 604800 ) {
+ $containerBuilder->registerExpectedReturnType( 'CachedPropertyValuesPrefetcher', CachedPropertyValuesPrefetcher::class );
+
+ $cachedPropertyValuesPrefetcher = new CachedPropertyValuesPrefetcher(
+ $containerBuilder->singleton( 'Store', null ),
+ $containerBuilder->create( 'BlobStore', CachedPropertyValuesPrefetcher::CACHE_NAMESPACE, $cacheType, $ttl )
+ );
+
+ return $cachedPropertyValuesPrefetcher;
+ } );
+
+ /**
+ * @var BufferedStatsdCollector
+ */
+ $containerBuilder->registerCallback( 'BufferedStatsdCollector', function( $containerBuilder, $id ) {
+ $containerBuilder->registerExpectedReturnType( 'BufferedStatsdCollector', '\SMW\Utils\BufferedStatsdCollector' );
+
+ // Explicitly use the DB to access a SqlBagOstuff instance
+ $cacheType = CACHE_DB;
+ $ttl = 0;
+
+ $bufferedStatsdCollector = new BufferedStatsdCollector(
+ $containerBuilder->create( 'BlobStore', BufferedStatsdCollector::CACHE_NAMESPACE, $cacheType, $ttl ),
+ $id
+ );
+
+ return $bufferedStatsdCollector;
+ } );
+
+ /**
+ * @var PropertySpecificationLookup
+ */
+ $containerBuilder->registerCallback( 'PropertySpecificationLookup', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'PropertySpecificationLookup', '\SMW\PropertySpecificationLookup' );
+
+ $propertySpecificationLookup = new PropertySpecificationLookup(
+ $containerBuilder->singleton( 'CachedPropertyValuesPrefetcher' ),
+ $containerBuilder->singleton( 'InMemoryPoolCache' )->getPoolCacheById( PropertySpecificationLookup::POOLCACHE_ID )
+ );
+
+ return $propertySpecificationLookup;
+ } );
+
+ /**
+ * @var ProtectionValidator
+ */
+ $containerBuilder->registerCallback( 'ProtectionValidator', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'ProtectionValidator', '\SMW\Protection\ProtectionValidator' );
+
+ $protectionValidator = new ProtectionValidator(
+ $containerBuilder->singleton( 'CachedPropertyValuesPrefetcher' ),
+ $containerBuilder->singleton( 'InMemoryPoolCache' )->getPoolCacheById( ProtectionValidator::POOLCACHE_ID )
+ );
+
+ $protectionValidator->setEditProtectionRight(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgEditProtectionRight' )
+ );
+
+ $protectionValidator->setCreateProtectionRight(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgCreateProtectionRight' )
+ );
+
+ $protectionValidator->setChangePropagationProtection(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgChangePropagationProtection' )
+ );
+
+ return $protectionValidator;
+ } );
+
+ /**
+ * @var PermissionPthValidator
+ */
+ $containerBuilder->registerCallback( 'PermissionPthValidator', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'PermissionPthValidator', '\SMW\PermissionPthValidator' );
+
+ $permissionPthValidator = new PermissionPthValidator(
+ $containerBuilder->create( 'ProtectionValidator' )
+ );
+
+ return $permissionPthValidator;
+ } );
+
+
+ /**
+ * @var EditProtectionUpdater
+ */
+ $containerBuilder->registerCallback( 'EditProtectionUpdater', function( $containerBuilder, \WikiPage $wikiPage, \User $user = null ) {
+ $containerBuilder->registerExpectedReturnType( 'EditProtectionUpdater', '\SMW\Protection\EditProtectionUpdater' );
+
+ $editProtectionUpdater = new EditProtectionUpdater(
+ $wikiPage,
+ $user
+ );
+
+ $editProtectionUpdater->setEditProtectionRight(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgEditProtectionRight' )
+ );
+
+ $editProtectionUpdater->setLogger(
+ $containerBuilder->singleton( 'MediaWikiLogger' )
+ );
+
+ return $editProtectionUpdater;
+ } );
+
+ /**
+ * @var PropertyRestrictionExaminer
+ */
+ $containerBuilder->registerCallback( 'PropertyRestrictionExaminer', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'PropertyRestrictionExaminer', '\SMW\PropertyRestrictionExaminer' );
+
+ $propertyRestrictionExaminer = new PropertyRestrictionExaminer();
+
+ $propertyRestrictionExaminer->setCreateProtectionRight(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgCreateProtectionRight' )
+ );
+
+ return $propertyRestrictionExaminer;
+ } );
+
+ /**
+ * @var HierarchyLookup
+ */
+ $containerBuilder->registerCallback( 'HierarchyLookup', function( $containerBuilder, $store = null, $cacheType = null ) {
+ $containerBuilder->registerExpectedReturnType( 'HierarchyLookup', '\SMW\HierarchyLookup' );
+
+ $hierarchyLookup = new HierarchyLookup(
+ $containerBuilder->singleton( 'Store', null ),
+ $containerBuilder->singleton( 'Cache', $cacheType )
+ );
+
+ $hierarchyLookup->setLogger(
+ $containerBuilder->singleton( 'MediaWikiLogger' )
+ );
+
+ $hierarchyLookup->setSubcategoryDepth(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgQSubcategoryDepth' )
+ );
+
+ $hierarchyLookup->setSubpropertyDepth(
+ $containerBuilder->singleton( 'Settings' )->get( 'smwgQSubpropertyDepth' )
+ );
+
+ return $hierarchyLookup;
+ } );
+
+ /**
+ * @var PropertyLabelFinder
+ */
+ $containerBuilder->registerCallback( 'PropertyLabelFinder', function( $containerBuilder ) {
+ $containerBuilder->registerExpectedReturnType( 'PropertyLabelFinder', '\SMW\PropertyLabelFinder' );
+
+ $lang = Localizer::getInstance()->getLang();
+
+ $propertyLabelFinder = new PropertyLabelFinder(
+ $containerBuilder->singleton( 'Store', null ),
+ $lang->getPropertyLabels(),
+ $lang->getCanonicalPropertyLabels(),
+ $lang->getCanonicalDatatypeLabels()
+ );
+
+ return $propertyLabelFinder;
+ } );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Site.php b/www/wiki/extensions/SemanticMediaWiki/src/Site.php
new file mode 100644
index 00000000..c2bd3281
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Site.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace SMW;
+
+use SiteStats;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Site {
+
+ /**
+ * Check whether the wiki is in read-only mode.
+ *
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public static function isReadOnly() {
+
+ // MediaWiki\Services\ServiceDisabledException from line 340 of
+ // ...\ServiceContainer.php: Service disabled: DBLoadBalancer
+ try {
+ $isReadOnly = wfReadOnly();
+ } catch( \MediaWiki\Services\ServiceDisabledException $e ) {
+ $isReadOnly = true;
+ }
+
+ return $isReadOnly;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public static function isBlocked() {
+ return defined( 'MEDIAWIKI_INSTALL' ) && MEDIAWIKI_INSTALL;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function name() {
+ return $GLOBALS['wgSitename'];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function wikiurl() {
+ return $GLOBALS['wgServer'] . str_replace( '$1', '', $GLOBALS['wgArticlePath'] );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function languageCode() {
+ return $GLOBALS['wgLanguageCode'];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public static function isCommandLineMode() {
+
+ // MW 1.27 wgCommandLineMode isn't set correctly
+ if ( ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) ) {
+ return true;
+ }
+
+ return $GLOBALS['wgCommandLineMode'];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return boolean
+ */
+ public static function isCapitalLinks() {
+ return $GLOBALS['wgCapitalLinks'];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param $affix string
+ *
+ * @return string
+ */
+ public static function id( $affix = '' ) {
+
+ if ( $affix !== '' && $affix{0} !== ':' ) {
+ $affix = ':' . $affix;
+ }
+
+ return wfWikiID() . $affix;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public static function stats() {
+ return [
+ 'pageCount' => SiteStats::pages(),
+ 'contentPageCount' => SiteStats::articles(),
+ 'mediaCount' => SiteStats::images(),
+ 'editCount' => SiteStats::edits(),
+ 'userCount' => SiteStats::users(),
+ 'adminCount' => SiteStats::numberingroup( 'sysop' )
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $typeFilter
+ *
+ * @return array
+ */
+ public static function getJobClasses( $typeFilter = '' ) {
+
+ if ( $typeFilter === 'SMW' ) {
+ $typeFilter = 'smw.';
+ }
+
+ $jobList = $GLOBALS['wgJobClasses'];
+
+ foreach ( $jobList as $type => $class ) {
+
+ if ( $typeFilter === '' ) {
+ continue;
+ }
+
+ if ( strpos( $type, $typeFilter ) === false ) {
+ unset( $jobList[$type] );
+ }
+ }
+
+ return $jobList;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Store.php b/www/wiki/extensions/SemanticMediaWiki/src/Store.php
new file mode 100644
index 00000000..4f1598f6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Store.php
@@ -0,0 +1,582 @@
+<?php
+
+namespace SMW;
+
+use InvalidArgumentException;
+use Onoi\MessageReporter\MessageReporterAwareTrait;
+use Psr\Log\LoggerAwareTrait;
+use SMW\Connection\ConnectionManager;
+use SMW\Utils\Timer;
+use SMWDataItem as DataItem;
+use SMWQuery;
+use SMWQueryResult;
+use SMWRequestOptions;
+use SMWSemanticData;
+use SMW\Services\Exception\ServiceNotFoundException;
+use Title;
+
+/**
+ * This group contains all parts of SMW that relate to storing and retrieving
+ * semantic data. SMW components that relate to semantic querying only have
+ * their own group.
+ *
+ * @defgroup SMWStore SMWStore
+ * @ingroup SMW
+ */
+
+/**
+ * The abstract base class for all classes that implement access to some
+ * semantic store. Besides the relevant interface, this class provides default
+ * implementations for some optional methods, which inform the caller that
+ * these methods are not implemented.
+ *
+ * @ingroup SMWStore
+ *
+ * @author Markus Krötzsch
+ */
+abstract class Store implements QueryEngine {
+
+ use MessageReporterAwareTrait;
+ use LoggerAwareTrait;
+
+ /**
+ * Option to define whether creating updates jobs is allowed for a request
+ * or not.
+ */
+ const OPT_CREATE_UPDATE_JOB = 'opt.create.update.job';
+
+ /**
+ * @var ConnectionManager
+ */
+ protected $connectionManager = null;
+
+ /**
+ * @var Options
+ */
+ protected $options = null;
+
+///// Reading methods /////
+
+ /**
+ * @see EntityLookup::getSemanticData
+ *
+ * @param DIWikiPage $subject
+ * @param string[]|bool $filter
+ */
+ public abstract function getSemanticData( DIWikiPage $subject, $filter = false );
+
+ /**
+ * @see EntityLookup::getPropertyValues
+ *
+ * @param $subject mixed SMWDIWikiPage or null
+ * @param $property DIProperty
+ * @param $requestoptions SMWRequestOptions
+ *
+ * @return array of DataItem
+ */
+ public abstract function getPropertyValues( $subject, DIProperty $property, $requestoptions = null );
+
+ /**
+ * @see EntityLookup::getPropertySubjects
+ *
+ * @return DIWikiPage[]
+ */
+ public abstract function getPropertySubjects( DIProperty $property, $value, $requestoptions = null );
+
+ /**
+ * Get an array of all subjects that have some value for the given
+ * property. The result is an array of DIWikiPage objects.
+ *
+ * @return DIWikiPage[]
+ */
+ public abstract function getAllPropertySubjects( DIProperty $property, $requestoptions = null );
+
+ /**
+ * @see EntityLookup::getProperties
+ *
+ * @param DIWikiPage $subject denoting the subject
+ * @param SMWRequestOptions|null $requestOptions optionally defining further options
+ *
+ * @return DataItem
+ */
+ public abstract function getProperties( DIWikiPage $subject, $requestOptions = null );
+
+ /**
+ * @see EntityLookup::getInProperties
+ *
+ * @param DataItem $object
+ * @param RequestOptions|null $requestOptions
+ *
+ * @return DataItem[]|[]
+ */
+ public abstract function getInProperties( DataItem $object, $requestoptions = null );
+
+ /**
+ * Convenience method to find the sortkey of an SMWDIWikiPage. The
+ * result is based on the contents of this store, and may differ from
+ * the MediaWiki database entry about a Title objects sortkey. If no
+ * sortkey is stored, the default sortkey (title string) is returned.
+ *
+ * @param DIWikiPage $dataItem
+ *
+ * @return string sortkey
+ */
+ public function getWikiPageSortKey( DIWikiPage $dataItem ) {
+
+ $dataItems = $this->getPropertyValues( $dataItem, new DIProperty( '_SKEY' ) );
+
+ if ( is_array( $dataItems ) && count( $dataItems ) > 0 ) {
+ return end( $dataItems )->getString();
+ }
+
+ return str_replace( '_', ' ', $dataItem->getDBkey() );
+ }
+
+ /**
+ * Convenience method to find the redirect target of a DIWikiPage
+ * or DIProperty object. Returns a dataitem of the same type that
+ * the input redirects to, or the input itself if there is no redirect.
+ *
+ * @param DataItem $dataItem
+ *
+ * @return DataItem
+ */
+ public function getRedirectTarget( DataItem $dataItem ) {
+
+ $type = $dataItem->getDIType();
+
+ if ( $type !== DataItem::TYPE_WIKIPAGE && $type !== DataItem::TYPE_PROPERTY ) {
+ throw new InvalidArgumentException( 'Store::getRedirectTarget expects a DIProperty or DIWikiPage object.' );
+ }
+
+ if ( $type === DataItem::TYPE_PROPERTY ) {
+
+ if ( !$dataItem->isUserDefined() ) {
+ return $dataItem;
+ }
+
+ $wikipage = $dataItem->getDiWikiPage();
+ } elseif ( $type === DataItem::TYPE_WIKIPAGE ) {
+ $wikipage = $dataItem;
+ }
+
+ $dataItems = $this->getPropertyValues( $wikipage, new DIProperty( '_REDI' ) );
+
+ if ( is_array( $dataItems ) && count( $dataItems ) > 0 ) {
+
+ $redirectDataItem = end( $dataItems );
+
+ if ( $type == DataItem::TYPE_PROPERTY && $redirectDataItem instanceof DIWikiPage ) {
+ $dataItem = DIProperty::newFromUserLabel( $redirectDataItem->getDBkey() );
+ } else {
+ $dataItem = $redirectDataItem;
+ }
+ }
+
+ return $dataItem;
+ }
+
+///// Writing methods /////
+
+ /**
+ * Delete all semantic properties that the given subject has. This
+ * includes relations, attributes, and special properties. This does
+ * not delete the respective text from the wiki, but only clears the
+ * stored data.
+ *
+ * @param Title $subject
+ */
+ public abstract function deleteSubject( Title $subject );
+
+ /**
+ * Update the semantic data stored for some individual. The data is
+ * given as a SemanticData object, which contains all semantic data
+ * for one particular subject.
+ *
+ * @param SemanticData $data
+ */
+ protected abstract function doDataUpdate( SemanticData $data );
+
+ /**
+ * Update the semantic data stored for some individual. The data is
+ * given as a SemanticData object, which contains all semantic data
+ * for one particular subject.
+ *
+ * @param SemanticData $semanticData
+ */
+ public function updateData( SemanticData $semanticData ) {
+
+ if ( !$this->getOption( 'smwgSemanticsEnabled' ) ) {
+ return;
+ }
+
+ Timer::start( __METHOD__ );
+
+ $applicationFactory = ApplicationFactory::getInstance();
+
+ $subject = $semanticData->getSubject();
+ $hash = $subject->getHash();
+
+ /**
+ * @since 1.6
+ */
+ \Hooks::run( 'SMWStore::updateDataBefore', [ $this, $semanticData ] );
+
+ $this->doDataUpdate( $semanticData );
+
+ /**
+ * @since 1.6
+ */
+ \Hooks::run( 'SMWStore::updateDataAfter', [ $this, $semanticData ] );
+
+ $context = [
+ 'method' => __METHOD__,
+ 'role' => 'production',
+ 'origin' => $hash,
+ 'procTime' => Timer::getElapsedTime( __METHOD__, 5 ),
+ ];
+
+ $this->logger->info( '[Store] Update completed: {origin} (procTime in sec: {procTime})', $context );
+
+ if ( !$this->getOption( 'smwgAutoRefreshSubject' ) || $semanticData->getOption( Enum::OPT_SUSPEND_PURGE ) ) {
+ return $this->logger->info( '[Store] Skipping html, parser cache purge', [ 'role' => 'user' ] );
+ }
+
+ $pageUpdater = $applicationFactory->newPageUpdater();
+
+ $pageUpdater->addPage( $subject->getTitle() );
+ $pageUpdater->waitOnTransactionIdle();
+ $pageUpdater->markAsPending();
+ $pageUpdater->setOrigin( __METHOD__ );
+
+ $pageUpdater->doPurgeParserCache();
+ $pageUpdater->doPurgeHtmlCache();
+ $pageUpdater->pushUpdate();
+ }
+
+ /**
+ * Clear all semantic data specified for some page.
+ *
+ * @param DIWikiPage $di
+ */
+ public function clearData( DIWikiPage $di ) {
+ $this->updateData( new SMWSemanticData( $di ) );
+ }
+
+ /**
+ * Update the store to reflect a renaming of some article. Normally
+ * this happens when moving pages in the wiki, and in this case there
+ * is also a new redirect page generated at the old position. The title
+ * objects given are only used to specify the name of the title before
+ * and after the move -- do not use their IDs for anything! The ID of
+ * the moved page is given in $pageid, and the ID of the newly created
+ * redirect, if any, is given by $redirid. If no new page was created,
+ * $redirid will be 0.
+ */
+ public abstract function changeTitle( Title $oldtitle, Title $newtitle, $pageid, $redirid = 0 );
+
+///// Query answering /////
+
+ /**
+ * @note Change the signature in 3.* to avoid for subclasses to manage the
+ * hooks; keep the current signature to adhere semver for the 2.* branch
+ *
+ * Execute the provided query and return the result as an
+ * SMWQueryResult if the query was a usual instance retrieval query. In
+ * the case that the query asked for a plain string (querymode
+ * MODE_COUNT or MODE_DEBUG) a plain wiki and HTML-compatible string is
+ * returned.
+ *
+ * @param SMWQuery $query
+ *
+ * @return SMWQueryResult
+ */
+ public abstract function getQueryResult( SMWQuery $query );
+
+ /**
+ * @note Change the signature to abstract for the 3.* branch
+ *
+ * @since 2.1
+ *
+ * @param SMWQuery $query
+ *
+ * @return SMWQueryResult
+ */
+ protected function fetchQueryResult( SMWQuery $query ) {
+ }
+
+///// Special page functions /////
+
+ /**
+ * Return all properties that have been used on pages in the wiki. The
+ * result is an array of arrays, each containing a property data item
+ * and a count. The expected order is alphabetical w.r.t. to property
+ * names.
+ *
+ * If there is an error on creating some property object, then a
+ * suitable SMWDIError object might be returned in its place. Even if
+ * there are errors, the function should always return the number of
+ * results requested (otherwise callers might assume that there are no
+ * further results to ask for).
+ *
+ * @param SMWRequestOptions $requestoptions
+ *
+ * @return array of array( DIProperty|SMWDIError, integer )
+ */
+ public abstract function getPropertiesSpecial( $requestoptions = null );
+
+ /**
+ * Return all properties that have been declared in the wiki but that
+ * are not used on any page. Stores might restrict here to those
+ * properties that have been given a type if they have no efficient
+ * means of accessing the set of all pages in the property namespace.
+ *
+ * If there is an error on creating some property object, then a
+ * suitable SMWDIError object might be returned in its place. Even if
+ * there are errors, the function should always return the number of
+ * results requested (otherwise callers might assume that there are no
+ * further results to ask for).
+ *
+ * @param SMWRequestOptions $requestoptions
+ *
+ * @return array of DIProperty|SMWDIError
+ */
+ public abstract function getUnusedPropertiesSpecial( $requestoptions = null );
+
+ /**
+ * Return all properties that are used on some page but that do not
+ * have any page describing them. Stores that have no efficient way of
+ * accessing the set of all existing pages can extend this list to all
+ * properties that are used but do not have a type assigned to them.
+ *
+ * @param SMWRequestOptions $requestoptions
+ *
+ * @return array of array( DIProperty, int )
+ */
+ public abstract function getWantedPropertiesSpecial( $requestoptions = null );
+
+ /**
+ * Return statistical information as an associative array with the
+ * following keys:
+ * - 'PROPUSES': Number of property instances (value assignments) in the datatbase
+ * - 'USEDPROPS': Number of properties that are used with at least one value
+ * - 'DECLPROPS': Number of properties that have been declared (i.e. assigned a type)
+ * - 'OWNPAGE': Number of properties with their own page
+ * - 'QUERY': Number of inline queries
+ * - 'QUERYSIZE': Represents collective query size
+ * - 'CONCEPTS': Number of declared concepts
+ * - 'SUBOBJECTS': Number of declared subobjects
+ *
+ * @return array
+ */
+ public abstract function getStatistics();
+
+ /**
+ * Store administration
+ */
+
+ /**
+ * @private
+ *
+ * Returns store specific services. Services are registered with the store
+ * implementation and may provide different services that are only available
+ * for a particular store.
+ *
+ * @since 3.0
+ *
+ * @param string $service
+ *
+ * @return mixed
+ * @throws ServiceNotFoundException
+ */
+ public function service( $service, ...$args ) {
+ throw new ServiceNotFoundException( $service );
+ }
+
+ /**
+ * Setup all storage structures properly for using the store. This
+ * function performs tasks like creation of database tables. It is
+ * called upon installation as well as on upgrade: hence it must be
+ * able to upgrade existing storage structures if needed. It should
+ * return "true" if successful and return a meaningful string error
+ * message otherwise.
+ *
+ * The parameter $verbose determines whether the procedure is allowed
+ * to report on its progress. This is doen by just using print and
+ * possibly ob_flush/flush. This is also relevant for preventing
+ * timeouts during long operations. All output must be valid in an HTML
+ * context, but should preferably be plain text, possibly with some
+ * linebreaks and weak markup.
+ *
+ * @param boolean $verbose
+ *
+ * @return boolean Success indicator
+ */
+ public abstract function setup( $verbose = true );
+
+ /**
+ * Drop (delete) all storage structures created by setup(). This will
+ * delete all semantic data and possibly leave the wiki uninitialised.
+ *
+ * @param boolean $verbose
+ */
+ public abstract function drop( $verbose = true );
+
+ /**
+ * Refresh some objects in the store, addressed by numerical ids. The
+ * meaning of the ids is private to the store, and does not need to
+ * reflect the use of IDs elsewhere (e.g. page ids). The store is to
+ * refresh $count objects starting from the given $index. Typically,
+ * updates are achieved by generating update jobs. After the operation,
+ * $index is set to the next index that should be used for continuing
+ * refreshing, or to -1 for signaling that no objects of higher index
+ * require refresh. The method returns a decimal number between 0 and 1
+ * to indicate the overall progress of the refreshing (e.g. 0.7 if 70%
+ * of all objects were refreshed).
+ *
+ * The optional parameter $namespaces may contain an array of namespace
+ * constants. If given, only objects from those namespaces will be
+ * refreshed. The default value FALSE disables this feature.
+ *
+ * The optional parameter $usejobs indicates whether updates should be
+ * processed later using MediaWiki jobs, instead of doing all updates
+ * immediately. The default is TRUE.
+ *
+ * @param $index integer
+ * @param $count integer
+ * @param $namespaces mixed array or false
+ * @param $usejobs boolean
+ *
+ * @return float between 0 and 1 to indicate the overall progress of the refreshing
+ */
+ public abstract function refreshData( &$index, $count, $namespaces = false, $usejobs = true );
+
+ /**
+ * Setup the store.
+ *
+ * @since 1.8
+ *
+ * @param bool $verbose
+ * @param Options|null $options
+ *
+ * @return boolean Success indicator
+ */
+ public static function setupStore( $verbose = true, $options = null ) {
+
+ // See notes in ExtensionSchemaUpdates
+ if ( is_bool( $verbose ) ) {
+ $verbose = $verbose;
+ }
+
+ if ( isset( $options['verbose'] ) ) {
+ $verbose = $options['verbose'];
+ }
+
+ if ( isset( $options['options'] ) ) {
+ $options = $options['options'];
+ }
+
+ $store = StoreFactory::getStore();
+
+ if ( $options instanceof Options ) {
+ foreach ( $options->getOptions() as $key => $value ) {
+ $store->getOptions()->set( $key, $value );
+ }
+ }
+
+ return $store->setup( $verbose );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return Options
+ */
+ public function getOptions() {
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ return $this->options;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ public function setOption( $key, $value ) {
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ return $this->options->set( $key, $value );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $key
+ * @param mixed $default
+ *
+ * @return mixed
+ */
+ public function getOption( $key, $default = null ) {
+
+ if ( $this->options === null ) {
+ $this->options = new Options();
+ }
+
+ return $this->options->safeGet( $key, $default );
+ }
+
+ /**
+ * @since 2.0
+ */
+ public function clear() {
+
+ if ( $this->connectionManager !== null ) {
+ $this->connectionManager->releaseConnections();
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|null $type
+ *
+ * @return array
+ */
+ public function getInfo( $type = null ) {
+ return [];
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param ConnectionManager $connectionManager
+ */
+ public function setConnectionManager( ConnectionManager $connectionManager ) {
+ $this->connectionManager = $connectionManager;
+ }
+
+ /**
+ * @since 2.1
+ *
+ * @param string $type
+ *
+ * @return mixed
+ */
+ public function getConnection( $type ) {
+
+ if ( $this->connectionManager === null ) {
+ $this->connectionManager = ApplicationFactory::getInstance()->getConnectionManager();
+ }
+
+ return $this->connectionManager->getConnection( $type );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/StoreAware.php b/www/wiki/extensions/SemanticMediaWiki/src/StoreAware.php
new file mode 100644
index 00000000..fb7cba14
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/StoreAware.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace SMW;
+
+/**
+ * Describes an instance that is aware of a Store object.
+ *
+ * @license GNU GPL v2
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+interface StoreAware {
+
+ /**
+ * @since 2.5
+ *
+ * @param Store $store
+ */
+ public function setStore( Store $store );
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/StoreFactory.php b/www/wiki/extensions/SemanticMediaWiki/src/StoreFactory.php
new file mode 100644
index 00000000..e6251185
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/StoreFactory.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace SMW;
+
+use RuntimeException;
+use SMW\Exception\StoreNotFoundException;
+use Onoi\MessageReporter\NullMessageReporter;
+use Psr\Log\NullLogger;
+
+/**
+ * Factory method that returns an instance of the default store, or an
+ * alternative store instance.
+ *
+ * @license GNU GPL v2+
+ * @since 1.9
+ *
+ * @author mwjames
+ */
+class StoreFactory {
+
+ /**
+ * @var array
+ */
+ private static $instance = [];
+
+ /**
+ * @since 1.9
+ *
+ * @param string|null $class
+ *
+ * @return Store
+ * @throws RuntimeException
+ * @throws StoreNotFoundException
+ */
+ public static function getStore( $class = null ) {
+
+ if ( $class === null ) {
+ $class = $GLOBALS['smwgDefaultStore'];
+ }
+
+ if ( !isset( self::$instance[$class] ) ) {
+ self::$instance[$class] = self::newFromClass( $class );
+ }
+
+ return self::$instance[$class];
+ }
+
+ /**
+ * @since 1.9
+ */
+ public static function clear() {
+ self::$instance = [];
+ }
+
+ private static function newFromClass( $class ) {
+
+ if ( !class_exists( $class ) ) {
+ throw new RuntimeException( "{$class} was not found!" );
+ }
+
+ $instance = new $class;
+
+ if ( !( $instance instanceof Store ) ) {
+ throw new StoreNotFoundException( "{$class} cannot be used as a store instance!" );
+ }
+
+ $instance->setMessageReporter( new NullMessageReporter() );
+ $instance->setLogger( new NullLogger() );
+
+ return $instance;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/StringCondition.php b/www/wiki/extensions/SemanticMediaWiki/src/StringCondition.php
new file mode 100644
index 00000000..950caf3b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/StringCondition.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace SMW;
+
+/**
+ * Small data container class for describing filtering conditions on the string
+ * label of some entity. States that a given string should either be prefix,
+ * postfix, or some arbitrary part of labels.
+ *
+ * @license GNU GPL v2+
+ * @since 1.0
+ *
+ * @author Markus Krötzsch
+ */
+class StringCondition {
+
+ /**
+ * String matches prefix
+ */
+ const COND_PRE = 0;
+ const STRCOND_PRE = self::COND_PRE; // Deprecated
+
+ /**
+ * String matches postfix
+ */
+ const COND_POST = 1;
+ const STRCOND_POST = self::COND_POST; // Deprecated
+
+ /**
+ * String matches to some inner part
+ */
+ const COND_MID = 2;
+ const STRCOND_MID = self::COND_MID; // Deprecated
+
+ /**
+ * String matches as equal
+ */
+ const COND_EQ = 3;
+
+ /**
+ * String to match.
+ *
+ * @var string
+ */
+ public $string;
+
+ /**
+ * Whether to match the strings as conjunction or
+ * disjunction.
+ *
+ * @var boolean
+ */
+ public $isOr;
+
+ /**
+ * @var boolean
+ */
+ public $isNot;
+
+ /**
+ * @var integer
+ */
+ public $condition;
+
+ /**
+ * @since 1.0
+ *
+ * @param srting $string
+ * @param integer $condition
+ * @param boolean $isOr
+ */
+ public function __construct( $string, $condition, $isOr = false, $isNot = false ) {
+ $this->string = $string;
+ $this->condition = $condition;
+ $this->isOr = $isOr;
+ $this->isNot = $isNot;
+ }
+
+ /**
+ * @since 2.4
+ *
+ * @return string
+ */
+ public function getHash() {
+ return $this->string . '#' . $this->condition . '#' . $this->isOr . '#' . $this->isNot;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/TypesRegistry.php b/www/wiki/extensions/SemanticMediaWiki/src/TypesRegistry.php
new file mode 100644
index 00000000..90afb490
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/TypesRegistry.php
@@ -0,0 +1,310 @@
+<?php
+
+namespace SMW;
+
+use SMW\DataValues\AllowsListValue;
+use SMW\DataValues\AllowsPatternValue;
+use SMW\DataValues\AllowsValue;
+use SMW\DataValues\BooleanValue;
+use SMW\DataValues\ErrorMsgTextValue;
+use SMW\DataValues\ExternalFormatterUriValue;
+use SMW\DataValues\ExternalIdentifierValue;
+use SMW\DataValues\ImportValue;
+use SMW\DataValues\KeywordValue;
+use SMW\DataValues\LanguageCodeValue;
+use SMW\DataValues\MonolingualTextValue;
+use SMW\DataValues\PropertyChainValue;
+use SMW\DataValues\PropertyValue;
+use SMW\DataValues\ReferenceValue;
+use SMW\DataValues\StringValue;
+use SMW\DataValues\TelephoneUriValue;
+use SMW\DataValues\TemperatureValue;
+use SMW\DataValues\TypesValue;
+use SMW\DataValues\UniquenessConstraintValue;
+use SMWDataItem as DataItem;
+use SMWNumberValue as NumberValue;
+use SMWQuantityValue as QuantityValue;
+use SMWTimeValue as TimeValue;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TypesRegistry {
+
+ /**
+ * @note All IDs must start with an underscore, two underscores indicate a
+ * truly internal (non user-interacted type). All others should also get a
+ * translation in the language files, or they won't be available for users.
+ *
+ * @since 2.5
+ *
+ * @return array
+ */
+ public static function getDataTypeList() {
+ return [
+
+ // ID => [ Class, DI type, isSubDataType, isBrowsable ]
+
+ // Special import vocabulary type
+ ImportValue::TYPE_ID => [ ImportValue::class, DataItem::TYPE_BLOB, false, false ],
+ // Property chain
+ PropertyChainValue::TYPE_ID => [ PropertyChainValue::class, DataItem::TYPE_BLOB, false, false ],
+ // Property type (possibly predefined, not always based on a page)
+ PropertyValue::TYPE_ID => [ PropertyValue::class, DataItem::TYPE_PROPERTY, false, false ],
+ // Text type
+ StringValue::TYPE_ID => [ StringValue::class, DataItem::TYPE_BLOB, false, false ],
+ // Code type
+ StringValue::TYPE_COD_ID => [ StringValue::class, DataItem::TYPE_BLOB, false, false ],
+ // Legacy string ID `_str`
+ StringValue::TYPE_LEGACY_ID => [ StringValue::class, DataItem::TYPE_BLOB, false, false ],
+ // Email type
+ '_ema' => [ 'SMWURIValue', DataItem::TYPE_URI, false, false ],
+ // URL/URI type
+ '_uri' => [ 'SMWURIValue', DataItem::TYPE_URI, false, false ],
+ // Annotation URI type
+ '_anu' => [ 'SMWURIValue', DataItem::TYPE_URI, false, false ],
+ // Phone number (URI) type
+ '_tel' => [ TelephoneUriValue::class, DataItem::TYPE_URI, false, false ],
+ // Page type
+ '_wpg' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Property page type TODO: make available to user space
+ '_wpp' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Category page type TODO: make available to user space
+ '_wpc' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Form page type for Semantic Forms
+ '_wpf' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Rule page
+ '_wps' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Number type
+ NumberValue::TYPE_ID => [ NumberValue::class, DataItem::TYPE_NUMBER, false, false ],
+ // Temperature type
+ TemperatureValue::TYPE_ID => [ TemperatureValue::class, DataItem::TYPE_NUMBER, false, false ],
+ // Time type
+ TimeValue::TYPE_ID => [ TimeValue::class, DataItem::TYPE_TIME, false, false ],
+ // Boolean type
+ '_boo' => [ BooleanValue::class, DataItem::TYPE_BOOLEAN, false, false ],
+ // Value list type (replacing former nary properties)
+ '_rec' => [ 'SMWRecordValue', DataItem::TYPE_WIKIPAGE, true, false ],
+ MonolingualTextValue::TYPE_ID => [ MonolingualTextValue::class, DataItem::TYPE_WIKIPAGE, true, false ],
+ ReferenceValue::TYPE_ID => [ ReferenceValue::class, DataItem::TYPE_WIKIPAGE, true, false ],
+ // Geographical coordinates
+ '_geo' => [ null, DataItem::TYPE_GEO, false, false ],
+ // Geographical polygon
+ '_gpo' => [ null, DataItem::TYPE_BLOB, false, false ],
+ // External identifier
+ ExternalIdentifierValue::TYPE_ID => [ ExternalIdentifierValue::class, DataItem::TYPE_BLOB, false, false ],
+ // KeywordValue
+ KeywordValue::TYPE_ID => [ KeywordValue::class, DataItem::TYPE_BLOB, false, false ],
+ // Type for numbers with units of measurement
+ QuantityValue::TYPE_ID => [ QuantityValue::class, DataItem::TYPE_NUMBER, false, false ],
+ // Special types are not avaialble directly for users (and have no local language name):
+ // Special type page type
+ TypesValue::TYPE_ID => [ TypesValue::class, DataItem::TYPE_URI, false, false ],
+ // Special type list for decalring _rec properties
+ '__pls' => [ 'SMWPropertyListValue', DataItem::TYPE_BLOB, false, false ],
+ // Special concept page type
+ '__con' => [ 'SMWConceptValue', DataItem::TYPE_CONCEPT, false, false ],
+ // Special string type
+ '__sps' => [ StringValue::class, DataItem::TYPE_BLOB, false, false ],
+ // Special uri type
+ '__spu' => [ 'SMWURIValue', DataItem::TYPE_URI, false, false ],
+ // Special subobject type
+ '__sob' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, true, true ],
+ // Special subproperty type
+ '__sup' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Special subcategory type
+ '__suc' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Special Form page type for Semantic Forms
+ '__spf' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Special instance of type
+ '__sin' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Special redirect type
+ '__red' => [ 'SMWWikiPageValue', DataItem::TYPE_WIKIPAGE, false, true ],
+ // Special error type
+ '__err' => [ 'SMWErrorValue', DataItem::TYPE_ERROR, false, false ],
+ // Special error type
+ '__errt' => [ ErrorMsgTextValue::class, DataItem::TYPE_BLOB, false, false ],
+ // Sort key of a page
+ '__key' => [ StringValue::class, DataItem::TYPE_BLOB, false, false ],
+ LanguageCodeValue::TYPE_ID => [ LanguageCodeValue::class, DataItem::TYPE_BLOB, false, false ],
+ AllowsValue::TYPE_ID => [ AllowsValue::class, DataItem::TYPE_BLOB, false, false ],
+ AllowsListValue::TYPE_ID => [ AllowsListValue::class, DataItem::TYPE_BLOB, false, false ],
+ AllowsPatternValue::TYPE_ID => [ AllowsPatternValue::class, DataItem::TYPE_BLOB, false, false ],
+ '__pvuc' => [ UniquenessConstraintValue::class, DataItem::TYPE_BOOLEAN, false, false ],
+ '__pefu' => [ ExternalFormatterUriValue::class, DataItem::TYPE_URI, false, false ]
+ ];
+ }
+
+ /**
+ * @note All ids must start with underscores. The translation for each ID,
+ * if any, is defined in the language files. Properties without translation
+ * cannot be entered by or displayed to users, whatever their "show" value
+ * below.
+ *
+ * @since 3.0
+ *
+ * @param boolean $useCategoryHierarchy
+ *
+ * @return array
+ */
+ public static function getPropertyList( $useCategoryHierarchy = true ) {
+ return [
+
+ // ID => [ valueType, isVisible, isAnnotable, isDeclarative ]
+
+ '_TYPE' => [ '__typ', true, true, true ], // "has type"
+ '_URI' => [ '__spu', true, true, false ], // "equivalent URI"
+ '_INST' => [ '__sin', false, true, false ], // instance of a category
+ '_UNIT' => [ '__sps', true, true, true ], // "displays unit"
+ '_IMPO' => [ '__imp', true, true, true ], // "imported from"
+ '_CONV' => [ '__sps', true, true, true ], // "corresponds to"
+ '_SERV' => [ '__sps', true, true, true ], // "provides service"
+ '_PVAL' => [ '__pval', true, true, true ], // "allows value"
+ '_REDI' => [ '__red', true, true, false ], // redirects to some page
+ '_SUBP' => [ '__sup', true, true, true ], // "subproperty of"
+ '_SUBC' => [ '__suc', !$useCategoryHierarchy, true, true ], // "subcategory of"
+ '_CONC' => [ '__con', false, true, false ], // associated concept
+ '_MDAT' => [ '_dat', false, false, false ], // "modification date"
+ '_CDAT' => [ '_dat', false, false, false ], // "creation date"
+ '_NEWP' => [ '_boo', false, false, false ], // "is a new page"
+ '_EDIP' => [ '_boo', true, true, false ], // "is edit protected"
+ '_LEDT' => [ '_wpg', false, false, false ], // "last editor is"
+ '_ERRC' => [ '__sob', false, false, false ], // "has error"
+ '_ERRT' => [ '__errt', false, false, false ], // "has error text"
+ '_ERRP' => [ '_wpp', false, false, false ], // "has improper value for"
+ '_LIST' => [ '__pls', true, true, true ], // "has fields"
+ '_SKEY' => [ '__key', false, true, false ], // sort key of a page
+
+ // FIXME SF related properties to be removed with 3.0
+ '_SF_DF' => [ '__spf', true, true, false ], // Semantic Form's default form property
+ '_SF_AF' => [ '__spf', true, true, false ], // Semantic Form's alternate form property
+
+ '_SOBJ' => [ '__sob', true, false, false ], // "has subobject"
+ '_ASK' => [ '__sob', false, false, false ], // "has query"
+ '_ASKST' => [ '_cod', true, false, false ], // "Query string"
+ '_ASKFO' => [ '_txt', true, false, false ], // "Query format"
+ '_ASKSI' => [ '_num', true, false, false ], // "Query size"
+ '_ASKDE' => [ '_num', true, false, false ], // "Query depth"
+ '_ASKDU' => [ '_num', true, false, false ], // "Query duration"
+ '_ASKSC' => [ '_txt', true, false, false ], // "Query source"
+ '_ASKPA' => [ '_cod', true, false, false ], // "Query parameters"
+ '_ASKCO' => [ '_num', true, false, false ], // "Query scode"
+ '_MEDIA' => [ '_txt', true, false, false ], // "has media type"
+ '_MIME' => [ '_txt', true, false, false ], // "has mime type"
+ '_PREC' => [ '_num', true, true, true ], // "Display precision of"
+ '_LCODE' => [ '__lcode', true, true, false ], // "Language code"
+ '_TEXT' => [ '_txt', true, true, false ], // "Text"
+ '_PDESC' => [ '_mlt_rec', true, true, true ], // "Property description"
+ '_PVAP' => [ '__pvap', true, true, true ], // "Allows pattern"
+ '_PVALI' => [ '__pvali', true, true, true ], // "Allows value list"
+ '_DTITLE' => [ '_txt', false, true, false ], // "Display title of"
+ '_PVUC' => [ '__pvuc', true, true, true ], // Uniqueness constraint
+ '_PEID' => [ '_eid', true, true, false ], // External identifier
+ '_PEFU' => [ '__pefu', true, true, true ], // External formatter uri
+ '_PPLB' => [ '_mlt_rec', true, true, true ], // Preferred property label
+ '_CHGPRO' => [ '_cod', true, false, true ], // "Change propagation"
+ '_PPGR' => [ '_boo', true, true, true ], // "Property group"
+
+ // Schema
+ '_SCHEMA_TYPE' => [ '_txt', true, false, false ], // "Schema type"
+ '_SCHEMA_DEF' => [ '_cod', true, false, false ], // "Schema definition"
+ '_SCHEMA_DESC' => [ '_txt', true, false, false ], // "Schema description"
+ '_SCHEMA_TAG' => [ '_txt', true, false, false ], // "Schema tag"
+ '_SCHEMA_LINK' => [ '_wps', true, false, false ], // "Schema link"
+
+ //
+ '_FORMAT_SCHEMA' => [ '_wps', true, true, false ], // "Formatter schema"
+
+ // File attachment
+ '_FILE_ATTCH' => [ '__sob', false, false, false ], // "File attachment"
+ '_CONT_TYPE' => [ '_txt', true, true, false ], // "Content type"
+ '_CONT_AUTHOR' => [ '_txt', true, true, false ], // "Content author"
+ '_CONT_LEN' => [ '_num', true, true, false ], // "Content length"
+ '_CONT_LANG' => [ '__lcode', true, true, false ], // "Content language"
+ '_CONT_TITLE' => [ '_txt', true, true, false ], // "Content title"
+ '_CONT_DATE' => [ '_dat', true, true, false ], // "Content date",
+ '_CONT_KEYW' => [ '_keyw', true, true, false ], // "Content keyword"
+
+ // Translation
+ '_TRANS' => [ '__sob', false, false, false ], // "Translation"
+ '_TRANS_SOURCE' => [ '_wpg', true, false, false ], // "Translation source"
+ '_TRANS_GROUP' => [ '_txt', true, false, false ], // "Translation group"
+ ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public static function getTypesByGroup( $group = '' ) {
+
+ if ( $group === 'primitive' ) {
+ return [ '_txt' => true , '_boo' => true , '_num' => true, '_dat' => true ];
+ }
+
+ if ( $group === 'compound' ) {
+ return [ '_ema' => true, '_tel' => true, '_tem' => true ];
+ }
+
+ return [];
+ }
+
+ /**
+ * Use pre-defined ids for Very Important Properties, avoiding frequent
+ * ID lookups for those.
+ *
+ * @note These constants also occur in the store. Changing them will
+ * require to run setup.php again.
+ *
+ * @since 3.0
+ *
+ * @return array
+ */
+ public static function getFixedPropertyIdList() {
+ return [
+ '_TYPE' => 1,
+ '_URI' => 2,
+ '_INST' => 4,
+ '_UNIT' => 7,
+ '_IMPO' => 8,
+ '_PPLB' => 9,
+ '_PDESC' => 10,
+ '_PREC' => 11,
+ '_CONV' => 12,
+ '_SERV' => 13,
+ '_PVAL' => 14,
+ '_REDI' => 15,
+ '_DTITLE' => 16,
+ '_SUBP' => 17,
+ '_SUBC' => 18,
+ '_CONC' => 19,
+ '_ERRP' => 22,
+ // '_1' => 23, // properties for encoding (short) lists
+ // '_2' => 24,
+ // '_3' => 25,
+ // '_4' => 26,
+ // '_5' => 27,
+ // '_SOBJ' => 27
+ '_LIST' => 28,
+ '_MDAT' => 29,
+ '_CDAT' => 30,
+ '_NEWP' => 31,
+ '_LEDT' => 32,
+ // properties related to query management
+ '_ASK' => 33,
+ '_ASKST' => 34,
+ '_ASKFO' => 35,
+ '_ASKSI' => 36,
+ '_ASKDE' => 37,
+ '_ASKPA' => 38,
+ '_ASKSC' => 39,
+ '_LCODE' => 40,
+ '_TEXT' => 41,
+ ];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/BufferedStatsdCollector.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/BufferedStatsdCollector.php
new file mode 100644
index 00000000..10d4b508
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/BufferedStatsdCollector.php
@@ -0,0 +1,235 @@
+<?php
+
+namespace SMW\Utils;
+
+use Onoi\BlobStore\BlobStore;
+use SMW\ApplicationFactory;
+
+/**
+ * Collect statistics in a provisional schema-free storage that depends on the
+ * availability of the cache back-end.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class BufferedStatsdCollector {
+
+ /**
+ * Update this version number when the serialization format
+ * changes.
+ */
+ const VERSION = '0.2';
+
+ /**
+ * Available operations
+ */
+ const STATS_INIT = 'init';
+ const STATS_INCR = 'incr';
+ const STATS_SET = 'set';
+ const STATS_MEDIAN = 'median';
+
+ /**
+ * Namespace occupied by the BlobStore
+ */
+ const CACHE_NAMESPACE = 'smw:stats:store';
+
+ /**
+ * @var BlobStore
+ */
+ private $blobStore;
+
+ /**
+ * @var string|integer
+ */
+ private $statsdId;
+
+ /**
+ * @var boolean
+ */
+ private $shouldRecord = true;
+
+ /**
+ * @var array
+ */
+ private $stats = [];
+
+ /**
+ * Identifies an update fingerprint to compare invoked deferred updates
+ * against each other and filter those with the same print to avoid recording
+ * duplicate stats.
+ *
+ * @var string
+ */
+ private $fingerprint = null;
+
+ /**
+ * @var array
+ */
+ private $operations = [];
+
+ /**
+ * @since 2.5
+ *
+ * @param BlobStore $blobStore
+ * @param string $statsdId
+ */
+ public function __construct( BlobStore $blobStore, $statsdId ) {
+ $this->blobStore = $blobStore;
+ $this->statsdId = $statsdId;
+ $this->fingerprint = $statsdId . uniqid();
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $shouldRecord
+ */
+ public function shouldRecord( $shouldRecord ) {
+ $this->shouldRecord = (bool)$shouldRecord;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @return array
+ */
+ public function getStats() {
+
+ $container = $this->blobStore->read(
+ md5( $this->statsdId . self::VERSION )
+ );
+
+ return StatsFormatter::getStatsFromFlatKey( $container->getData(), '.' );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|array $key
+ */
+ public function incr( $key ) {
+
+ if ( !isset( $this->stats[$key] ) ) {
+ $this->stats[$key] = 0;
+ }
+
+ $this->stats[$key]++;
+ $this->operations[$key] = self::STATS_INCR;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|array $key
+ * @param string|integer $default
+ */
+ public function init( $key, $default ) {
+ $this->stats[$key] = $default;
+ $this->operations[$key] = self::STATS_INIT;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|array $key
+ * @param string|integer $value
+ */
+ public function set( $key, $value ) {
+ $this->stats[$key] = $value;
+ $this->operations[$key] = self::STATS_SET;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string|array $key
+ * @param integer $value
+ */
+ public function calcMedian( $key, $value ) {
+
+ if ( !isset( $this->stats[$key] ) ) {
+ $this->stats[$key] = $value;
+ } else {
+ $this->stats[$key] = ( $this->stats[$key] + $value ) / 2;
+ }
+
+ $this->operations[$key] = self::STATS_MEDIAN;
+ }
+
+ /**
+ * @since 2.5
+ */
+ public function saveStats() {
+
+ if ( $this->stats === [] ) {
+ return;
+ }
+
+ $container = $this->blobStore->read(
+ md5( $this->statsdId . self::VERSION )
+ );
+
+ foreach ( $this->stats as $key => $value ) {
+
+ $old = $container->has( $key ) ? $container->get( $key ) : 0;
+
+ if ( $this->operations[$key] === self::STATS_INIT && $old != 0 ) {
+ $value = $old;
+ }
+
+ if ( $this->operations[$key] === self::STATS_INCR ) {
+ $value = $old + $value;
+ }
+
+ // Use as-is
+ // $this->operations[$key] === self::STATS_SET
+
+ if ( $this->operations[$key] === self::STATS_MEDIAN ) {
+ $value = $old > 0 ? ( $old + $value ) / 2 : $value;
+ }
+
+ $container->set( $key, $value );
+ }
+
+ $this->blobStore->save(
+ $container
+ );
+
+ $this->stats = [];
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param boolean $asPending
+ */
+ public function recordStats( $asPending = false ) {
+
+ if ( $this->shouldRecord === false ) {
+ return $this->stats = [];
+ }
+
+ // #2046
+ // __destruct as event trigger has shown to be unreliable in a MediaWiki
+ // environment therefore rely on the deferred update and any caller
+ // that invokes the recordStats method
+
+ $deferredTransactionalUpdate = ApplicationFactory::getInstance()->newDeferredTransactionalCallableUpdate(
+ function() { $this->saveStats();
+ }
+ );
+
+ $deferredTransactionalUpdate->setOrigin( __METHOD__ );
+ $deferredTransactionalUpdate->waitOnTransactionIdle();
+
+ $deferredTransactionalUpdate->setFingerprint(
+ __METHOD__ . $this->fingerprint
+ );
+
+ $deferredTransactionalUpdate->markAsPending( $asPending );
+ $deferredTransactionalUpdate->pushUpdate();
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/CharArmor.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/CharArmor.php
new file mode 100644
index 00000000..9a1e20c0
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/CharArmor.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class CharArmor {
+
+ /**
+ * Remove invisible control characters and unused code points (using a
+ * negated character class to avoid removing spaces)
+ *
+ * @see http://www.regular-expressions.info/unicode.html#category
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function removeControlChars( $text ) {
+ return preg_replace('/[^\PC\s]/u', '', $text );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return text
+ */
+ public static function removeSpecialChars( $text ) {
+ return str_replace(
+ [ '&shy;', '&lrm;', " ", " ", " " ],
+ [ '', '', ' ', ' ', ' ' ],
+ $text
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/CharExaminer.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/CharExaminer.php
new file mode 100644
index 00000000..e7d07c48
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/CharExaminer.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class CharExaminer {
+
+ const CYRILLIC = 'CYRILLIC';
+ const LATIN = 'LATIN';
+ const HIRAGANA_KATAKANA = 'HIRAGANA_KATAKANA';
+ const HANGUL = 'HANGUL';
+ const CJK_UNIFIED = 'CJK_UNIFIED';
+ const HAN = 'HAN';
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return boolean
+ */
+ public static function isCJK( $text ) {
+
+ if ( self::contains( self::HAN, $text ) ) {
+ return true;
+ }
+
+ if ( self::contains( self::HIRAGANA_KATAKANA, $text ) ) {
+ return true;
+ }
+
+ if ( self::contains( self::HANGUL, $text ) ) {
+ return true;
+ }
+
+ if ( self::contains( self::CJK_UNIFIED, $text ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @see http://jrgraphix.net/research/unicode_blocks.php
+ * @since 0.1
+ *
+ * @param string $type
+ * @param string $text
+ *
+ * @return boolean
+ */
+ public static function contains( $type, $text ) {
+
+ if ( $type === self::CYRILLIC ) {
+ return preg_match('/\p{Cyrillic}/u', $text ) > 0;
+ }
+
+ if ( $type === self::LATIN ) {
+ return preg_match('/\p{Latin}/u', $text ) > 0;
+ }
+
+ if ( $type === self::HAN ) {
+ return preg_match('/\p{Han}/u', $text ) > 0;
+ }
+
+ if ( $type === self::HIRAGANA_KATAKANA ) {
+ return preg_match('/[\x{3040}-\x{309F}]/u', $text ) > 0 || preg_match('/[\x{30A0}-\x{30FF}]/u', $text ) > 0; // isHiragana || isKatakana
+ }
+
+ if ( $type === self::HANGUL ) {
+ return preg_match('/[\x{3130}-\x{318F}]/u', $text ) > 0 || preg_match('/[\x{AC00}-\x{D7AF}]/u', $text ) > 0;
+ }
+
+ // @see https://en.wikipedia.org/wiki/CJK_Unified_Ideographs
+ // Chinese, Japanese and Korean (CJK) scripts share common characters
+ // known as CJK characters
+
+ if ( $type === self::CJK_UNIFIED ) {
+ return preg_match('/[\x{4e00}-\x{9fa5}]/u', $text ) > 0;
+ }
+
+ return false;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/CircularReferenceGuard.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/CircularReferenceGuard.php
new file mode 100644
index 00000000..26cb35ec
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/CircularReferenceGuard.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.2
+ *
+ * @author mwjames
+ */
+class CircularReferenceGuard {
+
+ /**
+ * @var array
+ */
+ private static $circularRefGuard = [];
+
+ /**
+ * @var string
+ */
+ private $namespace = '';
+
+ /**
+ * @var integer
+ */
+ private $maxRecursionDepth = 1;
+
+ /**
+ * @since 2.2
+ *
+ * @param string $namespace
+ */
+ public function __construct( $namespace = '' ) {
+ $this->namespace = $namespace;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param integer $maxRecursionDepth
+ */
+ public function setMaxRecursionDepth( $maxRecursionDepth ) {
+ $this->maxRecursionDepth = (int)$maxRecursionDepth;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $hash
+ */
+ public function mark( $hash ) {
+
+ if ( !isset( self::$circularRefGuard[$this->namespace][$hash] ) ) {
+ self::$circularRefGuard[$this->namespace][$hash] = 0;
+ }
+
+ self::$circularRefGuard[$this->namespace][$hash]++;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $hash
+ */
+ public function unmark( $hash ) {
+
+ if ( isset( self::$circularRefGuard[$this->namespace][$hash] ) && self::$circularRefGuard[$this->namespace][$hash] > 0 ) {
+ return self::$circularRefGuard[$this->namespace][$hash]--;
+ }
+
+ unset( self::$circularRefGuard[$this->namespace][$hash] );
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $hash
+ *
+ * @return boolean
+ */
+ public function isCircular( $hash ) {
+ return $this->get( $hash ) > $this->maxRecursionDepth;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $hash
+ *
+ * @return integer
+ */
+ public function get( $hash ) {
+
+ if ( isset( self::$circularRefGuard[$this->namespace][$hash] ) ) {
+ return self::$circularRefGuard[$this->namespace][$hash];
+ }
+
+ return 0;
+ }
+
+ /**
+ * @since 2.2
+ *
+ * @param string $namespace
+ */
+ public function reset( $namespace ) {
+ self::$circularRefGuard[$namespace] = [];
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/Csv.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Csv.php
new file mode 100644
index 00000000..3a3c0309
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Csv.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Csv {
+
+ const DEFAULT_SEP = ',';
+
+ /**
+ * @var boolean
+ */
+ private $show = false;
+
+ /**
+ * @var boolean
+ */
+ private $bom = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $show
+ * @param boolean $bom
+ */
+ public function __construct( $show = false, $bom = false ) {
+ $this->show = $show;
+ $this->bom = $bom;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $header
+ * @param array $rows
+ * @param string $sep
+ *
+ * @return string
+ */
+ public function toString( array $header, array $rows, $sep = self::DEFAULT_SEP ) {
+
+ $handle = fopen( 'php://temp', 'r+' );
+
+ // fputcsv(): delimiter must be a single character
+ $sep = $sep !== '' ? $sep{0} : self::DEFAULT_SEP;
+
+ // https://en.wikipedia.org/wiki/Comma-separated_values#Standardization
+ // http://php.net/manual/en/function.fputcsv.php
+ if ( $this->bom ) {
+ fputs( $handle, ( chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ) );
+ }
+
+ // https://en.wikipedia.org/wiki/Comma-separated_values#Application_support
+ if ( $this->show ) {
+ fputs( $handle, "sep=" . $sep . "\n" );
+ }
+
+ if ( $header !== [] ) {
+ fputcsv( $handle, $header, $sep );
+ }
+
+ foreach ( $rows as $row ) {
+ fputcsv( $handle, $row, $sep );
+ }
+
+ rewind( $handle );
+
+ return stream_get_contents( $handle );
+ }
+
+ /**
+ * Merge row and column values where the subject (first column) uses the same
+ * identifier.
+ *
+ * @since 3.0
+ *
+ * @param array $rows
+ * @param string $sep
+ *
+ * @return array
+ */
+ public function merge( $rows, $sep = ',' ) {
+
+ $map = [];
+ $order = [];
+
+ foreach ( $rows as $key => $row ) {
+
+ // First column is used to build the hash index to find rows with
+ // the same hash
+ $hash = md5( $row[0] );
+
+ // Retain the order
+ if ( !isset( $order[$hash] ) ) {
+ $order[$hash] = $key;
+ }
+
+ if ( !isset( $map[$hash] ) ) {
+ $map[$hash] = $row;
+ } else {
+ $concat = [];
+
+ foreach ( $map[$hash] as $k => $v ) {
+ // Index 0 represents the first column, same hash, only
+ // concatenate the rest of the columns
+ if ( $k != 0 ) {
+ $v = $v . ( isset( $row[$k] ) ? "$sep" . $row[$k] : '' );
+ // Filter duplicate values
+ $v = array_flip( explode( $sep, $v ) );
+ // Make it a simple list
+ $v = implode( $sep, array_keys( $v ) );
+ }
+
+ $concat[$k] = $v;
+ }
+
+ $map[$hash] = $concat;
+ }
+ }
+
+ $order = array_flip( $order );
+
+ foreach ( $order as $key => $hash ) {
+ $order[$key] = $map[$hash];
+ }
+
+ return $order;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/ErrorCodeFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/ErrorCodeFormatter.php
new file mode 100644
index 00000000..354902ad
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/ErrorCodeFormatter.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * Convenience method to retrieved stringified error codes.
+ *
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class ErrorCodeFormatter {
+
+ /**
+ * @var array
+ */
+ private static $constants = [];
+
+ /**
+ * @var array
+ */
+ private static $jsonErrors = [];
+
+ /**
+ * @see http://php.net/manual/en/function.json-decode.php
+ * @since 2.5
+ *
+ * @param integer $errorCode
+ *
+ * @return string
+ */
+ public static function getStringFromJsonErrorCode( $errorCode ) {
+
+ if ( self::$constants === [] ) {
+ self::$constants = get_defined_constants( true );
+ }
+
+ if ( isset( self::$constants["json"] ) && self::$jsonErrors === [] ) {
+ foreach ( self::$constants["json"] as $name => $value ) {
+ if ( !strncmp( $name, "JSON_ERROR_", 11 ) ) {
+ self::$jsonErrors[$value] = $name;
+ }
+ }
+ }
+
+ return isset( self::$jsonErrors[$errorCode] ) ? self::$jsonErrors[$errorCode] : 'UNKNOWN';
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param integer $errorCode
+ *
+ * @return string
+ */
+ public static function getMessageFromJsonErrorCode( $errorCode ) {
+
+ $errorMessages = [
+ JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch, malformed JSON',
+ JSON_ERROR_CTRL_CHAR => 'Unexpected control character found, possibly incorrectly encoded',
+ JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
+ JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded',
+ JSON_ERROR_DEPTH => 'The maximum stack depth has been exceeded'
+ ];
+
+ if ( !isset( $errorMessages[$errorCode] ) ) {
+ return self::getStringFromJsonErrorCode( $errorCode );
+ }
+
+ return sprintf(
+ "Expected a JSON compatible format but failed with '%s'",
+ $errorMessages[$errorCode]
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/File.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/File.php
new file mode 100644
index 00000000..f17154dd
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/File.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace SMW\Utils;
+
+use RuntimeException;
+use SMW\Exception\FileNotWritableException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class File {
+
+ /**
+ * @since 3.1
+ *
+ * @param string $file
+ *
+ * @return string
+ */
+ public static function dir( $file ) {
+ return str_replace( [ '\\', '//', '/' ], DIRECTORY_SEPARATOR, $file );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ * @param string $content
+ * @param integer $flags
+ */
+ public function write( $file, $contents, $flags = 0 ) {
+
+ $file = self::dir( $file );
+
+ if ( !is_writable( dirname( $file ) ) ) {
+ throw new FileNotWritableException( "$file" );
+ }
+
+ file_put_contents( $file, $contents, $flags );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ *
+ * @return boolean
+ */
+ public function exists( $file ) {
+ return file_exists( $file );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ * @param integer|null $checkSum
+ *
+ * @return string
+ * @throws RuntimeException
+ */
+ public function read( $file, $checkSum = null ) {
+
+ if ( !is_readable( $file ) ) {
+ throw new RuntimeException( "$file is not readable." );
+ }
+
+ if ( $checkSum !== null && $this->getCheckSum( $file ) !== $checkSum ) {
+ throw new RuntimeException( "Processing of $file failed with a checkSum error." );
+ }
+
+ return file_get_contents( $file );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ */
+ public function delete( $file ) {
+ @unlink( $file );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ *
+ * @return integer
+ */
+ public function getCheckSum( $file ) {
+ return md5_file( $file );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/HmacSerializer.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HmacSerializer.php
new file mode 100644
index 00000000..80d560b6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HmacSerializer.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * Serialize/encode a data element with a hmac hash to verify that the output are
+ * in fact the same as the input data, minimizing an attack vector on injecting
+ * malicious content when retrieving the data from en external systems (like
+ * a cache).
+ *
+ * The shared secret key to generate the HMAC is by default MediaWiki's
+ * $wgSecretKey.
+ *
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HmacSerializer {
+
+ /**
+ * @since 3.0
+ *
+ * @param mixed $data
+ * @param string $key
+ * @param string $algo = 'md5'
+ *
+ * @return string|boolean
+ */
+ public static function encode( $data, $key = null, $algo = 'md5' ) {
+
+ if ( $key === null ) {
+ $key = $GLOBALS['wgSecretKey'];
+ }
+
+ $data = json_encode( $data );
+ $hash = hash_hmac( $algo, $data, $key );
+
+ if ( $hash !== false ) {
+ return json_encode( [ 'hmac' => $hash, 'data' => $data ] );
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param mixed $data
+ * @param string $key
+ * @param string $algo = 'md5'
+ *
+ * @return string|boolean
+ */
+ public static function decode( $data, $key = null, $algo = 'md5' ) {
+
+ if ( $key === null ) {
+ $key = $GLOBALS['wgSecretKey'];
+ }
+
+ if ( !is_string( $data ) ) {
+ return false;
+ }
+
+ $hash = '';
+ $data = json_decode( $data, true );
+
+ // Timing attack safe string comparison
+ if ( isset( $data['hmac'] ) && hash_equals( hash_hmac( $algo, $data['data'], $key ), $data['hmac'] ) ) {
+ return json_decode( $data['data'], true );
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param mixed $data
+ * @param string $key
+ * @param string $algo = 'md5'
+ *
+ * @return string|boolean
+ */
+ public static function serialize( $data, $key = null, $algo = 'md5' ) {
+
+ if ( $key === null ) {
+ $key = $GLOBALS['wgSecretKey'];
+ }
+
+ $data = serialize( $data );
+ $hash = hash_hmac( $algo, $data, $key );
+
+ if ( $hash !== false ) {
+ return "$hash|$data";
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $data
+ * @param string $key
+ * @param string $algo = 'md5'
+ *
+ * @return mixed|boolean
+ */
+ public static function unserialize( $data, $key = null, $algo = 'md5' ) {
+
+ if ( $key === null ) {
+ $key = $GLOBALS['wgSecretKey'];
+ }
+
+ if ( !is_string( $data ) ) {
+ return false;
+ }
+
+ $hash = '';
+
+ if ( strpos( $data, '|' ) !== false ) {
+ list( $hash, $data ) = explode( '|', $data, 2 );
+ }
+
+ // Timing attack safe string comparison
+ if ( hash_equals( hash_hmac( $algo, $data, $key ), $hash ) ) {
+ return unserialize( $data );
+ }
+
+ return false;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param mixed $data
+ * @param string $key
+ * @param string $algo = 'md5'
+ *
+ * @return string|boolean
+ */
+ public static function compress( $data, $key = null, $algo = 'md5' ) {
+
+ if ( $key === null ) {
+ $key = $GLOBALS['wgSecretKey'];
+ }
+
+ $key = $key . 'compress';
+
+ return gzcompress( self::serialize( $data, $key, $algo ), 9 );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $data
+ * @param string $key
+ * @param string $algo = 'md5'
+ *
+ * @return mixed|boolean
+ */
+ public static function uncompress( $data, $key = null, $algo = 'md5' ) {
+
+ if ( $key === null ) {
+ $key = $GLOBALS['wgSecretKey'];
+ }
+
+ $key = $key . 'compress';
+
+ return self::unserialize( @gzuncompress( $data ), $key, $algo );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlColumns.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlColumns.php
new file mode 100644
index 00000000..d490db71
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlColumns.php
@@ -0,0 +1,335 @@
+<?php
+
+namespace SMW\Utils;
+
+use InvalidArgumentException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HtmlColumns {
+
+ /**
+ * Indexed content
+ */
+ const INDX_CONTENT = 'indexed.list';
+
+ /**
+ * Indexed content
+ */
+ const INDEXED_LIST = 'indexed.list';
+
+ /**
+ * List content
+ */
+ const LIST_CONTENT = 'list.content';
+
+ /**
+ * List content
+ */
+ const PLAIN_LIST = 'plain.list';
+
+ /**
+ * @var integer
+ */
+ private $columns = 1;
+
+ /**
+ * @var array
+ */
+ private $contents = [];
+
+ /**
+ * @var array
+ */
+ private $itemAttributes = [];
+
+ /**
+ * @var integer
+ */
+ private $numRows = 0;
+
+ /**
+ * @var integer
+ */
+ private $count = 0;
+
+ /**
+ * @var integer
+ */
+ private $rowsPerColumn = 0;
+
+ /**
+ * @var integer
+ */
+ private $columnWidth = 0;
+
+ /**
+ * @var string
+ */
+ private $listType = 'ul';
+
+ /**
+ * @var string
+ */
+ private $olType = '';
+
+ /**
+ * @var string
+ */
+ private $continueAbbrev = '';
+
+ /**
+ * @var string
+ */
+ private $columnListClass = 'smw-columnlist-container';
+
+ /**
+ * @var string
+ */
+ private $columnClass = 'smw-column';
+
+ /**
+ * @var boolean
+ */
+ private $isRTL = false;
+
+ /**
+ * @since 3.0
+ *
+ * @param string $columnListClass
+ */
+ public function setColumnListClass( $columnListClass ) {
+ $this->columnListClass = htmlspecialchars( $columnListClass );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $columnListClass
+ */
+ public function setColumnClass( $columnClass ) {
+ $this->columnClass = htmlspecialchars( $columnClass );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean $isRTL
+ */
+ public function isRTL( $isRTL ) {
+ $this->isRTL = (bool)$isRTL;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $columns
+ */
+ public function setColumns( $columns ) {
+ $this->columns = $columns;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $listType
+ * @param string $olType
+ */
+ public function setListType( $listType, $olType = '' ) {
+
+ if ( in_array( $listType, [ 'ul', 'ol' ] ) ) {
+ $this->listType = $listType;
+ }
+
+ if ( $this->listType === 'ol' && in_array( $olType, [ '1', 'a', 'A', 'i', 'I' ] ) ) {
+ $this->olType = $olType;
+ }
+ }
+
+ /**
+ * Allows to define attributes for an item such as:
+ *
+ * [md5( $itemContent )] = [
+ * 'id' => 'Foo'
+ * ]
+ *
+ * @since 3.0
+ *
+ * @param array $itemAttributes
+ */
+ public function setItemAttributes( array $itemAttributes ) {
+ $this->itemAttributes = $itemAttributes;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $continueAbbrev
+ */
+ public function setContinueAbbrev( $continueAbbrev ) {
+ $this->continueAbbrev = $continueAbbrev;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string[] $cnts
+ * @param string $type
+ */
+ public function addContents( array $cnts, $type = self::LIST_CONTENT ) {
+ $this->setContents( $cnts, $type );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string[] $cnts
+ * @param string $type
+ */
+ public function setContents( array $cnts, $type = self::LIST_CONTENT ) {
+
+ if ( $type === self::LIST_CONTENT ) {
+ $contents[''] = [];
+
+ foreach ( $cnts as $value ) {
+ $contents[''][] = $value;
+ }
+
+ } elseif ( $type === self::INDX_CONTENT ) {
+ $contents = $cnts;
+ } else {
+ throw new InvalidArgumentException( 'Missing a recognized type!');
+ }
+
+ $this->contents = $contents;
+ $this->count = count( $this->contents, COUNT_RECURSIVE ) - count( $this->contents );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public function getHtml() {
+
+ $result = '';
+ $usedColumnCloser = false;
+ $this->numRows = 0;
+
+ // Class to determine whether we want responsive columns width
+ if ( strpos( $this->columnClass, 'responsive' ) !== false ) {
+ $this->columnWidth = 100;
+ $this->columns = 1;
+ } else {
+ $this->columnWidth = floor( 100 / $this->columns );
+ }
+
+ $this->rowsPerColumn = ceil( $this->count / $this->columns );
+
+ foreach ( $this->contents as $key => $items ) {
+
+ if ( $items === [] ) {
+ continue;
+ }
+
+ $result .= $this->makeList(
+ $key,
+ $items,
+ $usedColumnCloser
+ );
+ }
+
+ if ( !$usedColumnCloser ) {
+ $result .= "</{$this->listType}></div> <!-- end column -->";
+ }
+
+ return $this->element(
+ 'div',
+ [
+ 'class' => $this->columnListClass,
+ 'dir' => $this->isRTL ? 'rtl' : 'ltr'
+ ],
+ $result . "\n" . '<br style="clear: both;"/>'
+ );
+ }
+
+ private function makeList( $key, $items, &$usedColumnCloser ) {
+
+ $result = '';
+ $previousKey = "";
+ $dir = $this->isRTL ? 'rtl' : 'ltr';
+
+ foreach ( $items as $item ) {
+
+ $attributes = [];
+
+ if ( $this->itemAttributes !== [] ) {
+ $hash = md5( $item );
+
+ if ( isset( $this->itemAttributes[$hash] ) ) {
+ $attributes = $this->itemAttributes[$hash];
+ }
+ }
+
+ if ( $this->numRows % $this->rowsPerColumn == 0 ) {
+ $result .= "<div class=\"$this->columnClass\" style=\"width:$this->columnWidth%;\" dir=\"$dir\">";
+
+ $numRowsInColumn = $this->numRows + 1;
+ $type = $this->olType !== '' ? " type={$this->olType}" : '';
+
+ if ( $key == $previousKey ) {
+ if ( $key !== '' ) {
+ $result .= $this->element(
+ 'div',
+ [
+ 'class' => 'smw-column-header'
+ ],
+ "$key {$this->continueAbbrev}"
+ );
+ }
+
+ $result .= "<{$this->listType}$type start={$numRowsInColumn}>";
+ }
+ }
+
+ // if we're at a new first letter, end
+ // the last list and start a new one
+ if ( $key != $previousKey ) {
+ $result .= $this->numRows % $this->rowsPerColumn > 0 ? "</{$this->listType}>" : '';
+ $result .= ( $key !== '' ? $this->element( 'div', [ 'class' => 'smw-column-header' ], $key ) : '' ) . "<{$this->listType}>";
+ }
+
+ $previousKey = $key;
+ $result .= $this->element( 'li', $attributes, $item );
+ $usedColumnCloser = false;
+
+ if ( ( $this->numRows + 1 ) % $this->rowsPerColumn == 0 && ( $this->numRows + 1 ) < $this->count ) {
+ $result .= "</{$this->listType}></div> <!-- end column -->";
+ $usedColumnCloser = true;
+ }
+
+ $this->numRows++;
+ }
+
+ return $result;
+ }
+
+ private function element( $type, $attributes, $content ) {
+
+ $attr = '';
+ $attributes = (array)$attributes;
+
+ if ( $attributes !== [] ) {
+ foreach ( $attributes as $key => $value ) {
+ $attr .= ' ' . $key . '="' . $value . '"';
+ }
+ }
+
+ return "<$type$attr>$content</$type>";
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlDivTable.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlDivTable.php
new file mode 100644
index 00000000..d968919b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlDivTable.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace SMW\Utils;
+
+use Html;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HtmlDivTable {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function table( $html = '', array $attributes = [] ) {
+ return self::open( $attributes ) . $html . self::close();
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function open( array $attributes = [] ) {
+ return Html::openElement(
+ 'div',
+ self::mergeAttributes( 'smw-table', $attributes ),
+ ''
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function header( $html = '', array $attributes = [] ) {
+ return Html::rawElement(
+ 'div',
+ self::mergeAttributes( 'smw-table-header', $attributes ),
+ $html
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function body( $html = '', array $attributes = [] ) {
+ return Html::rawElement(
+ 'div',
+ self::mergeAttributes( 'smw-table-body', $attributes ),
+ $html
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function footer( $html = '', array $attributes = [] ) {
+ return Html::rawElement(
+ 'div',
+ self::mergeAttributes( 'smw-table-footer', $attributes ),
+ $html
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function row( $html = '', array $attributes = [] ) {
+ return Html::rawElement(
+ 'div',
+ self::mergeAttributes( 'smw-table-row', $attributes ),
+ $html
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function cell( $html = '', array $attributes = [] ) {
+ return Html::rawElement(
+ 'div',
+ self::mergeAttributes( 'smw-table-cell', $attributes ),
+ $html
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ */
+ public static function close() {
+ return Html::closeElement(
+ 'div'
+ );
+ }
+
+ private static function mergeAttributes( $class, $attr ) {
+
+ $attributes = [];
+
+ // A bit of attribute order
+ if ( isset( $attr['id'] ) ) {
+ $attributes['id'] = $attr['id'];
+ }
+
+ if ( isset( $attr['class'] ) ) {
+ $attributes['class'] = $class . ' ' . $attr['class'];
+ } else {
+ $attributes['class'] = $class;
+ }
+
+ return $attributes += $attr;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlModal.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlModal.php
new file mode 100644
index 00000000..b8b99997
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlModal.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace SMW\Utils;
+
+use Html;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HtmlModal {
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public static function getModules() {
+ return [ 'ext.smw.modal' ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public static function getModuleStyles() {
+ return [ 'ext.smw.modal.styles' ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function link( $name, array $attributes = [] ) {
+
+ $attributes = self::mergeAttributes(
+ 'smw-modal-link is-disabled',
+ $attributes
+ );
+
+ return Html::rawElement(
+ 'span',
+ $attributes,
+ Html::rawElement(
+ 'a',
+ [
+ 'href' => '#help',
+ 'rel' => 'nofollow'
+ ],
+ $name
+ )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function modal( $title = '', $html = '', array $attributes = [] ) {
+
+ $attributes = self::mergeAttributes(
+ 'smw-modal',
+ $attributes
+ );
+
+ $title = Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-modal-title'
+ ],
+ $title
+ );
+
+ $html = Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-modal-content'
+ ],
+ Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-modal-header'
+ ],
+ Html::rawElement(
+ 'span',
+ [
+ 'class' => 'smw-modal-close'
+ ],
+ '&#215;'
+ ) . $title
+ ). Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-modal-body'
+ ],
+ $html
+ ) . Html::rawElement(
+ 'div',
+ [
+ 'class' => 'smw-modal-footer'
+ ],
+ ''
+ )
+ );
+
+ return Html::rawElement(
+ 'div',
+ $attributes,
+ $html
+ );
+ }
+
+ private static function mergeAttributes( $class, $attr ) {
+
+ $attributes = [];
+
+ // A bit of attribute order
+ if ( isset( $attr['id'] ) ) {
+ $attributes['id'] = $attr['id'];
+ }
+
+ if ( isset( $attr['class'] ) ) {
+ $attributes['class'] = $class . ' ' . $attr['class'];
+ } else {
+ $attributes['class'] = $class;
+ }
+
+ return $attributes += $attr;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlTable.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlTable.php
new file mode 100644
index 00000000..9a42386b
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlTable.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace SMW\Utils;
+
+use Html;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HtmlTable {
+
+ /**
+ * @var array
+ */
+ private $headers = [];
+
+ /**
+ * @var array
+ */
+ private $cells = [];
+
+ /**
+ * @var array
+ */
+ private $rows = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param string $content
+ * @param array $attributes
+ */
+ public function header( $content = '', $attributes = [] ) {
+ if ( $content !== '' ) {
+ $this->headers[] = [ 'content' => $content, 'attributes' => $attributes ];
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $content
+ * @param array $attributes
+ */
+ public function cell( $content = '', $attributes = [] ) {
+ if ( $content !== '' ) {
+ $this->cells[] = Html::rawElement( 'td', $attributes, $content );
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $attributes
+ *
+ * @return TableBuilder
+ */
+ public function row( $attributes = [] ) {
+ if ( $this->cells !== [] ) {
+ $this->rows[] = [ 'cells' => $this->cells, 'attributes' => $attributes ];
+ $this->cells = [];
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public function table( $attributes = [], $transpose = false, $htmlContext = false ) {
+
+ $table = $this->buildTable( $transpose, $htmlContext );
+
+ if ( $transpose ) {
+ $attributes['data-transpose'] = true;
+ }
+
+ $this->headers = [];
+ $this->rows = [];
+ $this->cells = [];
+
+ if ( $table !== '' ) {
+ return Html::rawElement( 'table', $attributes, $table );
+ }
+
+ return '';
+ }
+
+ private function buildTable( $transpose, $htmlContext ) {
+
+ if ( $transpose ) {
+ return $this->transpose( $htmlContext );
+ }
+
+ $headers = [];
+ $rows = [];
+
+ foreach( $this->headers as $i => $header ) {
+ $headers[] = Html::rawElement( 'th', $header['attributes'], $header['content'] );
+ }
+
+ foreach( $this->rows as $row ) {
+ $rows[] = $this->createRow( implode( '', $row['cells'] ), $row['attributes'], count( $rows ) );
+ }
+
+ return $this->concatenateHeaders( $headers, $htmlContext ) . $this->concatenateRows( $rows, $htmlContext );
+ }
+
+ private function transpose( $htmlContext ) {
+
+ $rows = [];
+
+ foreach( $this->headers as $hIndex => $header ) {
+ $cells = [];
+ $headerItem = Html::rawElement( 'th', $header['attributes'], $header['content'] );
+
+ foreach( $this->rows as $rIndex => $row ) {
+ $cells[] = $this->getTransposedCell( $hIndex, $row );
+ }
+
+ // Collect new rows
+ $rows[] = $this->createRow( $headerItem . implode( '', $cells ), [], count( $rows ) );
+ }
+
+ return $this->concatenateRows( $rows, $htmlContext );
+ }
+
+ private function createRow( $content = '', $attributes = [], $count ) {
+
+ $alternate = $count % 2 == 0 ? 'row-odd' : 'row-even';
+
+ if ( isset( $attributes['class'] ) ) {
+ $attributes['class'] = $attributes['class'] . ' ' . $alternate;
+ } else {
+ $attributes['class'] = $alternate;
+ }
+
+ return Html::rawElement( 'tr', $attributes, $content );
+ }
+
+ private function concatenateHeaders( $headers, $htmlContext ) {
+
+ if ( $htmlContext ) {
+ return Html::rawElement( 'thead', [], implode( '', $headers ) );
+ }
+
+ return implode( '', $headers );
+ }
+
+ private function concatenateRows( $rows, $htmlContext ) {
+
+ if ( $htmlContext ) {
+ return Html::rawElement( 'tbody', [], implode( '', $rows ) );
+ }
+
+ return implode( '', $rows );
+ }
+
+ private function getTransposedCell( $index, $row ) {
+
+ if ( isset( $row['cells'][$index] ) ) {
+ return $row['cells'][$index];
+ }
+
+ $attributes = [];
+
+ if ( isset( $row['attributes']['class'] ) && $row['attributes']['class'] === 'smwfooter' ) {
+ $attributes = [ 'class' => 'footer-cell' ];
+ }
+
+ return Html::rawElement( 'td', $attributes, '' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlTabs.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlTabs.php
new file mode 100644
index 00000000..8cca3bf8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlTabs.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace SMW\Utils;
+
+use Html;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HtmlTabs {
+
+ /**
+ * @var []
+ */
+ private $tabs = [];
+
+ /**
+ * @var []
+ */
+ private $contents = [];
+
+ /**
+ * @var []
+ */
+ private $hidden = [];
+
+ /**
+ * @var string|null
+ */
+ private $activeTab = null;
+
+ /**
+ * @var string
+ */
+ private $group = 'tabs';
+
+ /**
+ * @since 3.0
+ *
+ * @param string $activeTab
+ */
+ public function setActiveTab( $activeTab ) {
+ $this->activeTab = $activeTab;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $group
+ */
+ public function setGroup( $group ) {
+ $this->group = $group;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public function buildHTML( array $attributes = [] ) {
+
+ $tabs = $this->tabs;
+ $contents = $this->contents;
+
+ $this->tabs = [];
+ $this->contents = [];
+
+ $attributes = $this->mergeAttributes( 'smw-tabs', $attributes );
+
+ return Html::rawElement(
+ 'div',
+ $attributes,
+ implode( '', $tabs ) . implode( '', $contents )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ * @param string $name
+ * @param array $params
+ *
+ * @return string
+ */
+ public function html( $html, array $params = [] ) {
+
+ if ( isset( $params['hide'] ) && $params['hide'] ) {
+ return;
+ }
+
+ $this->tabs[] = $html;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ * @param string $name
+ * @param array $params
+ *
+ * @return string
+ */
+ public function tab( $id, $name = '', array $params = [] ) {
+
+ if ( isset( $params['hide'] ) && $params['hide'] ) {
+ return $this->hidden[$id] = true;
+ }
+
+ $isChecked = false;
+
+ // No acive tab means, select the first tab being added
+ if ( $this->activeTab === null ) {
+ $this->activeTab = $id;
+ }
+
+ if ( $id === $this->activeTab ) {
+ $isChecked = true;
+ }
+
+ $this->tabs[] = Html::rawElement(
+ 'input',
+ [
+ 'id' => "tab-$id",
+ 'class' => 'nav-tab',
+ 'type' => 'radio',
+ 'name' => $this->group
+ ] + ( $isChecked ? [ 'checked' => 'checked' ] : [] )
+ ) . Html::rawElement(
+ 'label',
+ [
+ 'id' => "tab-label-$id",
+ 'for' => "tab-$id"
+ ] + $this->mergeAttributes( 'nav-label', $params ),
+ $name
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $id
+ * @param string $content
+ */
+ public function content( $id, $content ) {
+
+ // Tab hidden?
+ if ( isset( $this->hidden[$id] ) ) {
+ return;
+ }
+
+ $this->contents[] = Html::rawElement(
+ 'section',
+ [
+ 'id' => "tab-content-$id"
+ ],
+ $content
+ );
+ }
+
+ private function mergeAttributes( $class, $attr ) {
+
+ $attributes = [];
+
+ // A bit of attribute order
+ if ( isset( $attr['id'] ) ) {
+ $attributes['id'] = $attr['id'];
+ }
+
+ if ( isset( $attr['class'] ) ) {
+ $attributes['class'] = $class . ' ' . $attr['class'];
+ } else {
+ $attributes['class'] = $class;
+ }
+
+ return $attributes += $attr;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlVTabs.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlVTabs.php
new file mode 100644
index 00000000..25ced9bc
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/HtmlVTabs.php
@@ -0,0 +1,195 @@
+<?php
+
+namespace SMW\Utils;
+
+use Html;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class HtmlVTabs {
+
+ /**
+ * Identifies which link/content to be active
+ */
+ const IS_ACTIVE = 'active';
+
+ /**
+ * Match an active status against a id
+ */
+ const FIND_ACTIVE_LINK = 'find';
+
+ /**
+ * Hide content
+ */
+ const IS_HIDDEN = 'hidden';
+
+ /**
+ * @var string
+ */
+ private static $active = '';
+
+ /**
+ * @var string
+ */
+ private static $direction = 'right';
+
+ /**
+ * @since 3.0
+ */
+ public static function init() {
+ self::$active = '';
+ self::$direction = 'right';
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public static function getModules() {
+ return [ 'ext.smw.vtabs' ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return array
+ */
+ public static function getModuleStyles() {
+ return [ 'ext.smw.vtabs.styles' ];
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $direction
+ */
+ public static function setDirection( $direction ) {
+ self::$direction = $direction;
+ }
+
+ /**
+ * Encapsulate generate tab links into a navigation container.
+ *
+ * @since 3.0
+ *
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function nav( $html = '', array $attributes = [] ) {
+
+ $direction = self::$direction === 'right' ? 'nav-right' : 'nav-left';
+
+ $attributes = self::mergeAttributes( "smw-vtab-nav", $attributes );
+ $attributes['class'] .= " $direction";
+
+ return Html::rawElement(
+ 'div',
+ $attributes,
+ $html
+ );
+ }
+
+ /**
+ * Generate an individual tab link.
+ *
+ * @since 3.0
+ *
+ * @param string $id
+ * @param string $label
+ * @param string|array $flag
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function navLink( $id, $label = '', $flag = false, array $attributes = [] ) {
+
+ if ( $flag === self::IS_HIDDEN ) {
+ return '';
+ }
+
+ // Match an active status against an id
+ if ( is_array( $flag ) && isset( $flag[self::FIND_ACTIVE_LINK] ) && $flag[self::FIND_ACTIVE_LINK] === $id ) {
+ $flag = self::IS_ACTIVE;
+ }
+
+ $id = 'tab-' . $id;
+ $direction = self::$direction === 'right' ? 'nav-right' : 'nav-left';
+
+ $attributes['data-id'] = $id;
+ $attributes['id'] = 'vtab-item-' . $id;
+
+ $attributes = self::mergeAttributes( "smw-vtab-link", $attributes );
+ $attributes['class'] .= " $direction";
+
+ if ( $flag === self::IS_ACTIVE && self::$active == '' ) {
+ $attributes['class'] .= ' active';
+ self::$active = $id;
+ }
+
+ return Html::rawElement(
+ 'button',
+ $attributes,
+ Html::rawElement( 'a', [ 'href' => '#' . $id ], $label )
+ );
+ }
+
+ /**
+ * Encapsulate the content that relates to a tab link using the ID as identifier
+ * to distinguish content sections.
+ *
+ * @since 3.0
+ *
+ * @param string $id
+ * @param string $html
+ * @param array $attributes
+ *
+ * @return string
+ */
+ public static function content( $id, $html = '', array $attributes = [] ) {
+
+ $id = 'tab-' . $id;
+ $attributes['id'] = $id;
+
+ if ( self::$active !== $id ) {
+ if ( !isset( $attributes['style'] ) ) {
+ $attributes['style'] = 'display:none;';
+ } else {
+ $attributes['style'] .= ' display:none;';
+ }
+ }
+
+ $attributes = self::mergeAttributes( 'smw-vtab-content', $attributes );
+
+ return Html::rawElement(
+ 'div',
+ $attributes,
+ $html
+ );
+ }
+
+ private static function mergeAttributes( $class, $attr ) {
+
+ $attributes = [];
+
+ // A bit of attribute order
+ if ( isset( $attr['id'] ) ) {
+ $attributes['id'] = $attr['id'];
+ }
+
+ if ( isset( $attr['class'] ) ) {
+ $attributes['class'] = $class . ' ' . $attr['class'];
+ } else {
+ $attributes['class'] = $class;
+ }
+
+ return $attributes += $attr;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/Image.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Image.php
new file mode 100644
index 00000000..46cb65c6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Image.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace SMW\Utils;
+
+use SMW\DIWikiPage;
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Image {
+
+ /**
+ * @see http://php.net/manual/en/function.image-type-to-extension.php
+ *
+ * @var []
+ */
+ private static $images_types = [
+ 'gif' => 'image/gif',
+ 'jpg' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'png' => 'image/png',
+ 'svg' => 'image/svg+xml',
+ 'swf' => 'application/x-shockwave-flash',
+ 'swc' => 'application/x-shockwave-flash',
+ 'psd' => 'image/psd',
+ 'bmp' => 'image/bmp',
+ 'jpc' => 'application/octet-stream',
+ 'jp2' => 'image/jp2',
+ 'jpf' => 'application/octet-stream',
+ 'jb2' => 'application/octet-stream',
+ 'xbm' => 'image/xbm',
+ 'tiff' => 'image/tiff',
+ 'aiff' => 'image/iff',
+ 'wbmp' => 'image/vnd.wap.wbmp'
+ ];
+
+ /**
+ * @since 3.0
+ *
+ * @param DIWikiPage $dataItem
+ *
+ * @return boolean
+ */
+ public static function isImage( DIWikiPage $dataItem ) {
+
+ if ( $dataItem->getNamespace() !== NS_FILE || $dataItem->getSubobjectName() !== '' ) {
+ return false;
+ }
+
+ $extension = strtolower(
+ substr( strrchr( $dataItem->getDBKey(), "." ) , 1 )
+ // pathinfo( $dataItem->getDBKey(), PATHINFO_EXTENSION )
+ );
+
+ return in_array( $extension, array_keys( self::$images_types ) );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/JsonSchemaValidator.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/JsonSchemaValidator.php
new file mode 100644
index 00000000..8a849ef6
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/JsonSchemaValidator.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace SMW\Utils;
+
+use JsonSchema\Exception\ResourceNotFoundException;
+use JsonSchema\Validator as SchemaValidator;
+use JsonSerializable;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class JsonSchemaValidator {
+
+ /**
+ * @var SchemaValidator
+ */
+ private $schemaValidator;
+
+ /**
+ * @var boolen
+ */
+ private $isValid = true;
+
+ /**
+ * @var []
+ */
+ private $errors = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param SchemaValidator|null $schemaValidator
+ */
+ public function __construct( SchemaValidator $schemaValidator = null ) {
+ $this->schemaValidator = $schemaValidator;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param JsonSerializable $data
+ * @param string|null $schemaLink
+ */
+ public function validate( JsonSerializable $data, $schemaLink = null ) {
+
+ if ( $this->schemaValidator === null || $schemaLink === null ) {
+ return;
+ }
+
+ // https://github.com/justinrainbow/json-schema/issues/203
+ $data = json_decode( $data->jsonSerialize() );
+
+ // https://github.com/justinrainbow/json-schema
+ try {
+ $this->schemaValidator->check(
+ $data,
+ (object)[ '$ref' => 'file://' . $schemaLink ]
+ );
+
+ $this->isValid = $this->schemaValidator->isValid();
+ $this->errors = $this->schemaValidator->getErrors();
+ } catch ( ResourceNotFoundException $e ) {
+ $this->isValid = false;
+ $this->errors[] = $e->getMessage();
+ }
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean
+ */
+ public function hasSchemaValidator() {
+ return $this->schemaValidator !== null;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param boolean
+ */
+ public function isValid() {
+ return $this->isValid;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @return []
+ */
+ public function getErrors() {
+ return $this->errors;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/Logger.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Logger.php
new file mode 100644
index 00000000..97ce40af
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Logger.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace SMW\Utils;
+
+use Psr\Log\AbstractLogger;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Logger extends AbstractLogger {
+
+ const ROLE_DEVELOPER = 'developer';
+ const ROLE_USER = 'user';
+ const ROLE_PRODUCTION = 'production';
+
+ /**
+ * @var LoggerInterface
+ */
+ protected $logger;
+
+ /**
+ * @var string
+ */
+ protected $role;
+
+ /**
+ * @since 3.0
+ *
+ * @param LoggerInterface $logger
+ * @param string $role
+ */
+ public function __construct( LoggerInterface $logger, $role = self::ROLE_DEVELOPER ) {
+ $this->logger = $logger;
+ $this->role = $role;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * {@inheritDoc}
+ */
+ public function log( $level, $message, array $context = [] ) {
+
+ $shouldLog = false;
+
+ // Everthings goes for the developer role!
+ if ( $this->role === self::ROLE_DEVELOPER ) {
+ $shouldLog = true;
+ } elseif ( isset( $context['role'] ) && $context['role'] === $this->role ) {
+ $shouldLog = true;
+ } elseif ( isset( $context['role'] ) && $context['role'] === self::ROLE_PRODUCTION && $this->role === self::ROLE_USER ) {
+ $shouldLog = true;
+ }
+
+ if ( !$shouldLog ) {
+ return;
+ }
+
+ // For convenience
+ if ( isset( $context['procTime'] ) ) {
+ $context['procTime'] = round( $context['procTime'], 5 );
+ }
+
+ if ( isset( $context['time'] ) ) {
+ $context['time'] = round( $context['time'], 5 );
+ }
+
+ if ( is_array( $message ) ) {
+ $message = array_shift( $message ) . ': ' . json_encode( $message );
+ }
+
+ foreach ( $context as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $context[$key] = json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
+ }
+ }
+
+ $this->logger->log( $level, $message, $context );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/Lru.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Lru.php
new file mode 100644
index 00000000..2f785bbf
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Lru.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Lru {
+
+ /**
+ * @var integer
+ */
+ private $size;
+
+ /**
+ * @var array
+ */
+ private $cache = [];
+
+ /**
+ * @var array
+ */
+ private $count = 0;
+
+ /**
+ * @since 3.0
+ *
+ * @param integer size
+ */
+ public function __construct( $size = 1000 ) {
+ $this->size = $size;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|integer $key
+ * @param mixed $value
+ */
+ public function set( $key, $value ) {
+
+ $this->count++;
+
+ if ( isset( $this->cache[$key] ) ) {
+ $this->count--;
+ $value = $this->cache[$key];
+ unset( $this->cache[$key] );
+ } elseif ( $this->count > $this->size ) {
+ $this->count--;
+ reset( $this->cache );
+ unset( $this->cache[ key( $this->cache ) ] );
+ }
+
+ $this->cache[$key] = $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|integer $key
+ *
+ * @return mixed
+ */
+ public function get( $key, $default = null ) {
+
+ if ( !isset( $this->cache[$key] ) ) {
+ return $default;
+ }
+
+ $value = $this->cache[$key];
+ unset( $this->cache[$key] );
+ $this->cache[$key] = $value;
+
+ return $value;
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string|integer $key
+ */
+ public function delete( $key ) {
+
+ if ( !isset( $this->cache[$key] ) ) {
+ return $default;
+ }
+
+ $this->count--;
+ unset( $this->cache[$key] );
+ }
+
+ /**
+ * @since 3.0
+ */
+ public function toArray() {
+ return $this->cache;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/Normalizer.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Normalizer.php
new file mode 100644
index 00000000..bb8a5fe8
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Normalizer.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class Normalizer {
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ *
+ * @return string
+ */
+ public static function toLowercase( $text ) {
+ return mb_strtolower( $text, mb_detect_encoding( $text ) );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $text
+ * @param integer|null $length
+ *
+ * @return string
+ */
+ public static function reduceLengthTo( $text, $length = null ) {
+
+ if ( $length === null || mb_strlen( $text ) <= $length ) {
+ return $text;
+ }
+
+ $encoding = mb_detect_encoding( $text );
+ $lastWholeWordPosition = $length;
+
+ if ( strpos( $text, ' ' ) !== false ) {
+ $lastWholeWordPosition = strrpos( mb_substr( $text, 0, $length, $encoding ), ' ' ); // last whole word
+ }
+
+ if ( $lastWholeWordPosition > 0 ) {
+ $length = $lastWholeWordPosition;
+ }
+
+ return mb_substr( $text, 0, $length, $encoding );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/StatsFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/StatsFormatter.php
new file mode 100644
index 00000000..021cd443
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/StatsFormatter.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class StatsFormatter {
+
+ /**
+ * Stats as plain string
+ */
+ const FORMAT_PLAIN = 'plain';
+
+ /**
+ * Stats as JSON output
+ */
+ const FORMAT_JSON = 'json';
+
+ /**
+ * Stats as HTML list output
+ */
+ const FORMAT_HTML = 'html';
+
+ /**
+ * @since 2.5
+ *
+ * @param array $stats
+ * @param string|null $format
+ *
+ * @return string|array
+ */
+ public static function format( array $stats, $format = null ) {
+
+ $output = '';
+
+ if ( $format === self::FORMAT_PLAIN ) {
+ foreach ( $stats as $key => $value ) {
+ $output .= '- ' . $key . "\n";
+
+ if ( !is_array( $value ) ) {
+ continue;
+ }
+
+ foreach ( $value as $k => $v ) {
+ $output .= ' - ' . $k . ': ' . $v . "\n";
+ }
+ }
+ }
+
+ if ( $format === self::FORMAT_HTML ) {
+ $output .= '<ul>';
+ foreach ( $stats as $key => $value ) {
+ $output .= '<li>' . $key . '<ul>';
+
+ if ( !is_array( $value ) ) {
+ continue;
+ }
+
+ foreach ( $value as $k => $v ) {
+ $output .= '<li>' . $k . ': ' . $v . "</li>";
+ }
+ $output .= '</ul></li>';
+ }
+ $output .= '</ul>';
+ }
+
+ if ( $format === self::FORMAT_JSON ) {
+ $output .= json_encode( $stats, JSON_PRETTY_PRINT );
+ }
+
+ if ( $format === null ) {
+ $output = $stats;
+ }
+
+ return $output;
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param array $stats
+ * @param string $separator
+ *
+ * @return array
+ */
+ public static function getStatsFromFlatKey( array $stats, $separator = '.' ) {
+
+ $data = $stats;
+ $stats = [];
+
+ foreach ( $data as $key => $value ) {
+ if ( strpos( $key, $separator ) !== false ) {
+ $stats = array_merge_recursive( $stats, self::stringToArray( $separator, $key, $value ) );
+ } else {
+ $stats[$key] = $value;
+ }
+ }
+
+ return $stats;
+ }
+
+ // http://stackoverflow.com/questions/10123604/multstatsdIdimensional-array-from-string
+ private static function stringToArray( $separator, $path, $value ) {
+
+ $pos = strpos( $path, $separator );
+
+ if ( $pos === false ) {
+ return [ $path => $value ];
+ }
+
+ $key = substr( $path, 0, $pos );
+ $path = substr( $path, $pos + 1 );
+
+ $result = [
+ $key => self::stringToArray( $separator, $path, $value )
+ ];
+
+ return $result;
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/TempFile.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/TempFile.php
new file mode 100644
index 00000000..518f624d
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/TempFile.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace SMW\Utils;
+
+use RuntimeException;
+
+/**
+ * @license GNU GPL v2+
+ * @since 3.0
+ *
+ * @author mwjames
+ */
+class TempFile extends File {
+
+ /**
+ * @since 3.0
+ *
+ * @return string
+ * @throws RuntimeException
+ */
+ public function generate() {
+
+ $args = func_get_args();
+ $key = array_shift( $args );
+
+ if ( $args === [] ) {
+ $key = '';
+ }
+
+ return $this->get(
+ $key . substr( base_convert( md5( json_encode( $args ) ), 16, 32 ), 0, 12 )
+ );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $file
+ *
+ * @return string
+ * @throws RuntimeException
+ */
+ public function get( $file ) {
+
+ $tmpDir = [];
+ $path = '';
+
+ if ( isset( $GLOBALS['wgTmpDirectory'] ) ) {
+ $tmpDir[] = $GLOBALS['wgTmpDirectory'];
+ }
+
+ $tmpDir[] = sys_get_temp_dir();
+ $tmpDir[] = ini_get( 'upload_tmp_dir' );
+
+ foreach ( $tmpDir as $tmp ) {
+ if ( $tmp != '' && is_dir( $tmp ) && is_writable( $tmp ) ) {
+ $path = $tmp;
+ break;
+ }
+ }
+
+ if ( $path !== '' ) {
+ return str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, $path . '/' . $file );
+ }
+
+ throw new RuntimeException( 'No writable temporary directory could be found.' );
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/Timer.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Timer.php
new file mode 100644
index 00000000..c9871e30
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Timer.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class Timer {
+
+ /**
+ * @var float|integer
+ */
+ private static $start = [];
+
+ /**
+ * @since 3.0
+ *
+ * @param integer $outputType
+ * @param integer $ts
+ *
+ * @return string|bool
+ */
+ public static function getTimestamp( $outputType = TS_UNIX, $ts = 0 ) {
+ return wfTimestamp( $outputType, $ts );
+ }
+
+ /**
+ * @since 2.5
+ */
+ public static function start( $name ) {
+ self::$start[$name] = microtime( true );
+ }
+
+ /**
+ * @since 2.5
+ *
+ * @param string $name
+ * @param integer|null $round
+ *
+ * @return float|integer
+ */
+ public static function getElapsedTime( $name, $round = null ) {
+
+ if ( !isset( self::$start[$name] ) ) {
+ return 0;
+ }
+
+ $time = microtime( true ) - self::$start[$name];
+
+ if ( $round === null ) {
+ return $time;
+ }
+
+ return round( $time, $round );
+ }
+
+ /**
+ * @since 3.0
+ *
+ * @param string $name
+ * @param integer|null $round
+ *
+ * @return string
+ */
+ public static function getElapsedTimeAsLoggableMessage( $name, $round = null ) {
+ return $name . ' (procTime in sec: '. self::getElapsedTime( $name, $round ) . ')';
+ }
+
+}
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/Utils/Tokenizer.php b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Tokenizer.php
new file mode 100644
index 00000000..0db427a1
--- /dev/null
+++ b/www/wiki/extensions/SemanticMediaWiki/src/Utils/Tokenizer.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace SMW\Utils;
+
+/**
+ * @license GNU GPL v2+
+ * @since 2.5
+ *
+ * @author mwjames
+ */
+class Tokenizer {
+
+ /**
+ * @since 2.5
+ *
+ * @param string $text
+ *
+ * @return array
+ */
+ public static function tokenize( $text ) {
+
+ if ( !class_exists( '\IntlRuleBasedBreakIterator' ) ) {
+ return explode( ' ', $text );
+ }
+
+ // As for CJK, this returns better results as trying to split tokens
+ // by a single character
+ $intlRuleBasedBreakIterator = \IntlRuleBasedBreakIterator::createWordInstance( 'en' );
+ $intlRuleBasedBreakIterator->setText( $text );
+
+ $prev = 0;
+ $tokens = [];
+
+ foreach ( $intlRuleBasedBreakIterator as $token ) {
+
+ if ( $token == 0 ) {
+ continue;
+ }
+
+ $res = substr( $text, $prev, $token - $prev );
+
+ if ( $res !== '' && $res !== ' ' ) {
+ $tokens[] = $res;
+ }
+
+ $prev = $token;
+ }
+
+ return $tokens;
+ }
+
+}